package expr_test import ( "context" "encoding/json" "fmt" "os" "reflect" "sync" "testing" "time" "github.com/expr-lang/expr/internal/testify/assert" "github.com/expr-lang/expr/internal/testify/require" "github.com/expr-lang/expr/types" "github.com/expr-lang/expr" "github.com/expr-lang/expr/ast" "github.com/expr-lang/expr/file" "github.com/expr-lang/expr/test/mock" ) func ExampleEval() { output, err := expr.Eval("greet + name", map[string]any{ "greet": "Hello, ", "name": "world!", }) if err != nil { fmt.Printf("err: %v", err) return } fmt.Printf("%v", output) // Output: Hello, world! } func ExampleEval_runtime_error() { _, err := expr.Eval(`map(1..3, {1 % (# - 3)})`, nil) fmt.Print(err) // Output: runtime error: integer divide by zero (1:14) // | map(1..3, {1 % (# - 3)}) // | .............^ } func ExampleCompile() { env := map[string]any{ "foo": 1, "bar": 99, } program, err := expr.Compile("foo in 1..99 and bar in 1..99", expr.Env(env)) if err != nil { fmt.Printf("%v", err) return } output, err := expr.Run(program, env) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output) // Output: true } func ExampleEnv() { type Segment struct { Origin string } type Passengers struct { Adults int } type Meta struct { Tags map[string]string } type Env struct { Meta Segments []*Segment Passengers *Passengers Marker string } code := `all(Segments, {.Origin == "MOW"}) && Passengers.Adults > 0 && Tags["foo"] startsWith "bar"` program, err := expr.Compile(code, expr.Env(Env{})) if err != nil { fmt.Printf("%v", err) return } env := Env{ Meta: Meta{ Tags: map[string]string{ "foo": "bar", }, }, Segments: []*Segment{ {Origin: "MOW"}, }, Passengers: &Passengers{ Adults: 2, }, Marker: "test", } output, err := expr.Run(program, env) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output) // Output: true } func ExampleEnv_tagged_field_names() { env := struct { FirstWord string Separator string `expr:"Space"` SecondWord string `expr:"second_word"` }{ FirstWord: "Hello", Separator: " ", SecondWord: "World", } output, err := expr.Eval(`FirstWord + Space + second_word`, env) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output) // Output : Hello World } func ExampleAsKind() { program, err := expr.Compile("{a: 1, b: 2}", expr.AsKind(reflect.Map)) if err != nil { fmt.Printf("%v", err) return } output, err := expr.Run(program, nil) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output) // Output: map[a:1 b:2] } func ExampleAsBool() { env := map[string]int{ "foo": 0, } program, err := expr.Compile("foo >= 0", expr.Env(env), expr.AsBool()) if err != nil { fmt.Printf("%v", err) return } output, err := expr.Run(program, env) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output.(bool)) // Output: true } func ExampleAsBool_error() { env := map[string]any{ "foo": 0, } _, err := expr.Compile("foo + 42", expr.Env(env), expr.AsBool()) fmt.Printf("%v", err) // Output: expected bool, but got int } func ExampleAsInt() { program, err := expr.Compile("42", expr.AsInt()) if err != nil { fmt.Printf("%v", err) return } output, err := expr.Run(program, nil) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%T(%v)", output, output) // Output: int(42) } func ExampleAsInt64() { env := map[string]any{ "rating": 5.5, } program, err := expr.Compile("rating", expr.Env(env), expr.AsInt64()) if err != nil { fmt.Printf("%v", err) return } output, err := expr.Run(program, env) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output.(int64)) // Output: 5 } func ExampleAsFloat64() { program, err := expr.Compile("42", expr.AsFloat64()) if err != nil { fmt.Printf("%v", err) return } output, err := expr.Run(program, nil) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output.(float64)) // Output: 42 } func ExampleAsFloat64_error() { _, err := expr.Compile(`!!true`, expr.AsFloat64()) fmt.Printf("%v", err) // Output: expected float64, but got bool } func ExampleWarnOnAny() { // Arrays always have []any type. The expression return type is any. // AsInt() instructs compiler to expect int or any, and cast to int, // if possible. WarnOnAny() instructs to return an error on any type. _, err := expr.Compile(`[42, true, "yes"][0]`, expr.AsInt(), expr.WarnOnAny()) fmt.Printf("%v", err) // Output: expected int, but got interface {} } func ExampleOperator() { code := ` Now() > CreatedAt && (Now() - CreatedAt).Hours() > 24 ` type Env struct { CreatedAt time.Time Now func() time.Time Sub func(a, b time.Time) time.Duration After func(a, b time.Time) bool } options := []expr.Option{ expr.Env(Env{}), expr.Operator(">", "After"), expr.Operator("-", "Sub"), } program, err := expr.Compile(code, options...) if err != nil { fmt.Printf("%v", err) return } env := Env{ CreatedAt: time.Date(2018, 7, 14, 0, 0, 0, 0, time.UTC), Now: func() time.Time { return time.Now() }, Sub: func(a, b time.Time) time.Duration { return a.Sub(b) }, After: func(a, b time.Time) bool { return a.After(b) }, } output, err := expr.Run(program, env) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output) // Output: true } func ExampleOperator_Decimal() { type Decimal struct{ N float64 } code := `A + B - C` type Env struct { A, B, C Decimal Sub func(a, b Decimal) Decimal Add func(a, b Decimal) Decimal } options := []expr.Option{ expr.Env(Env{}), expr.Operator("+", "Add"), expr.Operator("-", "Sub"), } program, err := expr.Compile(code, options...) if err != nil { fmt.Printf("Compile error: %v", err) return } env := Env{ A: Decimal{3}, B: Decimal{2}, C: Decimal{1}, Sub: func(a, b Decimal) Decimal { return Decimal{a.N - b.N} }, Add: func(a, b Decimal) Decimal { return Decimal{a.N + b.N} }, } output, err := expr.Run(program, env) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output) // Output: {4} } func fib(n int) int { if n <= 1 { return n } return fib(n-1) + fib(n-2) } func ExampleConstExpr() { code := `[fib(5), fib(3+3), fib(dyn)]` env := map[string]any{ "fib": fib, "dyn": 0, } options := []expr.Option{ expr.Env(env), expr.ConstExpr("fib"), // Mark fib func as constant expression. } program, err := expr.Compile(code, options...) if err != nil { fmt.Printf("%v", err) return } // Only fib(5) and fib(6) calculated on Compile, fib(dyn) can be called at runtime. env["dyn"] = 7 output, err := expr.Run(program, env) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v\n", output) // Output: [5 8 13] } func ExampleAllowUndefinedVariables() { code := `name == nil ? "Hello, world!" : sprintf("Hello, %v!", name)` env := map[string]any{ "sprintf": fmt.Sprintf, } options := []expr.Option{ expr.Env(env), expr.AllowUndefinedVariables(), // Allow to use undefined variables. } program, err := expr.Compile(code, options...) if err != nil { fmt.Printf("%v", err) return } output, err := expr.Run(program, env) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v\n", output) env["name"] = "you" // Define variables later on. output, err = expr.Run(program, env) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v\n", output) // Output: Hello, world! // Hello, you! } func ExampleAllowUndefinedVariables_zero_value() { code := `name == "" ? foo + bar : foo + name` // If environment has different zero values, then undefined variables // will have it as default value. env := map[string]string{} options := []expr.Option{ expr.Env(env), expr.AllowUndefinedVariables(), // Allow to use undefined variables. } program, err := expr.Compile(code, options...) if err != nil { fmt.Printf("%v", err) return } env = map[string]string{ "foo": "Hello, ", "bar": "world!", } output, err := expr.Run(program, env) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output) // Output: Hello, world! } func ExampleAllowUndefinedVariables_zero_value_functions() { code := `words == "" ? Split("foo,bar", ",") : Split(words, ",")` // Env is map[string]string type on which methods are defined. env := mock.MapStringStringEnv{} options := []expr.Option{ expr.Env(env), expr.AllowUndefinedVariables(), // Allow to use undefined variables. } program, err := expr.Compile(code, options...) if err != nil { fmt.Printf("%v", err) return } output, err := expr.Run(program, env) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output) // Output: [foo bar] } type patcher struct{} func (p *patcher) Visit(node *ast.Node) { switch n := (*node).(type) { case *ast.MemberNode: ast.Patch(node, &ast.CallNode{ Callee: &ast.IdentifierNode{Value: "get"}, Arguments: []ast.Node{n.Node, n.Property}, }) } } func ExamplePatch() { /* type patcher struct{} func (p *patcher) Visit(node *ast.Node) { switch n := (*node).(type) { case *ast.MemberNode: ast.Patch(node, &ast.CallNode{ Callee: &ast.IdentifierNode{Value: "get"}, Arguments: []ast.Node{n.Node, n.Property}, }) } } */ program, err := expr.Compile( `greet.you.world + "!"`, expr.Patch(&patcher{}), ) if err != nil { fmt.Printf("%v", err) return } env := map[string]any{ "greet": "Hello", "get": func(a, b string) string { return a + ", " + b }, } output, err := expr.Run(program, env) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output) // Output : Hello, you, world! } func ExampleWithContext() { env := map[string]any{ "fn": func(ctx context.Context, _, _ int) int { // An infinite loop that can be canceled by context. for { select { case <-ctx.Done(): return 42 } } }, "ctx": context.TODO(), // Context should be passed as a variable. } program, err := expr.Compile(`fn(1, 2)`, expr.Env(env), expr.WithContext("ctx"), // Pass context variable name. ) if err != nil { fmt.Printf("%v", err) return } // Cancel context after 100 milliseconds. ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100) defer cancel() // After program is compiled, context can be passed to Run. env["ctx"] = ctx // Run will return 42 after 100 milliseconds. output, err := expr.Run(program, env) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output) // Output: 42 } func ExampleWithTimezone() { program, err := expr.Compile(`now().Location().String()`, expr.Timezone("Asia/Kamchatka")) if err != nil { fmt.Printf("%v", err) return } output, err := expr.Run(program, nil) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output) // Output: Asia/Kamchatka } func TestExpr_readme_example(t *testing.T) { env := map[string]any{ "greet": "Hello, %v!", "names": []string{"world", "you"}, "sprintf": fmt.Sprintf, } code := `sprintf(greet, names[0])` program, err := expr.Compile(code, expr.Env(env)) require.NoError(t, err) output, err := expr.Run(program, env) require.NoError(t, err) require.Equal(t, "Hello, world!", output) } func TestExpr(t *testing.T) { date := time.Date(2017, time.October, 23, 18, 30, 0, 0, time.UTC) oneDay, _ := time.ParseDuration("24h") timeNowPlusOneDay := date.Add(oneDay) env := mock.Env{ Embed: mock.Embed{}, Ambiguous: "", Any: nil, Bool: true, Float: 0, Int64: 0, Int32: 0, Int: 0, One: 1, Two: 2, Uint32: 0, String: "string", BoolPtr: nil, FloatPtr: nil, IntPtr: nil, IntPtrPtr: nil, StringPtr: nil, Foo: mock.Foo{ Value: "foo", Bar: mock.Bar{ Baz: "baz", }, }, Abstract: nil, ArrayOfAny: nil, ArrayOfInt: []int{1, 2, 3, 4, 5}, ArrayOfFoo: []*mock.Foo{{Value: "foo"}, {Value: "bar"}, {Value: "baz"}}, MapOfFoo: nil, MapOfAny: nil, FuncParam: nil, FuncParamAny: nil, FuncTooManyReturns: nil, FuncNamed: nil, NilAny: nil, NilFn: nil, NilStruct: nil, Variadic: func(head int, xs ...int) bool { sum := 0 for _, x := range xs { sum += x } return head == sum }, Fast: nil, Time: date, TimePlusDay: timeNowPlusOneDay, Duration: oneDay, } tests := []struct { code string want any }{ { `1`, 1, }, { `-.5`, -.5, }, { `true && false || false`, false, }, { `Int == 0 && Int32 == 0 && Int64 == 0 && Float64 == 0 && Bool && String == "string"`, true, }, { `-Int64 == 0`, true, }, { `"a" != "b"`, true, }, { `"a" != "b" || 1 == 2`, true, }, { `Int + 0`, 0, }, { `Uint64 + 0`, 0, }, { `Uint64 + Int64`, 0, }, { `Int32 + Int64`, 0, }, { `Float64 + 0`, float64(0), }, { `0 + Float64`, float64(0), }, { `0 <= Float64`, true, }, { `Float64 < 1`, true, }, { `Int < 1`, true, }, { `2 + 2 == 4`, true, }, { `8 % 3`, 2, }, { `2 ** 8`, float64(256), }, { `2 ^ 8`, float64(256), }, { `-(2-5)**3-2/(+4-3)+-2`, float64(23), }, { `"hello" + " " + "world"`, "hello world", }, { `0 in -1..1 and 1 in 1..1`, true, }, { `Int in 0..1`, true, }, { `Int32 in 0..1`, true, }, { `Int64 in 0..1`, true, }, { `1 in [1, 2, 3] && "foo" in {foo: 0, bar: 1} && "Bar" in Foo`, true, }, { `1 in [1.5] || 1 not in [1]`, false, }, { `One in 0..1 && Two not in 0..1`, true, }, { `Two not in 0..1`, true, }, { `Two not in 0..1`, true, }, { `-1 not in [1]`, true, }, { `Int32 in [10, 20]`, false, }, { `String matches "s.+"`, true, }, { `String matches ("^" + String + "$")`, true, }, { `'foo' + 'bar' not matches 'foobar'`, false, }, { `"foobar" contains "bar"`, true, }, { `"foobar" startsWith "foo"`, true, }, { `"foobar" endsWith "bar"`, true, }, { `(0..10)[5]`, 5, }, { `Foo.Bar.Baz`, "baz", }, { `Add(10, 5) + GetInt()`, 15, }, { `Foo.Method().Baz`, `baz (from Foo.Method)`, }, { `Foo.MethodWithArgs("prefix ")`, "prefix foo", }, { `len([1, 2, 3])`, 3, }, { `len([1, Two, 3])`, 3, }, { `len(["hello", "world"])`, 2, }, { `len("hello, world")`, 12, }, { `len(ArrayOfInt)`, 5, }, { `len({a: 1, b: 2, c: 2})`, 3, }, { `max([1, 2, 3])`, 3, }, { `max(1, 2, 3)`, 3, }, { `min([1, 2, 3])`, 1, }, { `min(1, 2, 3)`, 1, }, { `{foo: 0, bar: 1}`, map[string]any{"foo": 0, "bar": 1}, }, { `{foo: 0, bar: 1}`, map[string]any{"foo": 0, "bar": 1}, }, { `(true ? 0+1 : 2+3) + (false ? -1 : -2)`, -1, }, { `filter(1..9, {# > 7})`, []any{8, 9}, }, { `map(1..3, {# * #})`, []any{1, 4, 9}, }, { `all(1..3, {# > 0})`, true, }, { `count(1..30, {# % 3 == 0})`, 10, }, { `count([true, true, false])`, 2, }, { `"a" < "b"`, true, }, { `Time.Sub(Time).String() == "0s"`, true, }, { `1 + 1`, 2, }, { `(One * Two) * 3 == One * (Two * 3)`, true, }, { `ArrayOfInt[1]`, 2, }, { `ArrayOfInt[0] < ArrayOfInt[1]`, true, }, { `ArrayOfInt[-1]`, 5, }, { `ArrayOfInt[1:2]`, []int{2}, }, { `ArrayOfInt[1:4]`, []int{2, 3, 4}, }, { `ArrayOfInt[-4:-1]`, []int{2, 3, 4}, }, { `ArrayOfInt[:3]`, []int{1, 2, 3}, }, { `ArrayOfInt[3:]`, []int{4, 5}, }, { `ArrayOfInt[0:5] == ArrayOfInt`, true, }, { `ArrayOfInt[0:] == ArrayOfInt`, true, }, { `ArrayOfInt[:5] == ArrayOfInt`, true, }, { `ArrayOfInt[:] == ArrayOfInt`, true, }, { `4 in 5..1`, false, }, { `4..0`, []int{}, }, { `NilStruct`, (*mock.Foo)(nil), }, { `NilAny == nil && nil == NilAny && nil == nil && NilAny == NilAny && NilInt == nil && NilSlice == nil && NilStruct == nil`, true, }, { `0 == nil || "str" == nil || true == nil`, false, }, { `Variadic(6, 1, 2, 3)`, true, }, { `Variadic(0)`, true, }, { `String[:]`, "string", }, { `String[:3]`, "str", }, { `String[:9]`, "string", }, { `String[3:9]`, "ing", }, { `String[7:9]`, "", }, { `map(filter(ArrayOfInt, # >= 3), # + 1)`, []any{4, 5, 6}, }, { `Time < Time + Duration`, true, }, { `Time + Duration > Time`, true, }, { `Time == Time`, true, }, { `Time >= Time`, true, }, { `Time <= Time`, true, }, { `Time == Time + Duration`, false, }, { `Time != Time`, false, }, { `TimePlusDay - Duration`, date, }, { `duration("1h") == duration("1h")`, true, }, { `TimePlusDay - Time >= duration("24h")`, true, }, { `duration("1h") > duration("1m")`, true, }, { `duration("1h") < duration("1m")`, false, }, { `duration("1h") >= duration("1m")`, true, }, { `duration("1h") <= duration("1m")`, false, }, { `duration("1h") > duration("1m")`, true, }, { `duration("1h") + duration("1m")`, time.Hour + time.Minute, }, { `7 * duration("1h")`, 7 * time.Hour, }, { `duration("1h") * 7`, 7 * time.Hour, }, { `duration("1s") * .5`, 5e8, }, { `1 /* one */ + 2 // two`, 3, }, { `let x = 1; x + 2`, 3, }, { `map(1..3, let x = #; let y = x * x; y * y)`, []any{1, 16, 81}, }, { `map(1..2, let x = #; map(2..3, let y = #; x + y))`, []any{[]any{3, 4}, []any{4, 5}}, }, { `len(filter(1..99, # % 7 == 0))`, 14, }, { `find(ArrayOfFoo, .Value == "baz")`, env.ArrayOfFoo[2], }, { `findIndex(ArrayOfFoo, .Value == "baz")`, 2, }, { `filter(ArrayOfFoo, .Value == "baz")[0]`, env.ArrayOfFoo[2], }, { `first(filter(ArrayOfFoo, .Value == "baz"))`, env.ArrayOfFoo[2], }, { `first(filter(ArrayOfFoo, false))`, nil, }, { `findLast(1..9, # % 2 == 0)`, 8, }, { `findLastIndex(1..9, # % 2 == 0)`, 7, }, { `filter(1..9, # % 2 == 0)[-1]`, 8, }, { `last(filter(1..9, # % 2 == 0))`, 8, }, { `map(filter(1..9, # % 2 == 0), # * 2)`, []any{4, 8, 12, 16}, }, { `map(map(filter(1..9, # % 2 == 0), # * 2), # * 2)`, []any{8, 16, 24, 32}, }, { `first(map(filter(1..9, # % 2 == 0), # * 2))`, 4, }, { `map(filter(1..9, # % 2 == 0), # * 2)[-1]`, 16, }, { `len(map(filter(1..9, # % 2 == 0), # * 2))`, 4, }, { `len(filter(map(1..9, # * 2), # % 2 == 0))`, 9, }, { `first(filter(map(1..9, # * 2), # % 2 == 0))`, 2, }, { `first(map(filter(1..9, # % 2 == 0), # * 2))`, 4, }, { `2^3 == 8`, true, }, { `4/2 == 2`, true, }, { `.5 in 0..1`, false, }, { `.5 in ArrayOfInt`, false, }, { `bitnot(10)`, -11, }, { `bitxor(15, 32)`, 47, }, { `bitand(90, 34)`, 2, }, { `bitnand(35, 9)`, 34, }, { `bitor(10, 5)`, 15, }, { `bitshr(7, 2)`, 1, }, { `bitshl(7, 2)`, 28, }, { `bitushr(-100, 5)`, 576460752303423484, }, { `"hello"[1:3]`, "el", }, { `[1, 2, 3]?.[0]`, 1, }, { `[[1, 2], 3, 4]?.[0]?.[1]`, 2, }, { `[nil, 3, 4]?.[0]?.[1]`, nil, }, { `1 > 2 < 3`, false, }, { `1 < 2 < 3`, true, }, { `1 < 2 < 3 > 4`, false, }, { `1 < 2 < 3 > 2`, true, }, { `1 < 2 < 3 == true`, true, }, } for _, tt := range tests { t.Run(tt.code, func(t *testing.T) { { program, err := expr.Compile(tt.code, expr.Env(mock.Env{})) require.NoError(t, err, "compile error") got, err := expr.Run(program, env) require.NoError(t, err, "run error") assert.Equal(t, tt.want, got) } { program, err := expr.Compile(tt.code, expr.Optimize(false)) require.NoError(t, err, "unoptimized") got, err := expr.Run(program, env) require.NoError(t, err, "unoptimized") assert.Equal(t, tt.want, got, "unoptimized") } { got, err := expr.Eval(tt.code, env) require.NoError(t, err, "eval") assert.Equal(t, tt.want, got, "eval") } { program, err := expr.Compile(tt.code, expr.Env(mock.Env{}), expr.Optimize(false)) require.NoError(t, err) code := program.Node().String() got, err := expr.Eval(code, env) require.NoError(t, err, code) assert.Equal(t, tt.want, got, code) } }) } } func TestExpr_error(t *testing.T) { env := mock.Env{ ArrayOfAny: []any{1, "2", 3, true}, } tests := []struct { code string want string }{ { `filter(1..9, # > 9)[0]`, `reflect: slice index out of range (1:20) | filter(1..9, # > 9)[0] | ...................^`, }, { `ArrayOfAny[-7]`, `index out of range: -3 (array length is 4) (1:11) | ArrayOfAny[-7] | ..........^`, }, } for _, tt := range tests { t.Run(tt.code, func(t *testing.T) { program, err := expr.Compile(tt.code, expr.Env(mock.Env{})) require.NoError(t, err) _, err = expr.Run(program, env) require.Error(t, err) assert.Equal(t, tt.want, err.Error()) }) } } func TestExpr_optional_chaining(t *testing.T) { env := map[string]any{} program, err := expr.Compile("foo?.bar.baz", expr.Env(env), expr.AllowUndefinedVariables()) require.NoError(t, err) got, err := expr.Run(program, env) require.NoError(t, err) assert.Equal(t, nil, got) } func TestExpr_optional_chaining_property(t *testing.T) { env := map[string]any{ "foo": map[string]any{}, } program, err := expr.Compile("foo.bar?.baz", expr.Env(env)) require.NoError(t, err) got, err := expr.Run(program, env) require.NoError(t, err) assert.Equal(t, nil, got) } func TestExpr_optional_chaining_nested_chains(t *testing.T) { env := map[string]any{ "foo": map[string]any{ "id": 1, "bar": []map[string]any{ 1: { "baz": "baz", }, }, }, } program, err := expr.Compile("foo?.bar[foo?.id]?.baz", expr.Env(env)) require.NoError(t, err) got, err := expr.Run(program, env) require.NoError(t, err) assert.Equal(t, "baz", got) } func TestExpr_optional_chaining_array(t *testing.T) { env := map[string]any{} program, err := expr.Compile("foo?.[1]?.[2]?.[3]", expr.Env(env), expr.AllowUndefinedVariables()) require.NoError(t, err) got, err := expr.Run(program, env) require.NoError(t, err) assert.Equal(t, nil, got) } func TestExpr_eval_with_env(t *testing.T) { _, err := expr.Eval("true", expr.Env(map[string]any{})) assert.Error(t, err) assert.Contains(t, err.Error(), "misused") } func TestExpr_fetch_from_func(t *testing.T) { _, err := expr.Eval("foo.Value", map[string]any{ "foo": func() {}, }) assert.Error(t, err) assert.Contains(t, err.Error(), "cannot fetch Value from func()") } func TestExpr_map_default_values(t *testing.T) { env := map[string]any{ "foo": map[string]string{}, "bar": map[string]*string{}, } input := `foo['missing'] == '' && bar['missing'] == nil` program, err := expr.Compile(input, expr.Env(env)) require.NoError(t, err) output, err := expr.Run(program, env) require.NoError(t, err) require.Equal(t, true, output) } func TestExpr_map_default_values_compile_check(t *testing.T) { tests := []struct { env any input string }{ { mock.MapStringStringEnv{"foo": "bar"}, `Split(foo, sep)`, }, { mock.MapStringIntEnv{"foo": 1}, `foo / bar`, }, } for _, tt := range tests { _, err := expr.Compile(tt.input, expr.Env(tt.env), expr.AllowUndefinedVariables()) require.NoError(t, err) } } func TestExpr_calls_with_nil(t *testing.T) { env := map[string]any{ "equals": func(a, b any) any { assert.Nil(t, a, "a is not nil") assert.Nil(t, b, "b is not nil") return a == b }, "is": mock.Is{}, } p, err := expr.Compile( "a == nil && equals(b, nil) && is.Nil(c)", expr.Env(env), expr.Operator("==", "equals"), expr.AllowUndefinedVariables(), ) require.NoError(t, err) out, err := expr.Run(p, env) require.NoError(t, err) require.Equal(t, true, out) } func TestExpr_call_float_arg_func_with_int(t *testing.T) { env := map[string]any{ "cnv": func(f float64) any { return f }, } tests := []struct { input string expected float64 }{ {"-1", -1.0}, {"1+1", 2.0}, {"+1", 1.0}, {"1-1", 0.0}, {"1/1", 1.0}, {"1*1", 1.0}, {"1^1", 1.0}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { p, err := expr.Compile(fmt.Sprintf("cnv(%s)", tt.input), expr.Env(env)) require.NoError(t, err) out, err := expr.Run(p, env) require.NoError(t, err) require.Equal(t, tt.expected, out) }) } } func TestConstExpr_error_panic(t *testing.T) { env := map[string]any{ "divide": func(a, b int) int { return a / b }, } _, err := expr.Compile( `1 + divide(1, 0)`, expr.Env(env), expr.ConstExpr("divide"), ) require.Error(t, err) require.Equal(t, "compile error: integer divide by zero (1:5)\n | 1 + divide(1, 0)\n | ....^", err.Error()) } type divideError struct{ Message string } func (e divideError) Error() string { return e.Message } func TestConstExpr_error_as_error(t *testing.T) { env := map[string]any{ "divide": func(a, b int) (int, error) { if b == 0 { return 0, divideError{"integer divide by zero"} } return a / b, nil }, } _, err := expr.Compile( `1 + divide(1, 0)`, expr.Env(env), expr.ConstExpr("divide"), ) require.Error(t, err) require.Equal(t, "integer divide by zero", err.Error()) require.IsType(t, divideError{}, err) } func TestConstExpr_error_wrong_type(t *testing.T) { env := map[string]any{ "divide": 0, } assert.Panics(t, func() { _, _ = expr.Compile( `1 + divide(1, 0)`, expr.Env(env), expr.ConstExpr("divide"), ) }) } func TestConstExpr_error_no_env(t *testing.T) { assert.Panics(t, func() { _, _ = expr.Compile( `1 + divide(1, 0)`, expr.ConstExpr("divide"), ) }) } var stringer = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() type stringerPatcher struct{} func (p *stringerPatcher) Visit(node *ast.Node) { t := (*node).Type() if t == nil { return } if t.Implements(stringer) { ast.Patch(node, &ast.CallNode{ Callee: &ast.MemberNode{ Node: *node, Property: &ast.StringNode{Value: "String"}, }, }) } } func TestPatch(t *testing.T) { program, err := expr.Compile( `Foo == "Foo.String"`, expr.Env(mock.Env{}), expr.Patch(&mock.StringerPatcher{}), ) require.NoError(t, err) output, err := expr.Run(program, mock.Env{}) require.NoError(t, err) require.Equal(t, true, output) } func TestCompile_exposed_error(t *testing.T) { _, err := expr.Compile(`1 == true`) require.Error(t, err) fileError, ok := err.(*file.Error) require.True(t, ok, "error should be of type *file.Error") require.Equal(t, "invalid operation: == (mismatched types int and bool) (1:3)\n | 1 == true\n | ..^", fileError.Error()) require.Equal(t, 2, fileError.Column) require.Equal(t, 1, fileError.Line) b, err := json.Marshal(err) require.NoError(t, err) require.Equal(t, `{"from":2,"to":4,"line":1,"column":2,"message":"invalid operation: == (mismatched types int and bool)","snippet":"\n | 1 == true\n | ..^","prev":null}`, string(b), ) } func TestAsBool_exposed_error(t *testing.T) { _, err := expr.Compile(`42`, expr.AsBool()) require.Error(t, err) _, ok := err.(*file.Error) require.False(t, ok, "error must not be of type *file.Error") require.Equal(t, "expected bool, but got int", err.Error()) } func TestEval_exposed_error(t *testing.T) { _, err := expr.Eval(`1 % 0`, nil) require.Error(t, err) fileError, ok := err.(*file.Error) require.True(t, ok, "error should be of type *file.Error") require.Equal(t, "integer divide by zero (1:3)\n | 1 % 0\n | ..^", fileError.Error()) require.Equal(t, 2, fileError.Column) require.Equal(t, 1, fileError.Line) } func TestIssue105(t *testing.T) { type A struct { Field string } type B struct { Field int } type C struct { A B } type Env struct { C } code := ` A.Field == '' && C.A.Field == '' && B.Field == 0 && C.B.Field == 0 ` _, err := expr.Compile(code, expr.Env(Env{})) require.NoError(t, err) } func TestIssue_nested_closures(t *testing.T) { code := `all(1..3, { all(1..3, { # > 0 }) and # > 0 })` program, err := expr.Compile(code) require.NoError(t, err) output, err := expr.Run(program, nil) require.NoError(t, err) require.True(t, output.(bool)) } func TestIssue138(t *testing.T) { env := map[string]any{} _, err := expr.Compile(`1 / (1 - 1)`, expr.Env(env)) require.NoError(t, err) _, err = expr.Compile(`1 % 0`, expr.Env(env)) require.Error(t, err) require.Equal(t, "integer divide by zero (1:3)\n | 1 % 0\n | ..^", err.Error()) } func TestIssue154(t *testing.T) { type Data struct { Array *[2]any Slice *[]any Map *map[string]any String *string } type Env struct { Data *Data } b := true i := 10 s := "value" Array := [2]any{ &b, &i, } Slice := []any{ &b, &i, } Map := map[string]any{ "Bool": &b, "Int": &i, } env := Env{ Data: &Data{ Array: &Array, Slice: &Slice, Map: &Map, String: &s, }, } tests := []string{ `Data.Array[0] == true`, `Data.Array[1] == 10`, `Data.Slice[0] == true`, `Data.Slice[1] == 10`, `Data.Map["Bool"] == true`, `Data.Map["Int"] == 10`, `Data.String == "value"`, } for _, input := range tests { program, err := expr.Compile(input, expr.Env(env)) require.NoError(t, err, input) output, err := expr.Run(program, env) require.NoError(t, err) assert.True(t, output.(bool), input) } } func TestIssue270(t *testing.T) { env := map[string]any{ "int8": int8(1), "int16": int16(3), "int32": int32(5), "int64": int64(7), "uint8": uint8(11), "uint16": uint16(13), "uint32": uint32(17), "uint64": uint64(19), "int8a": uint(23), "int8b": uint(29), "int16a": uint(31), "int16b": uint(37), "int32a": uint(41), "int32b": uint(43), "int64a": uint(47), "int64b": uint(53), "uint8a": uint(59), "uint8b": uint(61), "uint16a": uint(67), "uint16b": uint(71), "uint32a": uint(73), "uint32b": uint(79), "uint64a": uint(83), "uint64b": uint(89), "float32a": float32(97), "float32b": float32(101), "float64a": float64(103), "float64b": float64(107), } for _, each := range []struct { input string }{ {"int8 / int16"}, {"int32 / int64"}, {"uint8 / uint16"}, {"uint32 / uint64"}, {"int8 / uint64"}, {"int64 / uint8"}, {"int8a / int8b"}, {"int16a / int16b"}, {"int32a / int32b"}, {"int64a / int64b"}, {"uint8a / uint8b"}, {"uint16a / uint16b"}, {"uint32a / uint32b"}, {"uint64a / uint64b"}, {"float32a / float32b"}, {"float64a / float64b"}, } { p, err := expr.Compile(each.input, expr.Env(env)) require.NoError(t, err) out, err := expr.Run(p, env) require.NoError(t, err) require.IsType(t, float64(0), out) } } func TestIssue271(t *testing.T) { type BarArray []float64 type Foo struct { Bar BarArray Baz int } type Env struct { Foo Foo } code := `Foo.Bar[0]` program, err := expr.Compile(code, expr.Env(Env{})) require.NoError(t, err) output, err := expr.Run(program, Env{ Foo: Foo{ Bar: BarArray{1.0, 2.0, 3.0}, }, }) require.NoError(t, err) require.Equal(t, 1.0, output) } type Issue346Array []Issue346Type type Issue346Type struct { Bar string } func (i Issue346Array) Len() int { return len(i) } func TestIssue346(t *testing.T) { code := `Foo[0].Bar` env := map[string]any{ "Foo": Issue346Array{ {Bar: "bar"}, }, } program, err := expr.Compile(code, expr.Env(env)) require.NoError(t, err) output, err := expr.Run(program, env) require.NoError(t, err) require.Equal(t, "bar", output) } func TestCompile_allow_to_use_interface_to_get_an_element_from_map(t *testing.T) { code := `{"value": "ok"}[vars.key]` env := map[string]any{ "vars": map[string]any{ "key": "value", }, } program, err := expr.Compile(code, expr.Env(env)) assert.NoError(t, err) out, err := expr.Run(program, env) assert.NoError(t, err) assert.Equal(t, "ok", out) t.Run("with allow undefined variables", func(t *testing.T) { code := `{'key': 'value'}[Key]` env := mock.MapStringStringEnv{} options := []expr.Option{ expr.AllowUndefinedVariables(), } program, err := expr.Compile(code, options...) assert.NoError(t, err) out, err := expr.Run(program, env) assert.NoError(t, err) assert.Equal(t, nil, out) }) } func TestFastCall(t *testing.T) { env := map[string]any{ "func": func(in any) float64 { return 8 }, } code := `func("8")` program, err := expr.Compile(code, expr.Env(env)) assert.NoError(t, err) out, err := expr.Run(program, env) assert.NoError(t, err) assert.Equal(t, float64(8), out) } func TestFastCall_OpCallFastErr(t *testing.T) { env := map[string]any{ "func": func(...any) (any, error) { return 8, nil }, } code := `func("8")` program, err := expr.Compile(code, expr.Env(env)) assert.NoError(t, err) out, err := expr.Run(program, env) assert.NoError(t, err) assert.Equal(t, 8, out) } func TestRun_custom_func_returns_an_error_as_second_arg(t *testing.T) { env := map[string]any{ "semver": func(value string, cmp string) (bool, error) { return true, nil }, } p, err := expr.Compile(`semver("1.2.3", "= 1.2.3")`, expr.Env(env)) assert.NoError(t, err) out, err := expr.Run(p, env) assert.NoError(t, err) assert.Equal(t, true, out) } func TestFunction(t *testing.T) { add := expr.Function( "add", func(p ...any) (any, error) { out := 0 for _, each := range p { out += each.(int) } return out, nil }, new(func(...int) int), ) p, err := expr.Compile(`add() + add(1) + add(1, 2) + add(1, 2, 3) + add(1, 2, 3, 4)`, add) assert.NoError(t, err) out, err := expr.Run(p, nil) assert.NoError(t, err) assert.Equal(t, 20, out) } // Nil coalescing operator func TestRun_NilCoalescingOperator(t *testing.T) { env := map[string]any{ "foo": map[string]any{ "bar": "value", }, } t.Run("value", func(t *testing.T) { p, err := expr.Compile(`foo.bar ?? "default"`, expr.Env(env)) assert.NoError(t, err) out, err := expr.Run(p, env) assert.NoError(t, err) assert.Equal(t, "value", out) }) t.Run("default", func(t *testing.T) { p, err := expr.Compile(`foo.baz ?? "default"`, expr.Env(env)) assert.NoError(t, err) out, err := expr.Run(p, env) assert.NoError(t, err) assert.Equal(t, "default", out) }) t.Run("default with chain", func(t *testing.T) { p, err := expr.Compile(`foo?.bar ?? "default"`, expr.Env(env)) assert.NoError(t, err) out, err := expr.Run(p, map[string]any{}) assert.NoError(t, err) assert.Equal(t, "default", out) }) } func TestEval_nil_in_maps(t *testing.T) { env := map[string]any{ "m": map[any]any{nil: "bar"}, "empty": map[any]any{}, } t.Run("nil key exists", func(t *testing.T) { p, err := expr.Compile(`m[nil]`, expr.Env(env)) assert.NoError(t, err) out, err := expr.Run(p, env) assert.NoError(t, err) assert.Equal(t, "bar", out) }) t.Run("no nil key", func(t *testing.T) { p, err := expr.Compile(`empty[nil]`, expr.Env(env)) assert.NoError(t, err) out, err := expr.Run(p, env) assert.NoError(t, err) assert.Equal(t, nil, out) }) t.Run("nil in m", func(t *testing.T) { p, err := expr.Compile(`nil in m`, expr.Env(env)) assert.NoError(t, err) out, err := expr.Run(p, env) assert.NoError(t, err) assert.Equal(t, true, out) }) t.Run("nil in empty", func(t *testing.T) { p, err := expr.Compile(`nil in empty`, expr.Env(env)) assert.NoError(t, err) out, err := expr.Run(p, env) assert.NoError(t, err) assert.Equal(t, false, out) }) } // Test the use of env keyword. Forms env[] and env[”] are valid. // The enclosed identifier must be in the expression env. func TestEnv_keyword(t *testing.T) { env := map[string]any{ "space test": "ok", "space_test": "not ok", // Seems to be some underscore substituting happening, check that. "Section 1-2a": "ok", `c:\ndrive\2015 Information Table`: "ok", "%*worst function name ever!!": func() string { return "ok" }(), "1": "o", "2": "k", "num": 10, "mylist": []int{1, 2, 3, 4, 5}, "MIN": func(a, b int) int { if a < b { return a } else { return b } }, "red": "n", "irect": "um", "String Map": map[string]string{ "one": "two", "three": "four", }, "OtherMap": map[string]string{ "a": "b", "c": "d", }, } // No error cases var tests = []struct { code string want any }{ {"$env['space test']", "ok"}, {"$env['Section 1-2a']", "ok"}, {`$env["c:\\ndrive\\2015 Information Table"]`, "ok"}, {"$env['%*worst function name ever!!']", "ok"}, {"$env['String Map'].one", "two"}, {"$env['1'] + $env['2']", "ok"}, {"1 + $env['num'] + $env['num']", 21}, {"MIN($env['num'],0)", 0}, {"$env['nu' + 'm']", 10}, {"$env[red + irect]", 10}, {"$env['String Map']?.five", ""}, {"$env.red", "n"}, {"$env?.unknown", nil}, {"$env.mylist[1]", 2}, {"$env?.OtherMap?.a", "b"}, {"$env?.OtherMap?.d", ""}, {"'num' in $env", true}, {"get($env, 'num')", 10}, } for _, tt := range tests { t.Run(tt.code, func(t *testing.T) { program, err := expr.Compile(tt.code, expr.Env(env)) require.NoError(t, err, "compile error") got, err := expr.Run(program, env) require.NoError(t, err, "execution error") assert.Equal(t, tt.want, got, tt.code) }) } for _, tt := range tests { t.Run(tt.code, func(t *testing.T) { got, err := expr.Eval(tt.code, env) require.NoError(t, err, "eval error: "+tt.code) assert.Equal(t, tt.want, got, "eval: "+tt.code) }) } // error cases tests = []struct { code string want any }{ {"env()", "bad"}, } for _, tt := range tests { t.Run(tt.code, func(t *testing.T) { _, err := expr.Eval(tt.code, expr.Env(env)) require.Error(t, err, "compile error") }) } } func TestEnv_keyword_with_custom_functions(t *testing.T) { fn := expr.Function("fn", func(params ...any) (any, error) { return "ok", nil }) var tests = []struct { code string error bool }{ {`fn()`, false}, {`$env.fn()`, true}, {`$env["fn"]`, true}, } for _, tt := range tests { t.Run(tt.code, func(t *testing.T) { _, err := expr.Compile(tt.code, expr.Env(mock.Env{}), fn) if tt.error { require.Error(t, err) } else { require.NoError(t, err) } }) } } func TestIssue401(t *testing.T) { program, err := expr.Compile("(a - b + c) / d", expr.AllowUndefinedVariables()) require.NoError(t, err, "compile error") output, err := expr.Run(program, map[string]any{ "a": 1, "b": 2, "c": 3, "d": 4, }) require.NoError(t, err, "run error") require.Equal(t, 0.5, output) } func TestEval_slices_out_of_bound(t *testing.T) { tests := []struct { code string want any }{ {"[1, 2, 3][:99]", []any{1, 2, 3}}, {"[1, 2, 3][99:]", []any{}}, {"[1, 2, 3][:-99]", []any{}}, {"[1, 2, 3][-99:]", []any{1, 2, 3}}, } for _, tt := range tests { t.Run(tt.code, func(t *testing.T) { got, err := expr.Eval(tt.code, nil) require.NoError(t, err, "eval error: "+tt.code) assert.Equal(t, tt.want, got, "eval: "+tt.code) }) } } func TestMemoryBudget(t *testing.T) { tests := []struct { code string }{ {`map(1..100, {map(1..100, {map(1..100, {0})})})`}, {`len(1..10000000)`}, } for _, tt := range tests { t.Run(tt.code, func(t *testing.T) { program, err := expr.Compile(tt.code) require.NoError(t, err, "compile error") _, err = expr.Run(program, nil) assert.Error(t, err, "run error") assert.Contains(t, err.Error(), "memory budget exceeded") }) } } func TestExpr_custom_tests(t *testing.T) { f, err := os.Open("custom_tests.json") if os.IsNotExist(err) { t.Skip("no custom tests") return } require.NoError(t, err, "open file error") defer f.Close() var tests []string err = json.NewDecoder(f).Decode(&tests) require.NoError(t, err, "decode json error") for id, tt := range tests { t.Run(fmt.Sprintf("line %v", id+2), func(t *testing.T) { program, err := expr.Compile(tt) require.NoError(t, err) timeout := make(chan bool, 1) go func() { time.Sleep(time.Second) timeout <- true }() done := make(chan bool, 1) go func() { out, err := expr.Run(program, nil) // Make sure out is used. _ = fmt.Sprintf("%v", out) assert.Error(t, err) done <- true }() select { case <-done: // Success. case <-timeout: t.Fatal("timeout") } }) } } func TestIssue432(t *testing.T) { env := map[string]any{ "func": func( paramUint32 uint32, paramUint16 uint16, paramUint8 uint8, paramUint uint, paramInt32 int32, paramInt16 int16, paramInt8 int8, paramInt int, paramFloat64 float64, paramFloat32 float32, ) float64 { return float64(paramUint32) + float64(paramUint16) + float64(paramUint8) + float64(paramUint) + float64(paramInt32) + float64(paramInt16) + float64(paramInt8) + float64(paramInt) + float64(paramFloat64) + float64(paramFloat32) }, } code := `func(1,1,1,1,1,1,1,1,1,1)` program, err := expr.Compile(code, expr.Env(env)) assert.NoError(t, err) out, err := expr.Run(program, env) assert.NoError(t, err) assert.Equal(t, float64(10), out) } func TestIssue461(t *testing.T) { type EnvStr string type EnvField struct { S EnvStr Str string } type Env struct { S EnvStr Str string EnvField EnvField } var tests = []struct { input string env Env want bool }{ { input: "Str == S", env: Env{S: "string", Str: "string"}, want: false, }, { input: "Str == Str", env: Env{Str: "string"}, want: true, }, { input: "S == S", env: Env{Str: "string"}, want: true, }, { input: `Str == "string"`, env: Env{Str: "string"}, want: true, }, { input: `S == "string"`, env: Env{Str: "string"}, want: false, }, { input: "EnvField.Str == EnvField.S", env: Env{EnvField: EnvField{S: "string", Str: "string"}}, want: false, }, { input: "EnvField.Str == EnvField.Str", env: Env{EnvField: EnvField{Str: "string"}}, want: true, }, { input: "EnvField.S == EnvField.S", env: Env{EnvField: EnvField{Str: "string"}}, want: true, }, { input: `EnvField.Str == "string"`, env: Env{EnvField: EnvField{Str: "string"}}, want: true, }, { input: `EnvField.S == "string"`, env: Env{EnvField: EnvField{Str: "string"}}, want: false, }, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { program, err := expr.Compile(tt.input, expr.Env(tt.env), expr.AsBool()) out, err := expr.Run(program, tt.env) require.NoError(t, err) require.Equal(t, tt.want, out) }) } } func TestIssue462(t *testing.T) { env := map[string]any{ "foo": func() (string, error) { return "bar", nil }, } _, err := expr.Compile(`$env.unknown(int())`, expr.Env(env)) require.Error(t, err) } func TestIssue_embedded_pointer_struct(t *testing.T) { var tests = []struct { input string env mock.Env want any }{ { input: "EmbedPointerEmbedInt > 0", env: mock.Env{ Embed: mock.Embed{ EmbedPointerEmbed: &mock.EmbedPointerEmbed{ EmbedPointerEmbedInt: 123, }, }, }, want: true, }, { input: "(Embed).EmbedPointerEmbedInt > 0", env: mock.Env{ Embed: mock.Embed{ EmbedPointerEmbed: &mock.EmbedPointerEmbed{ EmbedPointerEmbedInt: 123, }, }, }, want: true, }, { input: "(Embed).EmbedPointerEmbedInt > 0", env: mock.Env{ Embed: mock.Embed{ EmbedPointerEmbed: &mock.EmbedPointerEmbed{ EmbedPointerEmbedInt: 0, }, }, }, want: false, }, { input: "(Embed).EmbedPointerEmbedMethod(0)", env: mock.Env{ Embed: mock.Embed{ EmbedPointerEmbed: &mock.EmbedPointerEmbed{ EmbedPointerEmbedInt: 0, }, }, }, want: "", }, { input: "(Embed).EmbedPointerEmbedPointerReceiverMethod(0)", env: mock.Env{ Embed: mock.Embed{ EmbedPointerEmbed: nil, }, }, want: "", }, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { program, err := expr.Compile(tt.input, expr.Env(tt.env)) require.NoError(t, err) out, err := expr.Run(program, tt.env) require.NoError(t, err) require.Equal(t, tt.want, out) }) } } func TestIssue474(t *testing.T) { testCases := []struct { code string fail bool }{ { code: `func("invalid")`, fail: true, }, { code: `func(true)`, fail: true, }, { code: `func([])`, fail: true, }, { code: `func({})`, fail: true, }, { code: `func(1)`, fail: false, }, { code: `func(1.5)`, fail: false, }, } for _, tc := range testCases { ltc := tc t.Run(ltc.code, func(t *testing.T) { t.Parallel() function := expr.Function("func", func(params ...any) (any, error) { return true, nil }, new(func(float64) bool)) _, err := expr.Compile(ltc.code, function) if ltc.fail { if err == nil { t.Error("expected an error, but it was nil") t.FailNow() } } else { if err != nil { t.Errorf("expected nil, but it was %v", err) t.FailNow() } } }) } } func TestRaceCondition_variables(t *testing.T) { program, err := expr.Compile(`let foo = 1; foo + 1`, expr.Env(mock.Env{})) require.NoError(t, err) var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() out, err := expr.Run(program, mock.Env{}) require.NoError(t, err) require.Equal(t, 2, out) }() } wg.Wait() } func TestOperatorDependsOnEnv(t *testing.T) { env := map[string]any{ "plus": func(a, b int) int { return 42 }, } program, err := expr.Compile(`1 + 2`, expr.Operator("+", "plus"), expr.Env(env)) require.NoError(t, err) out, err := expr.Run(program, env) require.NoError(t, err) assert.Equal(t, 42, out) } func TestIssue624(t *testing.T) { type tag struct { Name string } type item struct { Tags []tag } i := item{ Tags: []tag{ {Name: "one"}, {Name: "two"}, }, } rule := `[ true && true, one(Tags, .Name in ["one"]), one(Tags, .Name in ["two"]), one(Tags, .Name in ["one"]) && one(Tags, .Name in ["two"]) ]` resp, err := expr.Eval(rule, i) require.NoError(t, err) require.Equal(t, []interface{}{true, true, true, true}, resp) } func TestPredicateCombination(t *testing.T) { tests := []struct { code1 string code2 string }{ {"all(1..3, {# > 0}) && all(1..3, {# < 4})", "all(1..3, {# > 0 && # < 4})"}, {"all(1..3, {# > 1}) && all(1..3, {# < 4})", "all(1..3, {# > 1 && # < 4})"}, {"all(1..3, {# > 0}) && all(1..3, {# < 2})", "all(1..3, {# > 0 && # < 2})"}, {"all(1..3, {# > 1}) && all(1..3, {# < 2})", "all(1..3, {# > 1 && # < 2})"}, {"any(1..3, {# > 0}) || any(1..3, {# < 4})", "any(1..3, {# > 0 || # < 4})"}, {"any(1..3, {# > 1}) || any(1..3, {# < 4})", "any(1..3, {# > 1 || # < 4})"}, {"any(1..3, {# > 0}) || any(1..3, {# < 2})", "any(1..3, {# > 0 || # < 2})"}, {"any(1..3, {# > 1}) || any(1..3, {# < 2})", "any(1..3, {# > 1 || # < 2})"}, {"none(1..3, {# > 0}) && none(1..3, {# < 4})", "none(1..3, {# > 0 || # < 4})"}, {"none(1..3, {# > 1}) && none(1..3, {# < 4})", "none(1..3, {# > 1 || # < 4})"}, {"none(1..3, {# > 0}) && none(1..3, {# < 2})", "none(1..3, {# > 0 || # < 2})"}, {"none(1..3, {# > 1}) && none(1..3, {# < 2})", "none(1..3, {# > 1 || # < 2})"}, } for _, tt := range tests { t.Run(tt.code1, func(t *testing.T) { out1, err := expr.Eval(tt.code1, nil) require.NoError(t, err) out2, err := expr.Eval(tt.code2, nil) require.NoError(t, err) require.Equal(t, out1, out2) }) } } func TestArrayComparison(t *testing.T) { tests := []struct { env any code string }{ {[]string{"A", "B"}, "foo == ['A', 'B']"}, {[]int{1, 2}, "foo == [1, 2]"}, {[]uint8{1, 2}, "foo == [1, 2]"}, {[]float64{1.1, 2.2}, "foo == [1.1, 2.2]"}, {[]any{"A", 1, 1.1, true}, "foo == ['A', 1, 1.1, true]"}, {[]string{"A", "B"}, "foo != [1, 2]"}, } for _, tt := range tests { t.Run(tt.code, func(t *testing.T) { env := map[string]any{"foo": tt.env} program, err := expr.Compile(tt.code, expr.Env(env)) require.NoError(t, err) out, err := expr.Run(program, env) require.NoError(t, err) require.Equal(t, true, out) }) } } func TestIssue_570(t *testing.T) { type Student struct { Name string } env := map[string]any{ "student": (*Student)(nil), } program, err := expr.Compile("student?.Name", expr.Env(env)) require.NoError(t, err) out, err := expr.Run(program, env) require.NoError(t, err) require.IsType(t, nil, out) } func TestIssue_integer_truncated_by_compiler(t *testing.T) { env := map[string]any{ "fn": func(x byte) byte { return x }, } _, err := expr.Compile("fn(255)", expr.Env(env)) require.NoError(t, err) _, err = expr.Compile("fn(256)", expr.Env(env)) require.Error(t, err) } func TestExpr_crash(t *testing.T) { content, err := os.ReadFile("testdata/crash.txt") require.NoError(t, err) _, err = expr.Compile(string(content)) require.Error(t, err) } func TestExpr_nil_op_str(t *testing.T) { // Let's test operators, which do `.(string)` in VM, also check for nil. var str *string = nil env := map[string]any{ "nilString": str, } tests := []struct{ code string }{ {`nilString == "str"`}, {`nilString contains "str"`}, {`nilString matches "str"`}, {`nilString startsWith "str"`}, {`nilString endsWith "str"`}, {`"str" == nilString`}, {`"str" contains nilString`}, {`"str" matches nilString`}, {`"str" startsWith nilString`}, {`"str" endsWith nilString`}, } for _, tt := range tests { t.Run(tt.code, func(t *testing.T) { program, err := expr.Compile(tt.code) require.NoError(t, err) output, err := expr.Run(program, env) require.NoError(t, err) require.Equal(t, false, output) }) } } func TestExpr_env_types_map(t *testing.T) { envTypes := types.Map{ "foo": types.StrictMap{ "bar": types.String, }, } program, err := expr.Compile(`foo.bar`, expr.Env(envTypes)) require.NoError(t, err) env := map[string]any{ "foo": map[string]any{ "bar": "value", }, } output, err := expr.Run(program, env) require.NoError(t, err) require.Equal(t, "value", output) } func TestExpr_env_types_map_error(t *testing.T) { envTypes := types.Map{ "foo": types.StrictMap{ "bar": types.String, }, } program, err := expr.Compile(`foo.bar`, expr.Env(envTypes)) require.NoError(t, err) _, err = expr.Run(program, envTypes) require.Error(t, err) }