+ LoadEngine

This commit is contained in:
Max 2021-09-08 18:40:02 +08:00
parent b530daebfa
commit 96d37847b5
13 changed files with 768 additions and 31 deletions

3
.gitignore vendored
View file

@ -16,5 +16,4 @@
.DS_Store
.env*
.tmp
dist
ui
dist

View file

@ -111,7 +111,7 @@ bindata:
mkdir -p .tmp/data
cp -r ui .tmp/data/
cp -r xiang .tmp/data/
go-bindata -o data.go .tmp/data/...
go-bindata -fs -o data.go -prefix ".tmp/data/" .tmp/data/...
rm -rf .tmp/data
# 编译可执行文件

File diff suppressed because one or more lines are too long

View file

@ -23,13 +23,14 @@ type Config struct {
// XiangConfig 象传应用引擎配置
type XiangConfig struct {
Mode string `json:"mode,omitempty" env:"XIANG_MODE" envDefault:"release"` // 象传引擎模式 debug/release/test
Source string `json:"source,omitempty" env:"XIANG_SOURCE" envDefault:"fs://."` // 源码路径(用于调试时载入数据)
Root string `json:"root,omitempty" env:"XIANG_ROOT" envDefault:"."` // 应用文件目录
RootAPI string `json:"root_api,omitempty" env:"XIANG_ROOT_API"` // 应用API文件目录
RootModel string `json:"root_model,omitempty" env:"XIANG_ROOT_MODEL"` // 应用模型文件目录
RootFLow string `json:"root_flow,omitempty" env:"XIANG_ROOT_FLOW"` // 应用工作流文件目录
RootPlugin string `json:"root_plugin,omitempty" env:"XIANG_ROOT_FLOW"` // 应用插件文件目录
Mode string `json:"mode,omitempty" env:"XIANG_MODE" envDefault:"release"` // 象传引擎模式 debug/release/test
Source string `json:"source,omitempty" env:"XIANG_SOURCE" envDefault:"fs://."` // 源码路径(用于调试时载入数据)
Path string `json:"path,omitempty" env:"XIANG_PATH" envDefault:"bin://xiang"` // 引擎文件目录
Root string `json:"root,omitempty" env:"XIANG_ROOT" envDefault:"fs://."` // 应用文件目录
RootAPI string `json:"root_api,omitempty" env:"XIANG_ROOT_API"` // 应用API文件目录
RootModel string `json:"root_model,omitempty" env:"XIANG_ROOT_MODEL"` // 应用模型文件目录
RootFLow string `json:"root_flow,omitempty" env:"XIANG_ROOT_FLOW"` // 应用工作流文件目录
RootPlugin string `json:"root_plugin,omitempty" env:"XIANG_ROOT_FLOW"` // 应用插件文件目录
}
// ServiceConfig 服务配置

View file

@ -19,8 +19,8 @@ func TestNewConfig(t *testing.T) {
}
assert.Equal(t, cfg.Mode, os.Getenv("XIANG_MODE"))
assert.Equal(t, cfg.Source, os.Getenv("XIANG_SOURCE"))
assert.Equal(t, cfg.Root, os.Getenv("XIANG_ROOT"))
assert.Equal(t, cfg.Path, os.Getenv("XIANG_PATH"))
assert.Equal(t, cfg.Service.Debug, vBool(os.Getenv("XIANG_SERVICE_DEBUG")))
assert.Equal(t, strings.Join(cfg.Service.Allow, "|"), os.Getenv("XIANG_SERVICE_ALLOW"))

19
init_test.go Normal file
View file

@ -0,0 +1,19 @@
package main
import (
"os"
"testing"
)
var cfg Config
func TestMain(m *testing.M) {
cfg = NewConfig()
// Run test suites
exitVal := m.Run()
// we can do clean up code here
os.Exit(exitVal)
}

120
load.go
View file

@ -1,6 +1,126 @@
package main
import (
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"github.com/yaoapp/gou"
"github.com/yaoapp/kun/exception"
)
// Script 脚本文件类型
type Script struct {
Name string
Type string
Content []byte
}
// Load 根据配置加载 API, FLow, Model, Plugin
func Load() error {
return nil
}
// LoadEngine 载入引擎 API, Flow, Model 配置
func LoadEngine(from string) {
var scripts []Script
if strings.HasPrefix(from, "fs://") || !strings.Contains(from, "://") {
root := strings.TrimPrefix(from, "fs://")
scripts = getFilesFS(root, ".json")
} else if strings.HasPrefix(from, "bin://") {
root := strings.TrimPrefix(from, "bin://")
scripts = getFilesBin(root, ".json")
}
if scripts == nil {
exception.New("读取文件失败", 500, from).Throw()
}
if len(scripts) == 0 {
exception.New("读取文件失败, 未找到任何可执行脚本", 500, from).Throw()
}
// 加载 API, Flow, Models
for _, script := range scripts {
switch script.Type {
case "models":
gou.LoadModel(string(script.Content), script.Name)
break
case "flows":
gou.LoadFlow(string(script.Content), script.Name)
break
case "api":
gou.LoadAPI(string(script.Content), script.Name)
break
}
}
}
// getFilesFS 遍历目录,读取文件列表
func getFilesFS(root string, typ string) []Script {
files := []Script{}
root = path.Join(root, "/")
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
exception.Err(err, 500).Throw()
return err
}
if strings.HasSuffix(path, typ) {
filename := strings.TrimPrefix(path, root+"/")
name, typ := getTypeName(filename)
file, err := os.Open(path)
if err != nil {
exception.Err(err, 500).Throw()
}
defer file.Close()
content, err := ioutil.ReadAll(file)
if err != nil {
exception.Err(err, 500).Throw()
}
files = append(files, Script{
Name: name,
Type: typ,
Content: content,
})
}
return nil
})
return files
}
// getFilesBin 从 bindata 中读取文件列表
func getFilesBin(root string, typ string) []Script {
files := []Script{}
binfiles := AssetNames()
for _, path := range binfiles {
if strings.HasSuffix(path, typ) {
file := strings.TrimPrefix(path, root+"/")
name, typ := getTypeName(file)
content, err := Asset(path)
if err != nil {
exception.Err(err, 500).Throw()
}
files = append(files, Script{
Name: name,
Type: typ,
Content: content,
})
}
}
return files
}
func getTypeName(path string) (name string, typ string) {
namer := strings.Split(path, ".")
nametypes := strings.Split(namer[0], "/")
name = strings.Join(nametypes[1:], ".")
typ = nametypes[0]
return name, typ
}

