From 044d02bb768802b2c98ec4327d58f82dba215d22 Mon Sep 17 00:00:00 2001 From: Vivek Patel Date: Wed, 12 May 2021 06:32:30 +0530 Subject: [PATCH] Adds expr functions for KV, KVV, Vars manipulation - Adds/Refactors methods for Set, Merge, Filter, Delete - Renames expr/Vars.Merge to MustMerge, updates its usage - Appends kvFunctions to parser in pkg/expr - Update/Fixes tests and example testable --- automation/service/workflow.go | 10 +- automation/service/workflow_converter.go | 2 +- automation/types/expr.go | 6 +- automation/types/function.go | 4 +- pkg/expr/expr_types.go | 229 +++++++++++++- pkg/expr/expr_types_test.go | 372 ++++++++++++++++++++++- pkg/expr/func_json.go | 18 ++ pkg/expr/func_json_test.go | 19 ++ pkg/expr/func_str.go | 2 +- pkg/expr/parser.go | 2 + pkg/expr/vars.go | 73 ++++- pkg/expr/vars_test.go | 179 +++++++++++ pkg/wfexec/gateways.go | 2 +- pkg/wfexec/session.go | 6 +- 14 files changed, 896 insertions(+), 28 deletions(-) create mode 100644 pkg/expr/func_json.go create mode 100644 pkg/expr/func_json_test.go diff --git a/automation/service/workflow.go b/automation/service/workflow.go index 21618cef3..dcd47688b 100644 --- a/automation/service/workflow.go +++ b/automation/service/workflow.go @@ -538,7 +538,7 @@ func (svc *workflow) Exec(ctx context.Context, workflowID uint64, p types.Workfl }() // Start with workflow scope - scope := wf.Scope.Merge() + scope := wf.Scope.MustMerge() ssp := types.SessionStartParams{ WorkflowID: wf.ID, @@ -559,7 +559,7 @@ func (svc *workflow) Exec(ctx context.Context, workflowID uint64, p types.Workfl wap.setTrigger(t) // Add trigger's input to scope - scope = scope.Merge(t.Input) + scope = scope.MustMerge(t.Input) _ = scope.AssignFieldValue("eventType", expr.Must(expr.NewString(t.EventType))) _ = scope.AssignFieldValue("resourceType", expr.Must(expr.NewString(t.ResourceType))) @@ -572,7 +572,7 @@ func (svc *workflow) Exec(ctx context.Context, workflowID uint64, p types.Workfl } // Finally, assign input values - ssp.Input = scope.Merge(p.Input) + ssp.Input = scope.MustMerge(p.Input) if wf.RunAs > 0 { if runAs, err = DefaultUser.FindByID(ctx, wf.RunAs); err != nil { @@ -617,7 +617,7 @@ func makeWorkflowHandler(ac workflowExecController, s *session, t *types.Trigger var ( // create session scope from predefined workflow scope and trigger input - scope = wf.Scope.Merge(t.Input) + scope = wf.Scope.MustMerge(t.Input) evScope *expr.Vars wait WaitFn ) @@ -627,7 +627,7 @@ func makeWorkflowHandler(ac workflowExecController, s *session, t *types.Trigger return } - scope = scope.Merge(evScope) + scope = scope.MustMerge(evScope) } _ = scope.AssignFieldValue("eventType", expr.Must(expr.NewString(ev.EventType()))) diff --git a/automation/service/workflow_converter.go b/automation/service/workflow_converter.go index 31ec1e334..e20c91a07 100644 --- a/automation/service/workflow_converter.go +++ b/automation/service/workflow_converter.go @@ -450,7 +450,7 @@ func (svc workflowConverter) convPromptStep(s *types.WorkflowStep) (wfexec.Step, return wfexec.Prompt(ownerId, s.Ref, payload), nil } - results, err := res.Eval(ctx, r.Scope.Merge(r.Input)) + results, err := res.Eval(ctx, r.Scope.MustMerge(r.Input)) if err != nil { return nil, err } diff --git a/automation/types/expr.go b/automation/types/expr.go index 1af59a892..007508aab 100644 --- a/automation/types/expr.go +++ b/automation/types/expr.go @@ -87,7 +87,7 @@ func (set ExprSet) Validate(ctx context.Context, in *expr.Vars) (TestSet, error) err error // Copy/create scope - scope = (&expr.Vars{}).Merge(in) + scope = (&expr.Vars{}).MustMerge(in) ) for _, e := range set { @@ -108,7 +108,7 @@ func (set ExprSet) Eval(ctx context.Context, in *expr.Vars) (*expr.Vars, error) err error // Copy input to scope - scope = (&expr.Vars{}).Merge(in) + scope = (&expr.Vars{}).MustMerge(in) // Prepare output scope out, _ = expr.NewVars(nil) @@ -223,7 +223,7 @@ func ExpressionsStep(ee ...*Expr) *expressionsStep { } func (s expressionsStep) Exec(ctx context.Context, r *wfexec.ExecRequest) (wfexec.ExecResponse, error) { - result, err := s.Set.Eval(ctx, r.Scope.Merge(r.Input)) + result, err := s.Set.Eval(ctx, r.Scope.MustMerge(r.Input)) if err != nil { return nil, err } diff --git a/automation/types/function.go b/automation/types/function.go index ec82f9c61..5489245d4 100644 --- a/automation/types/function.go +++ b/automation/types/function.go @@ -98,7 +98,7 @@ func (f functionStep) Exec(ctx context.Context, r *wfexec.ExecRequest) (wfexec.E if len(f.arguments) > 0 { // Arguments defined, get values from scope and use them when calling // function/handler - args, err = f.arguments.Eval(ctx, r.Scope.Merge(r.Input)) + args, err = f.arguments.Eval(ctx, r.Scope.MustMerge(r.Input)) if err != nil { return nil, err } @@ -171,7 +171,7 @@ func (f *iteratorStep) Exec(ctx context.Context, r *wfexec.ExecRequest) (wfexec. if len(f.arguments) > 0 { // Arguments defined, get values from scope and use them when calling // iterator/handler - args, err = f.arguments.Eval(ctx, r.Scope.Merge(r.Input)) + args, err = f.arguments.Eval(ctx, r.Scope.MustMerge(r.Input)) if err != nil { return nil, err } diff --git a/pkg/expr/expr_types.go b/pkg/expr/expr_types.go index 560465f43..01b76a447 100644 --- a/pkg/expr/expr_types.go +++ b/pkg/expr/expr_types.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/PaesslerAG/gval" "io" "io/ioutil" "net/http" @@ -23,16 +24,71 @@ type ( Type ResolveTypes(func(string) Type) error } + + merger interface { + Merge(...Iterator) (TypedValue, error) + } + + filterer interface { + Filter(...string) (TypedValue, error) + } + + deleter interface { + Delete(...string) (TypedValue, error) + } ) +func KvFunctions() []gval.Language { + return []gval.Language{ + gval.Function("set", set), + gval.Function("merge", merge), + gval.Function("filter", filter), + gval.Function("omit", omit), + } +} + +func EmptyKV() *KV { + return &KV{value: make(map[string]string)} +} + +func EmptyKVV() *KVV { + return &KVV{value: make(map[string][]string)} +} + func EmptyVars() *Vars { - return &Vars{value: map[string]TypedValue{}} + return &Vars{value: make(map[string]TypedValue)} } func ResolveTypes(rt resolvableType, resolver func(typ string) Type) error { return rt.ResolveTypes(resolver) } +func set(m merger, key string, val TypedValue) (out TypedValue, err error) { + out, err = m.Merge() + if err != nil { + return + } + + err = Assign(out, key, val) + if err != nil { + return + } + + return +} + +func merge(m merger, nn ...Iterator) (TypedValue, error) { + return m.Merge(nn...) +} + +func filter(f filterer, ss ...string) (TypedValue, error) { + return f.Filter(ss...) +} + +func omit(d deleter, ss ...string) (TypedValue, error) { + return d.Delete(ss...) +} + // Typify detects input type and wraps it with expression type func Typify(in interface{}) (tv TypedValue, err error) { var is bool @@ -301,7 +357,7 @@ func CastToString(val interface{}) (out string, err error) { } } -func CastStringSlice(val interface{}) (out []string, err error) { +func CastToStringSlice(val interface{}) (out []string, err error) { return cast.ToStringSliceE(UntypedValue(val)) } @@ -404,6 +460,86 @@ func CastToKV(val interface{}) (out map[string]string, err error) { } } +func (t *KV) Each(fn func(k string, v TypedValue) error) (err error) { + if t == nil || t.value == nil { + return + } + + for k, v := range t.value { + var val TypedValue + val, err = Typify(v) + if err != nil { + return err + } + + if err = fn(k, val); err != nil { + return + } + } + + return +} + +// Merge combines the given KVs into KV +// NOTE: It will return CLONE of the original KV, if its called without any parameters +func (t *KV) Merge(nn ...Iterator) (out TypedValue, err error) { + kv := EmptyKV() + + nn = append([]Iterator{t}, nn...) + + for _, i := range nn { + err = i.Each(func(k string, v TypedValue) error { + str, err := cast.ToStringE(v.Get()) + kv.value[k] = str + return err + }) + if err != nil { + return + } + } + + return kv, nil +} + +// Filter take keys returns KV with only those key value pair +func (t *KV) Filter(keys ...string) (out TypedValue, err error) { + if t.value == nil { + return + } + kv := EmptyKV() + + for _, k := range keys { + _, has := t.value[k] + if has { + kv.value[k] = t.value[k] + } + } + + return kv, nil +} + +// Delete take keys returns KV without those key value pair +func (t *KV) Delete(keys ...string) (out TypedValue, err error) { + if t.value == nil { + return + } + + // get cloned KV + out, err = t.Merge() + if err != nil { + return + } + + kv := out.(*KV) + + // Delete key from out.value if exist + for _, k := range keys { + delete(kv.value, k) + } + + return kv, nil +} + func (t *KVV) AssignFieldValue(key string, val TypedValue) error { return assignToKVV(t, key, val) } @@ -449,3 +585,92 @@ func CastToReader(val interface{}) (out io.Reader, err error) { return nil, fmt.Errorf("unable to cast %T to io.Reader", val) } } + +func (t *KVV) Each(fn func(k string, v TypedValue) error) (err error) { + if t == nil || t.value == nil { + return + } + + for k, v := range t.value { + var val TypedValue + val, err = Typify(v) + if err != nil { + return err + } + + if err = fn(k, val); err != nil { + return + } + } + + return +} + +// Merge combines the given KVVs into KVV +// NOTE: It will return CLONE of the original KVV, if its called without any parameters +func (t *KVV) Merge(nn ...Iterator) (out TypedValue, err error) { + kvv := EmptyKVV() + + nn = append([]Iterator{t}, nn...) + + for _, i := range nn { + err = i.Each(func(k string, v TypedValue) error { + ss, err := cast.ToStringSliceE(v.Get()) + if err != nil { + return err + } + + _, has := kvv.value[k] + if has { + kvv.value[k] = append(kvv.value[k], ss...) + } else { + kvv.value[k] = ss + } + return nil + }) + if err != nil { + return + } + } + + return kvv, nil +} + +// Filter take keys returns KVV with only those key value pair +func (t *KVV) Filter(keys ...string) (out TypedValue, err error) { + if t.value == nil { + return + } + kvv := EmptyKVV() + + for _, k := range keys { + _, has := t.value[k] + if has { + kvv.value[k] = t.value[k] + } + } + + return kvv, nil +} + +// Delete take keys returns KVV without those key value pair +func (t *KVV) Delete(keys ...string) (out TypedValue, err error) { + if t.value == nil { + return + } + + // get cloned KVV + out, err = t.Merge() + if err != nil { + return + } + + kvv := out.(*KVV) + + // Delete key from t.value if exist + for _, k := range keys { + delete(kvv.value, k) + } + + return kvv, nil +} diff --git a/pkg/expr/expr_types_test.go b/pkg/expr/expr_types_test.go index 37f31b9b7..b92a0d03c 100644 --- a/pkg/expr/expr_types_test.go +++ b/pkg/expr/expr_types_test.go @@ -2,12 +2,180 @@ package expr import ( "context" + "fmt" "github.com/stretchr/testify/require" "net/http" "net/url" "testing" ) +func Example_set_kv() { + var ( + kv = &KV{} + p = map[string]interface{}{ + "kv": kv, + "key": "k1", + "value": Must(NewString("v11")), + } + ) + + eval(`set(kv, key, value)`, p) + fmt.Printf("\nOriginal KV should be %v", kv) + + // output: + // &{value:map[k1:v11]} + // Original KV should be &{map[]} +} + +func Example_merge_kv() { + var ( + kv = &KV{} + p = map[string]interface{}{ + "kv": kv, + "foo": &KV{value: map[string]string{ + "k1": "v1", + }}, + "bar": &KV{value: map[string]string{ + "k2": "v2", + }}, + } + ) + + eval(`merge(kv, foo, bar)`, p) + fmt.Printf("\nOriginal KV should be %v", kv) + + // output: + // &{value:map[k1:v1 k2:v2]} + // Original KV should be &{map[]} +} + +func Example_filter_kv() { + var ( + kv = &KV{value: map[string]string{ + "k1": "v1", + "k2": "v2", + }} + p = map[string]interface{}{ + "kv": kv, + "key1": "k1", + "key2": "k3", + } + ) + + eval(`filter(kv, key1, key2)`, p) + fmt.Printf("\nOriginal KV should be %v", kv) + + // output: + // &{value:map[k1:v1]} + // Original KV should be &{map[k1:v1 k2:v2]} +} + +func Example_omit_kv() { + var ( + kv = &KV{value: map[string]string{ + "k1": "v1", + "k2": "v2", + "k3": "v3", + }} + p = map[string]interface{}{ + "kv": kv, + "key1": "k1", + "key2": "k3", + } + ) + + eval(`omit(kv, key1, key2)`, p) + fmt.Printf("\nOriginal KV should be %v", kv) + + // output: + // &{value:map[k2:v2]} + // Original KV should be &{map[k1:v1 k2:v2 k3:v3]} +} + +func Example_set_kvv() { + var ( + kvv = &KVV{} + p = map[string]interface{}{ + "kvv": kvv, + "key": "foo", + "value": Must(NewString("bar")), + } + ) + + eval(`set(kvv, key, value)`, p) + fmt.Printf("\nOriginal KVV should be %v", kvv) + + // output: + // &{value:map[foo:[bar]]} + // Original KVV should be &{map[]} +} + +func Example_merge_kvv() { + var ( + kvv = &KVV{} + p = map[string]interface{}{ + "kvv": kvv, + "foo": &KVV{value: map[string][]string{ + "k1": {"v1"}, + }}, + "bar": &KVV{value: map[string][]string{ + "k1": {"v11"}, + "k2": {"v2"}, + }}, + } + ) + + eval(`merge(kvv, foo, bar)`, p) + fmt.Printf("\nOriginal KVV should be %v", kvv) + + // output: + // &{value:map[k1:[v1 v11] k2:[v2]]} + // Original KVV should be &{map[]} +} + +func Example_filter_kvv() { + var ( + kvv = &KVV{value: map[string][]string{ + "k1": {"v1"}, + "k2": {"v2"}, + }} + p = map[string]interface{}{ + "kv": kvv, + "key1": "k1", + "key2": "k3", + } + ) + + eval(`filter(kv, key1, key2)`, p) + fmt.Printf("\nOriginal KVV should be %v", kvv) + + // output: + // &{value:map[k1:[v1]]} + // Original KVV should be &{map[k1:[v1] k2:[v2]]} +} + +func Example_omit_kvv() { + var ( + kvv = &KVV{value: map[string][]string{ + "k1": {"v1"}, + "k2": {"v2"}, + "k3": {"v3"}, + }} + p = map[string]interface{}{ + "kvv": kvv, + "key1": "k1", + "key2": "k3", + } + ) + + eval(`omit(kvv, key1, key2)`, p) + fmt.Printf("\nOriginal KVV should be %v", kvv) + + // output: + // &{value:map[k2:[v2]]} + // Original KVV should be &{map[k1:[v1] k2:[v2] k3:[v3]]} +} + func TestTypedValueOperations(t *testing.T) { scope, _ := NewVars(map[string]interface{}{ "xUint": uint(1), @@ -55,7 +223,7 @@ func TestTypedValueOperations(t *testing.T) { } -func TestKV_Set(t *testing.T) { +func TestKV_Assign(t *testing.T) { var ( req = require.New(t) @@ -71,7 +239,108 @@ func TestKV_Set(t *testing.T) { } -func TestKVV_Set(t *testing.T) { +func TestKV_Set(t *testing.T) { + var ( + req = require.New(t) + + kv = &KV{value: map[string]string{ + "k1": "v1", + "k2": "v2", + }} + ) + + out, err := set(kv, "k1", Must(NewString("v11"))) + req.NoError(err) + req.Equal("v11", out.(*KV).GetValue()["k1"]) + + // Making sure empty KV updates without error + kv = &KV{} + out, err = set(kv, "k1", Must(NewString("v11"))) + req.NoError(err) + req.Equal("v11", out.(*KV).GetValue()["k1"]) +} + +func TestKV_Merge(t *testing.T) { + var ( + req = require.New(t) + + kv = &KV{} + foo = &KV{value: map[string]string{ + "k1": "v1", + }} + bar = &KV{value: map[string]string{ + "k2": "v2", + }} + expected = &KV{value: map[string]string{ + "k1": "v1", + "k2": "v2", + }} + ) + + out, err := merge(kv, foo, bar) + req.NoError(err) + req.Equal(expected, out) +} + +func TestKV_Clone(t *testing.T) { + var ( + req = require.New(t) + + kv = &KV{value: map[string]string{ + "k1": "v1", + "k2": "v2", + }} + expected = &KV{value: map[string]string{ + "k1": "v1", + "k2": "v2", + }} + ) + + out, err := kv.Merge() + req.NoError(err) + req.Equal(expected, out) +} + +func TestKV_Filter(t *testing.T) { + var ( + req = require.New(t) + + kv = &KV{value: map[string]string{ + "k1": "v1", + "k2": "v2", + }} + expected = &KV{value: map[string]string{ + "k1": "v1", + }} + ) + + out, err := filter(kv, "k1", "k3") + req.NoError(err) + req.Equal(expected, out) + req.NotEqual(kv, out) +} + +func TestKV_Omit(t *testing.T) { + var ( + req = require.New(t) + + kv = &KV{value: map[string]string{ + "k1": "v1", + "k2": "v2", + "k3": "v3", + }} + expected = &KV{value: map[string]string{ + "k2": "v2", + }} + ) + + out, err := omit(kv, "k1", "k3") + req.NoError(err) + req.Equal(expected, out) + req.NotEqual(kv, out) +} + +func TestKVV_Assign(t *testing.T) { var ( req = require.New(t) kvv KVV @@ -94,6 +363,105 @@ func TestKVV_Set(t *testing.T) { req.Equal([]string{"bar"}, kvv.value["foo"]) } +func TestKVV_Set(t *testing.T) { + var ( + req = require.New(t) + kvv = &KVV{value: map[string][]string{ + "k1": {"v1"}, + "k2": {"v2"}, + }} + ) + + out, err := set(kvv, "k1", Must(NewString("v11"))) + req.NoError(err) + req.Equal([]string{"v11"}, out.(*KVV).GetValue()["k1"]) + + // Making sure empty KV updates without error + kvv = &KVV{} + out, err = set(kvv, "foo", Must(NewString("bar"))) + req.NoError(err) + req.Equal([]string{"bar"}, out.(*KVV).GetValue()["foo"]) +} + +func TestKVV_Merge(t *testing.T) { + var ( + req = require.New(t) + + kvv *KVV + foo = KVV{value: map[string][]string{ + "k1": {"v1"}, + }} + bar = KVV{value: map[string][]string{ + "k1": {"v11"}, + "k2": {"v2"}, + }} + expected = &KVV{value: map[string][]string{ + "k1": {"v1", "v11"}, + "k2": {"v2"}, + }} + ) + + out, err := merge(kvv, &foo, &bar) + req.NoError(err) + req.Equal(expected, out) +} + +func TestKVV_Clone(t *testing.T) { + var ( + req = require.New(t) + + kvv = KVV{value: map[string][]string{ + "k1": {"v1", "v11"}, + "k2": {"v2"}, + }} + expected = &KVV{value: map[string][]string{ + "k1": {"v1", "v11"}, + "k2": {"v2"}, + }} + ) + + out, err := kvv.Merge() + req.NoError(err) + req.Equal(expected, out) +} + +func TestKVV_Filter(t *testing.T) { + var ( + req = require.New(t) + + kvv = &KVV{value: map[string][]string{ + "k1": {"v1"}, + "k2": {"v2"}, + }} + expected = &KVV{value: map[string][]string{ + "k1": {"v1"}, + }} + ) + + out, err := filter(kvv, "k1", "k3") + req.NoError(err) + req.Equal(expected, out) +} + +func TestKVV_Omit(t *testing.T) { + var ( + req = require.New(t) + + kvv = &KVV{value: map[string][]string{ + "k1": {"v1"}, + "k2": {"v2"}, + "k3": {"v3"}, + }} + expected = &KVV{value: map[string][]string{ + "k2": {"v2"}, + }} + ) + + out, err := omit(kvv, "k1", "k3") + req.NoError(err) + req.Equal(expected, out) +} + func TestCastEmptyString(t *testing.T) { var ( req = require.New(t) diff --git a/pkg/expr/func_json.go b/pkg/expr/func_json.go new file mode 100644 index 000000000..3c333b30b --- /dev/null +++ b/pkg/expr/func_json.go @@ -0,0 +1,18 @@ +package expr + +import ( + "encoding/json" + "github.com/PaesslerAG/gval" +) + +func JsonFunctions() []gval.Language { + return []gval.Language{ + // TODO: Json decoding, parsing, stringify, JQ, JSONPath + gval.Function("toJSON", toJSON), + } +} + +func toJSON(f interface{}) string { + b, _ := json.Marshal(f) + return string(b) +} diff --git a/pkg/expr/func_json_test.go b/pkg/expr/func_json_test.go new file mode 100644 index 000000000..01341030e --- /dev/null +++ b/pkg/expr/func_json_test.go @@ -0,0 +1,19 @@ +package expr + +func Example_toJSON() { + var ( + p = map[string]interface{}{ + "vars": Must(NewVars( + &Vars{value: map[string]TypedValue{ + "k1": &String{value: "v1"}, + "k2": &String{value: "v2"}, + }}, + )), + } + ) + + eval(`toJSON(vars)`, p) + + // output: + // {"k1":{"@value":"v1","@type":"String"},"k2":{"@value":"v2","@type":"String"}} +} diff --git a/pkg/expr/func_str.go b/pkg/expr/func_str.go index 7cc1cbeb2..050f6ffff 100644 --- a/pkg/expr/func_str.go +++ b/pkg/expr/func_str.go @@ -83,7 +83,7 @@ func join(arr interface{}, sep string) (out string, err error) { } else if i, is := arr.([]interface{}); is { // If slice of interfaces, we can try to cast them var aux []string - aux, err = CastStringSlice(i) + aux, err = CastToStringSlice(i) if err != nil { return } diff --git a/pkg/expr/parser.go b/pkg/expr/parser.go index 7c2766bec..5e9b5e1ae 100644 --- a/pkg/expr/parser.go +++ b/pkg/expr/parser.go @@ -87,9 +87,11 @@ func AllFunctions() []gval.Language { //ff = append(ff, GenericFunctions()...) ff = append(ff, StringFunctions()...) + ff = append(ff, JsonFunctions()...) ff = append(ff, NumericFunctions()...) ff = append(ff, TimeFunctions()...) ff = append(ff, ArrayFunctions()...) + ff = append(ff, KvFunctions()...) return ff } diff --git a/pkg/expr/vars.go b/pkg/expr/vars.go index d9858e1b9..69073099a 100644 --- a/pkg/expr/vars.go +++ b/pkg/expr/vars.go @@ -58,8 +58,25 @@ func (t Vars) ResolveTypes(res func(typ string) Type) (err error) { return nil } -// Assign takes base variables and assigns all new variables -func (t *Vars) Merge(nn ...Iterator) *Vars { +// Merge combines the given Vars(es) into Vars +// NOTE: It will return CLONE of the original Vars, if its called without any parameters +func (t *Vars) Merge(nn ...Iterator) (out TypedValue, err error) { + vars := EmptyVars() + + nn = append([]Iterator{t}, nn...) + + for _, i := range nn { + _ = i.Each(func(k string, v TypedValue) error { + vars.value[k] = v + return nil + }) + } + + return vars, nil +} + +// MustMerge returns Vars after merging the given Vars(es) into it +func (t *Vars) MustMerge(nn ...Iterator) *Vars { var ( out = &Vars{value: make(map[string]TypedValue)} ) @@ -76,7 +93,7 @@ func (t *Vars) Merge(nn ...Iterator) *Vars { return out } -// Assign takes base variables and assigns all new variables +// Copy takes base variables and copies all to dst func (t *Vars) Copy(dst *Vars, kk ...string) { if t == nil { return @@ -91,12 +108,12 @@ func (t *Vars) Copy(dst *Vars, kk ...string) { } } -// Returns true key is present +// Has returns true key is present func (t *Vars) Has(key string) bool { return t.HasAll(key) } -// Returns true if all keys are present +// HasAll returns true if all keys are present func (t *Vars) HasAll(key string, kk ...string) bool { if t == nil { return false @@ -111,7 +128,7 @@ func (t *Vars) HasAll(key string, kk ...string) bool { return true } -// Returns true if all keys are present +// HasAny returns true if all keys are present func (t *Vars) HasAny(key string, kk ...string) bool { if t == nil { return false @@ -230,7 +247,7 @@ func (t Vars) SelectGVal(_ context.Context, k string) (interface{}, error) { } } -// UnmarshalJSON +// UnmarshalJSON unmarshal JSON value into Vars func (t *Vars) UnmarshalJSON(in []byte) (err error) { var ( aux = make(map[string]*typedValueWrap) @@ -271,6 +288,7 @@ func (t *Vars) Each(fn func(k string, v TypedValue) error) (err error) { return } +// Set set/update the specific key value in KV func (t *Vars) Set(k string, v interface{}) (err error) { if t.value == nil { t.value = make(map[string]TypedValue) @@ -280,7 +298,7 @@ func (t *Vars) Set(k string, v interface{}) (err error) { return } -// UnmarshalJSON parses sort expression when passed inside JSON +// MarshalJSON returns JSON encoding of expression func (t Vars) MarshalJSON() ([]byte, error) { aux := make(map[string]*typedValueWrap) for k, v := range t.value { @@ -409,3 +427,42 @@ 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 +func (t *Vars) Filter(keys ...string) (out TypedValue, err error) { + if t.value == nil { + return + } + vars := EmptyVars() + + for _, k := range keys { + _, has := t.value[k] + if has { + vars.value[k] = t.value[k] + } + } + + return vars, nil +} + +// Delete take keys returns KV without those key value pair +func (t *Vars) Delete(keys ...string) (out TypedValue, err error) { + if t.value == nil { + return + } + + // get cloned Vars + out, err = t.Merge() + if err != nil { + return + } + + vars := out.(*Vars) + + // Delete key from t.value if exist + for _, k := range keys { + delete(vars.value, k) + } + + return vars, nil +} diff --git a/pkg/expr/vars_test.go b/pkg/expr/vars_test.go index a461d7057..1b5d9cbd3 100644 --- a/pkg/expr/vars_test.go +++ b/pkg/expr/vars_test.go @@ -2,12 +2,85 @@ package expr import ( "encoding/json" + "fmt" "github.com/stretchr/testify/require" "testing" ) // extract typed-value +func Example_set_vars() { + var ( + p = map[string]interface{}{ + "vars": &Vars{}, + "key": "foo", + "value": &String{value: "foo"}, + } + ) + + eval(`toJSON(set(vars, key, value))`, p) + + // output: + // {"foo":{"@value":"foo","@type":"String"}} +} + +func Example_merge_vars() { + var ( + p = map[string]interface{}{ + "vars": &Vars{}, + "foo": &Vars{value: map[string]TypedValue{ + "k1": &String{value: "v1"}, + }}, + "bar": &Vars{value: map[string]TypedValue{ + "k2": &String{value: "v2"}, + }}, + } + ) + + eval(`toJSON(merge(foo, bar))`, p) + + // output: + // {"k1":{"@value":"v1","@type":"String"},"k2":{"@value":"v2","@type":"String"}} +} + +func Example_filter_vars() { + var ( + p = map[string]interface{}{ + "vars": &Vars{value: map[string]TypedValue{ + "k1": &String{value: "v1"}, + "k5": &String{value: "v5"}, + }}, + "key1": "k1", + "key2": "k3", + "key3": "k5", + } + ) + + eval(`toJSON(filter(vars, key1, key2, key3))`, p) + + // output: + // {"k1":{"@value":"v1","@type":"String"},"k5":{"@value":"v5","@type":"String"}} +} + +func Example_omit_vars() { + var ( + p = map[string]interface{}{ + "vars": &Vars{value: map[string]TypedValue{ + "k1": &String{value: "v1"}, + "k2": &String{value: "v2"}, + "k3": &String{value: "v3"}, + }}, + "key1": "k1", + "key2": "k3", + } + ) + + eval(`toJSON(omit(vars, key1, key2))`, p) + + // output: + // {"k2":{"@value":"v2","@type":"String"}} +} + func TestVars_Decode(t *testing.T) { t.Run("mix", func(t *testing.T) { var ( @@ -107,6 +180,112 @@ func TestVars_Assign(t *testing.T) { req.NoError(Assign(vars, "foo", &String{value: "foo"})) req.NoError(Assign(vars, "vars", &Vars{})) req.NoError(Assign(vars, "vars.foo", &String{value: "foo"})) + + fmt.Println("Vars: ", vars) +} + +func TestVars_Set(t *testing.T) { + var ( + req = require.New(t) + + vars = &Vars{value: map[string]TypedValue{ + "k1": &String{value: "v1"}, + "k2": &String{value: "v2"}, + }} + ) + + out, err := set(vars, "k1", &String{value: "v11"}) + fmt.Println("Out: ", out) + req.NoError(err) + req.Equal(&String{value: "v11"}, out.(*Vars).GetValue()["k1"]) + + // Making sure empty vars updates without error + vars = &Vars{} + out, err = set(vars, "k2", &Array{value: []TypedValue{&String{value: "foo"}, &String{value: "bar"}}}) + req.NoError(err) + req.Equal(&Array{value: []TypedValue{&String{value: "foo"}, &String{value: "bar"}}}, out.(*Vars).GetValue()["k2"]) +} + +func TestVars_MergeVars(t *testing.T) { + var ( + req = require.New(t) + + vars Vars + foo = Vars{value: map[string]TypedValue{ + "k1": &String{value: "v1"}, + }} + bar = Vars{value: map[string]TypedValue{ + "k2": &String{value: "v2"}, + }} + expected = &Vars{value: map[string]TypedValue{ + "k1": &String{value: "v1"}, + "k2": &String{value: "v2"}, + }} + ) + + fmt.Println("vars: ", vars) + out := vars.MustMerge(&foo, &bar) + fmt.Println("vars: ", out) + req.Equal(expected, out) +} + +func TestVars_Merge(t *testing.T) { + var ( + req = require.New(t) + + vars = &Vars{} + foo = &Vars{value: map[string]TypedValue{ + "k1": &String{value: "v1"}, + }} + bar = &Vars{value: map[string]TypedValue{ + "k2": &String{value: "v2"}, + }} + expected = &Vars{value: map[string]TypedValue{ + "k1": &String{value: "v1"}, + "k2": &String{value: "v2"}, + }} + ) + + out, err := merge(vars, foo, bar) + req.NoError(err) + req.Equal(expected, out) +} + +func TestVars_Filter(t *testing.T) { + var ( + req = require.New(t) + + vars = &Vars{value: map[string]TypedValue{ + "k1": &String{value: "v1"}, + "k2": &String{value: "v2"}, + }} + expected = &Vars{value: map[string]TypedValue{ + "k1": &String{value: "v1"}, + }} + ) + + out, err := filter(vars, "k1", "k3") + req.NoError(err) + req.Equal(expected, out) +} + +func TestVars_Omit(t *testing.T) { + var ( + req = require.New(t) + + vars = Vars{value: map[string]TypedValue{ + "k1": &String{value: "v1"}, + "k2": &String{value: "v2"}, + "k3": &String{value: "v3"}, + }} + expected = &Vars{value: map[string]TypedValue{ + "k2": &String{value: "v2"}, + }} + ) + + out, err := omit(&vars, "k1", "k3") + req.NoError(err) + req.Equal(expected, out) } func TestVars_UnmarshalJSON(t *testing.T) { diff --git a/pkg/wfexec/gateways.go b/pkg/wfexec/gateways.go index 5bc44ebe6..4f5b99956 100644 --- a/pkg/wfexec/gateways.go +++ b/pkg/wfexec/gateways.go @@ -66,7 +66,7 @@ func (gw *joinGateway) Exec(_ context.Context, r *ExecRequest) (ExecResponse, er var merged *expr.Vars for _, p := range gw.paths { if gw.scopes[p] != nil { - merged = merged.Merge(gw.scopes[p]) + merged = merged.MustMerge(gw.scopes[p]) } } diff --git a/pkg/wfexec/session.go b/pkg/wfexec/session.go index 625e7fce8..30053d711 100644 --- a/pkg/wfexec/session.go +++ b/pkg/wfexec/session.go @@ -418,7 +418,7 @@ func (s *Session) worker(ctx context.Context) { defer s.mux.Unlock() // making sure result != nil - s.result = (&expr.Vars{}).Merge(st.scope) + s.result = (&expr.Vars{}).MustMerge(st.scope) // Call event handler with completed status s.eventHandler(SessionCompleted, st, s) @@ -549,7 +549,7 @@ func (s *Session) exec(ctx context.Context, st *State) (err error) { var ( result ExecResponse - scope = (&expr.Vars{}).Merge(st.scope) + scope = (&expr.Vars{}).MustMerge(st.scope) currLoop = st.loopCurr() @@ -635,7 +635,7 @@ func (s *Session) exec(ctx context.Context, st *State) (err error) { // most common (successful) result // session will continue with configured child steps st.results = result - scope = scope.Merge(st.results) + scope = scope.MustMerge(st.results) case *errHandler: st.action = "error handler initialized"