From ef904e0cd3ef72ad5aac4d5ddb8a21cf9dac0254 Mon Sep 17 00:00:00 2001 From: Vivek Patel Date: Wed, 16 Feb 2022 19:33:31 +0530 Subject: [PATCH] Extend set/omit expr function It updates 1st parameter of set expr function from merger type to interface, so It will accept constant values, also extend set/omit usage for ComposeRecordValues. --- compose/automation/expr_types.go | 102 ++++++++++++++++++ compose/automation/expr_types_test.go | 89 +++++++++++++++ compose/types/record_value.go | 4 +- pkg/expr/expr_types.go | 15 ++- pkg/expr/vars.go | 8 +- .../0016_set_expression_issue_test.go | 68 +++++++++++- .../S0016_set_expression_issue/workflow.yaml | 93 +++++++++++++++- 7 files changed, 367 insertions(+), 12 deletions(-) diff --git a/compose/automation/expr_types.go b/compose/automation/expr_types.go index 3ef25a702..51a50641e 100644 --- a/compose/automation/expr_types.go +++ b/compose/automation/expr_types.go @@ -518,3 +518,105 @@ func CastToAttachment(val interface{}) (out *types.Attachment, err error) { return nil, fmt.Errorf("unable to cast type %T to %T", val, out) } } + +func EmptyComposeRecordValues() *ComposeRecordValues { + return &ComposeRecordValues{value: &types.Record{Values: types.RecordValueSet{}}} +} + +func (t *ComposeRecordValues) Each(fn func(k string, v expr.TypedValue) error) (err error) { + if t.IsEmpty() { + return + } + + for _, vv := range t.GetValue() { + key := vv.Name + if len(key) == 0 { + continue + } + + var val expr.TypedValue + val, err = expr.Typify(vv) + if err = fn(key, val); err != nil { + return + } + } + + return +} + +// Merge combines the given ComposeRecordValues into ComposeRecordValues +// NOTE: It will return CLONE of the original ComposeRecordValues, if it's called without any parameters +func (t *ComposeRecordValues) Merge(nn ...expr.Iterator) (out expr.TypedValue, err error) { + rv := EmptyComposeRecordValues() + + nn = append([]expr.Iterator{t}, nn...) + + for _, i := range nn { + err = i.Each(func(k string, v expr.TypedValue) error { + var ( + rVal types.RecordValue + bb []byte + ) + + // @todo implement casting of RecordValue from TypedValue + bb, err = json.Marshal(v.Get()) + if err != nil { + return err + } + + err = json.Unmarshal(bb, &rVal) + if err != nil { + return err + } + + rr := rv.GetValue().Set(&rVal) + err = rv.Assign(rr) + if err != nil { + return err + } + + return err + }) + if err != nil { + return + } + } + + return rv, nil +} + +func (t *ComposeRecordValues) Delete(keys ...string) (out expr.TypedValue, err error) { + if t.IsEmpty() { + return + } + + // get cloned ComposeRecordValues + out, err = t.Merge() + if err != nil { + return + } + + rv := out.(*ComposeRecordValues) + if !rv.IsEmpty() { + rv.value.Values = types.RecordValueSet{} + } + + keyMap := make(map[string]string) + for _, k := range keys { + keyMap[k] = k + } + + // Push the only with values with non-matching Name + for _, val := range t.GetValue() { + _, ok := keyMap[val.Name] + if !ok { + rr := rv.GetValue().Set(val.Clone()) + err = rv.Assign(rr) + if err != nil { + return out, err + } + } + } + + return rv, nil +} diff --git a/compose/automation/expr_types_test.go b/compose/automation/expr_types_test.go index 96563b83a..bbd73365d 100644 --- a/compose/automation/expr_types_test.go +++ b/compose/automation/expr_types_test.go @@ -580,3 +580,92 @@ func TestGvalStringFunctionParamsIntCasting(t *testing.T) { }) } + +func TestRecordValues_Merge(t *testing.T) { + var ( + req = require.New(t) + + rv = &ComposeRecordValues{} + foo = &ComposeRecordValues{ + value: &types.Record{ + Values: []*types.RecordValue{ + { + Name: "k1", + Value: "testValue1", + }, + { + Name: "k2", + Value: "testValue2", + }, + }, + }, + } + bar = &ComposeRecordValues{ + value: &types.Record{ + Values: []*types.RecordValue{ + { + Name: "k3", + Value: "testValue3", + }, + }, + }, + } + expected = &types.Record{ + Values: []*types.RecordValue{ + { + Name: "k1", + Value: "testValue1", + }, + { + Name: "k2", + Value: "testValue2", + }, + { + Name: "k3", + Value: "testValue3", + }, + }, + } + ) + + out, err := rv.Merge(foo, bar) + req.NoError(err) + req.Equal(expected, out.Get()) +} + +func TestRecordValues_Omit(t *testing.T) { + var ( + req = require.New(t) + + rv = ComposeRecordValues{ + value: &types.Record{ + Values: []*types.RecordValue{ + { + Name: "k1", + Value: "testValue1", + }, + { + Name: "k2", + Value: "testValue2", + }, + { + Name: "k3", + Value: "testValue3", + }, + }, + }, + } + expected = &types.Record{ + Values: []*types.RecordValue{ + { + Name: "k2", + Value: "testValue2", + }, + }, + } + ) + + out, err := rv.Delete("k1", "k3") + req.NoError(err) + req.Equal(expected, out.Get()) +} diff --git a/compose/types/record_value.go b/compose/types/record_value.go index e03e0f62f..ca237e20a 100644 --- a/compose/types/record_value.go +++ b/compose/types/record_value.go @@ -120,7 +120,7 @@ func (set RecordValueSet) FilterByRecordID(recordID uint64) (vv RecordValueSet) return } -// Replaces existing values, remove extra +// Replace existing values, remove extra func (set RecordValueSet) Replace(name string, values ...string) (vv RecordValueSet) { for i := range set { if set[i].Name != name { @@ -160,7 +160,7 @@ func (set RecordValueSet) Set(v *RecordValue) RecordValueSet { return append(set, v) } -// Has value set? +// Get value set? func (set RecordValueSet) Get(name string, place uint) *RecordValue { for i := range set { if set[i].Name != name { diff --git a/pkg/expr/expr_types.go b/pkg/expr/expr_types.go index 7d9e939dc..fae386412 100644 --- a/pkg/expr/expr_types.go +++ b/pkg/expr/expr_types.go @@ -64,7 +64,20 @@ func ResolveTypes(rt resolvableType, resolver func(typ string) Type) error { return rt.ResolveTypes(resolver) } -func set(m merger, key string, val interface{}) (out TypedValue, err error) { +func set(i interface{}, key string, val interface{}) (out TypedValue, err error) { + m, ok := i.(merger) + if !ok { + i, err = Typify(i) + if err != nil { + return + } + + m, ok = i.(merger) + if !ok { + return out, fmt.Errorf("cannot set on unexpected type: %T", i) + } + } + out, err = m.Merge() if err != nil { return diff --git a/pkg/expr/vars.go b/pkg/expr/vars.go index a6c13f6a9..9d9a247be 100644 --- a/pkg/expr/vars.go +++ b/pkg/expr/vars.go @@ -75,7 +75,7 @@ func (t *Vars) ResolveTypes(res func(typ string) Type) (err error) { } // Merge combines the given Vars(es) into Vars -// NOTE: It will return CLONE of the original Vars, if its called without any parameters +// NOTE: It will return CLONE of the original Vars, if it's called without any parameters func (t *Vars) Merge(nn ...Iterator) (out TypedValue, err error) { return t.MustMerge(nn...), nil } @@ -336,7 +336,7 @@ func (t *Vars) Each(fn func(k string, v TypedValue) error) (err error) { return } -// Set set/update the specific key value in KV +// Set or update the specific key value in Vars func (t *Vars) Set(k string, v interface{}) (err error) { t.mux.RLock() defer t.mux.RUnlock() @@ -485,7 +485,7 @@ func CastToVars(val interface{}) (out map[string]TypedValue, err error) { return nil, fmt.Errorf("unable to cast type %T to %T", val, out) } -// Filter take keys returns KV with only those key value pair +// Filter take keys returns Vars with only those key value pair func (t *Vars) Filter(keys ...string) (out TypedValue, err error) { t.mux.RLock() defer t.mux.RUnlock() @@ -505,7 +505,7 @@ func (t *Vars) Filter(keys ...string) (out TypedValue, err error) { return vars, nil } -// Delete take keys returns KV without those key value pair +// Delete take keys returns Vars without those key value pair func (t *Vars) Delete(keys ...string) (out TypedValue, err error) { t.mux.RLock() defer t.mux.RUnlock() diff --git a/tests/workflows/0016_set_expression_issue_test.go b/tests/workflows/0016_set_expression_issue_test.go index 9f45a8cbe..a3dd73084 100644 --- a/tests/workflows/0016_set_expression_issue_test.go +++ b/tests/workflows/0016_set_expression_issue_test.go @@ -2,6 +2,7 @@ package workflows import ( "context" + cmpTypes "github.com/cortezaproject/corteza-server/compose/types" "github.com/cortezaproject/corteza-server/pkg/expr" "github.com/stretchr/testify/require" "testing" @@ -18,8 +19,16 @@ func Test0016_set_expression_issue(t *testing.T) { loadScenario(ctx, t) t.Run("set expressions", func(t *testing.T) { + type ( + testInput struct { + Out map[string]expr.TypedValue + OutConstStr map[string]expr.TypedValue + OutConstInt map[string]expr.TypedValue + OutRv *cmpTypes.Record + } + ) var ( - aux = struct{ Out map[string]expr.TypedValue }{} + aux = testInput{} vars, _ = mustExecWorkflow(ctx, t, "set_expression", types.WorkflowExecParams{}) testString = expr.Must(expr.NewString("testing string")) @@ -28,14 +37,67 @@ func Test0016_set_expression_issue(t *testing.T) { "testString": testString, "testFloat": expr.Must(expr.NewFloat(50)), })) - expected = map[string]expr.TypedValue{ + + expectedVars = map[string]expr.TypedValue{ "testString": testString, "testInt": testInt, "testVar": testVar, } + expected = testInput{ + Out: expectedVars, + OutConstStr: map[string]expr.TypedValue{ + "testConstKey": expr.Must(expr.NewString("testConstValue")), + }, + OutConstInt: map[string]expr.TypedValue{ + "testInt": testInt, + }, + + OutRv: &cmpTypes.Record{ + Values: []*cmpTypes.RecordValue{ + { + Name: "testRv", + Value: "testing string", + }, + { + Name: "testFloat", + Value: "50", + }, + }, + }, + } ) req.NoError(vars.Decode(&aux)) - req.Equal(expected, aux.Out) + req.Equal(expected, aux) + }) + + t.Run("omit expressions", func(t *testing.T) { + type ( + testInput struct { + Out map[string]expr.TypedValue + OutConstInt map[string]expr.TypedValue + OutRv *cmpTypes.Record + } + ) + var ( + aux = testInput{} + vars, _ = mustExecWorkflow(ctx, t, "omit_expression", types.WorkflowExecParams{}) + + expected = testInput{ + Out: map[string]expr.TypedValue{}, + OutConstInt: map[string]expr.TypedValue{}, + OutRv: &cmpTypes.Record{ + Values: []*cmpTypes.RecordValue{ + { + Name: "testFloat", + Value: "50", + }, + }, + }, + } + ) + + req.NoError(vars.Decode(&aux)) + req.Equal(expected, aux) }) } diff --git a/tests/workflows/testdata/S0016_set_expression_issue/workflow.yaml b/tests/workflows/testdata/S0016_set_expression_issue/workflow.yaml index 932ad63db..49d065a18 100644 --- a/tests/workflows/testdata/S0016_set_expression_issue/workflow.yaml +++ b/tests/workflows/testdata/S0016_set_expression_issue/workflow.yaml @@ -10,16 +10,105 @@ workflows: - stepID: 1 kind: expressions arguments: [ + ## Initialize empty vars { target: aux, type: Vars, value: null }, + + ## Initialize key and value(string) for set expression { target: testString, type: String, value: "testString" }, { target: foo, type: String, value: "testing string" }, + + # Evaluate set for Vars(1st argument) with string value(3rd argument); + # it will set string value type for given key to Vars(out) + { target: out, type: Vars, expr: "set(aux, testString, foo)" }, + + ## Initialize key and value(int) for set expression { target: testInt, type: String, value: "testInt" }, { target: bar, type: Integer, expr: "40" }, + + # Evaluate set for Vars(1st argument) with int value(3rd argument); + # it will set int value type for given key to Vars(out) + { target: out, type: Vars, expr: "set(out, testInt, bar)" }, + + ## Initialize key and value(Vars) for set expression { target: testVar, type: String, value: "testVar" }, { target: foobar, type: Vars, expr: "{\"testString\": \"testing string\", \"testFloat\": 50}" }, - { target: out, type: Vars, expr: "set(aux, testString, foo)" }, - { target: out, type: Vars, expr: "set(out, testInt, bar)" }, + + # Evaluate set for Vars(1st argument) with Vars value(3rd argument); + # it will set Vars value type for given key to Vars(out) { target: out, type: Vars, expr: "set(out, testVar, foobar)" }, + + ## Evaluate set for constant: `{}`(1st argument) with string value(3rd argument); + # it will set string value type for given key to Vars(outConstStr) + { target: outConstStr, type: Vars, expr: "set({}, \"testConstKey\", \"testConstValue\")" }, + + ## Evaluate set for constant value(1st argument) with int value(3rd argument); + # it will set int value type for given key to Vars(outConstInt) + { target: outConstInt, type: Vars, expr: "set({}, testInt, bar)" }, + + ## Initialize rv(ComposeRecordValues) and key for set expression + { target: rv, type: ComposeRecordValues, expr: "{}" }, + { target: testRv, type: String, value: "testRv" }, + + # Evaluate set for ComposeRecordValues(1st argument) with string value(3rd argument); + # it will set string value for given key(Name) to ComposeRecordValues(outRv) + { target: outRv, type: ComposeRecordValues, expr: "set(rv, testRv, foo)" }, + + # Evaluate set for ComposeRecordValues(1st argument) with float value(3rd argument); + # it will set float value for given key(Name) to ComposeRecordValues(outRv) + { target: outRv, type: ComposeRecordValues, expr: "set(outRv, \"testFloat\", 50)" }, + ] + - stepID: 2 + kind: termination + + paths: + - { parentID: 1, childID: 2 } + + omit_expression: + enabled: true + trace: true + triggers: + - enabled: true + stepID: 1 + + steps: + - stepID: 1 + kind: expressions + arguments: [ + # Initialize empty vars + { target: aux, type: Vars, value: null }, + + ## Initialize key and value(string) for set/omit expressions + { target: testString, type: String, value: "testString" }, + { target: foo, type: String, value: "testing string" }, + + # Set values into Vars + { target: out, type: Vars, expr: "set(aux, testString, foo)" }, + + # Evaluate omit for Vars(1st argument) for keys; + # it will omit values matching keys from Vars(outConstInt) + { target: out, type: Vars, expr: "omit(out, testString)" }, + + ## Set/Omit value into constant + { target: testInt, type: String, value: "testInt" }, + { target: bar, type: Integer, expr: "40" }, + { target: outConstInt, type: Vars, expr: "set({}, testInt, bar)" }, + + # Evaluate omit for Vars(1st argument) for keys; + # it will omit values matching keys from Vars(outConstInt) + { target: outConstInt, type: Vars, expr: "omit(outConstInt, testInt)" }, + + ## Initialize rv(ComposeRecordValues) and key for set/omit expressions + { target: rv, type: ComposeRecordValues, expr: "{}" }, + { target: testRv, type: String, value: "testRv" }, + + # Set values into ComposeRecordValues + { target: outRv, type: ComposeRecordValues, expr: "set(rv, testRv, foo)" }, + { target: outRv, type: ComposeRecordValues, expr: "set(rv, \"removeMe\", \"I will be removed\")" }, + { target: outRv, type: ComposeRecordValues, expr: "set(outRv, \"testFloat\", 50)" }, + + # Evaluate omit for ComposeRecordValues(1st argument) for key(Name); + # it will omit values matching Name from ComposeRecordValues(outRv) + { target: outRv, type: ComposeRecordValues, expr: "omit(outRv, \"testRv\", \"removeMe\")" }, ] - stepID: 2 kind: termination