From 6ef7688e6583ee3d85432990510182d9f59a6292 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 20 Oct 2022 22:56:09 +0800 Subject: [PATCH] [add] compute parser --- widgets/component/compute.go | 152 ++++++++++++++++++++++++ widgets/component/compute_test.go | 190 ++++++++++++++++++++++++++++++ widgets/component/handlers.go | 48 ++++++++ widgets/component/types.go | 30 ++++- 4 files changed, 415 insertions(+), 5 deletions(-) create mode 100644 widgets/component/compute.go create mode 100644 widgets/component/compute_test.go create mode 100644 widgets/component/handlers.go 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 02f3d1eb..b21a6598 100644 --- a/widgets/component/types.go +++ b/widgets/component/types.go @@ -2,11 +2,12 @@ package component // DSL the component DSL type DSL struct { - Bind string `json:"bind,omitempty"` - 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 @@ -45,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"`