View file

@ -1,6 +1,7 @@
package main
import (
"path"
"testing"
"github.com/stretchr/testify/assert"
@ -10,3 +11,20 @@ func TestLoad(t *testing.T) {
err := Load()
assert.Nil(t, err)
}
// 从文件系统载入引擎文件
func TestLoadEngineFS(t *testing.T) {
root := "fs://" + path.Join(cfg.Source, "/xiang")
assert.NotPanics(t, func() {
LoadEngine(root)
})
}
// 从BinDataz载入引擎文件
func TestLoadEngineBin(t *testing.T) {
root := "bin://xiang"
assert.NotPanics(t, func() {
LoadEngine(root)
})
}

1
ui/index.html Normal file
View file

@ -0,0 +1 @@
hello world

19
xiang/apis/sys.http.json Normal file
View file

@ -0,0 +1,19 @@
{
"name": "系统接口",
"version": "1.0.0",
"description": "系统接口API",
"group": "sys",
"guard": "bearer-jwt",
"paths": [
{
"path": "/info/:id",
"method": "GET",
"process": "models.user.Find",
"in": ["$param.id", ":params"],
"out": {
"status": 200,
"type": "application/json"
}
}
]
}

View file

@ -0,0 +1,23 @@
{
"label": "最新信息",
"version": "1.0.0",
"description": "最新信息",
"nodes": [
{
"name": "user",
"process": "models.user.get",
"args": [
{
"select": ["id", "name", "mobile"],
"limit": 20,
"orders": [{ "column": "created_at", "option": "desc" }],
"wheres": [
{ "column": "status", "value": "enabled" },
{ "column": "name", "value": "{{$in.0}}", "op": "like" }
]
}
],
"outs": ["{{$out}}"]
}
]
}

320
xiang/models/user.json Normal file
View file

@ -0,0 +1,320 @@
{
"name": "用户",
"table": {
"name": "user",
"comment": "用户表",
"engine": "InnoDB"
},
"columns": [
{ "label": "ID", "name": "id", "type": "ID" },
{
"label": "厂商",
"name": "manu_id",
"type": "bigInteger",
"length": 50,
"comment": "所属厂商",
"nullable": true,
"index": true,
"validations": [
{
"method": "typeof",
"args": ["integer"],
"message": "{{input}}类型错误, {{label}}应为数字"
},
{
"method": "min",
"args": [0],
"message": "{{label}}应大于0"
}
]
},
{
"label": "类型",
"name": "type",
"type": "enum",
"option": ["admin", "staff", "user"],
"comment": "账号类型 admin 管理员, staff 员工, user 用户",
"default": "staff",
"index": true,
"validations": [
{
"method": "typeof",
"args": ["string"],
"message": "{{input}}类型错误, {{label}}应该为字符串"
},
{
"method": "enum",
"args": ["admin", "staff", "user"],
"message": "{{input}}不在许可范围, {{label}}应该为 admin/staff/user"
}
]
},
{
"label": "手机号",
"name": "mobile",
"type": "string",
"length": 50,
"comment": "手机号",
"index": true,
"crypt": "AES",
"validations": [
{
"method": "typeof",
"args": ["string"],
"message": "{{input}}类型错误, {{label}}应该为字符串"
},
{
"method": "pattern",
"args": ["^1[3-9]\\d{9}$"],
"message": "{{input}}格式错误"
}
]
},
{
"label": "登录密码",
"name": "password",
"type": "string",
"length": 256,
"comment": "登录密码",
"crypt": "PASSWORD",
"index": true,
"validations": [
{
"method": "typeof",
"args": ["string"],
"message": "{{input}}类型错误, {{label}}应该为字符串"
},
{
"method": "minLength",
"args": [6],
"message": "{{label}}应该由6-18位大小写字母、数字和符号构成"
},
{
"method": "maxLength",
"args": [18],
"message": "{{label}}应该由6-18位大小写字母、数字和符号构成"
},
{
"method": "pattern",
"args": ["[0-9]+"],
"message": "{{label}}应该至少包含一个数字"
},
{
"method": "pattern",
"args": ["[A-Z]+"],
"message": "{{label}}应该至少包含一个大写字母"
},
{
"method": "pattern",
"args": ["[a-z]+"],
"message": "{{label}}应该至少包含一个小写字母"
},
{
"method": "pattern",
"args": ["[@#$&*]+"],
"message": "{{label}}应该至少包含一个符号"
}
]
},
{
"label": "姓名",
"name": "name",
"type": "string",
"length": 80,
"comment": "姓名",
"index": true,
"validations": [
{
"method": "typeof",
"args": ["string"],
"message": "{{input}}类型错误, {{label}}应该为字符串"
},
{
"method": "minLength",
"args": [2],
"message": "{{label}}至少需要2个字"
},
{
"method": "maxLength",
"args": [40],
"message": "{{label}}不能超过20个字"
}
]
},
{
"label": "身份证号码",
"name": "idcard",
"type": "string",
"length": 256,
"comment": "身份证号码",
"crypt": "AES",
"nullable": true,
"index": true,
"validations": [
{
"method": "typeof",
"args": ["string"],
"message": "{{input}}类型错误, {{label}}应该为字符串"
},
{
"method": "pattern",
"args": ["^(\\d{18})|(\\d{14}X)$"],
"message": "{{label}}格式错误"
}
]
},
{
"label": "账户余额",
"name": "balance",
"type": "integer",
"length": 20,
"comment": "账户余额(冗余)",
"default": 0,
"index": true,
"validations": [
{
"method": "typeof",
"args": ["integer"],
"message": "{{input}}类型错误, {{label}}应为数字"
},
{
"method": "min",
"args": [0],
"message": "{{label}}应大于0"
}
]
},
{
"label": "API Key",
"name": "key",
"type": "string",
"length": 256,
"comment": "API Key",
"nullable": true,
"unique": true,
"validations": [
{
"method": "typeof",
"args": ["string"],
"message": "{{input}}类型错误, {{label}}应该为字符串"
},
{
"method": "pattern",
"args": ["^[0-9A-Za-z@#$&*]{8}$"],
"message": " {{label}}应该由8位大小写字母、数字和符号构成"
}
]
},
{
"label": "API 密钥",
"name": "secret",
"type": "string",
"length": 256,
"nullable": true,
"crypt": "AES",
"comment": "API 密钥",
"index": true,
"validations": [
{
"method": "typeof",
"args": ["string"],
"message": "{{input}}类型错误, {{label}}应该为字符串"
},
{
"method": "pattern",
"args": ["^[0-9A-Za-z@#$&*]{32}$"],
"message": "{{label}}应该由32位大小写字母、数字和符号构成"
}
]
},
{
"label": "简历",
"name": "resume",
"type": "text",
"comment": "简历",
"nullable": true
},
{
"label": "扩展信息",
"name": "extra",
"type": "json",
"comment": "扩展信息",
"nullable": true
},
{
"label": "状态",
"comment": "用户状态 enabled 有效, disabled 无效",
"name": "status",
"type": "enum",
"default": "enabled",
"option": ["enabled", "disabled"],
"index": true,
"validations": [
{
"method": "typeof",
"args": ["string"],
"message": "{{input}}类型错误, {{label}}应该为字符串"
},
{
"method": "enum",
"args": ["enabled", "disabled"],
"message": "{{input}}不在许可范围, {{label}}应该为 enabled/disabled"
}
]
}
],
"relations": {},
"values": [
{
"name": "管理员",
"manu_id": 1,
"type": "admin",
"idcard": "230624198301170015",
"mobile": "13900001111",
"password": "cvSK@RY6",
"key": "FB3fxCeQ",
"secret": "XMTdNRVigbgUiAPdiJCfaWgWcz2PaQXw",
"status": "enabled",
"extra": { "sex": "男" }
},
{
"name": "员工",
"manu_id": 1,
"type": "staff",
"idcard": "23082619820207024X",
"mobile": "13900002222",
"password": "qV@uT1DI",
"key": "JDh2ZiUt",
"secret": "wBeYjL7FjbcvpAdBrxtDFfjydsoPKhRN",
"status": "enabled",
"extra": { "sex": "女" }
},
{
"name": "用户",
"manu_id": 2,
"type": "user",
"idcard": "23082619820207004X",
"mobile": "13900003333",
"password": "qV@uT1DI",
"key": "XZ12MiPz",
"secret": "wBeYjL7FjbcvpAdBrxtDFfjydsoPKhRN",
"status": "enabled",
"extra": { "sex": "女" }
}
],
"indexes": [
{
"comment": "厂商用户",
"name": "manu_id_mobile_unique",
"columns": ["manu_id", "mobile"],
"type": "unique"
},
{
"comment": "简历全文检索",
"name": "resume_fulltext",
"columns": ["resume"],
"type": "fulltext"
}
],
"option": { "timestamps": true, "soft_deletes": true }
}

View file

@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestMain(t *testing.T) {
func TestMainCMD(t *testing.T) {
assert.NotPanics(t, func() {
main()
})