package expr_test import ( "fmt" "strings" "testing" "time" "github.com/antonmedv/expr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func ExampleEval() { output, err := expr.Eval("'hello world'", nil) if err != nil { fmt.Printf("err: %v", err) return } fmt.Printf("%v", output) // Output: hello world } func ExampleEval_map() { env := map[string]interface{}{ "foo": 1, "bar": []string{"zero", "hello world"}, "swipe": func(in string) string { return strings.Replace(in, "world", "user", 1) }, } output, err := expr.Eval("swipe(bar[foo])", env) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output) // Output: hello user } func ExampleEval_map_method() { env := mockMapEnv{ "foo": 1, "bar": []string{"zero", "hello world"}, } program, err := expr.Compile("Swipe(bar[foo])", 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: hello user } func ExampleEval_struct() { type C struct{ C int } type B struct{ B *C } type A struct{ A B } env := A{B{&C{42}}} output, err := expr.Eval("A.B.C", env) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output) // Output: 42 } func ExampleEval_error() { output, err := expr.Eval("(boo + bar]", nil) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output) // Output: syntax error: mismatched input ']' expecting ')' (1:11) // | (boo + bar] // | ..........^ } func ExampleEval_matches() { output, err := expr.Eval(`"a" matches "a("`, nil) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output) // Output: error parsing regexp: missing closing ): `a(` (1:13) // | "a" matches "a(" // | ............^ } func ExampleCompile() { env := map[string]interface{}{ "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 Request struct { Segments []*Segment Passengers *Passengers Marker string Meta map[string]interface{} } code := `Segments[0].Origin == "MOW" && Passengers.Adults == 2 && Marker == "test" && Meta["accept"]` program, err := expr.Compile(code, expr.Env(&Request{})) if err != nil { fmt.Printf("%v", err) return } request := &Request{ Segments: []*Segment{ {Origin: "MOW"}, }, Passengers: &Passengers{ Adults: 2, }, Marker: "test", Meta: map[string]interface{}{"accept": true}, } output, err := expr.Run(program, request) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output) // Output: true } func ExampleEnv_with_undefined_variables() { env := map[string]interface{}{ "foo": 0, "bar": 0, } program, err := expr.Compile(`foo + (bar != nil ? bar : 2)`, expr.Env(env)) if err != nil { fmt.Printf("%v", err) return } request := map[string]interface{}{ "foo": 3, } output, err := expr.Run(program, request) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output) // Output: 5 } func ExampleEnv_allow_undefined_variables() { env := map[string]string{ "greet": "", } program, err := expr.Compile(`greet + name`, expr.Env(env), expr.AllowUndefinedVariables()) if err != nil { fmt.Printf("%v", err) return } params := map[string]string{ "greet": "hello, ", "name": "world", } output, err := expr.Run(program, params) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output) // Output: hello, world } 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 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 ExampleAsInt64() { env := map[string]float64{ "foo": 3, } program, err := expr.Compile("foo + 2", 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 ExampleOperator() { type Place struct { Code string } type Segment struct { Origin Place } type Helpers struct { PlaceEq func(p Place, s string) bool } type Request struct { Segments []*Segment Helpers } code := `Segments[0].Origin == "MOW" && PlaceEq(Segments[0].Origin, "MOW")` program, err := expr.Compile(code, expr.Env(&Request{}), expr.Operator("==", "PlaceEq")) if err != nil { fmt.Printf("%v", err) return } request := &Request{ Segments: []*Segment{ {Origin: Place{Code: "MOW"}}, }, Helpers: Helpers{PlaceEq: func(p Place, s string) bool { return p.Code == s }}, } output, err := expr.Run(program, request) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output) // Output: true } func ExampleOperator_time() { type Segment struct { Date time.Time } type Request struct { Segments []Segment Before func(a, b time.Time) bool Date func(s string) time.Time } code := `Date("2001-01-01") < Segments[0].Date` program, err := expr.Compile(code, expr.Env(&Request{}), expr.Operator("<", "Before")) if err != nil { fmt.Printf("%v", err) return } request := &Request{ Segments: []Segment{ {Date: time.Date(2019, 7, 1, 0, 0, 0, 0, time.UTC)}, }, Before: func(a, b time.Time) bool { return a.Before(b) }, Date: func(s string) time.Time { date, err := time.Parse("2006-01-02", s) if err != nil { panic(err) } return date }, } output, err := expr.Run(program, request) if err != nil { fmt.Printf("%v", err) return } fmt.Printf("%v", output) // Output: true } func TestOperator_struct(t *testing.T) { env := &mockEnv{ BirthDay: time.Date(2017, time.October, 23, 18, 30, 0, 0, time.UTC), } code := `BirthDay == "2017-10-23"` program, err := expr.Compile(code, expr.Env(&mockEnv{}), expr.Operator("==", "DateEqual")) require.NoError(t, err) output, err := expr.Run(program, env) require.NoError(t, err) require.Equal(t, true, output) } func TestOperator_interface(t *testing.T) { env := &mockEnv{ Ticket: &ticket{Price: 100}, } code := `Ticket == "$100" && "$100" == Ticket && Now != Ticket && Now == Now` program, err := expr.Compile( code, expr.Env(&mockEnv{}), expr.Operator("==", "StringerStringEqual", "StringStringerEqual", "StringerStringerEqual"), expr.Operator("!=", "NotStringerStringEqual", "NotStringStringerEqual", "NotStringerStringerEqual"), ) require.NoError(t, err) output, err := expr.Run(program, env) require.NoError(t, err) require.Equal(t, true, output) } func TestExpr(t *testing.T) { env := &mockEnv{ Any: "any", Int: 0, Int32: 0, Int64: 0, Uint64: 0, Float64: 0, Bool: true, String: "string", Array: []int{1, 2, 3, 4, 5}, Ticket: &ticket{ Price: 100, }, Passengers: &passengers{ Adults: 1, }, Segments: []*segment{ {Origin: "MOW", Destination: "LED"}, {Origin: "LED", Destination: "MOW"}, }, BirthDay: time.Date(2017, time.October, 23, 18, 30, 0, 0, time.UTC), Now: time.Now(), One: 1, Two: 2, Three: 3, MultiDimArray: [][]int{{1, 2, 3}, {1, 2, 3}}, Sum: func(list []int) int { var ret int for _, el := range list { ret += el } return ret }, Inc: func(a int) int { return a + 1 }, Nil: nil, } tests := []struct { code string want interface{} }{ { `1`, int(1), }, { `-.5`, float64(-.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`, int(0), }, { `Uint64 + Int64`, int64(0), }, { `Int32 + Int64`, 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-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} && "Price" in Ticket`, true, }, { `1 in [1.5] || 1 not in [1]`, false, }, { `One in 0..1 && Two not in 0..1`, true, }, { `Int32 in [10, 20]`, false, }, { `String matches "s.+"`, true, }, { `String matches ("^" + String + "$")`, true, }, { `"foobar" contains "bar"`, true, }, { `"foobar" startsWith "foo"`, true, }, { `"foobar" endsWith "bar"`, true, }, { `(0..10)[5]`, 5, }, { `Ticket.Price`, 100, }, { `Add(10, 5) + GetInt()`, 15, }, { `Ticket.String()`, `$100`, }, { `Ticket.PriceDiv(25)`, 4, }, { `len([1, 2, 3])`, 3, }, { `len([1, Two, 3])`, 3, }, { `len(["hello", "world"])`, 2, }, { `{foo: 0, bar: 1}`, map[string]interface{}{"foo": 0, "bar": 1}, }, { `{foo: 0, bar: 1}`, map[string]interface{}{"foo": 0, "bar": 1}, }, { `(true ? 0+1 : 2+3) + (false ? -1 : -2)`, -1, }, { `len(Array)`, 5, }, { `filter(1..9, {# > 7})`, []interface{}{8, 9}, }, { `map(1..3, {# * #})`, []interface{}{1, 4, 9}, }, { `all(1..3, {# > 0})`, true, }, { `none(1..3, {# == 0})`, true, }, { `any([1,1,0,1], {# == 0})`, true, }, { `one([1,1,0,1], {# == 0}) and not one([1,0,0,1], {# == 0})`, true, }, { `Now.After(BirthDay)`, true, }, { `"a" < "b"`, true, }, { `Now.Sub(Now).String() == Duration("0s").String()`, true, }, { `8.5 * Passengers.Adults * len(Segments)`, float64(17), }, { `1 + 1`, 2, }, { `(One * Two) * Three == One * (Two * Three)`, true, }, { `Array[0]`, 1, }, { `Sum(Array)`, 15, }, { `Array[0] < Array[1]`, true, }, { `Sum(MultiDimArray[0])`, 6, }, { `Sum(MultiDimArray[0]) + Sum(MultiDimArray[1])`, 12, }, { `Inc(Array[0] + Array[1])`, 4, }, { `Array[0] + Array[1]`, 3, }, { `Array[1:2]`, []int{2}, }, { `Array[0:5] == Array`, true, }, { `Array[0:] == Array`, true, }, { `Array[:5] == Array`, true, }, { `Array[:] == Array`, true, }, { `1 + 2 + Three`, 6, }, { `MapArg({foo: "bar"})`, "bar", }, { `NilStruct`, (*time.Time)(nil), }, { `Nil == nil && nil == Nil && nil == nil && Nil == Nil && NilInt == nil && NilSlice == nil && NilStruct == nil`, true, }, { `0 == nil || "str" == nil || true == nil`, false, }, { `Variadic("head", 1, 2, 3)`, []int{1, 2, 3}, }, { `Variadic("empty")`, []int{}, }, { `String[:]`, "string", }, { `String[:3]`, "str", }, { `String[:9]`, "string", }, { `String[3:9]`, "ing", }, { `String[7:9]`, "", }, { `Float(0)`, float64(0), }, } for _, tt := range tests { program, err := expr.Compile(tt.code, expr.Env(&mockEnv{})) require.NoError(t, err, tt.code) got, err := expr.Run(program, env) require.NoError(t, err, tt.code) assert.Equal(t, tt.want, got, tt.code) } for _, tt := range tests { program, err := expr.Compile(tt.code, expr.Optimize(false)) require.NoError(t, err, tt.code) got, err := expr.Run(program, env) require.NoError(t, err, tt.code) assert.Equal(t, tt.want, got, "unoptimized: "+tt.code) } for _, tt := range tests { got, err := expr.Eval(tt.code, env) require.NoError(t, err, tt.code) assert.Equal(t, tt.want, got, "eval: "+tt.code) } } func TestExpr_eval_with_env(t *testing.T) { _, err := expr.Eval("true", expr.Env(map[string]interface{}{})) 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]interface{}{ "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]interface{}{ "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 interface{} input string }{ { mockMapStringStringEnv{"foo": "bar"}, `Split(foo, sep)`, }, { mockMapStringIntEnv{"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]interface{}{ "equals": func(a, b interface{}) interface{} { assert.Nil(t, a, "a is not nil") assert.Nil(t, b, "b is not nil") return a == b }, "is": 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) } // // Mock types // type mockEnv struct { Any interface{} Int, One, Two, Three int Int32 int32 Int64 int64 Uint64 uint64 Float64 float64 Bool bool String string Array []int MultiDimArray [][]int Sum func(list []int) int Inc func(int) int Ticket *ticket Passengers *passengers Segments []*segment BirthDay time.Time Now time.Time Nil interface{} NilStruct *time.Time NilInt *int NilSlice []ticket } func (e *mockEnv) GetInt() int { return e.Int } func (*mockEnv) Add(a, b int) int { return int(a + b) } func (*mockEnv) Duration(s string) time.Duration { d, err := time.ParseDuration(s) if err != nil { panic(err) } return d } func (*mockEnv) MapArg(m map[string]interface{}) string { return m["foo"].(string) } func (*mockEnv) DateEqual(date time.Time, s string) bool { return date.Format("2006-01-02") == s } func (*mockEnv) StringerStringEqual(f fmt.Stringer, s string) bool { return f.String() == s } func (*mockEnv) StringStringerEqual(s string, f fmt.Stringer) bool { return s == f.String() } func (*mockEnv) StringerStringerEqual(f fmt.Stringer, g fmt.Stringer) bool { return f.String() == g.String() } func (*mockEnv) NotStringerStringEqual(f fmt.Stringer, s string) bool { return f.String() != s } func (*mockEnv) NotStringStringerEqual(s string, f fmt.Stringer) bool { return s != f.String() } func (*mockEnv) NotStringerStringerEqual(f fmt.Stringer, g fmt.Stringer) bool { return f.String() != g.String() } func (*mockEnv) Variadic(x string, xs ...int) []int { return xs } func (*mockEnv) Float(i interface{}) float64 { switch t := i.(type) { case int: return float64(t) case float64: return t default: panic("unexpected type") } } type ticket struct { Price int } func (t *ticket) PriceDiv(p int) int { return t.Price / p } func (t *ticket) String() string { return fmt.Sprintf("$%v", t.Price) } type passengers struct { Adults uint32 Children uint32 Infants uint32 } type segment struct { Origin string Destination string Date time.Time } type mockMapEnv map[string]interface{} func (mockMapEnv) Swipe(in string) string { return strings.Replace(in, "world", "user", 1) } type mockMapStringStringEnv map[string]string func (m mockMapStringStringEnv) Split(s, sep string) []string { return strings.Split(s, sep) } type mockMapStringIntEnv map[string]int type is struct{} func (is) Nil(a interface{}) bool { return a == nil }