重构: base 包资源核心类型迁移到 req/resx,新增 res_watcher/res_api_param
base/resource.go、resource_hooks.go、resource_test.go、query_field.go 删除,ResField 等具体实现搬到 req/resx(见 res_field.go 里的类型别名)。res_listener.go 替换成 res_watcher.go,对应资源变更监听概念改名。新增 res_api_param.go 及配套测试(ResApi 参数建模,给 MCP tool 的 JSON Schema 生成用)。
This commit is contained in:
@@ -0,0 +1,446 @@
|
||||
package base_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.fsdpf.net/go/contracts/base"
|
||||
jschema "github.com/google/jsonschema-go/jsonschema"
|
||||
)
|
||||
|
||||
// queryParams 查询接口的扁平参数列表,包含 conditions(过滤条件)和 select(查询字段)
|
||||
var queryParams = base.ResApiParams{
|
||||
// resource:目标资源
|
||||
{
|
||||
ID: -1, PID: 0, Code: "resource", DataType: "string", Name: "资源", Rank: 0,
|
||||
Desc: "查询目标资源编码(resource_code)",
|
||||
},
|
||||
{
|
||||
ID: 1, PID: 0, Code: "conditions", DataType: "object", Name: "过滤条件", Rank: 1,
|
||||
Desc: `WHERE 条件树。
|
||||
exprs 每项生成一段比较:[{columnSqlFunc}(]{columnResource}.{column}[)] {operator} {value},同层多项以 type(and/or) 连接;
|
||||
children 为同结构子节点,递归嵌套实现复杂过滤。`,
|
||||
},
|
||||
{
|
||||
ID: 2, PID: 1, Code: "type", DataType: "string", Name: "逻辑运算符", Rank: 0,
|
||||
Desc: "当前节点的逻辑运算符,将 exprs 各项与 children 各组以 AND 或 OR 连接",
|
||||
DefaultVal: "and",
|
||||
Constraints: map[string]any{"enum": []any{"and", "or"}},
|
||||
},
|
||||
{
|
||||
ID: 3, PID: 1, Code: "exprs", DataType: "array", Name: "条件表达式列表", Rank: 1,
|
||||
Desc: "当前层的叶子比较列表,每项生成一个 column {operator} value 片段,同层多项以 type 连接",
|
||||
},
|
||||
{ID: 4, PID: 3, Code: "items", DataType: "object", Rank: 0},
|
||||
{
|
||||
ID: 5, PID: 4, Code: "column", DataType: "string", Name: "字段", Rank: 0,
|
||||
Desc: "字段编码", IsRequired: true,
|
||||
},
|
||||
{
|
||||
ID: 6, PID: 4, Code: "columnResource", DataType: "string", Name: "字段资源", Rank: 1,
|
||||
Desc: "字段所属资源标识,取值为顶层 resource 或 relations[].code",
|
||||
},
|
||||
{
|
||||
ID: 7, PID: 4, Code: "operator", DataType: "string", Name: "运算符", Rank: 2,
|
||||
Desc: `标准 SQL 比较运算符:
|
||||
= / != / > / >= / < / <=
|
||||
LIKE:模糊匹配(%keyword%)
|
||||
IN:包含,value 逗号分隔多值
|
||||
IS NULL / IS NOT NULL:空值判断,无需 value
|
||||
REGEXP:正则匹配`,
|
||||
IsRequired: true,
|
||||
DefaultVal: "=",
|
||||
},
|
||||
{
|
||||
ID: 8, PID: 4, Code: "value", DataType: "string", Name: "比较值", Rank: 3,
|
||||
Desc: "比较目标,内容由 valueType 决定。operator 为 IS NULL/IS NOT NULL 时可省略",
|
||||
},
|
||||
{
|
||||
ID: 9, PID: 4, Code: "valueType", DataType: "string", Name: "值类型", Rank: 4,
|
||||
Desc: `先选类型再填 value:
|
||||
string:value 填字面量如 '2024-01-01'
|
||||
func:value 填内置函数名 UserID|UserUuid|UserRolesUuid|UserPlatform|UserSaaS
|
||||
param:value 填请求参数路径如 body.status
|
||||
sql:value 填原始SQL如 CURRENT_DATE
|
||||
默认 string`,
|
||||
Constraints: map[string]any{"enum": []any{"string", "func", "sql", "param"}},
|
||||
},
|
||||
{
|
||||
ID: 10, PID: 4, Code: "columnSqlFunc", DataType: "string", Name: "列SQL函数", Rank: 5,
|
||||
Desc: `对列应用 SQL 函数后再参与比较,拼接为 {func}({columnResource}.{column}) {operator} {value}。
|
||||
支持 SQL-92 / SQL:1999 / SQL:2003 标准函数及数据库方言函数,如 DATE、UPPER、ROUND。
|
||||
需要第二参数的函数(如 ROUND、CAST)配合 columnSqlFuncParam 使用。`,
|
||||
},
|
||||
{
|
||||
ID: 13, PID: 4, Code: "columnSqlFuncParam", DataType: "string", Name: "SQL函数参数", Rank: 6,
|
||||
Desc: `columnSqlFunc 的附加参数,按函数类型用途不同:
|
||||
|
||||
通用函数(有 param 时):{func}({columnResource}.{column}, {columnSqlFuncParam}) {operator} {value}
|
||||
示例:ROUND(col, 2) → columnSqlFuncParam="2"
|
||||
SUBSTRING(col, 1, 10) → columnSqlFuncParam="1, 10"
|
||||
|
||||
JSON 函数(param 为 JSON 路径表达式):
|
||||
json_member_of(value 是否是列 JSON 数组的成员):
|
||||
无 param:JSON_CONTAINS({value}, JSON_ARRAY({column}))
|
||||
有 param:JSON_CONTAINS({columnSqlFuncParam}, JSON_ARRAY({column}))
|
||||
json_contains(列 JSON 是否包含 value):
|
||||
无 param:JSON_CONTAINS({column}, JSON_ARRAY({value}))
|
||||
有 param:JSON_CONTAINS({columnSqlFuncParam}, JSON_ARRAY({value}))`,
|
||||
},
|
||||
{
|
||||
ID: 14, PID: 4, Code: "ignoreEmptyParam", DataType: "boolean", Name: "忽略空参数", Rank: 7,
|
||||
Desc: "当 valueType=param 且请求参数值为空(空字符串或 null)时,跳过该条件不生成 SQL。用于实现可选过滤项,默认 false",
|
||||
},
|
||||
{
|
||||
ID: 11, PID: 1, Code: "children", DataType: "array", Name: "子条件", Rank: 2,
|
||||
Desc: "子条件节点,递归结构",
|
||||
},
|
||||
{ID: 12, PID: 11, Code: "items", DataType: "object", RefID: 1, Rank: 0},
|
||||
// fields 用 oneOf 子行表示
|
||||
{
|
||||
ID: 15, PID: 0, Code: "fields", DataType: "array", Name: "查询字段", Rank: 2,
|
||||
Desc: "查询字段,不传则返回所有字段",
|
||||
},
|
||||
{
|
||||
ID: 55, PID: 15, Code: "oneOf", DataType: "string", Name: "字段", Rank: 0,
|
||||
Desc: "直接填字段编码,默认归属于顶层 resource。等价于 {column: \"field\", columnResource: resource}",
|
||||
},
|
||||
{ID: 16, PID: 15, Code: "oneOf", DataType: "object", Name: "字段引用", Rank: 1},
|
||||
{ID: 44, PID: 16, Code: "column", DataType: "string", Name: "字段", Rank: 0, IsRequired: true},
|
||||
{
|
||||
ID: 46, PID: 16, Code: "columnResource", DataType: "string", Name: "字段资源", Rank: 1,
|
||||
Desc: "字段所属资源标识,取值为顶层 resource 或 relations[].code",
|
||||
},
|
||||
{ID: 45, PID: 16, Code: "alias", DataType: "string", Name: "结果别名", Rank: 2},
|
||||
{ID: 17, PID: 15, Code: "oneOf", DataType: "object", Name: "表达式字段", Rank: 2},
|
||||
{ID: 18, PID: 17, Code: "expr", DataType: "string", Name: "SQL 表达式", Rank: 0, IsRequired: true,
|
||||
Desc: "{column} / {columnResource}.{column} / SQL 表达式"},
|
||||
{ID: 19, PID: 17, Code: "alias", DataType: "string", Name: "结果别名", Rank: 1},
|
||||
|
||||
// orderBy:items 用 oneOf 表示三种形式,每个变体含 direction
|
||||
{
|
||||
ID: 20, PID: 0, Code: "orderBy", DataType: "array", Name: "排序规则", Rank: 3,
|
||||
Desc: "排序规则列表,多项按顺序依次应用",
|
||||
},
|
||||
|
||||
{ID: 48, PID: 20, Code: "oneOf", DataType: "object", Name: "资源字段排序", Rank: 1},
|
||||
{ID: 49, PID: 48, Code: "column", DataType: "string", Name: "字段", Rank: 0, IsRequired: true},
|
||||
{
|
||||
ID: 50, PID: 48, Code: "columnResource", DataType: "string", Name: "字段资源", Rank: 1,
|
||||
Desc: "字段所属资源标识,取值为顶层 resource 或 relations[].code",
|
||||
},
|
||||
{
|
||||
ID: 53, PID: 48, Code: "direction", RefID: 56,
|
||||
},
|
||||
|
||||
{ID: 51, PID: 20, Code: "oneOf", DataType: "object", Name: "SQL 表达式排序", Rank: 2},
|
||||
{ID: 52, PID: 51, Code: "expr", DataType: "string", Name: "字段表达式", Rank: 0, IsRequired: true},
|
||||
{
|
||||
ID: 54, PID: 51, Code: "direction", RefID: 56,
|
||||
},
|
||||
|
||||
{
|
||||
ID: 57, PID: 20, Code: "oneOf", DataType: "string", Name: "SQL 表达式 (ASC)", Rank: 0,
|
||||
Desc: "直接填字段编码,默认升序(asc)",
|
||||
},
|
||||
// groupBy:分组字段
|
||||
{
|
||||
ID: 24, PID: 0, Code: "groupBy", DataType: "array", Name: "分组规则", Rank: 4,
|
||||
Desc: "GROUP BY 字段列表,配合 fields 中的聚合字段使用",
|
||||
},
|
||||
{
|
||||
ID: 58, PID: 24, Code: "oneOf", DataType: "string", Name: "分组表达式", Desc: "{column} / {columnResource}.{column} / SQL 表达式", Rank: 0,
|
||||
},
|
||||
{
|
||||
ID: 59, PID: 24, Code: "oneOf", DataType: "object", Name: "资源字段分组", Rank: 0,
|
||||
},
|
||||
{ID: 60, PID: 59, Code: "column", DataType: "string", Name: "字段编码", Rank: 0, IsRequired: true},
|
||||
{
|
||||
ID: 61, PID: 59, Code: "columnResource", DataType: "string", Name: "所属资源", Rank: 1,
|
||||
Desc: "字段所属资源标识,取值为顶层 resource 或 relations[].code",
|
||||
},
|
||||
// limit / offset:分页
|
||||
{
|
||||
ID: 26, PID: 0, Code: "limit", DataType: "integer", Name: "每页条数", Rank: 5,
|
||||
Desc: "返回的最大记录数,默认 30", DefaultVal: "30", Constraints: map[string]any{"maximum": 50},
|
||||
},
|
||||
{
|
||||
ID: 27, PID: 0, Code: "offset", DataType: "integer", Name: "偏移量", Rank: 6,
|
||||
Desc: "跳过的记录数,默认 0",
|
||||
},
|
||||
// relations:资源关联
|
||||
{
|
||||
ID: 28, PID: 0, Code: "relations", DataType: "array", Name: "资源关联", Rank: 7,
|
||||
Desc: "关联配置列表。\ninner / left / right:SQL JOIN,要求关联资源与主资源在同一数据库连接,将关联字段合并到主查询行。\nhasOne / hasMany:独立子查询,支持跨数据库连接,结果聚合为 JSON 字段(hasOne 为单对象,hasMany 为数组)。\n跨库关联必须使用 hasOne 或 hasMany,不可使用 JOIN 类型。",
|
||||
},
|
||||
{ID: 29, PID: 28, Code: "items", DataType: "object", Rank: 0},
|
||||
{
|
||||
ID: 30, PID: 29, Code: "code", DataType: "string", Name: "关联标识", Rank: 0,
|
||||
Desc: "关联唯一标识,同时作为别名:JOIN 时为表别名(AS code),子查询时为结果 JSON 字段的键名",
|
||||
IsRequired: true,
|
||||
},
|
||||
{
|
||||
ID: 38, PID: 29, Code: "name", DataType: "string", Name: "关联名称", Rank: 1,
|
||||
Desc: "关联的显示名称",
|
||||
},
|
||||
{
|
||||
ID: 39, PID: 29, Code: "type", DataType: "string", Name: "关联类型", Rank: 2,
|
||||
Desc: `关联模式:
|
||||
inner / left / right:SQL JOIN,要求与主资源同一数据库连接,将关联字段合并到主查询行
|
||||
hasOne:独立子查询,支持跨库,结果聚合为单个对象(result[code] = {})
|
||||
hasMany:独立子查询,支持跨库,结果聚合为对象数组(result[code] = [...])
|
||||
跨数据库连接的关联必须使用 hasOne 或 hasMany`,
|
||||
Constraints: map[string]any{"enum": []any{"inner", "left", "right", "hasOne", "hasMany"}},
|
||||
},
|
||||
{ID: 40, PID: 29, Code: "actuallyResource", DataType: "string", Name: "实际资源", Rank: 3,
|
||||
Desc: "被关联的目标资源编码(相当于 SQL 表名)。JOIN 拼接为:{type} JOIN {actuallyResource} AS {code} ON {code}.{actuallyField} = {relationResource}.{relationField}"},
|
||||
{ID: 41, PID: 29, Code: "actuallyField", DataType: "string", Name: "实际资源字段", Rank: 4,
|
||||
Desc: "目标资源(code)参与 ON 条件的字段"},
|
||||
{ID: 42, PID: 29, Code: "relationResource", DataType: "string", Name: "被关联资源", Rank: 5,
|
||||
Desc: "ON 条件对端的资源编码(通常为主资源或父关联资源)"},
|
||||
{ID: 43, PID: 29, Code: "relationField", DataType: "string", Name: "被关联字段", Rank: 6,
|
||||
Desc: "ON 条件对端资源的字段"},
|
||||
{ID: 31, PID: 29, Code: "conditions", RefID: 1, Rank: 7, Desc: "关联的过滤条件"},
|
||||
{ID: 33, PID: 29, Code: "orderBy", RefID: 20, Rank: 8, Desc: "关联排序,仅 hasOne/hasMany 有效"},
|
||||
{ID: 34, PID: 29, Code: "groupBy", RefID: 24, Rank: 9, Desc: "关联分组,仅 hasOne/hasMany 有效"},
|
||||
{ID: 35, PID: 29, Code: "limit", RefID: 26, Rank: 10, Desc: "关联条数限制,仅 hasOne/hasMany 有效"},
|
||||
{ID: 36, PID: 29, Code: "offset", RefID: 27, Rank: 11, Desc: "关联偏移量,仅 hasOne/hasMany 有效"},
|
||||
}
|
||||
|
||||
func TestResApiParams_PrintSchema(t *testing.T) {
|
||||
b, _ := json.MarshalIndent(queryParams.ToJSONSchema(), "", " ")
|
||||
t.Log("\n" + string(b))
|
||||
}
|
||||
|
||||
func TestResApiParams_ToJSONSchema(t *testing.T) {
|
||||
schema := queryParams.ToJSONSchema()
|
||||
|
||||
if schema.Type != "object" {
|
||||
t.Fatalf("expected type=object, got %s", schema.Type)
|
||||
}
|
||||
|
||||
cond, ok := schema.Properties["conditions"]
|
||||
if !ok {
|
||||
t.Fatal("missing property: conditions")
|
||||
}
|
||||
if cond.Type != "object" {
|
||||
t.Errorf("conditions: expected type=object, got %s", cond.Type)
|
||||
}
|
||||
if cond.Description != "WHERE 条件树。\nexprs 每项生成一段比较:[{columnSqlFunc}(]{columnResource}.{column}[)] {operator} {value},同层多项以 type(and/or) 连接;\nchildren 为同结构子节点,递归嵌套实现复杂过滤。" {
|
||||
t.Errorf("conditions: unexpected description: %s", cond.Description)
|
||||
}
|
||||
|
||||
// conditions.type 枚举和默认值
|
||||
typeProp, ok := cond.Properties["type"]
|
||||
if !ok {
|
||||
t.Fatal("missing property: conditions.type")
|
||||
}
|
||||
if len(typeProp.Enum) != 2 {
|
||||
t.Errorf("conditions.type: expected 2 enum values, got %d", len(typeProp.Enum))
|
||||
}
|
||||
var defaultVal string
|
||||
if err := json.Unmarshal(typeProp.Default, &defaultVal); err != nil || defaultVal != "and" {
|
||||
t.Errorf("conditions.type: expected default=and, got %s", typeProp.Default)
|
||||
}
|
||||
|
||||
// conditions.exprs 是 array,items 是 object
|
||||
exprsProp, ok := cond.Properties["exprs"]
|
||||
if !ok {
|
||||
t.Fatal("missing property: conditions.exprs")
|
||||
}
|
||||
if exprsProp.Type != "array" {
|
||||
t.Errorf("conditions.exprs: expected type=array, got %s", exprsProp.Type)
|
||||
}
|
||||
if exprsProp.Items == nil {
|
||||
t.Fatal("conditions.exprs: missing items")
|
||||
}
|
||||
if exprsProp.Items.Type != "object" {
|
||||
t.Errorf("conditions.exprs.items: expected type=object, got %s", exprsProp.Items.Type)
|
||||
}
|
||||
|
||||
// exprs.items required: column, operator
|
||||
requiredSet := map[string]bool{}
|
||||
for _, r := range exprsProp.Items.Required {
|
||||
requiredSet[r] = true
|
||||
}
|
||||
if !requiredSet["column"] || !requiredSet["operator"] {
|
||||
t.Errorf("conditions.exprs.items: expected required=[column, operator], got %v", exprsProp.Items.Required)
|
||||
}
|
||||
|
||||
// children.items $ref
|
||||
childrenProp, ok := cond.Properties["children"]
|
||||
if !ok {
|
||||
t.Fatal("missing property: conditions.children")
|
||||
}
|
||||
if childrenProp.Items == nil {
|
||||
t.Fatal("conditions.children: missing items")
|
||||
}
|
||||
if childrenProp.Items.Ref != "#/properties/conditions" {
|
||||
t.Errorf("conditions.children.items: expected $ref=#/properties/conditions, got %s", childrenProp.Items.Ref)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResApiParamsFromJSONSchema(t *testing.T) {
|
||||
original := queryParams.ToJSONSchema()
|
||||
params := base.ResApiParamsFromJSONSchema(original)
|
||||
|
||||
if len(params) == 0 {
|
||||
t.Fatal("ResApiParamsFromJSONSchema returned empty params")
|
||||
}
|
||||
|
||||
// 转回 schema 后结构应一致
|
||||
rebuilt := params.ToJSONSchema()
|
||||
|
||||
// 将 original 和 rebuilt 都序列化为 map[string]any 后 DeepEqual 比较
|
||||
toMap := func(s *jschema.Schema) map[string]any {
|
||||
b, _ := json.Marshal(s)
|
||||
var m map[string]any
|
||||
json.Unmarshal(b, &m)
|
||||
return m
|
||||
}
|
||||
originalMap := toMap(original)
|
||||
rebuiltMap := toMap(rebuilt)
|
||||
|
||||
if !reflect.DeepEqual(originalMap, rebuiltMap) {
|
||||
ob, _ := json.MarshalIndent(originalMap, "", " ")
|
||||
rb, _ := json.MarshalIndent(rebuiltMap, "", " ")
|
||||
t.Errorf("schema mismatch:\noriginal: %s\nrebuilt: %s", ob, rb)
|
||||
}
|
||||
}
|
||||
|
||||
// createParams res-create 接口参数,data 字段为 any 类型(oneOf object | array<object>)
|
||||
var createParams = base.ResApiParams{
|
||||
{ID: 315, PID: 0, Code: "resource", DataType: "string", Name: "主资源", Rank: 1,
|
||||
Desc: "目标资源编码,数据将写入该资源对应的表"},
|
||||
{ID: 316, PID: 0, Code: "data", DataType: "any", Name: "创建数据", Rank: 2,
|
||||
Desc: "单条记录传对象,批量创建传对象数组"},
|
||||
// oneOf 分支1:单条记录(object + additionalProperties: true)
|
||||
{ID: 317, PID: 316, Code: "oneOf", DataType: "json", Name: "单条记录", Rank: 1,
|
||||
Desc: "创建单条记录,key 为字段编码,value 为字段值",
|
||||
Constraints: map[string]any{"additionalProperties": true}},
|
||||
// oneOf 分支2:批量记录(array,items 为 object + additionalProperties: true)
|
||||
{ID: 318, PID: 316, Code: "oneOf", DataType: "array", Name: "批量记录", Rank: 2,
|
||||
Desc: "批量创建,每个元素为一条记录对象"},
|
||||
{ID: 319, PID: 318, Code: "items", DataType: "json", Name: "记录对象", Rank: 1,
|
||||
Desc: "批量创建,每个元素为一条记录对象",
|
||||
Constraints: map[string]any{"additionalProperties": true}},
|
||||
}
|
||||
|
||||
func TestResApiParams_AnyType(t *testing.T) {
|
||||
schema := createParams.ToJSONSchema()
|
||||
|
||||
// data 字段存在
|
||||
data, ok := schema.Properties["data"]
|
||||
if !ok {
|
||||
t.Fatal("missing property: data")
|
||||
}
|
||||
|
||||
// any 类型不应有 type
|
||||
if data.Type != "" {
|
||||
t.Errorf("data: expected empty type for any, got %q", data.Type)
|
||||
}
|
||||
|
||||
// oneOf 应有 2 个分支
|
||||
if len(data.OneOf) != 2 {
|
||||
t.Fatalf("data.oneOf: expected 2 branches, got %d", len(data.OneOf))
|
||||
}
|
||||
|
||||
// 分支1:object
|
||||
branch1 := data.OneOf[0]
|
||||
if branch1.Type != "object" {
|
||||
t.Errorf("data.oneOf[0]: expected type=object, got %q", branch1.Type)
|
||||
}
|
||||
if branch1.AdditionalProperties == nil {
|
||||
t.Error("data.oneOf[0]: expected additionalProperties to be set")
|
||||
}
|
||||
|
||||
// 分支2:array,items 为 object
|
||||
branch2 := data.OneOf[1]
|
||||
if branch2.Type != "array" {
|
||||
t.Errorf("data.oneOf[1]: expected type=array, got %q", branch2.Type)
|
||||
}
|
||||
if branch2.Items == nil {
|
||||
t.Fatal("data.oneOf[1]: missing items")
|
||||
}
|
||||
if branch2.Items.Type != "object" {
|
||||
t.Errorf("data.oneOf[1].items: expected type=object, got %q", branch2.Items.Type)
|
||||
}
|
||||
if branch2.Items.AdditionalProperties == nil {
|
||||
t.Error("data.oneOf[1].items: expected additionalProperties to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResApiParams_AdditionalProperties(t *testing.T) {
|
||||
params := base.ResApiParams{
|
||||
{ID: 1, PID: 0, Code: "open", DataType: "json", Name: "开放对象",
|
||||
Constraints: map[string]any{"additionalProperties": true}},
|
||||
{ID: 2, PID: 0, Code: "strict", DataType: "json", Name: "严格对象",
|
||||
Constraints: map[string]any{"additionalProperties": false}},
|
||||
}
|
||||
|
||||
schema := params.ToJSONSchema()
|
||||
|
||||
open := schema.Properties["open"]
|
||||
if open.AdditionalProperties == nil {
|
||||
t.Fatal("open: expected additionalProperties to be set")
|
||||
}
|
||||
// true → Not == nil
|
||||
if open.AdditionalProperties.Not != nil {
|
||||
t.Error("open: additionalProperties should be true (Not == nil)")
|
||||
}
|
||||
|
||||
strict := schema.Properties["strict"]
|
||||
if strict.AdditionalProperties == nil {
|
||||
t.Fatal("strict: expected additionalProperties to be set")
|
||||
}
|
||||
// false → Not != nil
|
||||
if strict.AdditionalProperties.Not == nil {
|
||||
t.Error("strict: additionalProperties should be false (Not != nil)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResApiParams_AnyType_Print(t *testing.T) {
|
||||
b, _ := json.MarshalIndent(createParams.ToJSONSchema(), "", " ")
|
||||
t.Log("\n" + string(b))
|
||||
}
|
||||
|
||||
func TestResApiParams_RoundTrip(t *testing.T) {
|
||||
input := &jschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jschema.Schema{
|
||||
"name": {
|
||||
Type: "string",
|
||||
Description: "姓名",
|
||||
MinLength: jschema.Ptr(1),
|
||||
MaxLength: jschema.Ptr(100),
|
||||
},
|
||||
"age": {
|
||||
Type: "integer",
|
||||
Minimum: jschema.Ptr(float64(0)),
|
||||
Maximum: jschema.Ptr(float64(150)),
|
||||
},
|
||||
"tags": {
|
||||
Type: "array",
|
||||
Items: &jschema.Schema{Type: "string"},
|
||||
},
|
||||
},
|
||||
Required: []string{"name"},
|
||||
}
|
||||
|
||||
params := base.ResApiParamsFromJSONSchema(input)
|
||||
output := params.ToJSONSchema()
|
||||
|
||||
if _, ok := output.Properties["name"]; !ok {
|
||||
t.Error("round-trip: missing property name")
|
||||
}
|
||||
if _, ok := output.Properties["age"]; !ok {
|
||||
t.Error("round-trip: missing property age")
|
||||
}
|
||||
tags, ok := output.Properties["tags"]
|
||||
if !ok {
|
||||
t.Error("round-trip: missing property tags")
|
||||
} else if tags.Items == nil || tags.Items.Type != "string" {
|
||||
t.Errorf("round-trip: tags.items expected type=string, got %+v", tags.Items)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user