[add] compute parser

This commit is contained in:
Max 2022-10-20 22:56:09 +08:00
parent a57d13e49e
commit 6ef7688e65
4 changed files with 415 additions and 5 deletions

View file

@ -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
}

View file

@ -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("[]"),
}
}

View file

@ -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
}

View file

@ -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"`