diff --git a/widgets/app/app.go b/widgets/app/app.go index f25f2e2f..ca59a80d 100644 --- a/widgets/app/app.go +++ b/widgets/app/app.go @@ -191,7 +191,7 @@ func exportAPI() error { Path: "/service/:name", Guard: "bearer-jwt", Method: "POST", - Process: "yao.app.Serivce", + Process: "yao.app.Service", In: []string{"$param.name", ":payload"}, Out: gou.Out{Status: 200, Type: "application/json"}, } diff --git a/widgets/component/component.go b/widgets/component/component.go index cf9f1a7d..00204409 100644 --- a/widgets/component/component.go +++ b/widgets/component/component.go @@ -1,5 +1,9 @@ package component +import ( + jsoniter "github.com/json-iterator/go" +) + // process // yao.component.TagView // yao.component.TagEdit @@ -12,19 +16,27 @@ func Export() error { return nil } +// MarshalJSON Custom JSON parse +func (dsl DSL) MarshalJSON() ([]byte, error) { + return jsoniter.Marshal(dsl.Map()) +} + // Map cast to map[string]interface{} func (dsl DSL) Map() map[string]interface{} { - return map[string]interface{}{ + res := map[string]interface{}{ "type": dsl.Type, - "in": dsl.In, - "out": dsl.Out, "props": map[string]interface{}(dsl.Props), } + if dsl.Bind != "" { + res["bind"] = dsl.Bind + } + return res } // Clone Component func (dsl *DSL) Clone() *DSL { new := DSL{ + Bind: dsl.Bind, Type: dsl.Type, In: dsl.In, Out: dsl.Out, diff --git a/widgets/component/compute.go b/widgets/component/compute.go new file mode 100644 index 00000000..b53f261e --- /dev/null +++ b/widgets/component/compute.go @@ -0,0 +1,152 @@ +package component + +import ( + "fmt" + "reflect" + "strings" + + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/maps" +) + +// "$C(value)", "$C(props)", "$C(type)"} +var defaults = []CArg{ + {IsExp: true, key: "value", value: nil}, + {IsExp: true, key: "props", value: nil}, + {IsExp: true, key: "type", value: nil}, +} + +// Value compute value +func (compute *Compute) Value(data maps.MapStr, sid string, global map[string]interface{}) (interface{}, error) { + + if compute.Process == "" { + return nil, fmt.Errorf("compute process is required") + } + + // Build-In handlers + args := compute.GetArgs(data) + if handler, has := hanlders[compute.Process]; has { + return handler(args...) + } + + if !strings.Contains(compute.Process, ".") { + return nil, fmt.Errorf("compute %s does not found", compute.Process) + } + + // Run process + process, err := gou.ProcessOf(compute.Process, args...) + if err != nil { + return nil, err + } + + res, err := process.WithSID(sid).WithGlobal(global).Exec() + if err != nil { + return nil, err + } + + return res, nil +} + +// GetArgs return args +func (compute *Compute) GetArgs(data maps.MapStr) []interface{} { + args := []interface{}{} + for _, arg := range compute.Args { + args = append(args, arg.Value(data)) + } + return args +} + +// Value compute arg value +func (arg CArg) Value(data maps.MapStr) interface{} { + if !arg.IsExp { + return arg.value + } + return data.Get(arg.key) +} + +// MarshalJSON Custom JSON parse +func (compute Compute) MarshalJSON() ([]byte, error) { + + if compute.Args == nil || len(compute.Args) == 0 || reflect.DeepEqual(compute.Args, defaults) { + return jsoniter.Marshal(compute.Process) + } + + return jsoniter.Marshal(computeAlias(compute)) +} + +// UnmarshalJSON Custom JSON parse +func (compute *Compute) UnmarshalJSON(data []byte) error { + + // allow null + if data == nil || len(data) < 1 || (len(data) == 2 && data[0] == '"' && data[1] == '"') { + *compute = Compute{Args: []CArg{}} + return fmt.Errorf("Compute should be {} or string") + } + + switch data[0] { + + case '[': + *compute = Compute{Args: []CArg{}} + return fmt.Errorf("Compute should be {} or string") + + case '{': // json + var new computeAlias + err := jsoniter.Unmarshal(data, &new) + if err != nil { + return err + } + new.Process = strings.TrimSpace(new.Process) + *compute = Compute(new) + return nil + + default: + compute.Process = strings.TrimSpace((strings.Trim(string(data), `"`))) + compute.Args = defaults + return nil + } +} + +// MarshalJSON for JSON parse +func (arg CArg) MarshalJSON() ([]byte, error) { + if arg.IsExp { + return []byte(fmt.Sprintf(`"$C(%s)"`, arg.key)), nil + } + + if v, ok := arg.value.(string); ok && strings.HasPrefix(v, "::") { + return jsoniter.Marshal(fmt.Sprintf("\\%s", v)) + } + + return jsoniter.Marshal(arg.value) +} + +// UnmarshalJSON for JSON parse +func (arg *CArg) UnmarshalJSON(data []byte) error { + + if data == nil || len(data) < 1 { + *arg = CArg{value: nil, IsExp: false} + return nil + } + + // "$C(value)", "$C(props)", "$C(type)"} + if len(data) > 3 && data[0] == '"' && data[1] == '$' && data[2] == 'C' && data[3] == '(' { + key := strings.TrimSpace(strings.TrimRight(strings.TrimLeft(string(data), `"$C(`), `)"`)) + *arg = CArg{key: key, IsExp: true} + return nil + + } else if len(data) > 4 && data[0] == '"' && data[1] == '\\' && data[2] == '\\' && data[3] == ':' && data[4] == ':' { + + // ["$C(row.type)", "\\::", "$C(value)", "-", "$C(row.status)"] + value := string(data[3 : len(data)-1]) + *arg = CArg{value: value, IsExp: false} + return nil + } + + var v interface{} + err := jsoniter.Unmarshal(data, &v) + if err != nil { + return err + } + *arg = CArg{value: v, IsExp: false} + return nil +} diff --git a/widgets/component/compute_test.go b/widgets/component/compute_test.go new file mode 100644 index 00000000..da349e05 --- /dev/null +++ b/widgets/component/compute_test.go @@ -0,0 +1,190 @@ +package component + +import ( + "testing" + + jsoniter "github.com/json-iterator/go" + "github.com/stretchr/testify/assert" + "github.com/yaoapp/gou/session" + "github.com/yaoapp/kun/maps" +) + +func TestComputeUnmarshalJSON(t *testing.T) { + + tests := testComputeData() + + var compute Compute + err := jsoniter.Unmarshal(tests["Trim"], &compute) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "Trim", compute.Process) + assert.Equal(t, true, compute.Args[0].IsExp) + assert.Equal(t, "value", compute.Args[0].key) + assert.Equal(t, nil, compute.Args[0].value) + assert.Equal(t, true, compute.Args[1].IsExp) + assert.Equal(t, "props", compute.Args[1].key) + assert.Equal(t, nil, compute.Args[1].value) + assert.Equal(t, true, compute.Args[2].IsExp) + assert.Equal(t, "type", compute.Args[2].key) + assert.Equal(t, nil, compute.Args[2].value) + + err = jsoniter.Unmarshal(tests["Concat"], &compute) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "Concat", compute.Process) + assert.Equal(t, false, compute.Args[1].IsExp) + assert.Equal(t, "::", compute.Args[1].value) + assert.Equal(t, "", compute.Args[1].key) + + err = jsoniter.Unmarshal(tests["Mapping"], &compute) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "Mapping", compute.Process) + assert.Equal(t, false, compute.Args[1].IsExp) + assert.Equal(t, "checked", compute.Args[1].value.(map[string]interface{})["0"]) + assert.Equal(t, "", compute.Args[1].key) + + err = jsoniter.Unmarshal(tests["MappingOnline"], &compute) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "scripts.compute.MappingOnline", compute.Process) + + err = jsoniter.Unmarshal(tests["Empty"], &compute) + assert.NotNil(t, err) + assert.Equal(t, "", compute.Process) + + err = jsoniter.Unmarshal(tests["Error"], &compute) + assert.NotNil(t, err) + assert.Equal(t, "", compute.Process) +} + +func TestComputeMarshalJSON(t *testing.T) { + + tests := testComputeData() + + var compute Compute + err := jsoniter.Unmarshal(tests["Trim"], &compute) + if err != nil { + t.Fatal(err) + } + bytes, err := jsoniter.Marshal(compute) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, tests["Trim"], bytes) + + err = jsoniter.Unmarshal(tests["Concat"], &compute) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "Concat", compute.Process) + bytes, err = jsoniter.Marshal(compute) + if err != nil { + t.Fatal(err) + } + assert.Contains(t, string(bytes), `Concat`) + assert.Contains(t, string(bytes), `$C(value)`) + assert.Contains(t, string(bytes), `\\::`) + + err = jsoniter.Unmarshal(tests["Mapping"], &compute) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "Mapping", compute.Process) + bytes, err = jsoniter.Marshal(compute) + if err != nil { + t.Fatal(err) + } + assert.Contains(t, string(bytes), `Mapping`) + assert.Contains(t, string(bytes), `curing`) + + err = jsoniter.Unmarshal(tests["Empty"], &compute) + assert.NotNil(t, err) + assert.Equal(t, "", compute.Process) + bytes, err = jsoniter.Marshal(compute) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, `""`, string(bytes)) + + err = jsoniter.Unmarshal(tests["Error"], &compute) + assert.NotNil(t, err) + assert.Equal(t, "", compute.Process) + bytes, err = jsoniter.Marshal(compute) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, `""`, string(bytes)) + +} + +func TestComputeValue(t *testing.T) { + tests := testComputeData() + + data := maps.MapStr{ + "value": " Concat-Test ", + "row.type": "UnitTest", + "row.status": "enabled", + } + + var compute Compute + err := jsoniter.Unmarshal(tests["Concat"], &compute) + if err != nil { + t.Fatal(err) + } + + id := session.ID() + res, err := compute.Value(data, id, nil) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "UnitTest:: Concat-Test -enabled", res) + + err = jsoniter.Unmarshal(tests["Trim"], &compute) + if err != nil { + t.Fatal(err) + } + + res, err = compute.Value(data, id, nil) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "Concat-Test", res) + + compute = Compute{Process: "NotFound", Args: []CArg{}} + res, err = compute.Value(data, id, nil) + assert.Contains(t, err.Error(), "does not found") + assert.Nil(t, res) +} + +func testComputeData() map[string][]byte { + + return map[string][]byte{ + "Trim": []byte(`"Trim"`), + + "Concat": []byte(`{ + "process": "Concat", + "args": ["$C(row.type)", "\\::", "$C(value)", "-", "$C(row.status)"] + }`), + + "Mapping": []byte(` { + "process": "Mapping", + "args": [ + "$C(value)", + { "0": "checked", "1": "curing", "2": "cured" } + ] + }`), + + "MappingOnline": []byte(`{ + "process": "scripts.compute.MappingOnline", + "args": ["$C(value)", "$C(props.mapping)"] + }`), + + "Empty": []byte(`""`), + "Error": []byte("[]"), + } +} diff --git a/widgets/component/handlers.go b/widgets/component/handlers.go new file mode 100644 index 00000000..02f0083d --- /dev/null +++ b/widgets/component/handlers.go @@ -0,0 +1,48 @@ +package component + +import ( + "fmt" + "strings" +) + +var hanlders = map[string]ComputeHanlder{ + "Trim": Trim, + "Concat": Concat, + "QueryString": Trim, + "ImagesView": Trim, + "ImagesEdit": Trim, + "Duration": Trim, + "HumanDataTime": Trim, + "Mapping": Trim, + "Currency": Trim, +} + +// Trim string +func Trim(args ...interface{}) (interface{}, error) { + if len(args) < 1 { + return nil, fmt.Errorf("Trim args[0] is required") + } + + if args[0] == nil { + return "", nil + } + + v, ok := args[0].(string) + if !ok { + return nil, fmt.Errorf("Trim args[0] is not a string value") + } + + return strings.TrimSpace(v), nil +} + +// Concat string +func Concat(args ...interface{}) (interface{}, error) { + res := "" + for _, arg := range args { + if arg == nil { + continue + } + res = fmt.Sprintf("%v%v", res, arg) + } + return res, nil +} diff --git a/widgets/component/types.go b/widgets/component/types.go index a88b88e3..b21a6598 100644 --- a/widgets/component/types.go +++ b/widgets/component/types.go @@ -2,10 +2,12 @@ package component // DSL the component DSL type DSL struct { - Type string `json:"type,omitempty"` - In string `json:"in,omitempty"` - Out string `json:"out,omitempty"` - Props PropsDSL `json:"props,omitempty"` + Bind string `json:"bind,omitempty"` + Type string `json:"type,omitempty"` + In string `json:"in,omitempty"` + Out string `json:"out,omitempty"` + Compute *Compute `json:"compute,omitempty"` + Props PropsDSL `json:"props,omitempty"` } // Actions the actions @@ -44,6 +46,25 @@ type PropsDSL map[string]interface{} // ParamsDSL action params type ParamsDSL map[string]interface{} +// Compute process +type Compute struct { + Process string `json:"process"` + Args []CArg `json:"args,omitempty"` +} + +// computeAlias for JSON UnmarshalJSON +type computeAlias Compute + +// CArg compute interface{} +type CArg struct { + IsExp bool + key string + value interface{} +} + +// ComputeHanlder computeHanlder +type ComputeHanlder func(args ...interface{}) (interface{}, error) + // CloudPropsDSL the cloud props type CloudPropsDSL struct { Xpath string `json:"xpath,omitempty"` diff --git a/widgets/field/column.go b/widgets/field/column.go index 6457f817..fe19ed11 100644 --- a/widgets/field/column.go +++ b/widgets/field/column.go @@ -61,11 +61,13 @@ func (column *ColumnDSL) Clone() *ColumnDSL { // Map cast to map[string]inteface{} func (column ColumnDSL) Map() map[string]interface{} { res := map[string]interface{}{ - "key": column.Key, - "link": column.Link, "bind": column.Bind, } + if column.Link != "" { + res["link"] = column.Link + } + if column.View != nil { res["view"] = column.View.Map() } @@ -109,20 +111,20 @@ func (columns Columns) ComputeFieldsMerge(computeInFields map[string]string, com // Compute In if column.In != "" { - if !strings.Contains(column.In, ".") { - column.In = fmt.Sprintf("yao.component.%s", column.In) + if !strings.Contains(string(column.In), ".") { + column.In = Compute(fmt.Sprintf("yao.component.%s", column.In)) } - computeInFields[column.Bind] = column.In - computeInFields[name] = column.In + computeInFields[column.Bind] = string(column.In) + computeInFields[name] = string(column.In) } // Compute Out if column.Out != "" { - if !strings.Contains(column.Out, ".") { - column.In = fmt.Sprintf("yao.component.%s", column.Out) + if !strings.Contains(string(column.Out), ".") { + column.In = Compute(fmt.Sprintf("yao.component.%s", column.Out)) } - computeOutFields[column.Bind] = column.Out - computeOutFields[name] = column.Out + computeOutFields[column.Bind] = string(column.Out) + computeOutFields[name] = string(column.Out) } } } diff --git a/widgets/field/filter.go b/widgets/field/filter.go index 707798ab..a797f70d 100644 --- a/widgets/field/filter.go +++ b/widgets/field/filter.go @@ -43,7 +43,6 @@ func (filter *FilterDSL) Clone() *FilterDSL { // Map cast to map[string]inteface{} func (filter FilterDSL) Map() map[string]interface{} { res := map[string]interface{}{ - "key": filter.Key, "bind": filter.Bind, } diff --git a/widgets/field/types.go b/widgets/field/types.go index d80e9c8a..b667d7e5 100644 --- a/widgets/field/types.go +++ b/widgets/field/types.go @@ -21,8 +21,8 @@ type ColumnDSL struct { Key string `json:"key,omitempty"` Bind string `json:"bind,omitempty"` Link string `json:"link,omitempty"` - In string `json:"in,omitempty"` - Out string `json:"out,omitempty"` + In Compute `json:"in,omitempty"` + Out Compute `json:"out,omitempty"` View *component.DSL `json:"view,omitempty"` Edit *component.DSL `json:"edit,omitempty"` } @@ -34,6 +34,9 @@ type FilterDSL struct { Edit *component.DSL `json:"edit,omitempty"` } +// Compute the compute filed +type Compute string + // Transform the field transform type Transform struct { Variables map[string]interface{} `json:"variables,omitempty"` diff --git a/widgets/table/fields.go b/widgets/table/fields.go index cddfbad1..6ee72ea8 100644 --- a/widgets/table/fields.go +++ b/widgets/table/fields.go @@ -83,6 +83,7 @@ func (fields *FieldsDSL) Xgen(layout *LayoutDSL) (map[string]interface{}, error) filters := map[string]interface{}{} tables := map[string]interface{}{} + if layout.Filter != nil { for i, f := range layout.Filter.Columns { name := f.Name diff --git a/widgets/table/fields_test.go b/widgets/table/fields_test.go index f0d5ca10..16674f54 100644 --- a/widgets/table/fields_test.go +++ b/widgets/table/fields_test.go @@ -18,6 +18,6 @@ func TestFiledsBindModel(t *testing.T) { t.Fatal(err) } assert.Equal(t, "id", tab.Fields.Table["ID"].Bind) - assert.Equal(t, 15, len(tab.Fields.Table)) - assert.Equal(t, 4, len(tab.Fields.Filter)) + assert.Equal(t, 17, len(tab.Fields.Table)) + assert.Equal(t, 6, len(tab.Fields.Filter)) } diff --git a/widgets/table/table_test.go b/widgets/table/table_test.go index 5a11c2f6..33d83ecf 100644 --- a/widgets/table/table_test.go +++ b/widgets/table/table_test.go @@ -22,7 +22,7 @@ func TestLoad(t *testing.T) { if err != nil { t.Fatal(err) } - assert.Equal(t, 9, len(Tables)) + assert.Equal(t, 10, len(Tables)) } func TestLoadID(t *testing.T) {