diff --git a/checker/checker.go b/checker/checker.go index d36d0aad7..564086647 100644 --- a/checker/checker.go +++ b/checker/checker.go @@ -51,7 +51,7 @@ okay: } type visitor struct { - types conf.TypesTable + types conf.TypeFinder operators conf.OperatorsTable expect reflect.Kind collections []reflect.Type @@ -124,7 +124,7 @@ func (v *visitor) IdentifierNode(node *ast.IdentifierNode) reflect.Type { if v.types == nil { return interfaceType } - if t, ok := v.types[node.Value]; ok { + if t, ok := v.types.LookupType(node.Value); ok { return t.Type } panic(v.error(node, "unknown name %v", node.Value)) @@ -312,7 +312,7 @@ func (v *visitor) SliceNode(node *ast.SliceNode) reflect.Type { } func (v *visitor) FunctionNode(node *ast.FunctionNode) reflect.Type { - if f, ok := v.types[node.Name]; ok { + if f, ok := v.types.LookupType(node.Name); ok { if fn, ok := isFuncType(f.Type); ok { if isInterface(fn) { return interfaceType diff --git a/checker/patcher.go b/checker/patcher.go index 88f512b5a..314a1d77d 100644 --- a/checker/patcher.go +++ b/checker/patcher.go @@ -8,7 +8,7 @@ import ( type operatorPatcher struct { ops map[string][]string - types conf.TypesTable + types conf.TypeFinder } func (p *operatorPatcher) Enter(node *ast.Node) {} diff --git a/docs/Usage.md b/docs/Usage.md index 778bbb4bf..7335f9069 100644 --- a/docs/Usage.md +++ b/docs/Usage.md @@ -21,7 +21,7 @@ fmt.Println(out) // outputs 3 ## Passing in Variables -You can also pass variables into the expression, which can be map or struct: +You can also pass variables into the expression, which can be map, struct, or lookup function: ```go env := map[string]interface{}{ @@ -35,6 +35,14 @@ env := Env{ Bar: ... } +// or +env := func(identifier string) interface{} { + switch (ident) { + case "Foo": ... + case "Bar": ... + } +} + // Pass env option to compile for static type checking. program, err := expr.Compile(`Foo == Bar`, expr.Env(env)) diff --git a/expr.go b/expr.go index 82bf16d69..a35f294ff 100644 --- a/expr.go +++ b/expr.go @@ -35,13 +35,19 @@ func Eval(input string, env interface{}) (interface{}, error) { // If struct is passed, all fields will be treated as variables, // as well as all fields of embedded structs and struct itself. // If map is passed, all items will be treated as variables. +// If func(string) interface{} is passed, variables will be +// looked up dynamically. // Methods defined on this type will be available as functions. func Env(i interface{}) conf.Option { return func(c *conf.Config) { if _, ok := i.(map[string]interface{}); ok { c.MapEnv = true + c.Types = conf.CreateTypesTable(i) + } else if fn, ok := i.(func(string) interface{}); ok { + c.Types = conf.TypesFunction(fn) + } else { + c.Types = conf.CreateTypesTable(i) } - c.Types = conf.CreateTypesTable(i) } } diff --git a/expr_test.go b/expr_test.go index 61da6517a..a9efbaebf 100644 --- a/expr_test.go +++ b/expr_test.go @@ -903,3 +903,53 @@ type segment struct { Destination string Date time.Time } + +func envFunction(key string) interface{} { + switch key { + case "name": + return "Dave" + case "i": + return int(10) + case "upper": + return strings.ToUpper + case "now": + return time.Now() + } + return nil +} + +func TestEval_func(t *testing.T) { + tests := []struct { + Input string + Result interface{} + }{ + {`i + i`, int(20)}, + {`name`, `Dave`}, + {`upper(name)`, `DAVE`}, + {`now.IsZero()`, false}, + } + + for _, tt := range tests { + program, err := expr.Compile(tt.Input, expr.Env(envFunction)) + require.NoError(t, err) + + result, err := expr.Run(program, envFunction) + require.NoError(t, err, program.Disassemble()) + require.Equal(t, tt.Result, result) + } +} + +func TestEval_funcTypeCheck(t *testing.T) { + tests := []string{ + `i + name`, + `name()`, + `upper(123)`, + `now.IsUnknown()`, + `unknown`, + } + + for _, tt := range tests { + _, err := expr.Compile(tt, expr.Env(envFunction)) + require.Error(t, err, tt) + } +} diff --git a/internal/conf/config.go b/internal/conf/config.go index 56991b1f0..1c641ea28 100644 --- a/internal/conf/config.go +++ b/internal/conf/config.go @@ -7,23 +7,27 @@ import ( type Config struct { MapEnv bool - Types TypesTable + Types TypeFinder Operators OperatorsTable Expect reflect.Kind Optimize bool } func New(i interface{}) *Config { - var mapEnv bool - if _, ok := i.(map[string]interface{}); ok { - mapEnv = true + config := &Config{ + Optimize: true, } - return &Config{ - MapEnv: mapEnv, - Types: CreateTypesTable(i), - Optimize: true, + if _, ok := i.(map[string]interface{}); ok { + config.MapEnv = true + config.Types = CreateTypesTable(i) + } else if fn, ok := i.(func(string) interface{}); ok { + config.Types = TypesFunction(fn) + } else { + config.Types = CreateTypesTable(i) } + + return config } // Check validates the compiler configuration. @@ -32,7 +36,7 @@ func (c *Config) Check() error { // exist in environment and have correct signatures. for op, fns := range c.Operators { for _, fn := range fns { - fnType, ok := c.Types[fn] + fnType, ok := c.Types.LookupType(fn) if !ok || fnType.Type.Kind() != reflect.Func { return fmt.Errorf("function %s for %s operator does not exist in environment", fn, op) } diff --git a/internal/conf/operators_table.go b/internal/conf/operators_table.go index 5368366db..d2ffacd9e 100644 --- a/internal/conf/operators_table.go +++ b/internal/conf/operators_table.go @@ -6,9 +6,9 @@ import "reflect" // Functions should be provided in the environment to allow operator overloading. type OperatorsTable map[string][]string -func FindSuitableOperatorOverload(fns []string, types TypesTable, l, r reflect.Type) (reflect.Type, string, bool) { +func FindSuitableOperatorOverload(fns []string, types TypeFinder, l, r reflect.Type) (reflect.Type, string, bool) { for _, fn := range fns { - fnType := types[fn] + fnType, _ := types.LookupType(fn) firstInIndex := 0 if fnType.Method { firstInIndex = 1 // As first argument to method is receiver. diff --git a/internal/conf/types_table.go b/internal/conf/types_table.go index 760b180f1..607c86bfb 100644 --- a/internal/conf/types_table.go +++ b/internal/conf/types_table.go @@ -7,8 +7,30 @@ type Tag struct { Method bool } +type TypeFinder interface { + LookupType(identifier string) (Tag, bool) +} + type TypesTable map[string]Tag +func (t TypesTable) LookupType(identifier string) (Tag, bool) { + tag, ok := t[identifier] + return tag, ok +} + +type TypesFunction func(identifier string) interface{} + +func (fn TypesFunction) LookupType(identifier string) (Tag, bool) { + i := fn(identifier) + if i == nil { + return Tag{}, false + } + + return Tag{ + Type: reflect.TypeOf(i), + }, true +} + // CreateTypesTable creates types table for type checks during parsing. // If struct is passed, all fields will be treated as variables, // as well as all fields of embedded structs and struct itself. diff --git a/vm/runtime.go b/vm/runtime.go index e9e8874bc..c5aac89c3 100644 --- a/vm/runtime.go +++ b/vm/runtime.go @@ -15,7 +15,13 @@ type Call struct { type Scope map[string]interface{} -func fetch(from interface{}, i interface{}) interface{} { +func fetch(op byte, from interface{}, i interface{}) interface{} { + if op == OpFetch { + if fn, ok := from.(func(string) interface{}); ok { + return fn(i.(string)) + } + } + v := reflect.ValueOf(from) switch v.Kind() { @@ -41,7 +47,7 @@ func fetch(from interface{}, i interface{}) interface{} { case reflect.Ptr: value := v.Elem() if value.IsValid() && value.CanInterface() { - return fetch(value.Interface(), i) + return fetch(op, value.Interface(), i) } } @@ -68,7 +74,13 @@ func slice(array, from, to interface{}) interface{} { panic(fmt.Sprintf("cannot slice %v", from)) } -func fetchFn(from interface{}, name string) reflect.Value { +func fetchFn(op byte, from interface{}, name string) reflect.Value { + if op == OpCall { + if fn, ok := from.(func(string) interface{}); ok { + return reflect.ValueOf(fn(name)) + } + } + v := reflect.ValueOf(from) // Methods can be defined on any type. diff --git a/vm/vm.go b/vm/vm.go index 1f817cc0f..bb0f51d8d 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -79,7 +79,7 @@ func (vm *VM) Run(program *Program, env interface{}) interface{} { vm.push(a) case OpFetch: - vm.push(fetch(env, vm.constants[vm.arg()])) + vm.push(fetch(op, env, vm.constants[vm.arg()])) case OpFetchMap: vm.push(env.(map[string]interface{})[vm.constants[vm.arg()].(string)]) @@ -229,7 +229,7 @@ func (vm *VM) Run(program *Program, env interface{}) interface{} { case OpIndex: b := vm.pop() a := vm.pop() - vm.push(fetch(a, b)) + vm.push(fetch(op, a, b)) case OpSlice: from := vm.pop() @@ -240,7 +240,7 @@ func (vm *VM) Run(program *Program, env interface{}) interface{} { case OpProperty: a := vm.pop() b := vm.constants[vm.arg()] - vm.push(fetch(a, b)) + vm.push(fetch(op, a, b)) case OpCall: call := vm.constants[vm.arg()].(Call) @@ -250,7 +250,7 @@ func (vm *VM) Run(program *Program, env interface{}) interface{} { in[i] = reflect.ValueOf(vm.pop()) } - out := fetchFn(env, call.Name).Call(in) + out := fetchFn(op, env, call.Name).Call(in) vm.push(out[0].Interface()) case OpMethod: @@ -263,7 +263,7 @@ func (vm *VM) Run(program *Program, env interface{}) interface{} { obj := vm.pop() - out := fetchFn(obj, call.Name).Call(in) + out := fetchFn(op, obj, call.Name).Call(in) vm.push(out[0].Interface()) case OpArray: