Przeglądaj źródła

feat(func): add function had_changed

Signed-off-by: Jiyong Huang <huangjy@emqx.io>
Jiyong Huang 3 lat temu
rodzic
commit
9bbb375ae5

+ 60 - 15
internal/binder/function/funcs_misc.go

@@ -512,31 +512,76 @@ func registerMiscFunc() {
 			return nil
 		},
 	}
-	builtins["object_construct"] = builtinFunc{
+	builtins["changed_col"] = builtinFunc{
+		fType: FuncTypeScalar,
+		exec: func(ctx api.FunctionContext, args []interface{}) (interface{}, bool) {
+			ignoreNull, ok := args[0].(bool)
+			if !ok {
+				return fmt.Errorf("first arg is not a bool but got %v", args[0]), false
+			}
+			if ignoreNull && args[1] == nil {
+				return nil, true
+			}
+			lv, err := ctx.GetState("self")
+			if err != nil {
+				return err, false
+			}
+			if !reflect.DeepEqual(args[1], lv) {
+				err := ctx.PutState("self", args[1])
+				if err != nil {
+					return err, false
+				}
+				return args[1], true
+			}
+			return nil, true
+		},
+		val: func(_ api.FunctionContext, args []ast.Expr) error {
+			if err := ValidateLen(2, len(args)); err != nil {
+				return err
+			}
+			if ast.IsNumericArg(args[0]) || ast.IsTimeArg(args[0]) || ast.IsStringArg(args[0]) {
+				return ProduceErrInfo(0, "boolean")
+			}
+			return nil
+		},
+	}
+	builtins["had_changed"] = builtinFunc{
 		fType: FuncTypeScalar,
 		exec: func(ctx api.FunctionContext, args []interface{}) (interface{}, bool) {
-			result := make(map[string]interface{})
-			for i := 0; i < len(args); i += 2 {
-				if args[i+1] != nil {
-					s, err := cast.ToString(args[i], cast.CONVERT_SAMEKIND)
+			if len(args) <= 1 {
+				return fmt.Errorf("expect more than one arg but got %d", len(args)), false
+			}
+			ignoreNull, ok := args[0].(bool)
+			if !ok {
+				return fmt.Errorf("first arg is not a bool but got %v", args[0]), false
+			}
+			result := false
+			for i := 1; i < len(args); i++ {
+				v := args[i]
+				k := strconv.Itoa(i)
+				if ignoreNull && v == nil {
+					continue
+				}
+				lv, err := ctx.GetState(k)
+				if err != nil {
+					return fmt.Errorf("error getting state for %s: %v", k, err), false
+				}
+				if !reflect.DeepEqual(v, lv) {
+					result = true
+					err := ctx.PutState(k, v)
 					if err != nil {
-						return fmt.Errorf("key %v is not a string", args[i]), false
+						return fmt.Errorf("error setting state for %s: %v", k, err), false
 					}
-					result[s] = args[i+1]
 				}
 			}
 			return result, true
 		},
 		val: func(_ api.FunctionContext, args []ast.Expr) error {
-			if len(args)%2 != 0 {
-				return fmt.Errorf("the args must be key value pairs")
+			if len(args) <= 1 {
+				return fmt.Errorf("expect more than one arg but got %d", len(args))
 			}
-			for i, arg := range args {
-				if i%2 == 0 {
-					if ast.IsNumericArg(arg) || ast.IsTimeArg(arg) || ast.IsBooleanArg(arg) {
-						return ProduceErrInfo(i, "string")
-					}
-				}
+			if ast.IsNumericArg(args[0]) || ast.IsTimeArg(args[0]) || ast.IsStringArg(args[0]) {
+				return ProduceErrInfo(0, "bool")
 			}
 			return nil
 		},

+ 219 - 0
internal/binder/function/funcs_misc_test.go

@@ -179,3 +179,222 @@ func TestToMap(t *testing.T) {
 		}
 	}
 }
+
+func TestHadChangedValidation(t *testing.T) {
+	f, ok := builtins["had_changed"]
+	if !ok {
+		t.Fatal("builtin not found")
+	}
+	var tests = []struct {
+		args []ast.Expr
+		err  error
+	}{
+		{
+			args: []ast.Expr{
+				&ast.StringLiteral{Val: "foo"},
+			},
+			err: fmt.Errorf("expect more than one arg but got 1"),
+		}, {
+			args: []ast.Expr{
+				&ast.StringLiteral{Val: "foo"},
+				&ast.StringLiteral{Val: "bar"},
+				&ast.StringLiteral{Val: "baz"},
+			},
+			err: fmt.Errorf("Expect bool type for parameter 1"),
+		}, {
+			args: []ast.Expr{
+				&ast.IntegerLiteral{Val: 20},
+				&ast.BooleanLiteral{Val: true},
+				&ast.StringLiteral{Val: "baz"},
+			},
+			err: fmt.Errorf("Expect bool type for parameter 1"),
+		}, {
+			args: []ast.Expr{
+				&ast.FieldRef{
+					StreamName: "demo",
+					Name:       "a",
+					AliasRef:   nil,
+				},
+				&ast.BooleanLiteral{Val: true},
+				&ast.StringLiteral{Val: "baz"},
+			},
+			err: nil,
+		},
+	}
+	for i, tt := range tests {
+		err := f.val(nil, tt.args)
+		if !reflect.DeepEqual(err, tt.err) {
+			t.Errorf("%d result mismatch,\ngot:\t%v \nwant:\t%v", i, err, tt.err)
+		}
+	}
+}
+
+func TestHadChangedExec(t *testing.T) {
+	f, ok := builtins["had_changed"]
+	if !ok {
+		t.Fatal("builtin not found")
+	}
+	contextLogger := conf.Log.WithField("rule", "testExec")
+	ctx := kctx.WithValue(kctx.Background(), kctx.LoggerKey, contextLogger)
+	tempStore, _ := state.CreateStore("mockRule0", api.AtMostOnce)
+	fctx := kctx.NewDefaultFuncContext(ctx.WithMeta("mockRule0", "test", tempStore), 1)
+	var tests = []struct {
+		args   []interface{}
+		result interface{}
+	}{
+		{ // 0
+			args: []interface{}{
+				"foo",
+				"bar",
+				"baz",
+			},
+			result: fmt.Errorf("first arg is not a bool but got foo"),
+		}, { // 1
+			args: []interface{}{
+				"foo",
+				"bar",
+			},
+			result: fmt.Errorf("first arg is not a bool but got foo"),
+		}, { // 2
+			args: []interface{}{
+				true,
+				"bar",
+				20,
+			},
+			result: true,
+		}, { // 3
+			args: []interface{}{
+				true,
+				"baz",
+				44,
+			},
+			result: true,
+		}, { // 4
+			args: []interface{}{
+				true,
+				"baz",
+				44,
+			},
+			result: false,
+		}, { // 5
+			args: []interface{}{
+				true,
+				"foo",
+				44,
+			},
+			result: true,
+		}, { // 6
+			args: []interface{}{
+				true,
+				"foo",
+				45,
+			},
+			result: true,
+		}, { // 7
+			args: []interface{}{
+				true,
+				"foo",
+				45,
+			},
+			result: false,
+		}, { // 8
+			args: []interface{}{
+				true,
+				"baz",
+				44,
+			},
+			result: true,
+		},
+	}
+	for i, tt := range tests {
+		result, _ := f.exec(fctx, tt.args)
+		if !reflect.DeepEqual(result, tt.result) {
+			t.Errorf("%d result mismatch,\ngot:\t%v \nwant:\t%v", i, result, tt.result)
+		}
+	}
+}
+
+func TestHadChangedExecIgnoreNull(t *testing.T) {
+	f, ok := builtins["had_changed"]
+	if !ok {
+		t.Fatal("builtin not found")
+	}
+	contextLogger := conf.Log.WithField("rule", "testExec")
+	ctx := kctx.WithValue(kctx.Background(), kctx.LoggerKey, contextLogger)
+	tempStore, _ := state.CreateStore("mockRule0", api.AtMostOnce)
+	fctx := kctx.NewDefaultFuncContext(ctx.WithMeta("mockRule0", "test", tempStore), 1)
+	var tests = []struct {
+		args   []interface{}
+		result interface{}
+	}{
+		{ // 0
+			args: []interface{}{
+				"foo",
+				"bar",
+				"baz",
+			},
+			result: fmt.Errorf("first arg is not a bool but got foo"),
+		}, { // 1
+			args: []interface{}{
+				"foo",
+				"bar",
+			},
+			result: fmt.Errorf("first arg is not a bool but got foo"),
+		}, { // 2
+			args: []interface{}{
+				false,
+				"bar",
+				20,
+			},
+			result: true,
+		}, { // 3
+			args: []interface{}{
+				false,
+				"baz",
+				nil,
+			},
+			result: true,
+		}, { // 4
+			args: []interface{}{
+				false,
+				"baz",
+				44,
+			},
+			result: true,
+		}, { // 5
+			args: []interface{}{
+				false,
+				nil,
+				44,
+			},
+			result: false,
+		}, { // 6
+			args: []interface{}{
+				false,
+				"baz",
+				44,
+			},
+			result: false,
+		}, { // 7
+			args: []interface{}{
+				true,
+				nil,
+				nil,
+			},
+			result: false,
+		}, { // 8
+			args: []interface{}{
+				true,
+				"baz",
+				44,
+			},
+			result: false,
+		},
+	}
+	for i, tt := range tests {
+		result, _ := f.exec(fctx, tt.args)
+		if !reflect.DeepEqual(result, tt.result) {
+			t.Errorf("%d result mismatch,\ngot:\t%v \nwant:\t%v", i, result, tt.result)
+		}
+	}
+}