├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── benchmarks_test.go ├── evaluable.go ├── evaluable_test.go ├── example_test.go ├── functions.go ├── functions_test.go ├── go.mod ├── go.sum ├── gval.go ├── gval_evaluationFailure_test.go ├── gval_noparameter_test.go ├── gval_parameterized_test.go ├── gval_parsingFailure_test.go ├── gval_test.go ├── language.go ├── operator.go ├── operator_test.go ├── parse.go ├── parser.go ├── parser_test.go ├── prtg-batmin-gopher.png └── random_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | coverage.out 25 | 26 | manual_test.go 27 | *.out 28 | *.err 29 | 30 | .vscode -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | before_install: 4 | - go get golang.org/x/tools/cmd/cover 5 | - go get github.com/mattn/goveralls 6 | 7 | script: 8 | - go test -bench=. -benchmem -timeout 9m -coverprofile coverage.out 9 | - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken=$COVERALLS_TOKEN 10 | - go test -bench=Random -benchtime 3m -timeout 9m -benchmem -coverprofile coverage.out 11 | 12 | go: "1.15" 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Paessler AG 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gval 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/PaesslerAG/gval.svg)](https://pkg.go.dev/github.com/PaesslerAG/gval) 4 | [![Build Status](https://api.travis-ci.org/PaesslerAG/gval.svg?branch=master)](https://travis-ci.org/PaesslerAG/gval) 5 | [![Coverage Status](https://coveralls.io/repos/github/PaesslerAG/gval/badge.svg?branch=master)](https://coveralls.io/github/PaesslerAG/gval?branch=master) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/PaesslerAG/gval)](https://goreportcard.com/report/github.com/PaesslerAG/gval) 7 | 8 | Gval (Go eVALuate) provides support for evaluating arbitrary expressions, in particular Go-like expressions. 9 | 10 | ![gopher](./prtg-batmin-gopher.png) 11 | 12 | ## Evaluate 13 | 14 | Gval can evaluate expressions with parameters, arimethetic, logical, and string operations: 15 | 16 | - basic expression: [10 > 0](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Basic) 17 | - parameterized expression: [foo > 0](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Parameter) 18 | - nested parameterized expression: [foo.bar > 0](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-NestedParameter) 19 | - arithmetic expression: [(requests_made * requests_succeeded / 100) >= 90](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Arithmetic) 20 | - string expression: [http_response_body == "service is ok"](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-String) 21 | - float64 expression: [(mem_used / total_mem) * 100](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Float64) 22 | 23 | It can easily be extended with custom functions or operators: 24 | 25 | - custom date comparator: [date(\`2014-01-02\`) > date(\`2014-01-01 23:59:59\`)](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-DateComparison) 26 | - string length: [strlen("someReallyLongInputString") <= 16](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Strlen) 27 | 28 | You can parse gval.Expressions once and re-use them multiple times. Parsing is the compute-intensive phase of the process, so if you intend to use the same expression with different parameters, just parse it once: 29 | 30 | - [Parsing and Evaluation](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluable) 31 | 32 | The normal Go-standard order of operators is respected. When writing an expression, be sure that you either order the operators correctly, or use parentheses to clarify which portions of an expression should be run first. 33 | 34 | Strings, numbers, and booleans can be used like in Go: 35 | 36 | - [(7 < "47" == true ? "hello world!\n\u263a") + \` more text\`](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Encoding) 37 | 38 | ## Parameter 39 | 40 | Variables can be accessed via string literals. They can be used for values with string keys if the parameter is a `map[string]interface{}` or `map[interface{}]interface{}` and for fields or methods if the parameter is a struct. 41 | 42 | - [foo > 0](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Parameter) 43 | 44 | ### Bracket Selector 45 | 46 | Map and array elements and Struct Field can be accessed via `[]`. 47 | 48 | - [foo[0]](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Array) 49 | - [foo["b" + "a" + "r"]](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-ExampleEvaluate_ComplexAccessor) 50 | 51 | ### Dot Selector 52 | 53 | A nested variable with a name containing only letters and underscores can be accessed via a dot selector. 54 | 55 | - [foo.bar > 0](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-NestedParameter) 56 | 57 | ### Custom Selector 58 | 59 | Parameter names like `response-time` will be interpreted as `response` minus `time`. While gval doesn't support these parameter names directly, you can easily access them via a custom extension like [JSON Path](https://github.com/PaesslerAG/jsonpath): 60 | 61 | - [$["response-time"]](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Jsonpath) 62 | 63 | Jsonpath is also suitable for accessing array elements. 64 | 65 | ### Fields and Methods 66 | 67 | If you have structs in your parameters, you can access their fields and methods in the usual way: 68 | 69 | - [foo.Hello + foo.World()](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-FlatAccessor) 70 | 71 | It also works if the parameter is a struct directly 72 | [Hello + World()](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Accessor) 73 | or if the fields are nested 74 | [foo.Hello + foo.World()](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-NestedAccessor) 75 | 76 | This may be convenient but note that using accessors on strucs makes the expression about four times slower than just using a parameter (consult the benchmarks for more precise measurements on your system). If there are functions you want to use, it's faster (and probably cleaner) to define them as functions (see the Evaluate section). These approaches use no reflection, and are designed to be fast and clean. 77 | 78 | ## Default Language 79 | 80 | The default language is in serveral sub languages like text, arithmetic or propositional logic defined. See [Godoc](https://pkg.go.dev/github.com/PaesslerAG/gval/#Gval) for details. All sub languages are merged into gval.Full which contains the following elements: 81 | 82 | - Modifiers: `+` `-` `/` `*` `&` `|` `^` `**` `%` `>>` `<<` 83 | - Comparators: `>` `>=` `<` `<=` `==` `!=` `=~` `!~` 84 | - Logical ops: `||` `&&` 85 | - Numeric constants, as 64-bit floating point (`12345.678`) 86 | - String constants (double quotes: `"foobar"`) 87 | - Date function 'Date(x)', using any permutation of RFC3339, ISO8601, ruby date, or unix date 88 | - Boolean constants: `true` `false` 89 | - Parentheses to control order of evaluation `(` `)` 90 | - Json Arrays : `[1, 2, "foo"]` 91 | - Json Objects : `{"a":1, "b":2, "c":"foo"}` 92 | - Prefixes: `!` `-` `~` 93 | - Ternary conditional: `?` `:` 94 | - Null coalescence: `??` 95 | 96 | ## Customize 97 | 98 | Gval is completly customizable. Every constant, function or operator can be defined separately and existing expression languages can be reused: 99 | 100 | - [foo.Hello + foo.World()](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Language) 101 | 102 | For details see [Godoc](https://pkg.go.dev/github.com/PaesslerAG/gval). 103 | 104 | ### Implementing custom selector 105 | 106 | In a case you want to provide custom logic for selectors you can implement `SelectGVal(ctx context.Context, k string) (interface{}, error)` on your struct. 107 | Function receives next part of the path and can return any type of var that is again evaluated through standard gval procedures. 108 | 109 | [Example Custom Selector](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-custom-selector) 110 | 111 | ### External gval Languages 112 | 113 | A list of external libraries for gval. Feel free to add your own library. 114 | 115 | - [gvalstrings](https://github.com/generikvault/gvalstrings) parse single quoted strings in gval. 116 | - [jsonpath](https://github.com/PaesslerAG/jsonpath) full support for jsonpath in gval. 117 | 118 | ## Performance 119 | 120 | The library is built with the intention of being quick but has not been aggressively profiled and optimized. For most applications, though, it is completely fine. 121 | If performance is an issue, make sure to create your expression language with all functions, constants and operators only once. Evaluating an expression like gval.Evaluate("expression, const1, func1, func2, ...) creates a new gval.Language everytime it is called and slows execution. 122 | 123 | The library comes with a bunch of benchmarks to measure the performance of parsing and evaluating expressions. You can run them with `go test -bench=.`. 124 | 125 | For a very rough idea of performance, here are the results from a benchmark run on a Dell Latitude E7470 Win 10 i5-6300U. 126 | 127 | ``` text 128 | BenchmarkGval/const_evaluation-4 500000000 3.57 ns/op 129 | BenchmarkGval/const_parsing-4 1000000 1144 ns/op 130 | BenchmarkGval/single_parameter_evaluation-4 10000000 165 ns/op 131 | BenchmarkGval/single_parameter_parsing-4 1000000 1648 ns/op 132 | BenchmarkGval/parameter_evaluation-4 5000000 352 ns/op 133 | BenchmarkGval/parameter_parsing-4 500000 2773 ns/op 134 | BenchmarkGval/common_evaluation-4 3000000 434 ns/op 135 | BenchmarkGval/common_parsing-4 300000 4419 ns/op 136 | BenchmarkGval/complex_evaluation-4 100000000 11.6 ns/op 137 | BenchmarkGval/complex_parsing-4 100000 17936 ns/op 138 | BenchmarkGval/literal_evaluation-4 300000000 3.84 ns/op 139 | BenchmarkGval/literal_parsing-4 500000 2559 ns/op 140 | BenchmarkGval/modifier_evaluation-4 500000000 3.54 ns/op 141 | BenchmarkGval/modifier_parsing-4 500000 3755 ns/op 142 | BenchmarkGval/regex_evaluation-4 50000 21347 ns/op 143 | BenchmarkGval/regex_parsing-4 200000 6480 ns/op 144 | BenchmarkGval/constant_regex_evaluation-4 1000000 1000 ns/op 145 | BenchmarkGval/constant_regex_parsing-4 200000 9417 ns/op 146 | BenchmarkGval/accessors_evaluation-4 3000000 417 ns/op 147 | BenchmarkGval/accessors_parsing-4 1000000 1778 ns/op 148 | BenchmarkGval/accessors_method_evaluation-4 1000000 1931 ns/op 149 | BenchmarkGval/accessors_method_parsing-4 1000000 1729 ns/op 150 | BenchmarkGval/accessors_method_parameter_evaluation-4 1000000 2162 ns/op 151 | BenchmarkGval/accessors_method_parameter_parsing-4 500000 2618 ns/op 152 | BenchmarkGval/nested_accessors_evaluation-4 2000000 681 ns/op 153 | BenchmarkGval/nested_accessors_parsing-4 1000000 2115 ns/op 154 | BenchmarkRandom-4 500000 3631 ns/op 155 | ok 156 | ``` 157 | 158 | ## API Breaks 159 | 160 | Gval is designed with easy expandability in mind and API breaks will be avoided if possible. If API breaks are unavoidable they wil be explicitly stated via an increased major version number. 161 | 162 | ------------------------------------- 163 | Credits to Reene French for the gophers. 164 | -------------------------------------------------------------------------------- /benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package gval 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func BenchmarkGval(bench *testing.B) { 9 | benchmarks := []evaluationTest{ 10 | { 11 | // Serves as a "water test" to give an idea of the general overhead 12 | name: "const", 13 | expression: "1", 14 | }, 15 | { 16 | name: "single parameter", 17 | expression: "requests_made", 18 | parameter: map[string]interface{}{ 19 | "requests_made": 99.0, 20 | }, 21 | }, 22 | { 23 | name: "parameter", 24 | expression: "requests_made > requests_succeeded", 25 | parameter: map[string]interface{}{ 26 | "requests_made": 99.0, 27 | "requests_succeeded": 90.0, 28 | }, 29 | }, 30 | { 31 | // The most common use case, a single variable, modified slightly, compared to a constant. 32 | // This is the "expected" use case. 33 | name: "common", 34 | expression: "(requests_made * requests_succeeded / 100) >= 90", 35 | parameter: map[string]interface{}{ 36 | "requests_made": 99.0, 37 | "requests_succeeded": 90.0, 38 | }, 39 | }, 40 | { 41 | // All major possibilities in one expression. 42 | name: "complex", 43 | expression: `2 > 1 && 44 | "something" != "nothing" || 45 | date("2014-01-20") < date("Wed Jul 8 23:07:35 MDT 2015") && 46 | object["Variable name with spaces"] <= array[0] && 47 | modifierTest + 1000 / 2 > (80 * 100 % 2)`, 48 | parameter: map[string]interface{}{ 49 | "object": map[string]interface{}{"Variable name with spaces": 10.}, 50 | "array": []interface{}{0.}, 51 | "modifierTest": 7.3, 52 | }, 53 | }, 54 | { 55 | // no variables, no modifiers 56 | name: "literal", 57 | expression: "(2) > (1)", 58 | }, 59 | { 60 | name: "modifier", 61 | expression: "(2) + (2) == (4)", 62 | }, 63 | { 64 | // Benchmarks uncompiled parameter regex operators, which are the most expensive of the lot. 65 | // Note that regex compilation times are unpredictable and wily things. The regex engine has a lot of edge cases 66 | // and possible performance pitfalls. This test doesn't aim to be comprehensive against all possible regex scenarios, 67 | // it is primarily concerned with tracking how much longer it takes to compile a regex at evaluation-time than during parse-time. 68 | name: "regex", 69 | expression: "(foo !~ bar) && (foo + bar =~ oba)", 70 | parameter: map[string]interface{}{ 71 | "foo": "foo", 72 | "bar": "bar", 73 | "baz": "baz", 74 | "oba": ".*oba.*", 75 | }, 76 | }, 77 | { 78 | // Benchmarks pre-compilable regex patterns. Meant to serve as a sanity check that constant strings used as regex patterns 79 | // are actually being precompiled. 80 | // Also demonstrates that (generally) compiling a regex at evaluation-time takes an order of magnitude more time than pre-compiling. 81 | name: "constant regex", 82 | expression: `(foo !~ "[bB]az") && (bar =~ "[bB]ar")`, 83 | parameter: map[string]interface{}{ 84 | "foo": "foo", 85 | "bar": "bar", 86 | "baz": "baz", 87 | "oba": ".*oba.*", 88 | }, 89 | }, 90 | { 91 | name: "accessors", 92 | expression: "foo.Int", 93 | parameter: fooFailureParameters, 94 | }, 95 | { 96 | name: "accessors method", 97 | expression: "foo.Func()", 98 | parameter: fooFailureParameters, 99 | }, 100 | { 101 | name: "accessors method parameter", 102 | expression: `foo.FuncArgStr("bonk")`, 103 | parameter: fooFailureParameters, 104 | }, 105 | { 106 | name: "nested accessors", 107 | expression: `foo.Nested.Funk`, 108 | parameter: fooFailureParameters, 109 | }, 110 | { 111 | name: "decimal arithmetic", 112 | expression: "(requests_made * requests_succeeded / 100)", 113 | extension: decimalArithmetic, 114 | parameter: map[string]interface{}{ 115 | "requests_made": 99.0, 116 | "requests_succeeded": 90.0, 117 | }, 118 | }, 119 | { 120 | name: "decimal logic", 121 | expression: "(requests_made * requests_succeeded / 100) >= 90", 122 | extension: decimalArithmetic, 123 | parameter: map[string]interface{}{ 124 | "requests_made": 99.0, 125 | "requests_succeeded": 90.0, 126 | }, 127 | }, 128 | } 129 | for _, benchmark := range benchmarks { 130 | eval, err := Full().NewEvaluable(benchmark.expression) 131 | if err != nil { 132 | bench.Fatal(err) 133 | } 134 | _, err = eval(context.Background(), benchmark.parameter) 135 | if err != nil { 136 | bench.Fatal(err) 137 | } 138 | bench.Run(benchmark.name+"_evaluation", func(bench *testing.B) { 139 | for i := 0; i < bench.N; i++ { 140 | eval(context.Background(), benchmark.parameter) 141 | } 142 | }) 143 | bench.Run(benchmark.name+"_parsing", func(bench *testing.B) { 144 | for i := 0; i < bench.N; i++ { 145 | Full().NewEvaluable(benchmark.expression) 146 | } 147 | }) 148 | 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /evaluable.go: -------------------------------------------------------------------------------- 1 | package gval 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // Selector allows for custom variable selection from structs 13 | // 14 | // Return value is again handled with variable() until end of the given path 15 | type Selector interface { 16 | SelectGVal(c context.Context, key string) (interface{}, error) 17 | } 18 | 19 | // Evaluable evaluates given parameter 20 | type Evaluable func(c context.Context, parameter interface{}) (interface{}, error) 21 | 22 | // EvalInt evaluates given parameter to an int 23 | func (e Evaluable) EvalInt(c context.Context, parameter interface{}) (int, error) { 24 | v, err := e(c, parameter) 25 | if err != nil { 26 | return 0, err 27 | } 28 | 29 | f, ok := convertToFloat(v) 30 | if !ok { 31 | return 0, fmt.Errorf("expected number but got %v (%T)", v, v) 32 | } 33 | return int(f), nil 34 | } 35 | 36 | // EvalFloat64 evaluates given parameter to a float64 37 | func (e Evaluable) EvalFloat64(c context.Context, parameter interface{}) (float64, error) { 38 | v, err := e(c, parameter) 39 | if err != nil { 40 | return 0, err 41 | } 42 | 43 | f, ok := convertToFloat(v) 44 | if !ok { 45 | return 0, fmt.Errorf("expected number but got %v (%T)", v, v) 46 | } 47 | return f, nil 48 | } 49 | 50 | // EvalBool evaluates given parameter to a bool 51 | func (e Evaluable) EvalBool(c context.Context, parameter interface{}) (bool, error) { 52 | v, err := e(c, parameter) 53 | if err != nil { 54 | return false, err 55 | } 56 | 57 | b, ok := convertToBool(v) 58 | if !ok { 59 | return false, fmt.Errorf("expected bool but got %v (%T)", v, v) 60 | } 61 | return b, nil 62 | } 63 | 64 | // EvalString evaluates given parameter to a string 65 | func (e Evaluable) EvalString(c context.Context, parameter interface{}) (string, error) { 66 | o, err := e(c, parameter) 67 | if err != nil { 68 | return "", err 69 | } 70 | return fmt.Sprintf("%v", o), nil 71 | } 72 | 73 | // Const Evaluable represents given constant 74 | func (*Parser) Const(value interface{}) Evaluable { 75 | return constant(value) 76 | } 77 | 78 | //go:noinline 79 | func constant(value interface{}) Evaluable { 80 | return func(c context.Context, v interface{}) (interface{}, error) { 81 | return value, nil 82 | } 83 | } 84 | 85 | // Var Evaluable represents value at given path. 86 | // It supports with default language VariableSelector: 87 | // 88 | // map[interface{}]interface{}, 89 | // map[string]interface{} and 90 | // []interface{} and via reflect 91 | // struct fields, 92 | // struct methods, 93 | // slices and 94 | // map with int or string key. 95 | func (p *Parser) Var(path ...Evaluable) Evaluable { 96 | if p.selector == nil { 97 | return variable(path) 98 | } 99 | return p.selector(path) 100 | } 101 | 102 | // Evaluables is a slice of Evaluable. 103 | type Evaluables []Evaluable 104 | 105 | // EvalStrings evaluates given parameter to a string slice 106 | func (evs Evaluables) EvalStrings(c context.Context, parameter interface{}) ([]string, error) { 107 | strs := make([]string, len(evs)) 108 | for i, p := range evs { 109 | k, err := p.EvalString(c, parameter) 110 | if err != nil { 111 | return nil, err 112 | } 113 | strs[i] = k 114 | } 115 | return strs, nil 116 | } 117 | 118 | func variable(path Evaluables) Evaluable { 119 | return func(c context.Context, v interface{}) (interface{}, error) { 120 | keys, err := path.EvalStrings(c, v) 121 | if err != nil { 122 | return nil, err 123 | } 124 | for i, k := range keys { 125 | switch o := v.(type) { 126 | case Selector: 127 | v, err = o.SelectGVal(c, k) 128 | if err != nil { 129 | return nil, fmt.Errorf("failed to select '%s' on %T: %w", k, o, err) 130 | } 131 | continue 132 | case map[interface{}]interface{}: 133 | v = o[k] 134 | continue 135 | case map[string]interface{}: 136 | v = o[k] 137 | continue 138 | case []interface{}: 139 | if i, err := strconv.Atoi(k); err == nil && i >= 0 && len(o) > i { 140 | v = o[i] 141 | continue 142 | } 143 | default: 144 | var ok bool 145 | v, ok = reflectSelect(k, o) 146 | if !ok { 147 | return nil, fmt.Errorf("unknown parameter %s", strings.Join(keys[:i+1], ".")) 148 | } 149 | } 150 | } 151 | return v, nil 152 | } 153 | } 154 | 155 | func reflectSelect(key string, value interface{}) (selection interface{}, ok bool) { 156 | vv := reflect.ValueOf(value) 157 | vvElem := resolvePotentialPointer(vv) 158 | 159 | switch vvElem.Kind() { 160 | case reflect.Map: 161 | mapKey, ok := reflectConvertTo(vv.Type().Key().Kind(), key) 162 | if !ok { 163 | return nil, false 164 | } 165 | 166 | vvElem = vv.MapIndex(reflect.ValueOf(mapKey)) 167 | vvElem = resolvePotentialPointer(vvElem) 168 | 169 | if vvElem.IsValid() { 170 | return vvElem.Interface(), true 171 | } 172 | 173 | // key didn't exist. Check if there is a bound method 174 | method := vv.MethodByName(key) 175 | if method.IsValid() { 176 | return method.Interface(), true 177 | } 178 | 179 | case reflect.Slice: 180 | if i, err := strconv.Atoi(key); err == nil && i >= 0 && vv.Len() > i { 181 | vvElem = resolvePotentialPointer(vv.Index(i)) 182 | return vvElem.Interface(), true 183 | } 184 | 185 | // key not an int. Check if there is a bound method 186 | method := vv.MethodByName(key) 187 | if method.IsValid() { 188 | return method.Interface(), true 189 | } 190 | 191 | case reflect.Struct: 192 | field := vvElem.FieldByName(key) 193 | if field.IsValid() { 194 | return field.Interface(), true 195 | } 196 | 197 | method := vv.MethodByName(key) 198 | if method.IsValid() { 199 | return method.Interface(), true 200 | } 201 | } 202 | return nil, false 203 | } 204 | 205 | func resolvePotentialPointer(value reflect.Value) reflect.Value { 206 | if value.Kind() == reflect.Ptr { 207 | return value.Elem() 208 | } 209 | return value 210 | } 211 | 212 | func reflectConvertTo(k reflect.Kind, value string) (interface{}, bool) { 213 | switch k { 214 | case reflect.String: 215 | return value, true 216 | case reflect.Int: 217 | if i, err := strconv.Atoi(value); err == nil { 218 | return i, true 219 | } 220 | } 221 | return nil, false 222 | } 223 | 224 | func (*Parser) callFunc(fun function, args ...Evaluable) Evaluable { 225 | return func(c context.Context, v interface{}) (ret interface{}, err error) { 226 | a := make([]interface{}, len(args)) 227 | for i, arg := range args { 228 | ai, err := arg(c, v) 229 | if err != nil { 230 | return nil, err 231 | } 232 | a[i] = ai 233 | } 234 | return fun(c, a...) 235 | } 236 | } 237 | 238 | func (*Parser) callEvaluable(fullname string, fun Evaluable, args ...Evaluable) Evaluable { 239 | return func(c context.Context, v interface{}) (ret interface{}, err error) { 240 | f, err := fun(c, v) 241 | 242 | if err != nil { 243 | return nil, fmt.Errorf("could not call function: %w", err) 244 | } 245 | 246 | defer func() { 247 | if r := recover(); r != nil { 248 | err = fmt.Errorf("failed to execute function '%s': %s", fullname, r) 249 | ret = nil 250 | } 251 | }() 252 | 253 | ff := reflect.ValueOf(f) 254 | 255 | if ff.Kind() != reflect.Func { 256 | return nil, fmt.Errorf("could not call '%s' type %T", fullname, f) 257 | } 258 | 259 | a := make([]reflect.Value, len(args)) 260 | for i := range args { 261 | arg, err := args[i](c, v) 262 | if err != nil { 263 | return nil, err 264 | } 265 | a[i] = reflect.ValueOf(arg) 266 | } 267 | 268 | rr := ff.Call(a) 269 | 270 | r := make([]interface{}, len(rr)) 271 | for i, e := range rr { 272 | r[i] = e.Interface() 273 | } 274 | 275 | errorInterface := reflect.TypeOf((*error)(nil)).Elem() 276 | if len(r) > 0 && ff.Type().Out(len(r)-1).Implements(errorInterface) { 277 | if r[len(r)-1] != nil { 278 | err = r[len(r)-1].(error) 279 | } 280 | r = r[0 : len(r)-1] 281 | } 282 | 283 | switch len(r) { 284 | case 0: 285 | return err, nil 286 | case 1: 287 | return r[0], err 288 | default: 289 | return r, err 290 | } 291 | } 292 | } 293 | 294 | // IsConst returns if the Evaluable is a Parser.Const() value 295 | func (e Evaluable) IsConst() bool { 296 | pc := reflect.ValueOf(constant(nil)).Pointer() 297 | pe := reflect.ValueOf(e).Pointer() 298 | return pc == pe 299 | } 300 | 301 | func regEx(a, b Evaluable) (Evaluable, error) { 302 | if !b.IsConst() { 303 | return func(c context.Context, o interface{}) (interface{}, error) { 304 | a, err := a.EvalString(c, o) 305 | if err != nil { 306 | return nil, err 307 | } 308 | b, err := b.EvalString(c, o) 309 | if err != nil { 310 | return nil, err 311 | } 312 | matched, err := regexp.MatchString(b, a) 313 | return matched, err 314 | }, nil 315 | } 316 | s, err := b.EvalString(context.TODO(), nil) 317 | if err != nil { 318 | return nil, err 319 | } 320 | regex, err := regexp.Compile(s) 321 | if err != nil { 322 | return nil, err 323 | } 324 | return func(c context.Context, v interface{}) (interface{}, error) { 325 | s, err := a.EvalString(c, v) 326 | if err != nil { 327 | return nil, err 328 | } 329 | return regex.MatchString(s), nil 330 | }, nil 331 | } 332 | 333 | func notRegEx(a, b Evaluable) (Evaluable, error) { 334 | if !b.IsConst() { 335 | return func(c context.Context, o interface{}) (interface{}, error) { 336 | a, err := a.EvalString(c, o) 337 | if err != nil { 338 | return nil, err 339 | } 340 | b, err := b.EvalString(c, o) 341 | if err != nil { 342 | return nil, err 343 | } 344 | matched, err := regexp.MatchString(b, a) 345 | return !matched, err 346 | }, nil 347 | } 348 | s, err := b.EvalString(context.TODO(), nil) 349 | if err != nil { 350 | return nil, err 351 | } 352 | regex, err := regexp.Compile(s) 353 | if err != nil { 354 | return nil, err 355 | } 356 | return func(c context.Context, v interface{}) (interface{}, error) { 357 | s, err := a.EvalString(c, v) 358 | if err != nil { 359 | return nil, err 360 | } 361 | return !regex.MatchString(s), nil 362 | }, nil 363 | } 364 | -------------------------------------------------------------------------------- /evaluable_test.go: -------------------------------------------------------------------------------- 1 | package gval 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestEvaluable_IsConst(t *testing.T) { 13 | p := Parser{} 14 | tests := []struct { 15 | name string 16 | e Evaluable 17 | want bool 18 | }{ 19 | { 20 | "const", 21 | p.Const(80.5), 22 | true, 23 | }, 24 | { 25 | "var", 26 | p.Var(), 27 | false, 28 | }, 29 | } 30 | for _, tt := range tests { 31 | t.Run(tt.name, func(t *testing.T) { 32 | if got := tt.e.IsConst(); got != tt.want { 33 | t.Errorf("Evaluable.IsConst() = %v, want %v", got, tt.want) 34 | } 35 | }) 36 | } 37 | } 38 | 39 | func TestEvaluable_EvalInt(t *testing.T) { 40 | tests := []struct { 41 | name string 42 | e Evaluable 43 | want int 44 | wantErr bool 45 | }{ 46 | { 47 | "point", 48 | constant("5.3"), 49 | 5, 50 | false, 51 | }, 52 | { 53 | "number", 54 | constant(255.), 55 | 255, 56 | false, 57 | }, 58 | { 59 | "error", 60 | constant("5.3 cm"), 61 | 0, 62 | true, 63 | }, 64 | } 65 | for _, tt := range tests { 66 | t.Run(tt.name, func(t *testing.T) { 67 | got, err := tt.e.EvalInt(context.Background(), nil) 68 | if (err != nil) != tt.wantErr { 69 | t.Errorf("Evaluable.EvalInt() error = %v, wantErr %v", err, tt.wantErr) 70 | return 71 | } 72 | if got != tt.want { 73 | t.Errorf("Evaluable.EvalInt() = %v, want %v", got, tt.want) 74 | } 75 | }) 76 | } 77 | } 78 | 79 | func TestEvaluable_EvalFloat64(t *testing.T) { 80 | tests := []struct { 81 | name string 82 | e Evaluable 83 | want float64 84 | wantErr bool 85 | }{ 86 | { 87 | "point", 88 | constant("5.3"), 89 | 5.3, 90 | false, 91 | }, 92 | { 93 | "number", 94 | constant(255.), 95 | 255, 96 | false, 97 | }, 98 | { 99 | "error", 100 | constant("5.3 cm"), 101 | 0, 102 | true, 103 | }, 104 | } 105 | for _, tt := range tests { 106 | t.Run(tt.name, func(t *testing.T) { 107 | got, err := tt.e.EvalFloat64(context.Background(), nil) 108 | if (err != nil) != tt.wantErr { 109 | t.Errorf("Evaluable.EvalFloat64() error = %v, wantErr %v", err, tt.wantErr) 110 | return 111 | } 112 | if got != tt.want { 113 | t.Errorf("Evaluable.EvalFloat64() = %v, want %v", got, tt.want) 114 | } 115 | }) 116 | } 117 | } 118 | 119 | type testSelector struct { 120 | str string 121 | Map map[string]interface{} 122 | } 123 | 124 | func (s testSelector) SelectGVal(ctx context.Context, k string) (interface{}, error) { 125 | if k == "str" { 126 | return s.str, nil 127 | } 128 | 129 | if k == "map" { 130 | return s.Map, nil 131 | } 132 | 133 | if strings.HasPrefix(k, "deep") { 134 | return s, nil 135 | } 136 | 137 | return nil, fmt.Errorf("unknown-key") 138 | 139 | } 140 | 141 | func TestEvaluable_CustomSelector(t *testing.T) { 142 | var ( 143 | lang = Base() 144 | tests = []struct { 145 | name string 146 | expr string 147 | params interface{} 148 | want interface{} 149 | wantErr bool 150 | }{ 151 | { 152 | "unknown", 153 | "s.Foo", 154 | map[string]interface{}{"s": &testSelector{}}, 155 | nil, 156 | true, 157 | }, 158 | { 159 | "field directly", 160 | "s.Str", 161 | map[string]interface{}{"s": &testSelector{str: "test-value"}}, 162 | nil, 163 | true, 164 | }, 165 | { 166 | "field via selector", 167 | "s.str", 168 | map[string]interface{}{"s": &testSelector{str: "test-value"}}, 169 | "test-value", 170 | false, 171 | }, 172 | { 173 | "flat", 174 | "str", 175 | &testSelector{str: "test-value"}, 176 | "test-value", 177 | false, 178 | }, 179 | { 180 | "map field", 181 | "s.map.foo", 182 | map[string]interface{}{"s": &testSelector{Map: map[string]interface{}{"foo": "bar"}}}, 183 | "bar", 184 | false, 185 | }, 186 | { 187 | "crawl to val", 188 | "deep.deeper.deepest.str", 189 | &testSelector{str: "foo"}, 190 | "foo", 191 | false, 192 | }, 193 | { 194 | "crawl to struct", 195 | "deep.deeper.deepest", 196 | &testSelector{}, 197 | testSelector{}, 198 | false, 199 | }, 200 | } 201 | 202 | booltests = []struct { 203 | name string 204 | expr string 205 | params interface{} 206 | want interface{} 207 | wantErr bool 208 | }{ 209 | { 210 | "test method", 211 | "s.IsZero", 212 | map[string]interface{}{"s": time.Now()}, 213 | false, 214 | false, 215 | }, 216 | } 217 | ) 218 | 219 | for _, tt := range tests { 220 | t.Run(tt.name, func(t *testing.T) { 221 | got, err := lang.Evaluate(tt.expr, tt.params) 222 | if (err != nil) != tt.wantErr { 223 | t.Errorf("Evaluable.Evaluate() error = %v, wantErr %v", err, tt.wantErr) 224 | return 225 | } 226 | if !reflect.DeepEqual(got, tt.want) { 227 | t.Errorf("Evaluable.Evaluate() = %v, want %v", got, tt.want) 228 | } 229 | }) 230 | } 231 | 232 | for _, tt := range booltests { 233 | t.Run(tt.name, func(t *testing.T) { 234 | got, err := lang.Evaluate(tt.expr, tt.params) 235 | if (err != nil) != tt.wantErr { 236 | t.Errorf("Evaluable.Evaluate() error = %v, wantErr %v", err, tt.wantErr) 237 | return 238 | } 239 | got, ok := convertToBool(got) 240 | if !ok { 241 | t.Errorf("Evaluable.Evaluate() error = nok, wantErr %v", tt.wantErr) 242 | return 243 | } 244 | 245 | if !reflect.DeepEqual(got, tt.want) { 246 | t.Errorf("Evaluable.Evaluate() = %v, want %v", got, tt.want) 247 | } 248 | }) 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package gval_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/PaesslerAG/gval" 10 | "github.com/PaesslerAG/jsonpath" 11 | ) 12 | 13 | func Example() { 14 | 15 | vars := map[string]interface{}{"name": "World"} 16 | 17 | value, err := gval.Evaluate(`"Hello " + name + "!"`, vars) 18 | if err != nil { 19 | fmt.Println(err) 20 | } 21 | 22 | fmt.Print(value) 23 | 24 | // Output: 25 | // Hello World! 26 | } 27 | 28 | func ExampleEvaluate() { 29 | 30 | value, err := gval.Evaluate("foo > 0", map[string]interface{}{ 31 | "foo": -1., 32 | }) 33 | if err != nil { 34 | fmt.Println(err) 35 | } 36 | 37 | fmt.Print(value) 38 | 39 | // Output: 40 | // false 41 | } 42 | 43 | func ExampleEvaluate_nestedParameter() { 44 | 45 | value, err := gval.Evaluate("foo.bar > 0", map[string]interface{}{ 46 | "foo": map[string]interface{}{"bar": -1.}, 47 | }) 48 | if err != nil { 49 | fmt.Println(err) 50 | } 51 | 52 | fmt.Print(value) 53 | 54 | // Output: 55 | // false 56 | } 57 | 58 | func ExampleEvaluate_array() { 59 | 60 | value, err := gval.Evaluate("foo[0]", map[string]interface{}{ 61 | "foo": []interface{}{-1.}, 62 | }) 63 | if err != nil { 64 | fmt.Println(err) 65 | } 66 | 67 | fmt.Print(value) 68 | 69 | // Output: 70 | // -1 71 | } 72 | 73 | func ExampleEvaluate_complexAccessor() { 74 | 75 | value, err := gval.Evaluate(`foo["b" + "a" + "r"]`, map[string]interface{}{ 76 | "foo": map[string]interface{}{"bar": -1.}, 77 | }) 78 | if err != nil { 79 | fmt.Println(err) 80 | } 81 | 82 | fmt.Print(value) 83 | 84 | // Output: 85 | // -1 86 | } 87 | 88 | func ExampleEvaluate_arithmetic() { 89 | 90 | value, err := gval.Evaluate("(requests_made * requests_succeeded / 100) >= 90", 91 | map[string]interface{}{ 92 | "requests_made": 100, 93 | "requests_succeeded": 80, 94 | }) 95 | if err != nil { 96 | fmt.Println(err) 97 | } 98 | 99 | fmt.Print(value) 100 | 101 | // Output: 102 | // false 103 | } 104 | 105 | func ExampleEvaluate_string() { 106 | 107 | value, err := gval.Evaluate(`http_response_body == "service is ok"`, 108 | map[string]interface{}{ 109 | "http_response_body": "service is ok", 110 | }) 111 | if err != nil { 112 | fmt.Println(err) 113 | } 114 | 115 | fmt.Print(value) 116 | 117 | // Output: 118 | // true 119 | } 120 | 121 | func ExampleEvaluate_float64() { 122 | 123 | value, err := gval.Evaluate("(mem_used / total_mem) * 100", 124 | map[string]interface{}{ 125 | "total_mem": 1024, 126 | "mem_used": 512, 127 | }) 128 | if err != nil { 129 | fmt.Println(err) 130 | } 131 | 132 | fmt.Print(value) 133 | 134 | // Output: 135 | // 50 136 | } 137 | 138 | func ExampleEvaluate_dateComparison() { 139 | 140 | value, err := gval.Evaluate("date(`2014-01-02`) > date(`2014-01-01 23:59:59`)", 141 | nil, 142 | // define Date comparison because it is not part expression language gval 143 | gval.InfixOperator(">", func(a, b interface{}) (interface{}, error) { 144 | date1, ok1 := a.(time.Time) 145 | date2, ok2 := b.(time.Time) 146 | 147 | if ok1 && ok2 { 148 | return date1.After(date2), nil 149 | } 150 | return nil, fmt.Errorf("unexpected operands types (%T) > (%T)", a, b) 151 | }), 152 | ) 153 | if err != nil { 154 | fmt.Println(err) 155 | } 156 | 157 | fmt.Print(value) 158 | 159 | // Output: 160 | // true 161 | } 162 | 163 | func ExampleEvaluable() { 164 | eval, err := gval.Full(gval.Constant("maximum_time", 52)). 165 | NewEvaluable("response_time <= maximum_time") 166 | if err != nil { 167 | fmt.Println(err) 168 | } 169 | 170 | for i := 50; i < 55; i++ { 171 | value, err := eval(context.Background(), map[string]interface{}{ 172 | "response_time": i, 173 | }) 174 | if err != nil { 175 | fmt.Println(err) 176 | 177 | } 178 | 179 | fmt.Println(value) 180 | } 181 | 182 | // Output: 183 | // true 184 | // true 185 | // true 186 | // false 187 | // false 188 | } 189 | 190 | func ExampleEvaluate_strlen() { 191 | 192 | value, err := gval.Evaluate(`strlen("someReallyLongInputString") <= 16`, 193 | nil, 194 | gval.Function("strlen", func(args ...interface{}) (interface{}, error) { 195 | length := len(args[0].(string)) 196 | return (float64)(length), nil 197 | })) 198 | if err != nil { 199 | fmt.Println(err) 200 | } 201 | 202 | fmt.Print(value) 203 | 204 | // Output: 205 | // false 206 | } 207 | 208 | func ExampleEvaluate_encoding() { 209 | 210 | value, err := gval.Evaluate(`(7 < "47" == true ? "hello world!\n\u263a" : "good bye\n")`+" + ` more text`", 211 | nil, 212 | gval.Function("strlen", func(args ...interface{}) (interface{}, error) { 213 | length := len(args[0].(string)) 214 | return (float64)(length), nil 215 | })) 216 | if err != nil { 217 | fmt.Println(err) 218 | } 219 | 220 | fmt.Print(value) 221 | 222 | // Output: 223 | // hello world! 224 | // ☺ more text 225 | } 226 | 227 | type exampleType struct { 228 | Hello string 229 | } 230 | 231 | func (e exampleType) World() string { 232 | return "world" 233 | } 234 | 235 | func ExampleEvaluate_accessor() { 236 | 237 | value, err := gval.Evaluate(`foo.Hello + foo.World()`, 238 | map[string]interface{}{ 239 | "foo": exampleType{Hello: "hello "}, 240 | }) 241 | if err != nil { 242 | fmt.Println(err) 243 | } 244 | 245 | fmt.Print(value) 246 | 247 | // Output: 248 | // hello world 249 | } 250 | 251 | func ExampleEvaluate_flatAccessor() { 252 | 253 | value, err := gval.Evaluate(`Hello + World()`, 254 | exampleType{Hello: "hello "}, 255 | ) 256 | if err != nil { 257 | fmt.Println(err) 258 | } 259 | 260 | fmt.Print(value) 261 | 262 | // Output: 263 | // hello world 264 | } 265 | 266 | func ExampleEvaluate_nestedAccessor() { 267 | 268 | value, err := gval.Evaluate(`foo.Bar.Hello + foo.Bar.World()`, 269 | map[string]interface{}{ 270 | "foo": struct{ Bar exampleType }{ 271 | Bar: exampleType{Hello: "hello "}, 272 | }, 273 | }) 274 | if err != nil { 275 | fmt.Println(err) 276 | } 277 | 278 | fmt.Print(value) 279 | 280 | // Output: 281 | // hello world 282 | } 283 | 284 | func ExampleVariableSelector() { 285 | value, err := gval.Evaluate(`hello.world`, 286 | "!", 287 | gval.VariableSelector(func(path gval.Evaluables) gval.Evaluable { 288 | return func(c context.Context, v interface{}) (interface{}, error) { 289 | keys, err := path.EvalStrings(c, v) 290 | if err != nil { 291 | return nil, err 292 | } 293 | return fmt.Sprintf("%s%s", strings.Join(keys, " "), v), nil 294 | } 295 | }), 296 | ) 297 | if err != nil { 298 | fmt.Println(err) 299 | } 300 | 301 | fmt.Print(value) 302 | 303 | // Output: 304 | // hello world! 305 | } 306 | 307 | func ExampleEvaluable_EvalInt() { 308 | eval, err := gval.Full().NewEvaluable("1 + x") 309 | if err != nil { 310 | fmt.Println(err) 311 | return 312 | } 313 | 314 | value, err := eval.EvalInt(context.Background(), map[string]interface{}{"x": 5}) 315 | if err != nil { 316 | fmt.Println(err) 317 | } 318 | 319 | fmt.Print(value) 320 | 321 | // Output: 322 | // 6 323 | } 324 | 325 | func ExampleEvaluable_EvalBool() { 326 | eval, err := gval.Full().NewEvaluable("1 == x") 327 | if err != nil { 328 | fmt.Println(err) 329 | return 330 | } 331 | 332 | value, err := eval.EvalBool(context.Background(), map[string]interface{}{"x": 1}) 333 | if err != nil { 334 | fmt.Println(err) 335 | } 336 | 337 | if value { 338 | fmt.Print("yeah") 339 | } 340 | 341 | // Output: 342 | // yeah 343 | } 344 | 345 | func ExampleEvaluate_jsonpath() { 346 | 347 | value, err := gval.Evaluate(`$["response-time"]`, 348 | map[string]interface{}{ 349 | "response-time": 100, 350 | }, 351 | jsonpath.Language(), 352 | ) 353 | if err != nil { 354 | fmt.Println(err) 355 | } 356 | 357 | fmt.Print(value) 358 | 359 | // Output: 360 | // 100 361 | } 362 | 363 | func ExampleLanguage() { 364 | lang := gval.NewLanguage(gval.JSON(), gval.Arithmetic(), 365 | //pipe operator 366 | gval.PostfixOperator("|", func(c context.Context, p *gval.Parser, pre gval.Evaluable) (gval.Evaluable, error) { 367 | post, err := p.ParseExpression(c) 368 | if err != nil { 369 | return nil, err 370 | } 371 | return func(c context.Context, v interface{}) (interface{}, error) { 372 | v, err := pre(c, v) 373 | if err != nil { 374 | return nil, err 375 | } 376 | return post(c, v) 377 | }, nil 378 | })) 379 | 380 | eval, err := lang.NewEvaluable(`{"foobar": 50} | foobar + 100`) 381 | if err != nil { 382 | fmt.Println(err) 383 | } 384 | 385 | value, err := eval(context.Background(), nil) 386 | 387 | if err != nil { 388 | fmt.Println(err) 389 | } 390 | 391 | fmt.Println(value) 392 | 393 | // Output: 394 | // 150 395 | } 396 | 397 | type exampleCustomSelector struct{ hidden string } 398 | 399 | var _ gval.Selector = &exampleCustomSelector{} 400 | 401 | func (s *exampleCustomSelector) SelectGVal(ctx context.Context, k string) (interface{}, error) { 402 | if k == "hidden" { 403 | return s.hidden, nil 404 | } 405 | 406 | return nil, nil 407 | } 408 | 409 | func ExampleSelector() { 410 | lang := gval.Base() 411 | value, err := lang.Evaluate( 412 | "myStruct.hidden", 413 | map[string]interface{}{"myStruct": &exampleCustomSelector{hidden: "hello world"}}, 414 | ) 415 | 416 | if err != nil { 417 | fmt.Println(err) 418 | } 419 | 420 | fmt.Println(value) 421 | 422 | // Output: 423 | // hello world 424 | } 425 | 426 | func parseSub(ctx context.Context, p *gval.Parser) (gval.Evaluable, error) { 427 | return p.ParseSublanguage(ctx, subLang) 428 | } 429 | 430 | var ( 431 | superLang = gval.NewLanguage( 432 | gval.PrefixExtension('$', parseSub), 433 | ) 434 | subLang = gval.NewLanguage( 435 | gval.Init(func(ctx context.Context, p *gval.Parser) (gval.Evaluable, error) { return p.Const("hello world"), nil }), 436 | ) 437 | ) 438 | 439 | func ExampleParser_ParseSublanguage() { 440 | value, err := superLang.Evaluate("$", nil) 441 | 442 | if err != nil { 443 | fmt.Println(err) 444 | } 445 | 446 | fmt.Println(value) 447 | 448 | // Output: 449 | // hello world 450 | } 451 | -------------------------------------------------------------------------------- /functions.go: -------------------------------------------------------------------------------- 1 | package gval 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | ) 8 | 9 | type function func(ctx context.Context, arguments ...interface{}) (interface{}, error) 10 | 11 | func toFunc(f interface{}) function { 12 | if f, ok := f.(func(arguments ...interface{}) (interface{}, error)); ok { 13 | return function(func(ctx context.Context, arguments ...interface{}) (interface{}, error) { 14 | var v interface{} 15 | errCh := make(chan error, 1) 16 | go func() { 17 | defer func() { 18 | if recovered := recover(); recovered != nil { 19 | errCh <- fmt.Errorf("%v", recovered) 20 | } 21 | }() 22 | result, err := f(arguments...) 23 | v = result 24 | errCh <- err 25 | }() 26 | 27 | select { 28 | case <-ctx.Done(): 29 | return nil, ctx.Err() 30 | case err := <-errCh: 31 | close(errCh) 32 | return v, err 33 | } 34 | }) 35 | } 36 | if f, ok := f.(func(ctx context.Context, arguments ...interface{}) (interface{}, error)); ok { 37 | return function(f) 38 | } 39 | 40 | fun := reflect.ValueOf(f) 41 | t := fun.Type() 42 | return func(ctx context.Context, args ...interface{}) (interface{}, error) { 43 | var v interface{} 44 | errCh := make(chan error, 1) 45 | go func() { 46 | defer func() { 47 | if recovered := recover(); recovered != nil { 48 | errCh <- fmt.Errorf("%v", recovered) 49 | } 50 | }() 51 | in, err := createCallArguments(ctx, t, args) 52 | if err != nil { 53 | errCh <- err 54 | return 55 | } 56 | out := fun.Call(in) 57 | 58 | r := make([]interface{}, len(out)) 59 | for i, e := range out { 60 | r[i] = e.Interface() 61 | } 62 | 63 | err = nil 64 | errorInterface := reflect.TypeOf((*error)(nil)).Elem() 65 | if len(r) > 0 && t.Out(len(r)-1).Implements(errorInterface) { 66 | if r[len(r)-1] != nil { 67 | err = r[len(r)-1].(error) 68 | } 69 | r = r[0 : len(r)-1] 70 | } 71 | 72 | switch len(r) { 73 | case 0: 74 | v = nil 75 | case 1: 76 | v = r[0] 77 | default: 78 | v = r 79 | } 80 | errCh <- err 81 | }() 82 | 83 | select { 84 | case <-ctx.Done(): 85 | return nil, ctx.Err() 86 | case err := <-errCh: 87 | close(errCh) 88 | return v, err 89 | } 90 | } 91 | } 92 | 93 | func createCallArguments(ctx context.Context, t reflect.Type, args []interface{}) ([]reflect.Value, error) { 94 | variadic := t.IsVariadic() 95 | numIn := t.NumIn() 96 | 97 | // if first argument is a context, use the given execution context 98 | if numIn > 0 { 99 | thisFun := reflect.ValueOf(createCallArguments) 100 | thisT := thisFun.Type() 101 | if t.In(0) == thisT.In(0) { 102 | args = append([]interface{}{ctx}, args...) 103 | } 104 | } 105 | 106 | if (!variadic && len(args) != numIn) || (variadic && len(args) < numIn-1) { 107 | return nil, fmt.Errorf("invalid number of parameters") 108 | } 109 | 110 | in := make([]reflect.Value, len(args)) 111 | var inType reflect.Type 112 | for i, arg := range args { 113 | if !variadic || i < numIn-1 { 114 | inType = t.In(i) 115 | } else if i == numIn-1 { 116 | inType = t.In(numIn - 1).Elem() 117 | } 118 | argVal := reflect.ValueOf(arg) 119 | if arg == nil { 120 | argVal = reflect.Zero(reflect.TypeOf((*interface{})(nil)).Elem()) 121 | } else if !argVal.Type().AssignableTo(inType) { 122 | return nil, fmt.Errorf("expected type %s for parameter %d but got %T", 123 | inType.String(), i, arg) 124 | } 125 | in[i] = argVal 126 | } 127 | return in, nil 128 | } 129 | -------------------------------------------------------------------------------- /functions_test.go: -------------------------------------------------------------------------------- 1 | package gval 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func Test_toFunc(t *testing.T) { 12 | myError := fmt.Errorf("my error") 13 | tests := []struct { 14 | name string 15 | function interface{} 16 | arguments []interface{} 17 | want interface{} 18 | wantErr error 19 | wantAnyErr bool 20 | }{ 21 | { 22 | name: "empty", 23 | function: func() {}, 24 | }, 25 | { 26 | name: "one arg", 27 | function: func(a interface{}) { 28 | if a != true { 29 | panic("fail") 30 | } 31 | }, 32 | arguments: []interface{}{true}, 33 | }, 34 | { 35 | name: "three args", 36 | function: func(a, b, c interface{}) { 37 | if a != 1 || b != 2 || c != 3 { 38 | panic("fail") 39 | } 40 | }, 41 | arguments: []interface{}{1, 2, 3}, 42 | }, 43 | { 44 | name: "input types", 45 | function: func(a int, b string, c bool) { 46 | if a != 1 || b != "2" || !c { 47 | panic("fail") 48 | } 49 | }, 50 | arguments: []interface{}{1, "2", true}, 51 | }, 52 | { 53 | name: "wronge input type int", 54 | function: func(a int, b string, c bool) {}, 55 | arguments: []interface{}{"1", "2", true}, 56 | wantAnyErr: true, 57 | }, 58 | { 59 | name: "wronge input type string", 60 | function: func(a int, b string, c bool) {}, 61 | arguments: []interface{}{1, 2, true}, 62 | wantAnyErr: true, 63 | }, 64 | { 65 | name: "wronge input type bool", 66 | function: func(a int, b string, c bool) {}, 67 | arguments: []interface{}{1, "2", "true"}, 68 | wantAnyErr: true, 69 | }, 70 | { 71 | name: "wronge input number", 72 | function: func(a int, b string, c bool) {}, 73 | arguments: []interface{}{1, "2"}, 74 | wantAnyErr: true, 75 | }, 76 | { 77 | name: "one return", 78 | function: func() bool { 79 | return true 80 | }, 81 | want: true, 82 | }, 83 | { 84 | name: "three returns", 85 | function: func() (bool, string, int) { 86 | return true, "2", 3 87 | }, 88 | want: []interface{}{true, "2", 3}, 89 | }, 90 | { 91 | name: "error", 92 | function: func() error { 93 | return myError 94 | }, 95 | wantErr: myError, 96 | }, 97 | { 98 | name: "none error", 99 | function: func() error { 100 | return nil 101 | }, 102 | }, 103 | { 104 | name: "one return with error", 105 | function: func() (bool, error) { 106 | return false, myError 107 | }, 108 | want: false, 109 | wantErr: myError, 110 | }, 111 | { 112 | name: "three returns with error", 113 | function: func() (bool, string, int, error) { 114 | return false, "", 0, myError 115 | }, 116 | want: []interface{}{false, "", 0}, 117 | wantErr: myError, 118 | }, 119 | { 120 | name: "context not expiring", 121 | function: func(ctx context.Context) error { 122 | return nil 123 | }, 124 | }, 125 | { 126 | name: "context expires", 127 | function: func(ctx context.Context) error { 128 | time.Sleep(20 * time.Millisecond) 129 | return nil 130 | }, 131 | wantErr: context.DeadlineExceeded, 132 | }, 133 | { 134 | name: "nil arg", 135 | function: func(a interface{}) bool { 136 | return a == nil 137 | }, 138 | arguments: []interface{}{nil}, 139 | want: true, 140 | }, 141 | } 142 | for _, tt := range tests { 143 | t.Run(tt.name, func(t *testing.T) { 144 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) 145 | got, err := toFunc(tt.function)(ctx, tt.arguments...) 146 | cancel() 147 | 148 | if tt.wantAnyErr { 149 | if err != nil { 150 | return 151 | } 152 | t.Fatalf("toFunc()(args...) = error(nil), but wantAnyErr") 153 | } 154 | if err != tt.wantErr { 155 | t.Fatalf("toFunc()(args...) = error(%v), wantErr (%v)", err, tt.wantErr) 156 | } 157 | if !reflect.DeepEqual(got, tt.want) { 158 | t.Errorf("toFunc()(args...) = %v, want %v", got, tt.want) 159 | } 160 | }) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/PaesslerAG/gval 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/PaesslerAG/jsonpath v0.1.0 7 | github.com/shopspring/decimal v1.3.1 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/PaesslerAG/jsonpath v0.1.0 h1:gADYeifvlqK3R3i2cR5B4DGgxLXIPb3TRTH1mGi0jPI= 2 | github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8= 3 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= 4 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 5 | -------------------------------------------------------------------------------- /gval.go: -------------------------------------------------------------------------------- 1 | // Package gval provides a generic expression language. 2 | // All functions, infix and prefix operators can be replaced by composing languages into a new one. 3 | // 4 | // The package contains concrete expression languages for common application in text, arithmetic, decimal arithmetic, propositional logic and so on. 5 | // They can be used as basis for a custom expression language or to evaluate expressions directly. 6 | package gval 7 | 8 | import ( 9 | "context" 10 | "fmt" 11 | "math" 12 | "reflect" 13 | "text/scanner" 14 | "time" 15 | 16 | "github.com/shopspring/decimal" 17 | ) 18 | 19 | // Evaluate given parameter with given expression in gval full language 20 | func Evaluate(expression string, parameter interface{}, opts ...Language) (interface{}, error) { 21 | return EvaluateWithContext(context.Background(), expression, parameter, opts...) 22 | } 23 | 24 | // Evaluate given parameter with given expression in gval full language using a context 25 | func EvaluateWithContext(c context.Context, expression string, parameter interface{}, opts ...Language) (interface{}, error) { 26 | l := full 27 | if len(opts) > 0 { 28 | l = NewLanguage(append([]Language{l}, opts...)...) 29 | } 30 | return l.EvaluateWithContext(c, expression, parameter) 31 | } 32 | 33 | // Full is the union of Arithmetic, Bitmask, Text, PropositionalLogic, TernaryOperator, and Json 34 | // 35 | // Operator in: a in b is true iff value a is an element of array b 36 | // Operator ??: a ?? b returns a if a is not false or nil, otherwise n 37 | // 38 | // Function Date: Date(a) parses string a. a must match RFC3339, ISO8601, ruby date, or unix date 39 | func Full(extensions ...Language) Language { 40 | if len(extensions) == 0 { 41 | return full 42 | } 43 | return NewLanguage(append([]Language{full}, extensions...)...) 44 | } 45 | 46 | // TernaryOperator contains following Operator 47 | // 48 | // ?: a ? b : c returns b if bool a is true, otherwise b 49 | func TernaryOperator() Language { 50 | return ternaryOperator 51 | } 52 | 53 | // Arithmetic contains base, plus(+), minus(-), divide(/), power(**), negative(-) 54 | // and numerical order (<=,<,>,>=) 55 | // 56 | // Arithmetic operators expect float64 operands. 57 | // Called with unfitting input, they try to convert the input to float64. 58 | // They can parse strings and convert any type of int or float. 59 | func Arithmetic() Language { 60 | return arithmetic 61 | } 62 | 63 | // DecimalArithmetic contains base, plus(+), minus(-), divide(/), power(**), negative(-) 64 | // and numerical order (<=,<,>,>=) 65 | // 66 | // DecimalArithmetic operators expect decimal.Decimal operands (github.com/shopspring/decimal) 67 | // and are used to calculate money/decimal rather than floating point calculations. 68 | // Called with unfitting input, they try to convert the input to decimal.Decimal. 69 | // They can parse strings and convert any type of int or float. 70 | func DecimalArithmetic() Language { 71 | return decimalArithmetic 72 | } 73 | 74 | // Bitmask contains base, bitwise and(&), bitwise or(|) and bitwise not(^). 75 | // 76 | // Bitmask operators expect float64 operands. 77 | // Called with unfitting input they try to convert the input to float64. 78 | // They can parse strings and convert any type of int or float. 79 | func Bitmask() Language { 80 | return bitmask 81 | } 82 | 83 | // Text contains base, lexical order on strings (<=,<,>,>=), 84 | // regex match (=~) and regex not match (!~) 85 | func Text() Language { 86 | return text 87 | } 88 | 89 | // PropositionalLogic contains base, not(!), and (&&), or (||) and Base. 90 | // 91 | // Propositional operator expect bool operands. 92 | // Called with unfitting input they try to convert the input to bool. 93 | // Numbers other than 0 and the strings "TRUE" and "true" are interpreted as true. 94 | // 0 and the strings "FALSE" and "false" are interpreted as false. 95 | func PropositionalLogic() Language { 96 | return propositionalLogic 97 | } 98 | 99 | // JSON contains json objects ({string:expression,...}) 100 | // and json arrays ([expression, ...]) 101 | func JSON() Language { 102 | return ljson 103 | } 104 | 105 | // Parentheses contains support for parentheses. 106 | func Parentheses() Language { 107 | return parentheses 108 | } 109 | 110 | // Ident contains support for variables and functions. 111 | func Ident() Language { 112 | return ident 113 | } 114 | 115 | // Base contains equal (==) and not equal (!=), perentheses and general support for variables, constants and functions 116 | // It contains true, false, (floating point) number, string ("" or “) and char (”) constants 117 | func Base() Language { 118 | return base 119 | } 120 | 121 | var full = NewLanguage(arithmetic, bitmask, text, propositionalLogic, ljson, 122 | 123 | InfixOperator("in", inArray), 124 | 125 | InfixShortCircuit("??", func(a interface{}) (interface{}, bool) { 126 | v := reflect.ValueOf(a) 127 | return a, a != nil && !v.IsZero() 128 | }), 129 | InfixOperator("??", func(a, b interface{}) (interface{}, error) { 130 | if v := reflect.ValueOf(a); a == nil || v.IsZero() { 131 | return b, nil 132 | } 133 | return a, nil 134 | }), 135 | 136 | ternaryOperator, 137 | 138 | Function("date", func(arguments ...interface{}) (interface{}, error) { 139 | if len(arguments) != 1 { 140 | return nil, fmt.Errorf("date() expects exactly one string argument") 141 | } 142 | s, ok := arguments[0].(string) 143 | if !ok { 144 | return nil, fmt.Errorf("date() expects exactly one string argument") 145 | } 146 | for _, format := range [...]string{ 147 | time.ANSIC, 148 | time.UnixDate, 149 | time.RubyDate, 150 | time.Kitchen, 151 | time.RFC3339, 152 | time.RFC3339Nano, 153 | "2006-01-02", // RFC 3339 154 | "2006-01-02 15:04", // RFC 3339 with minutes 155 | "2006-01-02 15:04:05", // RFC 3339 with seconds 156 | "2006-01-02 15:04:05-07:00", // RFC 3339 with seconds and timezone 157 | "2006-01-02T15Z0700", // ISO8601 with hour 158 | "2006-01-02T15:04Z0700", // ISO8601 with minutes 159 | "2006-01-02T15:04:05Z0700", // ISO8601 with seconds 160 | "2006-01-02T15:04:05.999999999Z0700", // ISO8601 with nanoseconds 161 | } { 162 | ret, err := time.ParseInLocation(format, s, time.Local) 163 | if err == nil { 164 | return ret, nil 165 | } 166 | } 167 | return nil, fmt.Errorf("date() could not parse %s", s) 168 | }), 169 | ) 170 | 171 | var ternaryOperator = PostfixOperator("?", parseIf) 172 | 173 | var ljson = NewLanguage( 174 | PrefixExtension('[', parseJSONArray), 175 | PrefixExtension('{', parseJSONObject), 176 | ) 177 | 178 | var arithmetic = NewLanguage( 179 | InfixNumberOperator("+", func(a, b float64) (interface{}, error) { return a + b, nil }), 180 | InfixNumberOperator("-", func(a, b float64) (interface{}, error) { return a - b, nil }), 181 | InfixNumberOperator("*", func(a, b float64) (interface{}, error) { return a * b, nil }), 182 | InfixNumberOperator("/", func(a, b float64) (interface{}, error) { return a / b, nil }), 183 | InfixNumberOperator("%", func(a, b float64) (interface{}, error) { return math.Mod(a, b), nil }), 184 | InfixNumberOperator("**", func(a, b float64) (interface{}, error) { return math.Pow(a, b), nil }), 185 | 186 | InfixNumberOperator(">", func(a, b float64) (interface{}, error) { return a > b, nil }), 187 | InfixNumberOperator(">=", func(a, b float64) (interface{}, error) { return a >= b, nil }), 188 | InfixNumberOperator("<", func(a, b float64) (interface{}, error) { return a < b, nil }), 189 | InfixNumberOperator("<=", func(a, b float64) (interface{}, error) { return a <= b, nil }), 190 | 191 | InfixNumberOperator("==", func(a, b float64) (interface{}, error) { return a == b, nil }), 192 | InfixNumberOperator("!=", func(a, b float64) (interface{}, error) { return a != b, nil }), 193 | 194 | base, 195 | ) 196 | 197 | var decimalArithmetic = NewLanguage( 198 | InfixDecimalOperator("+", func(a, b decimal.Decimal) (interface{}, error) { return a.Add(b), nil }), 199 | InfixDecimalOperator("-", func(a, b decimal.Decimal) (interface{}, error) { return a.Sub(b), nil }), 200 | InfixDecimalOperator("*", func(a, b decimal.Decimal) (interface{}, error) { return a.Mul(b), nil }), 201 | InfixDecimalOperator("/", func(a, b decimal.Decimal) (interface{}, error) { return a.Div(b), nil }), 202 | InfixDecimalOperator("%", func(a, b decimal.Decimal) (interface{}, error) { return a.Mod(b), nil }), 203 | InfixDecimalOperator("**", func(a, b decimal.Decimal) (interface{}, error) { return a.Pow(b), nil }), 204 | 205 | InfixDecimalOperator(">", func(a, b decimal.Decimal) (interface{}, error) { return a.GreaterThan(b), nil }), 206 | InfixDecimalOperator(">=", func(a, b decimal.Decimal) (interface{}, error) { return a.GreaterThanOrEqual(b), nil }), 207 | InfixDecimalOperator("<", func(a, b decimal.Decimal) (interface{}, error) { return a.LessThan(b), nil }), 208 | InfixDecimalOperator("<=", func(a, b decimal.Decimal) (interface{}, error) { return a.LessThanOrEqual(b), nil }), 209 | 210 | InfixDecimalOperator("==", func(a, b decimal.Decimal) (interface{}, error) { return a.Equal(b), nil }), 211 | InfixDecimalOperator("!=", func(a, b decimal.Decimal) (interface{}, error) { return !a.Equal(b), nil }), 212 | base, 213 | //Base is before these overrides so that the Base options are overridden 214 | PrefixExtension(scanner.Int, parseDecimal), 215 | PrefixExtension(scanner.Float, parseDecimal), 216 | PrefixOperator("-", func(c context.Context, v interface{}) (interface{}, error) { 217 | i, ok := convertToFloat(v) 218 | if !ok { 219 | return nil, fmt.Errorf("unexpected %v(%T) expected number", v, v) 220 | } 221 | return decimal.NewFromFloat(i).Neg(), nil 222 | }), 223 | ) 224 | 225 | var bitmask = NewLanguage( 226 | InfixNumberOperator("^", func(a, b float64) (interface{}, error) { return float64(int64(a) ^ int64(b)), nil }), 227 | InfixNumberOperator("&", func(a, b float64) (interface{}, error) { return float64(int64(a) & int64(b)), nil }), 228 | InfixNumberOperator("|", func(a, b float64) (interface{}, error) { return float64(int64(a) | int64(b)), nil }), 229 | InfixNumberOperator("<<", func(a, b float64) (interface{}, error) { return float64(int64(a) << uint64(b)), nil }), 230 | InfixNumberOperator(">>", func(a, b float64) (interface{}, error) { return float64(int64(a) >> uint64(b)), nil }), 231 | 232 | PrefixOperator("~", func(c context.Context, v interface{}) (interface{}, error) { 233 | i, ok := convertToFloat(v) 234 | if !ok { 235 | return nil, fmt.Errorf("unexpected %T expected number", v) 236 | } 237 | return float64(^int64(i)), nil 238 | }), 239 | ) 240 | 241 | var text = NewLanguage( 242 | InfixTextOperator("+", func(a, b string) (interface{}, error) { return fmt.Sprintf("%v%v", a, b), nil }), 243 | 244 | InfixTextOperator("<", func(a, b string) (interface{}, error) { return a < b, nil }), 245 | InfixTextOperator("<=", func(a, b string) (interface{}, error) { return a <= b, nil }), 246 | InfixTextOperator(">", func(a, b string) (interface{}, error) { return a > b, nil }), 247 | InfixTextOperator(">=", func(a, b string) (interface{}, error) { return a >= b, nil }), 248 | 249 | InfixEvalOperator("=~", regEx), 250 | InfixEvalOperator("!~", notRegEx), 251 | base, 252 | ) 253 | 254 | var propositionalLogic = NewLanguage( 255 | PrefixOperator("!", func(c context.Context, v interface{}) (interface{}, error) { 256 | b, ok := convertToBool(v) 257 | if !ok { 258 | return nil, fmt.Errorf("unexpected %T expected bool", v) 259 | } 260 | return !b, nil 261 | }), 262 | 263 | InfixShortCircuit("&&", func(a interface{}) (interface{}, bool) { return false, a == false }), 264 | InfixBoolOperator("&&", func(a, b bool) (interface{}, error) { return a && b, nil }), 265 | InfixShortCircuit("||", func(a interface{}) (interface{}, bool) { return true, a == true }), 266 | InfixBoolOperator("||", func(a, b bool) (interface{}, error) { return a || b, nil }), 267 | 268 | InfixBoolOperator("==", func(a, b bool) (interface{}, error) { return a == b, nil }), 269 | InfixBoolOperator("!=", func(a, b bool) (interface{}, error) { return a != b, nil }), 270 | 271 | base, 272 | ) 273 | 274 | var parentheses = NewLanguage( 275 | PrefixExtension('(', parseParentheses), 276 | ) 277 | 278 | var ident = NewLanguage( 279 | PrefixMetaPrefix(scanner.Ident, parseIdent), 280 | ) 281 | 282 | var base = NewLanguage( 283 | PrefixExtension(scanner.Int, parseNumber), 284 | PrefixExtension(scanner.Float, parseNumber), 285 | PrefixOperator("-", func(c context.Context, v interface{}) (interface{}, error) { 286 | i, ok := convertToFloat(v) 287 | if !ok { 288 | return nil, fmt.Errorf("unexpected %v(%T) expected number", v, v) 289 | } 290 | return -i, nil 291 | }), 292 | 293 | PrefixExtension(scanner.String, parseString), 294 | PrefixExtension(scanner.Char, parseString), 295 | PrefixExtension(scanner.RawString, parseString), 296 | 297 | Constant("true", true), 298 | Constant("false", false), 299 | 300 | InfixOperator("==", func(a, b interface{}) (interface{}, error) { return reflect.DeepEqual(a, b), nil }), 301 | InfixOperator("!=", func(a, b interface{}) (interface{}, error) { return !reflect.DeepEqual(a, b), nil }), 302 | parentheses, 303 | 304 | Precedence("??", 0), 305 | 306 | Precedence("||", 20), 307 | Precedence("&&", 21), 308 | 309 | Precedence("==", 40), 310 | Precedence("!=", 40), 311 | Precedence(">", 40), 312 | Precedence(">=", 40), 313 | Precedence("<", 40), 314 | Precedence("<=", 40), 315 | Precedence("=~", 40), 316 | Precedence("!~", 40), 317 | Precedence("in", 40), 318 | 319 | Precedence("^", 60), 320 | Precedence("&", 60), 321 | Precedence("|", 60), 322 | 323 | Precedence("<<", 90), 324 | Precedence(">>", 90), 325 | 326 | Precedence("+", 120), 327 | Precedence("-", 120), 328 | 329 | Precedence("*", 150), 330 | Precedence("/", 150), 331 | Precedence("%", 150), 332 | 333 | Precedence("**", 200), 334 | 335 | ident, 336 | ) 337 | -------------------------------------------------------------------------------- /gval_evaluationFailure_test.go: -------------------------------------------------------------------------------- 1 | package gval 2 | 3 | /* 4 | Tests to make sure evaluation fails in the expected ways. 5 | */ 6 | import ( 7 | "errors" 8 | "fmt" 9 | "testing" 10 | ) 11 | 12 | func TestModifierTyping(test *testing.T) { 13 | var ( 14 | invalidOperator = "invalid operation" 15 | unknownParameter = "unknown parameter" 16 | invalidRegex = "error parsing regex" 17 | tooFewArguments = "reflect: Call with too few input arguments" 18 | tooManyArguments = "reflect: Call with too many input arguments" 19 | mismatchedParameters = "reflect: Call using" 20 | custom = "test error" 21 | ) 22 | evaluationTests := []evaluationTest{ 23 | //ModifierTyping 24 | { 25 | name: "PLUS literal number to literal bool", 26 | expression: "1 + true", 27 | want: "1true", // + on string is defined 28 | }, 29 | { 30 | name: "PLUS number to bool", 31 | expression: "number + bool", 32 | want: "1true", // + on string is defined 33 | }, 34 | { 35 | name: "MINUS number to bool", 36 | expression: "number - bool", 37 | wantErr: invalidOperator, 38 | }, 39 | { 40 | name: "MINUS number to bool", 41 | expression: "number - bool", 42 | wantErr: invalidOperator, 43 | }, 44 | { 45 | name: "MULTIPLY number to bool", 46 | expression: "number * bool", 47 | wantErr: invalidOperator, 48 | }, 49 | { 50 | name: "DIVIDE number to bool", 51 | expression: "number / bool", 52 | wantErr: invalidOperator, 53 | }, 54 | { 55 | name: "EXPONENT number to bool", 56 | expression: "number ** bool", 57 | wantErr: invalidOperator, 58 | }, 59 | { 60 | name: "MODULUS number to bool", 61 | expression: "number % bool", 62 | wantErr: invalidOperator, 63 | }, 64 | { 65 | name: "XOR number to bool", 66 | expression: "number % bool", 67 | wantErr: invalidOperator, 68 | }, 69 | { 70 | name: "BITWISE_OR number to bool", 71 | expression: "number | bool", 72 | wantErr: invalidOperator, 73 | }, 74 | { 75 | name: "BITWISE_AND number to bool", 76 | expression: "number & bool", 77 | wantErr: invalidOperator, 78 | }, 79 | { 80 | name: "BITWISE_XOR number to bool", 81 | expression: "number ^ bool", 82 | wantErr: invalidOperator, 83 | }, 84 | { 85 | name: "BITWISE_LSHIFT number to bool", 86 | expression: "number << bool", 87 | wantErr: invalidOperator, 88 | }, 89 | { 90 | name: "BITWISE_RSHIFT number to bool", 91 | expression: "number >> bool", 92 | wantErr: invalidOperator, 93 | }, 94 | //LogicalOperatorTyping 95 | { 96 | name: "AND number to number", 97 | expression: "number && number", 98 | want: true, // number != 0 is true 99 | }, 100 | { 101 | 102 | name: "OR number to number", 103 | expression: "number || number", 104 | want: true, // number != 0 is true 105 | }, 106 | { 107 | name: "AND string to string", 108 | expression: "string && string", 109 | wantErr: invalidOperator, 110 | }, 111 | { 112 | name: "OR string to string", 113 | expression: "string || string", 114 | wantErr: invalidOperator, 115 | }, 116 | { 117 | name: "AND number to string", 118 | expression: "number && string", 119 | wantErr: invalidOperator, 120 | }, 121 | { 122 | name: "OR number to string", 123 | expression: "number || string", 124 | wantErr: invalidOperator, 125 | }, 126 | { 127 | name: "AND bool to string", 128 | expression: "bool && string", 129 | wantErr: invalidOperator, 130 | }, 131 | { 132 | name: "OR string to bool", 133 | expression: "string || bool", 134 | wantErr: invalidOperator, 135 | }, 136 | //ComparatorTyping 137 | { 138 | name: "GT literal bool to literal bool", 139 | expression: "true > true", 140 | want: false, //lexical order on "true" 141 | }, 142 | { 143 | name: "GT bool to bool", 144 | expression: "bool > bool", 145 | want: false, //lexical order on "true" 146 | }, 147 | { 148 | name: "GTE bool to bool", 149 | expression: "bool >= bool", 150 | want: true, //lexical order on "true" 151 | }, 152 | { 153 | name: "LT bool to bool", 154 | expression: "bool < bool", 155 | want: false, //lexical order on "true" 156 | }, 157 | { 158 | name: "LTE bool to bool", 159 | expression: "bool <= bool", 160 | want: true, //lexical order on "true" 161 | }, 162 | { 163 | name: "GT number to string", 164 | expression: "number > string", 165 | want: false, //lexical order "1" < "foo" 166 | }, 167 | { 168 | 169 | name: "GTE number to string", 170 | expression: "number >= string", 171 | want: false, //lexical order "1" < "foo" 172 | }, 173 | { 174 | name: "LT number to string", 175 | expression: "number < string", 176 | want: true, //lexical order "1" < "foo" 177 | }, 178 | { 179 | name: "REQ number to string", 180 | expression: "number =~ string", 181 | want: false, 182 | }, 183 | { 184 | name: "REQ number to bool", 185 | expression: "number =~ bool", 186 | want: false, 187 | }, 188 | { 189 | name: "REQ bool to number", 190 | expression: "bool =~ number", 191 | want: false, 192 | }, 193 | { 194 | name: "REQ bool to string", 195 | expression: "bool =~ string", 196 | want: false, 197 | }, 198 | { 199 | name: "NREQ number to string", 200 | expression: "number !~ string", 201 | want: true, 202 | }, 203 | { 204 | name: "NREQ number to bool", 205 | expression: "number !~ bool", 206 | want: true, 207 | }, 208 | { 209 | name: "NREQ bool to number", 210 | expression: "bool !~ number", 211 | want: true, 212 | }, 213 | { 214 | 215 | name: "NREQ bool to string", 216 | expression: "bool !~ string", 217 | want: true, 218 | }, 219 | { 220 | name: "IN non-array numeric", 221 | expression: "1 in 2", 222 | wantErr: "expected type []interface{} for in operator but got float64", 223 | }, 224 | { 225 | name: "IN non-array string", 226 | expression: `1 in "foo"`, 227 | wantErr: "expected type []interface{} for in operator but got string", 228 | }, 229 | { 230 | 231 | name: "IN non-array boolean", 232 | expression: "1 in true", 233 | wantErr: "expected type []interface{} for in operator but got bool", 234 | }, 235 | //TernaryTyping 236 | { 237 | name: "Ternary with number", 238 | expression: "10 ? true", 239 | want: true, // 10 != nil && 10 != false 240 | }, 241 | { 242 | name: "Ternary with string", 243 | expression: `"foo" ? true`, 244 | want: true, // "foo" != nil && "foo" != false 245 | }, 246 | //RegexParameterCompilation 247 | { 248 | name: "Regex equality runtime parsing", 249 | expression: `"foo" =~ foo`, 250 | parameter: map[string]interface{}{ 251 | "foo": "[foo", 252 | }, 253 | wantErr: invalidRegex, 254 | }, 255 | { 256 | name: "Regex inequality runtime parsing", 257 | expression: `"foo" !~ foo`, 258 | parameter: map[string]interface{}{ 259 | "foo": "[foo", 260 | }, 261 | wantErr: invalidRegex, 262 | }, 263 | { 264 | name: "Regex equality runtime right side evaluation", 265 | expression: `"foo" =~ error()`, 266 | wantErr: custom, 267 | }, 268 | { 269 | name: "Regex inequality runtime right side evaluation", 270 | expression: `"foo" !~ error()`, 271 | wantErr: custom, 272 | }, 273 | { 274 | name: "Regex equality runtime left side evaluation", 275 | expression: `error() =~ "."`, 276 | wantErr: custom, 277 | }, 278 | { 279 | name: "Regex inequality runtime left side evaluation", 280 | expression: `error() !~ "."`, 281 | wantErr: custom, 282 | }, 283 | //FuncExecution 284 | { 285 | name: "Func error bubbling", 286 | expression: "error()", 287 | extension: Function("error", func(arguments ...interface{}) (interface{}, error) { 288 | return nil, errors.New("Huge problems") 289 | }), 290 | wantErr: "Huge problems", 291 | }, 292 | //InvalidParameterCalls 293 | { 294 | name: "Missing parameter field reference", 295 | expression: "foo.NotExists", 296 | parameter: fooFailureParameters, 297 | wantErr: unknownParameter, 298 | }, 299 | { 300 | name: "Parameter method call on missing function", 301 | expression: "foo.NotExist()", 302 | parameter: fooFailureParameters, 303 | wantErr: unknownParameter, 304 | }, 305 | { 306 | name: "Nested missing parameter field reference", 307 | expression: "foo.Nested.NotExists", 308 | parameter: fooFailureParameters, 309 | wantErr: unknownParameter, 310 | }, 311 | { 312 | name: "Parameter method call returns error", 313 | expression: "foo.AlwaysFail()", 314 | parameter: fooFailureParameters, 315 | wantErr: "function should always fail", 316 | }, 317 | { 318 | name: "Too few arguments to parameter call", 319 | expression: "foo.FuncArgStr()", 320 | parameter: fooFailureParameters, 321 | wantErr: tooFewArguments, 322 | }, 323 | { 324 | name: "Too many arguments to parameter call", 325 | expression: `foo.FuncArgStr("foo", "bar", 15)`, 326 | parameter: fooFailureParameters, 327 | wantErr: tooManyArguments, 328 | }, 329 | { 330 | name: "Mismatched parameters", 331 | expression: "foo.FuncArgStr(5)", 332 | parameter: fooFailureParameters, 333 | wantErr: mismatchedParameters, 334 | }, 335 | { 336 | name: "Negative Array Index", 337 | expression: "foo[-1]", 338 | parameter: map[string]interface{}{ 339 | "foo": []int{1, 2, 3}, 340 | }, 341 | wantErr: unknownParameter, 342 | }, 343 | { 344 | name: "Nested slice call index out of bound", 345 | expression: `foo.Nested.Slice[10]`, 346 | parameter: map[string]interface{}{"foo": foo}, 347 | wantErr: unknownParameter, 348 | }, 349 | { 350 | name: "Nested map call missing key", 351 | expression: `foo.Nested.Map["d"]`, 352 | parameter: map[string]interface{}{"foo": foo}, 353 | wantErr: unknownParameter, 354 | }, 355 | { 356 | name: "invalid selector", 357 | expression: "hello[world()]", 358 | extension: NewLanguage(Base(), Function("world", func() (int, error) { 359 | return 0, fmt.Errorf("test error") 360 | })), 361 | wantErr: "test error", 362 | }, 363 | { 364 | name: "eval `nil > 1` returns true #23", 365 | expression: `nil > 1`, 366 | wantErr: "invalid operation () > (float64)", 367 | }, 368 | { 369 | name: "map with unknown func", 370 | expression: `foo.MapWithFunc.NotExist()`, 371 | parameter: map[string]interface{}{"foo": foo}, 372 | wantErr: unknownParameter, 373 | }, 374 | { 375 | name: "map with unknown func", 376 | expression: `foo.SliceWithFunc.NotExist()`, 377 | parameter: map[string]interface{}{"foo": foo}, 378 | wantErr: unknownParameter, 379 | }, 380 | } 381 | 382 | for i := range evaluationTests { 383 | if evaluationTests[i].parameter == nil { 384 | evaluationTests[i].parameter = map[string]interface{}{ 385 | "number": 1, 386 | "string": "foo", 387 | "bool": true, 388 | "error": func() (int, error) { 389 | return 0, fmt.Errorf("test error") 390 | }, 391 | } 392 | } 393 | } 394 | 395 | testEvaluate(evaluationTests, test) 396 | } 397 | -------------------------------------------------------------------------------- /gval_noparameter_test.go: -------------------------------------------------------------------------------- 1 | package gval 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | "text/scanner" 8 | ) 9 | 10 | func TestNoParameter(t *testing.T) { 11 | testEvaluate( 12 | []evaluationTest{ 13 | { 14 | name: "Number", 15 | expression: "100", 16 | want: 100.0, 17 | }, 18 | { 19 | name: "Single PLUS", 20 | expression: "51 + 49", 21 | want: 100.0, 22 | }, 23 | { 24 | name: "Single MINUS", 25 | expression: "100 - 51", 26 | want: 49.0, 27 | }, 28 | { 29 | name: "Single BITWISE AND", 30 | expression: "100 & 50", 31 | want: 32.0, 32 | }, 33 | { 34 | name: "Single BITWISE OR", 35 | expression: "100 | 50", 36 | want: 118.0, 37 | }, 38 | { 39 | name: "Single BITWISE XOR", 40 | expression: "100 ^ 50", 41 | want: 86.0, 42 | }, 43 | { 44 | name: "Single shift left", 45 | expression: "2 << 1", 46 | want: 4.0, 47 | }, 48 | { 49 | name: "Single shift right", 50 | expression: "2 >> 1", 51 | want: 1.0, 52 | }, 53 | { 54 | name: "Single BITWISE NOT", 55 | expression: "~10", 56 | want: -11.0, 57 | }, 58 | { 59 | 60 | name: "Single MULTIPLY", 61 | expression: "5 * 20", 62 | want: 100.0, 63 | }, 64 | { 65 | 66 | name: "Single DIVIDE", 67 | expression: "100 / 20", 68 | want: 5.0, 69 | }, 70 | { 71 | 72 | name: "Single even MODULUS", 73 | expression: "100 % 2", 74 | want: 0.0, 75 | }, 76 | { 77 | name: "Single odd MODULUS", 78 | expression: "101 % 2", 79 | want: 1.0, 80 | }, 81 | { 82 | 83 | name: "Single EXPONENT", 84 | expression: "10 ** 2", 85 | want: 100.0, 86 | }, 87 | { 88 | 89 | name: "Compound PLUS", 90 | expression: "20 + 30 + 50", 91 | want: 100.0, 92 | }, 93 | { 94 | 95 | name: "Compound BITWISE AND", 96 | expression: "20 & 30 & 50", 97 | want: 16.0, 98 | }, 99 | { 100 | name: "Mutiple operators", 101 | expression: "20 * 5 - 49", 102 | want: 51.0, 103 | }, 104 | { 105 | name: "Parenthesis usage", 106 | expression: "100 - (5 * 10)", 107 | want: 50.0, 108 | }, 109 | { 110 | 111 | name: "Nested parentheses", 112 | expression: "50 + (5 * (15 - 5))", 113 | want: 100.0, 114 | }, 115 | { 116 | 117 | name: "Nested parentheses with bitwise", 118 | expression: "100 ^ (23 * (2 | 5))", 119 | want: 197.0, 120 | }, 121 | { 122 | name: "Logical OR operation of two clauses", 123 | expression: "(1 == 1) || (true == true)", 124 | want: true, 125 | }, 126 | { 127 | name: "Logical AND operation of two clauses", 128 | expression: "(1 == 1) && (true == true)", 129 | want: true, 130 | }, 131 | { 132 | 133 | name: "Implicit boolean", 134 | expression: "2 > 1", 135 | want: true, 136 | }, 137 | { 138 | name: "Equal test minus numbers and no spaces", 139 | expression: "-1==-1", 140 | want: true, 141 | }, 142 | { 143 | 144 | name: "Compound boolean", 145 | expression: "5 < 10 && 1 < 5", 146 | want: true, 147 | }, 148 | { 149 | name: "Evaluated true && false operation (for issue #8)", 150 | expression: "1 > 10 && 11 > 10", 151 | want: false, 152 | }, 153 | { 154 | 155 | name: "Evaluated true && false operation (for issue #8)", 156 | expression: "true == true && false == true", 157 | want: false, 158 | }, 159 | { 160 | 161 | name: "Parenthesis boolean", 162 | expression: "10 < 50 && (1 != 2 && 1 > 0)", 163 | want: true, 164 | }, 165 | { 166 | name: "Comparison of string constants", 167 | expression: `"foo" == "foo"`, 168 | want: true, 169 | }, 170 | { 171 | 172 | name: "NEQ comparison of string constants", 173 | expression: `"foo" != "bar"`, 174 | want: true, 175 | }, 176 | { 177 | 178 | name: "REQ comparison of string constants", 179 | expression: `"foobar" =~ "oba"`, 180 | want: true, 181 | }, 182 | { 183 | 184 | name: "NREQ comparison of string constants", 185 | expression: `"foo" !~ "bar"`, 186 | want: true, 187 | }, 188 | { 189 | 190 | name: "Multiplicative/additive order", 191 | expression: "5 + 10 * 2", 192 | want: 25.0, 193 | }, 194 | { 195 | name: "Multiple constant multiplications", 196 | expression: "10 * 10 * 10", 197 | want: 1000.0, 198 | }, 199 | { 200 | 201 | name: "Multiple adds/multiplications", 202 | expression: "10 * 10 * 10 + 1 * 10 * 10", 203 | want: 1100.0, 204 | }, 205 | { 206 | 207 | name: "Modulus operatorPrecedence", 208 | expression: "1 + 101 % 2 * 5", 209 | want: 6.0, 210 | }, 211 | { 212 | name: "Exponent operatorPrecedence", 213 | expression: "1 + 5 ** 3 % 2 * 5", 214 | want: 6.0, 215 | }, 216 | { 217 | 218 | name: "Bit shift operatorPrecedence", 219 | expression: "50 << 1 & 90", 220 | want: 64.0, 221 | }, 222 | { 223 | 224 | name: "Bit shift operatorPrecedence", 225 | expression: "90 & 50 << 1", 226 | want: 64.0, 227 | }, 228 | { 229 | 230 | name: "Bit shift operatorPrecedence amongst non-bitwise", 231 | expression: "90 + 50 << 1 * 5", 232 | want: 4480.0, 233 | }, 234 | { 235 | name: "Order of non-commutative same-operatorPrecedence operators (additive)", 236 | expression: "1 - 2 - 4 - 8", 237 | want: -13.0, 238 | }, 239 | { 240 | name: "Order of non-commutative same-operatorPrecedence operators (multiplicative)", 241 | expression: "1 * 4 / 2 * 8", 242 | want: 16.0, 243 | }, 244 | { 245 | name: "Null coalesce operatorPrecedence", 246 | expression: "true ?? true ? 100 + 200 : 400", 247 | want: 300.0, 248 | }, 249 | { 250 | name: "Identical date equivalence", 251 | expression: `"2014-01-02 14:12:22" == "2014-01-02 14:12:22"`, 252 | want: true, 253 | }, 254 | { 255 | name: "Positive date GT", 256 | expression: `"2014-01-02 14:12:22" > "2014-01-02 12:12:22"`, 257 | want: true, 258 | }, 259 | { 260 | name: "Negative date GT", 261 | expression: `"2014-01-02 14:12:22" > "2014-01-02 16:12:22"`, 262 | want: false, 263 | }, 264 | { 265 | name: "Positive date GTE", 266 | expression: `"2014-01-02 14:12:22" >= "2014-01-02 12:12:22"`, 267 | want: true, 268 | }, 269 | { 270 | name: "Negative date GTE", 271 | expression: `"2014-01-02 14:12:22" >= "2014-01-02 16:12:22"`, 272 | want: false, 273 | }, 274 | { 275 | name: "Positive date LT", 276 | expression: `"2014-01-02 14:12:22" < "2014-01-02 16:12:22"`, 277 | want: true, 278 | }, 279 | { 280 | 281 | name: "Negative date LT", 282 | expression: `"2014-01-02 14:12:22" < "2014-01-02 11:12:22"`, 283 | want: false, 284 | }, 285 | { 286 | 287 | name: "Positive date LTE", 288 | expression: `"2014-01-02 09:12:22" <= "2014-01-02 12:12:22"`, 289 | want: true, 290 | }, 291 | { 292 | name: "Negative date LTE", 293 | expression: `"2014-01-02 14:12:22" <= "2014-01-02 11:12:22"`, 294 | want: false, 295 | }, 296 | { 297 | 298 | name: "Sign prefix comparison", 299 | expression: "-1 < 0", 300 | want: true, 301 | }, 302 | { 303 | 304 | name: "Lexicographic LT", 305 | expression: `"ab" < "abc"`, 306 | want: true, 307 | }, 308 | { 309 | name: "Lexicographic LTE", 310 | expression: `"ab" <= "abc"`, 311 | want: true, 312 | }, 313 | { 314 | 315 | name: "Lexicographic GT", 316 | expression: `"aba" > "abc"`, 317 | want: false, 318 | }, 319 | { 320 | 321 | name: "Lexicographic GTE", 322 | expression: `"aba" >= "abc"`, 323 | want: false, 324 | }, 325 | { 326 | 327 | name: "Boolean sign prefix comparison", 328 | expression: "!true == false", 329 | want: true, 330 | }, 331 | { 332 | name: "Inversion of clause", 333 | expression: "!(10 < 0)", 334 | want: true, 335 | }, 336 | { 337 | 338 | name: "Negation after modifier", 339 | expression: "10 * -10", 340 | want: -100.0, 341 | }, 342 | { 343 | 344 | name: "Ternary with single boolean", 345 | expression: "true ? 10", 346 | want: 10.0, 347 | }, 348 | { 349 | 350 | name: "Ternary nil with single boolean", 351 | expression: "false ? 10", 352 | want: nil, 353 | }, 354 | { 355 | name: "Ternary with comparator boolean", 356 | expression: "10 > 5 ? 35.50", 357 | want: 35.50, 358 | }, 359 | { 360 | 361 | name: "Ternary nil with comparator boolean", 362 | expression: "1 > 5 ? 35.50", 363 | want: nil, 364 | }, 365 | { 366 | 367 | name: "Ternary with parentheses", 368 | expression: "(5 * (15 - 5)) > 5 ? 35.50", 369 | want: 35.50, 370 | }, 371 | { 372 | 373 | name: "Ternary operatorPrecedence", 374 | expression: "true ? 35.50 > 10", 375 | want: true, 376 | }, 377 | { 378 | name: "Ternary-else", 379 | expression: "false ? 35.50 : 50", 380 | want: 50.0, 381 | }, 382 | { 383 | 384 | name: "Ternary-else inside clause", 385 | expression: "(false ? 5 : 35.50) > 10", 386 | want: true, 387 | }, 388 | { 389 | 390 | name: "Ternary-else (true-case) inside clause", 391 | expression: "(true ? 1 : 5) < 10", 392 | want: true, 393 | }, 394 | { 395 | 396 | name: "Ternary-else before comparator (negative case)", 397 | expression: "true ? 1 : 5 > 10", 398 | want: 1.0, 399 | }, 400 | { 401 | name: "Nested ternaries (#32)", 402 | expression: "(2 == 2) ? 1 : (true ? 2 : 3)", 403 | want: 1.0, 404 | }, 405 | { 406 | 407 | name: "Nested ternaries, right case (#32)", 408 | expression: "false ? 1 : (true ? 2 : 3)", 409 | want: 2.0, 410 | }, 411 | { 412 | 413 | name: "Doubly-nested ternaries (#32)", 414 | expression: "true ? (false ? 1 : (false ? 2 : 3)) : (false ? 4 : 5)", 415 | want: 3.0, 416 | }, 417 | { 418 | 419 | name: "String to string concat", 420 | expression: `"foo" + "bar" == "foobar"`, 421 | want: true, 422 | }, 423 | { 424 | name: "String to float64 concat", 425 | expression: `"foo" + 123 == "foo123"`, 426 | want: true, 427 | }, 428 | { 429 | 430 | name: "Float64 to string concat", 431 | expression: `123 + "bar" == "123bar"`, 432 | want: true, 433 | }, 434 | { 435 | 436 | name: "String to date concat", 437 | expression: `"foo" + "02/05/1970" == "foobar"`, 438 | want: false, 439 | }, 440 | { 441 | 442 | name: "String to bool concat", 443 | expression: `"foo" + true == "footrue"`, 444 | want: true, 445 | }, 446 | { 447 | name: "Bool to string concat", 448 | expression: `true + "bar" == "truebar"`, 449 | want: true, 450 | }, 451 | { 452 | 453 | name: "Null coalesce left", 454 | expression: "1 ?? 2", 455 | want: 1.0, 456 | }, 457 | { 458 | 459 | name: "Array membership literals", 460 | expression: "1 in [1, 2, 3]", 461 | want: true, 462 | }, 463 | { 464 | 465 | name: "Array membership literal with inversion", 466 | expression: "!(1 in [1, 2, 3])", 467 | want: false, 468 | }, 469 | { 470 | name: "Logical operator reordering (#30)", 471 | expression: "(true && true) || (true && false)", 472 | want: true, 473 | }, 474 | { 475 | 476 | name: "Logical operator reordering without parens (#30)", 477 | expression: "true && true || true && false", 478 | want: true, 479 | }, 480 | { 481 | 482 | name: "Logical operator reordering with multiple OR (#30)", 483 | expression: "false || true && true || false", 484 | want: true, 485 | }, 486 | { 487 | name: "Left-side multiple consecutive (should be reordered) operators", 488 | expression: "(10 * 10 * 10) > 10", 489 | want: true, 490 | }, 491 | { 492 | 493 | name: "Three-part non-paren logical op reordering (#44)", 494 | expression: "false && true || true", 495 | want: true, 496 | }, 497 | { 498 | 499 | name: "Three-part non-paren logical op reordering (#44), second one", 500 | expression: "true || false && true", 501 | want: true, 502 | }, 503 | { 504 | name: "Logical operator reordering without parens (#45)", 505 | expression: "true && true || false && false", 506 | want: true, 507 | }, 508 | { 509 | name: "Single function", 510 | expression: "foo()", 511 | extension: Function("foo", func(arguments ...interface{}) (interface{}, error) { 512 | return true, nil 513 | }), 514 | 515 | want: true, 516 | }, 517 | { 518 | name: "Func with argument", 519 | expression: "passthrough(1)", 520 | extension: Function("passthrough", func(arguments ...interface{}) (interface{}, error) { 521 | return arguments[0], nil 522 | }), 523 | want: 1.0, 524 | }, 525 | { 526 | name: "Func with arguments", 527 | expression: "passthrough(1, 2)", 528 | extension: Function("passthrough", func(arguments ...interface{}) (interface{}, error) { 529 | return arguments[0].(float64) + arguments[1].(float64), nil 530 | }), 531 | want: 3.0, 532 | }, 533 | { 534 | name: "Nested function with operatorPrecedence", 535 | expression: "sum(1, sum(2, 3), 2 + 2, true ? 4 : 5)", 536 | extension: Function("sum", func(arguments ...interface{}) (interface{}, error) { 537 | sum := 0.0 538 | for _, v := range arguments { 539 | sum += v.(float64) 540 | } 541 | return sum, nil 542 | }), 543 | want: 14.0, 544 | }, 545 | { 546 | name: "Empty function and modifier, compared", 547 | expression: "numeric()-1 > 0", 548 | extension: Function("numeric", func(arguments ...interface{}) (interface{}, error) { 549 | return 2.0, nil 550 | }), 551 | want: true, 552 | }, 553 | { 554 | name: "Empty function comparator", 555 | expression: "numeric() > 0", 556 | extension: Function("numeric", func(arguments ...interface{}) (interface{}, error) { 557 | return 2.0, nil 558 | }), 559 | want: true, 560 | }, 561 | { 562 | 563 | name: "Empty function logical operator", 564 | expression: "success() && !false", 565 | extension: Function("success", func(arguments ...interface{}) (interface{}, error) { 566 | return true, nil 567 | }), 568 | want: true, 569 | }, 570 | { 571 | name: "Empty function ternary", 572 | expression: "nope() ? 1 : 2.0", 573 | extension: Function("nope", func(arguments ...interface{}) (interface{}, error) { 574 | return false, nil 575 | }), 576 | want: 2.0, 577 | }, 578 | { 579 | 580 | name: "Empty function null coalesce", 581 | expression: "null() ?? 2", 582 | extension: Function("null", func(arguments ...interface{}) (interface{}, error) { 583 | return nil, nil 584 | }), 585 | want: 2.0, 586 | }, 587 | { 588 | name: "Empty function with prefix", 589 | expression: "-ten()", 590 | extension: Function("ten", func(arguments ...interface{}) (interface{}, error) { 591 | return 10.0, nil 592 | }), 593 | want: -10.0, 594 | }, 595 | { 596 | name: "Empty function as part of chain", 597 | expression: "10 - numeric() - 2", 598 | extension: Function("numeric", func(arguments ...interface{}) (interface{}, error) { 599 | return 5.0, nil 600 | }), 601 | want: 3.0, 602 | }, 603 | { 604 | name: "Empty function near separator", 605 | expression: "10 in [1, 2, 3, ten(), 8]", 606 | extension: Function("ten", func(arguments ...interface{}) (interface{}, error) { 607 | return 10.0, nil 608 | }), 609 | want: true, 610 | }, 611 | { 612 | name: "Enclosed empty function with modifier and comparator (#28)", 613 | expression: "(ten() - 1) > 3", 614 | extension: Function("ten", func(arguments ...interface{}) (interface{}, error) { 615 | return 10.0, nil 616 | }), 617 | want: true, 618 | }, 619 | { 620 | name: "Array", 621 | expression: `[(ten() - 1) > 3, (ten() - 1),"hey"]`, 622 | extension: Function("ten", func(arguments ...interface{}) (interface{}, error) { 623 | return 10.0, nil 624 | }), 625 | want: []interface{}{true, 9., "hey"}, 626 | }, 627 | { 628 | name: "Object", 629 | expression: `{1: (ten() - 1) > 3, 7 + ".X" : (ten() - 1),"hello" : "hey"}`, 630 | extension: Function("ten", func(arguments ...interface{}) (interface{}, error) { 631 | return 10.0, nil 632 | }), 633 | want: map[string]interface{}{"1": true, "7.X": 9., "hello": "hey"}, 634 | }, 635 | { 636 | name: "Object negativ value", 637 | expression: `{1: -1,"hello" : "hey"}`, 638 | want: map[string]interface{}{"1": -1., "hello": "hey"}, 639 | }, 640 | { 641 | name: "Empty Array", 642 | expression: `[]`, 643 | want: []interface{}{}, 644 | }, 645 | { 646 | name: "Empty Object", 647 | expression: `{}`, 648 | want: map[string]interface{}{}, 649 | }, 650 | { 651 | name: "Variadic", 652 | expression: `sum(1,2,3,4)`, 653 | extension: Function("sum", func(arguments ...float64) (interface{}, error) { 654 | sum := 0. 655 | for _, a := range arguments { 656 | sum += a 657 | } 658 | return sum, nil 659 | }), 660 | want: 10.0, 661 | }, 662 | { 663 | name: "Ident Operator", 664 | expression: `1 plus 1`, 665 | extension: InfixNumberOperator("plus", func(a, b float64) (interface{}, error) { 666 | return a + b, nil 667 | }), 668 | want: 2.0, 669 | }, 670 | { 671 | name: "Postfix Operator", 672 | expression: `4§`, 673 | extension: PostfixOperator("§", func(_ context.Context, _ *Parser, eval Evaluable) (Evaluable, error) { 674 | return func(ctx context.Context, parameter interface{}) (interface{}, error) { 675 | i, err := eval.EvalInt(ctx, parameter) 676 | if err != nil { 677 | return nil, err 678 | } 679 | return fmt.Sprintf("§%d", i), nil 680 | }, nil 681 | }), 682 | want: "§4", 683 | }, 684 | { 685 | name: "Tabs as non-whitespace", 686 | expression: "4\t5\t6", 687 | extension: NewLanguage( 688 | Init(func(ctx context.Context, p *Parser) (Evaluable, error) { 689 | p.SetWhitespace('\n', '\r', ' ') 690 | return p.ParseExpression(ctx) 691 | }), 692 | InfixNumberOperator("\t", func(a, b float64) (interface{}, error) { 693 | return a * b, nil 694 | }), 695 | ), 696 | want: 120.0, 697 | }, 698 | { 699 | name: "Handle all other prefixes", 700 | expression: "^foo + $bar + &baz", 701 | extension: DefaultExtension(func(ctx context.Context, p *Parser) (Evaluable, error) { 702 | var mul int 703 | switch p.TokenText() { 704 | case "^": 705 | mul = 1 706 | case "$": 707 | mul = 2 708 | case "&": 709 | mul = 3 710 | } 711 | 712 | switch p.Scan() { 713 | case scanner.Ident: 714 | return p.Const(mul * len(p.TokenText())), nil 715 | default: 716 | return nil, p.Expected("length multiplier", scanner.Ident) 717 | } 718 | }), 719 | want: 18.0, 720 | }, 721 | { 722 | name: "Embed languages", 723 | expression: "left { 5 + 5 } right", 724 | extension: func() Language { 725 | step := func(ctx context.Context, p *Parser, cur Evaluable) (Evaluable, error) { 726 | next, err := p.ParseExpression(ctx) 727 | if err != nil { 728 | return nil, err 729 | } 730 | 731 | return func(ctx context.Context, parameter interface{}) (interface{}, error) { 732 | us, err := cur.EvalString(ctx, parameter) 733 | if err != nil { 734 | return nil, err 735 | } 736 | 737 | them, err := next.EvalString(ctx, parameter) 738 | if err != nil { 739 | return nil, err 740 | } 741 | 742 | return us + them, nil 743 | }, nil 744 | } 745 | 746 | return NewLanguage( 747 | Init(func(ctx context.Context, p *Parser) (Evaluable, error) { 748 | p.SetWhitespace() 749 | p.SetMode(0) 750 | 751 | return p.ParseExpression(ctx) 752 | }), 753 | DefaultExtension(func(ctx context.Context, p *Parser) (Evaluable, error) { 754 | return step(ctx, p, p.Const(p.TokenText())) 755 | }), 756 | PrefixExtension(scanner.EOF, func(ctx context.Context, p *Parser) (Evaluable, error) { 757 | return p.Const(""), nil 758 | }), 759 | PrefixExtension('{', func(ctx context.Context, p *Parser) (Evaluable, error) { 760 | eval, err := p.ParseSublanguage(ctx, Full()) 761 | if err != nil { 762 | return nil, err 763 | } 764 | 765 | switch p.Scan() { 766 | case '}': 767 | default: 768 | return nil, p.Expected("embedded", '}') 769 | } 770 | 771 | return step(ctx, p, eval) 772 | }), 773 | ) 774 | }(), 775 | want: "left 10 right", 776 | }, 777 | { 778 | name: "Late binding", 779 | expression: "5 * [ 10 * { 20 / [ 10 ] } ]", 780 | extension: func() Language { 781 | var inner, outer Language 782 | 783 | parseCurly := func(ctx context.Context, p *Parser) (Evaluable, error) { 784 | eval, err := p.ParseSublanguage(ctx, outer) 785 | if err != nil { 786 | return nil, err 787 | } 788 | 789 | if p.Scan() != '}' { 790 | return nil, p.Expected("end", '}') 791 | } 792 | 793 | return eval, nil 794 | } 795 | 796 | parseSquare := func(ctx context.Context, p *Parser) (Evaluable, error) { 797 | eval, err := p.ParseSublanguage(ctx, inner) 798 | if err != nil { 799 | return nil, err 800 | } 801 | 802 | if p.Scan() != ']' { 803 | return nil, p.Expected("end", ']') 804 | } 805 | 806 | return eval, nil 807 | } 808 | 809 | inner = Full(PrefixExtension('{', parseCurly)) 810 | outer = Full(PrefixExtension('[', parseSquare)) 811 | return outer 812 | }(), 813 | want: 100.0, 814 | }, 815 | }, 816 | t, 817 | ) 818 | } 819 | -------------------------------------------------------------------------------- /gval_parameterized_test.go: -------------------------------------------------------------------------------- 1 | package gval 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/shopspring/decimal" 12 | ) 13 | 14 | func TestParameterized(t *testing.T) { 15 | testEvaluate( 16 | []evaluationTest{ 17 | { 18 | name: "Single parameter modified by constant", 19 | expression: "foo + 2", 20 | parameter: map[string]interface{}{ 21 | "foo": 2.0, 22 | }, 23 | want: 4.0, 24 | }, 25 | { 26 | 27 | name: "Single parameter modified by variable", 28 | expression: "foo * bar", 29 | parameter: map[string]interface{}{ 30 | "foo": 5.0, 31 | "bar": 2.0, 32 | }, 33 | want: 10.0, 34 | }, 35 | { 36 | 37 | name: "Single parameter modified by variable", 38 | expression: `foo["hey"] * bar[1]`, 39 | parameter: map[string]interface{}{ 40 | "foo": map[string]interface{}{"hey": 5.0}, 41 | "bar": []interface{}{7., 2.0}, 42 | }, 43 | want: 10.0, 44 | }, 45 | { 46 | 47 | name: "Multiple multiplications of the same parameter", 48 | expression: "foo * foo * foo", 49 | parameter: map[string]interface{}{ 50 | "foo": 10.0, 51 | }, 52 | want: 1000.0, 53 | }, 54 | { 55 | 56 | name: "Multiple additions of the same parameter", 57 | expression: "foo + foo + foo", 58 | parameter: map[string]interface{}{ 59 | "foo": 10.0, 60 | }, 61 | want: 30.0, 62 | }, 63 | { 64 | name: "NoSpaceOperator", 65 | expression: "true&&name", 66 | parameter: map[string]interface{}{ 67 | "name": true, 68 | }, 69 | want: true, 70 | }, 71 | { 72 | 73 | name: "Parameter name sensitivity", 74 | expression: "foo + FoO + FOO", 75 | parameter: map[string]interface{}{ 76 | "foo": 8.0, 77 | "FoO": 4.0, 78 | "FOO": 2.0, 79 | }, 80 | want: 14.0, 81 | }, 82 | { 83 | 84 | name: "Sign prefix comparison against prefixed variable", 85 | expression: "-1 < -foo", 86 | parameter: map[string]interface{}{"foo": -8.0}, 87 | want: true, 88 | }, 89 | { 90 | 91 | name: "Fixed-point parameter", 92 | expression: "foo > 1", 93 | parameter: map[string]interface{}{"foo": 2}, 94 | want: true, 95 | }, 96 | { 97 | 98 | name: "Modifier after closing clause", 99 | expression: "(2 + 2) + 2 == 6", 100 | want: true, 101 | }, 102 | { 103 | 104 | name: "Comparator after closing clause", 105 | expression: "(2 + 2) >= 4", 106 | want: true, 107 | }, 108 | { 109 | 110 | name: "Two-boolean logical operation (for issue #8)", 111 | expression: "(foo == true) || (bar == true)", 112 | parameter: map[string]interface{}{ 113 | "foo": true, 114 | "bar": false, 115 | }, 116 | want: true, 117 | }, 118 | { 119 | 120 | name: "Two-variable integer logical operation (for issue #8)", 121 | expression: "foo > 10 && bar > 10", 122 | parameter: map[string]interface{}{ 123 | "foo": 1, 124 | "bar": 11, 125 | }, 126 | want: false, 127 | }, 128 | { 129 | 130 | name: "Regex against right-hand parameter", 131 | expression: `"foobar" =~ foo`, 132 | parameter: map[string]interface{}{ 133 | "foo": "obar", 134 | }, 135 | want: true, 136 | }, 137 | { 138 | 139 | name: "Not-regex against right-hand parameter", 140 | expression: `"foobar" !~ foo`, 141 | parameter: map[string]interface{}{ 142 | "foo": "baz", 143 | }, 144 | want: true, 145 | }, 146 | { 147 | 148 | name: "Regex against two parameter", 149 | expression: `foo =~ bar`, 150 | parameter: map[string]interface{}{ 151 | "foo": "foobar", 152 | "bar": "oba", 153 | }, 154 | want: true, 155 | }, 156 | { 157 | 158 | name: "Not-regex against two parameter", 159 | expression: "foo !~ bar", 160 | parameter: map[string]interface{}{ 161 | "foo": "foobar", 162 | "bar": "baz", 163 | }, 164 | want: true, 165 | }, 166 | { 167 | 168 | name: "Pre-compiled regex", 169 | expression: "foo =~ bar", 170 | parameter: map[string]interface{}{ 171 | "foo": "foobar", 172 | "bar": regexp.MustCompile("[fF][oO]+"), 173 | }, 174 | want: true, 175 | }, 176 | { 177 | 178 | name: "Pre-compiled not-regex", 179 | expression: "foo !~ bar", 180 | parameter: map[string]interface{}{ 181 | "foo": "foobar", 182 | "bar": regexp.MustCompile("[fF][oO]+"), 183 | }, 184 | want: false, 185 | }, 186 | { 187 | 188 | name: "Single boolean parameter", 189 | expression: "commission ? 10", 190 | parameter: map[string]interface{}{ 191 | "commission": true}, 192 | want: 10.0, 193 | }, 194 | { 195 | 196 | name: "True comparator with a parameter", 197 | expression: `partner == "amazon" ? 10`, 198 | parameter: map[string]interface{}{ 199 | "partner": "amazon"}, 200 | want: 10.0, 201 | }, 202 | { 203 | 204 | name: "False comparator with a parameter", 205 | expression: `partner == "amazon" ? 10`, 206 | parameter: map[string]interface{}{ 207 | "partner": "ebay"}, 208 | want: nil, 209 | }, 210 | { 211 | 212 | name: "True comparator with multiple parameters", 213 | expression: "theft && period == 24 ? 60", 214 | parameter: map[string]interface{}{ 215 | "theft": true, 216 | "period": 24, 217 | }, 218 | want: 60.0, 219 | }, 220 | { 221 | 222 | name: "False comparator with multiple parameters", 223 | expression: "theft && period == 24 ? 60", 224 | parameter: map[string]interface{}{ 225 | "theft": false, 226 | "period": 24, 227 | }, 228 | want: nil, 229 | }, 230 | { 231 | 232 | name: "String concat with single string parameter", 233 | expression: `foo + "bar"`, 234 | parameter: map[string]interface{}{ 235 | "foo": "baz"}, 236 | want: "bazbar", 237 | }, 238 | { 239 | 240 | name: "String concat with multiple string parameter", 241 | expression: "foo + bar", 242 | parameter: map[string]interface{}{ 243 | "foo": "baz", 244 | "bar": "quux", 245 | }, 246 | want: "bazquux", 247 | }, 248 | { 249 | 250 | name: "String concat with float parameter", 251 | expression: "foo + bar", 252 | parameter: map[string]interface{}{ 253 | "foo": "baz", 254 | "bar": 123.0, 255 | }, 256 | want: "baz123", 257 | }, 258 | { 259 | 260 | name: "Mixed multiple string concat", 261 | expression: `foo + 123 + "bar" + true`, 262 | parameter: map[string]interface{}{"foo": "baz"}, 263 | want: "baz123bartrue", 264 | }, 265 | { 266 | 267 | name: "Integer width spectrum", 268 | expression: "uint8 + uint16 + uint32 + uint64 + int8 + int16 + int32 + int64", 269 | parameter: map[string]interface{}{ 270 | "uint8": uint8(0), 271 | "uint16": uint16(0), 272 | "uint32": uint32(0), 273 | "uint64": uint64(0), 274 | "int8": int8(0), 275 | "int16": int16(0), 276 | "int32": int32(0), 277 | "int64": int64(0), 278 | }, 279 | want: 0.0, 280 | }, 281 | { 282 | 283 | name: "Null coalesce right", 284 | expression: "foo ?? 1.0", 285 | parameter: map[string]interface{}{"foo": nil}, 286 | want: 1.0, 287 | }, 288 | { 289 | 290 | name: "Multiple comparator/logical operators (#30)", 291 | expression: "(foo >= 2887057408 && foo <= 2887122943) || (foo >= 168100864 && foo <= 168118271)", 292 | parameter: map[string]interface{}{"foo": 2887057409}, 293 | want: true, 294 | }, 295 | { 296 | 297 | name: "Multiple comparator/logical operators, opposite order (#30)", 298 | expression: "(foo >= 168100864 && foo <= 168118271) || (foo >= 2887057408 && foo <= 2887122943)", 299 | parameter: map[string]interface{}{"foo": 2887057409}, 300 | want: true, 301 | }, 302 | { 303 | 304 | name: "Multiple comparator/logical operators, small value (#30)", 305 | expression: "(foo >= 2887057408 && foo <= 2887122943) || (foo >= 168100864 && foo <= 168118271)", 306 | parameter: map[string]interface{}{"foo": 168100865}, 307 | want: true, 308 | }, 309 | { 310 | 311 | name: "Multiple comparator/logical operators, small value, opposite order (#30)", 312 | expression: "(foo >= 168100864 && foo <= 168118271) || (foo >= 2887057408 && foo <= 2887122943)", 313 | parameter: map[string]interface{}{"foo": 168100865}, 314 | want: true, 315 | }, 316 | { 317 | 318 | name: "Incomparable array equality comparison", 319 | expression: "arr == arr", 320 | parameter: map[string]interface{}{"arr": []int{0, 0, 0}}, 321 | want: true, 322 | }, 323 | { 324 | 325 | name: "Incomparable array not-equality comparison", 326 | expression: "arr != arr", 327 | parameter: map[string]interface{}{"arr": []int{0, 0, 0}}, 328 | want: false, 329 | }, 330 | { 331 | 332 | name: "Mixed function and parameters", 333 | expression: "sum(1.2, amount) + name", 334 | extension: Function("sum", func(arguments ...interface{}) (interface{}, error) { 335 | sum := 0.0 336 | for _, v := range arguments { 337 | sum += v.(float64) 338 | } 339 | return sum, nil 340 | }, 341 | ), 342 | parameter: map[string]interface{}{"amount": .8, 343 | "name": "awesome", 344 | }, 345 | 346 | want: "2awesome", 347 | }, 348 | { 349 | 350 | name: "Short-circuit OR", 351 | expression: "true || fail()", 352 | extension: Function("fail", func(arguments ...interface{}) (interface{}, error) { 353 | return nil, fmt.Errorf("Did not short-circuit") 354 | }), 355 | want: true, 356 | }, 357 | { 358 | 359 | name: "Short-circuit AND", 360 | expression: "false && fail()", 361 | extension: Function("fail", func(arguments ...interface{}) (interface{}, error) { 362 | return nil, fmt.Errorf("Did not short-circuit") 363 | }), 364 | want: false, 365 | }, 366 | { 367 | 368 | name: "Short-circuit ternary", 369 | expression: "true ? 1 : fail()", 370 | extension: Function("fail", func(arguments ...interface{}) (interface{}, error) { 371 | return nil, fmt.Errorf("Did not short-circuit") 372 | }), 373 | want: 1.0, 374 | }, 375 | { 376 | 377 | name: "Short-circuit coalesce", 378 | expression: `"foo" ?? fail()`, 379 | extension: Function("fail", func(arguments ...interface{}) (interface{}, error) { 380 | return nil, fmt.Errorf("Did not short-circuit") 381 | }), 382 | want: "foo", 383 | }, 384 | { 385 | 386 | name: "Simple parameter call", 387 | expression: "foo.String", 388 | parameter: map[string]interface{}{"foo": foo}, 389 | want: foo.String, 390 | }, 391 | { 392 | 393 | name: "Simple parameter function call", 394 | expression: "foo.Func()", 395 | parameter: map[string]interface{}{"foo": foo}, 396 | want: "funk", 397 | }, 398 | { 399 | 400 | name: "Simple parameter call from pointer", 401 | expression: "fooptr.String", 402 | parameter: map[string]interface{}{"fooptr": &foo}, 403 | want: foo.String, 404 | }, 405 | { 406 | 407 | name: "Simple parameter function call from pointer", 408 | expression: "fooptr.Func()", 409 | parameter: map[string]interface{}{"fooptr": &foo}, 410 | want: "funk", 411 | }, 412 | { 413 | 414 | name: "Simple parameter call", 415 | expression: `foo.String == "hi"`, 416 | parameter: map[string]interface{}{"foo": foo}, 417 | want: false, 418 | }, 419 | { 420 | 421 | name: "Simple parameter call with modifier", 422 | expression: `foo.String + "hi"`, 423 | parameter: map[string]interface{}{"foo": foo}, 424 | want: foo.String + "hi", 425 | }, 426 | { 427 | 428 | name: "Simple parameter function call, two-arg return", 429 | expression: `foo.Func2()`, 430 | parameter: map[string]interface{}{"foo": foo}, 431 | want: "frink", 432 | }, 433 | { 434 | 435 | name: "Simple parameter function call, one arg", 436 | expression: `foo.FuncArgStr("boop")`, 437 | parameter: map[string]interface{}{"foo": foo}, 438 | want: "boop", 439 | }, 440 | { 441 | 442 | name: "Simple parameter function call, one arg", 443 | expression: `foo.FuncArgStr("boop") + "hi"`, 444 | parameter: map[string]interface{}{"foo": foo}, 445 | want: "boophi", 446 | }, 447 | { 448 | 449 | name: "Nested parameter function call", 450 | expression: `foo.Nested.Dunk("boop")`, 451 | parameter: map[string]interface{}{"foo": foo}, 452 | want: "boopdunk", 453 | }, 454 | { 455 | 456 | name: "Nested parameter call", 457 | expression: "foo.Nested.Funk", 458 | parameter: map[string]interface{}{"foo": foo}, 459 | want: "funkalicious", 460 | }, 461 | { 462 | name: "Nested map call", 463 | expression: `foo.Nested.Map["a"]`, 464 | parameter: map[string]interface{}{"foo": foo}, 465 | want: 1, 466 | }, 467 | { 468 | name: "Nested slice call", 469 | expression: `foo.Nested.Slice[1]`, 470 | parameter: map[string]interface{}{"foo": foo}, 471 | want: 2, 472 | }, 473 | { 474 | 475 | name: "Parameter call with + modifier", 476 | expression: "1 + foo.Int", 477 | parameter: map[string]interface{}{"foo": foo}, 478 | want: 102.0, 479 | }, 480 | { 481 | 482 | name: "Parameter string call with + modifier", 483 | expression: `"woop" + (foo.String)`, 484 | parameter: map[string]interface{}{"foo": foo}, 485 | want: "woopstring!", 486 | }, 487 | { 488 | 489 | name: "Parameter call with && operator", 490 | expression: "true && foo.BoolFalse", 491 | parameter: map[string]interface{}{"foo": foo}, 492 | want: false, 493 | }, 494 | { 495 | name: "Null coalesce nested parameter", 496 | expression: "foo.Nil ?? false", 497 | parameter: map[string]interface{}{"foo": foo}, 498 | want: false, 499 | }, 500 | { 501 | name: "input functions", 502 | expression: "func1() + func2()", 503 | parameter: map[string]interface{}{ 504 | "func1": func() float64 { return 2000 }, 505 | "func2": func() float64 { return 2001 }, 506 | }, 507 | want: 4001.0, 508 | }, 509 | { 510 | name: "input functions", 511 | expression: "func1(date1) + func2(date2)", 512 | parameter: map[string]interface{}{ 513 | "date1": func() interface{} { 514 | y2k, _ := time.Parse("2006", "2000") 515 | return y2k 516 | }(), 517 | "date2": func() interface{} { 518 | y2k1, _ := time.Parse("2006", "2001") 519 | return y2k1 520 | }(), 521 | }, 522 | extension: NewLanguage( 523 | Function("func1", func(arguments ...interface{}) (interface{}, error) { 524 | return float64(arguments[0].(time.Time).Year()), nil 525 | }), 526 | Function("func2", func(arguments ...interface{}) (interface{}, error) { 527 | return float64(arguments[0].(time.Time).Year()), nil 528 | }), 529 | ), 530 | want: 4001.0, 531 | }, 532 | { 533 | name: "complex64 number as parameter", 534 | expression: "complex64", 535 | parameter: map[string]interface{}{ 536 | "complex64": complex64(0), 537 | "complex128": complex128(0), 538 | }, 539 | want: complex64(0), 540 | }, 541 | { 542 | name: "complex128 number as parameter", 543 | expression: "complex128", 544 | parameter: map[string]interface{}{ 545 | "complex64": complex64(0), 546 | "complex128": complex128(0), 547 | }, 548 | want: complex128(0), 549 | }, 550 | { 551 | name: "coalesce with undefined", 552 | expression: "fooz ?? foo", 553 | parameter: map[string]interface{}{ 554 | "foo": "bar", 555 | }, 556 | want: "bar", 557 | }, 558 | { 559 | name: "map[interface{}]interface{}", 560 | expression: "foo", 561 | parameter: map[interface{}]interface{}{ 562 | "foo": "bar", 563 | }, 564 | want: "bar", 565 | }, 566 | { 567 | name: "method on pointer type", 568 | expression: "foo.PointerFunc()", 569 | parameter: map[string]interface{}{ 570 | "foo": &dummyParameter{}, 571 | }, 572 | want: "point", 573 | }, 574 | { 575 | name: "custom selector", 576 | expression: "hello.world", 577 | parameter: "!", 578 | extension: NewLanguage(Base(), VariableSelector(func(path Evaluables) Evaluable { 579 | return func(c context.Context, v interface{}) (interface{}, error) { 580 | keys, err := path.EvalStrings(c, v) 581 | if err != nil { 582 | return nil, err 583 | } 584 | return fmt.Sprintf("%s%s", strings.Join(keys, " "), v), nil 585 | } 586 | })), 587 | want: "hello world!", 588 | }, 589 | { 590 | name: "map[int]int", 591 | expression: `a[0] + a[2]`, 592 | parameter: map[string]interface{}{ 593 | "a": map[int]int{0: 1, 2: 1}, 594 | }, 595 | want: 2., 596 | }, 597 | { 598 | name: "map[int]string", 599 | expression: `a[0] * a[2]`, 600 | parameter: map[string]interface{}{ 601 | "a": map[int]string{0: "1", 2: "1"}, 602 | }, 603 | want: 1., 604 | }, 605 | { 606 | name: "coalesce typed nil 0", 607 | expression: `ProjectID ?? 0`, 608 | parameter: struct { 609 | ProjectID *uint 610 | }{}, 611 | want: 0., 612 | }, 613 | { 614 | name: "coalesce typed nil 99", 615 | expression: `ProjectID ?? 99`, 616 | parameter: struct { 617 | ProjectID *uint 618 | }{}, 619 | want: 99., 620 | }, 621 | { 622 | name: "operator with typed nil 99", 623 | expression: `ProjectID + 99`, 624 | parameter: struct { 625 | ProjectID *uint 626 | }{}, 627 | want: "99", 628 | }, 629 | { 630 | name: "operator with typed nil if", 631 | expression: `Flag ? 1 : 2`, 632 | parameter: struct { 633 | Flag *uint 634 | }{}, 635 | want: 2., 636 | }, 637 | { 638 | name: "Decimal math doesn't experience rounding error", 639 | expression: "(x * 12.146) - y", 640 | extension: decimalArithmetic, 641 | parameter: map[string]interface{}{ 642 | "x": 12.5, 643 | "y": -5, 644 | }, 645 | want: decimal.NewFromFloat(156.825), 646 | equalityFunc: decimalEqualityFunc, 647 | }, 648 | { 649 | name: "Decimal logical operators fractional difference", 650 | expression: "((x * 12.146) - y) > 156.824999999", 651 | extension: decimalArithmetic, 652 | parameter: map[string]interface{}{ 653 | "x": 12.5, 654 | "y": -5, 655 | }, 656 | want: true, 657 | }, 658 | { 659 | name: "Decimal logical operators whole number difference", 660 | expression: "((x * 12.146) - y) > 156", 661 | extension: decimalArithmetic, 662 | parameter: map[string]interface{}{ 663 | "x": 12.5, 664 | "y": -5, 665 | }, 666 | want: true, 667 | }, 668 | { 669 | name: "Decimal logical operators exact decimal match against GT", 670 | expression: "((x * 12.146) - y) > 156.825", 671 | extension: decimalArithmetic, 672 | parameter: map[string]interface{}{ 673 | "x": 12.5, 674 | "y": -5, 675 | }, 676 | want: false, 677 | }, 678 | { 679 | name: "Decimal logical operators exact equality", 680 | expression: "((x * 12.146) - y) == 156.825", 681 | extension: decimalArithmetic, 682 | parameter: map[string]interface{}{ 683 | "x": 12.5, 684 | "y": -5, 685 | }, 686 | want: true, 687 | }, 688 | { 689 | name: "Decimal mixes with string logic with force fail", 690 | expression: `(((x * 12.146) - y) == 156.825) && a == "test" && !b && b`, 691 | extension: decimalArithmetic, 692 | parameter: map[string]interface{}{ 693 | "x": 12.5, 694 | "y": -5, 695 | "a": "test", 696 | "b": false, 697 | }, 698 | want: false, 699 | }, 700 | { 701 | name: "Typed map with function call", 702 | expression: `foo.MapWithFunc.Sum("a")`, 703 | parameter: map[string]interface{}{ 704 | "foo": foo, 705 | }, 706 | want: 3, 707 | }, 708 | { 709 | name: "Types slice with function call", 710 | expression: `foo.SliceWithFunc.Sum("a")`, 711 | parameter: map[string]interface{}{ 712 | "foo": foo, 713 | }, 714 | want: 2, 715 | }, 716 | }, 717 | t, 718 | ) 719 | } 720 | -------------------------------------------------------------------------------- /gval_parsingFailure_test.go: -------------------------------------------------------------------------------- 1 | package gval 2 | 3 | import ( 4 | "regexp/syntax" 5 | "testing" 6 | ) 7 | 8 | func TestParsingFailure(t *testing.T) { 9 | testEvaluate( 10 | []evaluationTest{ 11 | { 12 | name: "Invalid equality comparator", 13 | expression: "1 = 1", 14 | wantErr: unexpected(`"="`, "operator"), 15 | }, 16 | { 17 | name: "Invalid equality comparator", 18 | expression: "1 === 1", 19 | wantErr: unexpected(`"="`, "extension"), 20 | }, 21 | { 22 | name: "Too many characters for logical operator", 23 | expression: "true &&& false", 24 | wantErr: unexpected(`"&"`, "extension"), 25 | }, 26 | { 27 | 28 | name: "Too many characters for logical operator", 29 | expression: "true ||| false", 30 | wantErr: unexpected(`"|"`, "extension"), 31 | }, 32 | { 33 | 34 | name: "Premature end to expression, via modifier", 35 | expression: "10 > 5 +", 36 | wantErr: unexpected("EOF", "extensions"), 37 | }, 38 | { 39 | name: "Premature end to expression, via comparator", 40 | expression: "10 + 5 >", 41 | wantErr: unexpected("EOF", "extensions"), 42 | }, 43 | { 44 | name: "Premature end to expression, via logical operator", 45 | expression: "10 > 5 &&", 46 | wantErr: unexpected("EOF", "extensions"), 47 | }, 48 | { 49 | 50 | name: "Premature end to expression, via ternary operator", 51 | expression: "true ?", 52 | wantErr: unexpected("EOF", "extensions"), 53 | }, 54 | { 55 | name: "Hanging REQ", 56 | expression: "`wat` =~", 57 | wantErr: unexpected("EOF", "extensions"), 58 | }, 59 | { 60 | 61 | name: "Invalid operator change to REQ", 62 | expression: " / =~", 63 | wantErr: unexpected(`"/"`, "extensions"), 64 | }, 65 | { 66 | name: "Invalid starting token, comparator", 67 | expression: "> 10", 68 | wantErr: unexpected(`">"`, "extensions"), 69 | }, 70 | { 71 | name: "Invalid starting token, modifier", 72 | expression: "+ 5", 73 | wantErr: unexpected(`"+"`, "extensions"), 74 | }, 75 | { 76 | name: "Invalid starting token, logical operator", 77 | expression: "&& 5 < 10", 78 | wantErr: unexpected(`"&"`, "extensions"), 79 | }, 80 | { 81 | name: "Invalid NUMERIC transition", 82 | expression: "10 10", 83 | wantErr: unexpected(`Int`, "operator"), 84 | }, 85 | { 86 | name: "Invalid STRING transition", 87 | expression: "`foo` `foo`", 88 | wantErr: `String while scanning operator`, // can't use func unexpected because the token was changed from String to RawString in go 1.11 89 | }, 90 | { 91 | name: "Invalid operator transition", 92 | expression: "10 > < 10", 93 | wantErr: unexpected(`"<"`, "extensions"), 94 | }, 95 | { 96 | 97 | name: "Starting with unbalanced parens", 98 | expression: " ) ( arg2", 99 | wantErr: unexpected(`")"`, "extensions"), 100 | }, 101 | { 102 | 103 | name: "Unclosed bracket", 104 | expression: "[foo bar", 105 | wantErr: unexpected(`EOF`, "extensions"), 106 | }, 107 | { 108 | 109 | name: "Unclosed quote", 110 | expression: "foo == `responseTime", 111 | wantErr: "could not parse string", 112 | }, 113 | { 114 | 115 | name: "Constant regex pattern fail to compile", 116 | expression: "foo =~ `[abc`", 117 | wantErr: string(syntax.ErrMissingBracket), 118 | }, 119 | { 120 | 121 | name: "Constant unmatch regex pattern fail to compile", 122 | expression: "foo !~ `[abc`", 123 | wantErr: string(syntax.ErrMissingBracket), 124 | }, 125 | { 126 | 127 | name: "Unbalanced parentheses", 128 | expression: "10 > (1 + 50", 129 | wantErr: unexpected(`EOF`, "parentheses"), 130 | }, 131 | { 132 | 133 | name: "Multiple radix", 134 | expression: "127.0.0.1", 135 | wantErr: unexpected(`Float`, "operator"), 136 | }, 137 | { 138 | 139 | name: "Hanging accessor", 140 | expression: "foo.Bar.", 141 | wantErr: unexpected(`EOF`, "field"), 142 | }, 143 | { 144 | name: "Incomplete Hex", 145 | expression: "0x", 146 | wantErr: `strconv.ParseFloat: parsing "0x": invalid syntax`, 147 | }, 148 | { 149 | name: "Invalid Hex literal", 150 | expression: "0x > 0", 151 | wantErr: `strconv.ParseFloat: parsing "0x": invalid syntax`, 152 | }, 153 | { 154 | name: "Hex float (Unsupported)", 155 | expression: "0x1.1", 156 | wantErr: `strconv.ParseFloat: parsing "0x1.1": invalid syntax`, 157 | }, 158 | { 159 | name: "Hex invalid letter", 160 | expression: "0x12g1", 161 | wantErr: `strconv.ParseFloat: parsing "0x12": invalid syntax`, 162 | }, 163 | { 164 | name: "Error after camouflage", 165 | expression: "0 + ,", 166 | wantErr: `unexpected "," while scanning extensions`, 167 | }, 168 | }, 169 | t, 170 | ) 171 | } 172 | 173 | func unknownOp(op string) string { 174 | return "unknown operator " + op 175 | } 176 | 177 | func unexpected(token, unit string) string { 178 | return "unexpected " + token + " while scanning " + unit 179 | } 180 | -------------------------------------------------------------------------------- /gval_test.go: -------------------------------------------------------------------------------- 1 | package gval 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/shopspring/decimal" 10 | ) 11 | 12 | type evaluationTest struct { 13 | name string 14 | expression string 15 | extension Language 16 | parameter interface{} 17 | want interface{} 18 | equalityFunc func(x, y interface{}) bool 19 | wantErr string 20 | } 21 | 22 | func testEvaluate(tests []evaluationTest, t *testing.T) { 23 | for _, tt := range tests { 24 | t.Run(tt.name, func(t *testing.T) { 25 | got, err := Evaluate(tt.expression, tt.parameter, tt.extension) 26 | 27 | if tt.wantErr != "" { 28 | if err == nil { 29 | t.Fatalf("Evaluate(%s) expected error but got %v", tt.expression, got) 30 | } 31 | if !strings.Contains(err.Error(), tt.wantErr) { 32 | t.Fatalf("Evaluate(%s) expected error %s but got error %v", tt.expression, tt.wantErr, err) 33 | } 34 | return 35 | } 36 | if err != nil { 37 | t.Errorf("Evaluate() error = %v", err) 38 | return 39 | } 40 | if ef := tt.equalityFunc; ef != nil { 41 | if !ef(got, tt.want) { 42 | t.Errorf("Evaluate(%s) = %v, want %v", tt.expression, got, tt.want) 43 | } 44 | } else if !reflect.DeepEqual(got, tt.want) { 45 | t.Errorf("Evaluate(%s) = %v, want %v", tt.expression, got, tt.want) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | // dummyParameter used to test "parameter calls". 52 | type dummyParameter struct { 53 | String string 54 | Int int 55 | BoolFalse bool 56 | Nil interface{} 57 | Nested dummyNestedParameter 58 | MapWithFunc dummyMapWithFunc 59 | SliceWithFunc dummySliceWithFunc 60 | } 61 | 62 | func (d dummyParameter) Func() string { 63 | return "funk" 64 | } 65 | 66 | func (d dummyParameter) Func2() (string, error) { 67 | return "frink", nil 68 | } 69 | 70 | func (d *dummyParameter) PointerFunc() (string, error) { 71 | return "point", nil 72 | } 73 | 74 | func (d dummyParameter) FuncErr() (string, error) { 75 | return "", fmt.Errorf("fumps") 76 | } 77 | 78 | func (d dummyParameter) FuncArgStr(arg1 string) string { 79 | return arg1 80 | } 81 | 82 | func (d dummyParameter) AlwaysFail() (interface{}, error) { 83 | return nil, fmt.Errorf("function should always fail") 84 | } 85 | 86 | type dummyNestedParameter struct { 87 | Funk string 88 | Map map[string]int 89 | Slice []int 90 | } 91 | 92 | func (d dummyNestedParameter) Dunk(arg1 string) string { 93 | return arg1 + "dunk" 94 | } 95 | 96 | var foo = dummyParameter{ 97 | String: "string!", 98 | Int: 101, 99 | BoolFalse: false, 100 | Nil: nil, 101 | Nested: dummyNestedParameter{ 102 | Funk: "funkalicious", 103 | Map: map[string]int{"a": 1, "b": 2, "c": 3}, 104 | Slice: []int{1, 2, 3}, 105 | }, 106 | MapWithFunc: dummyMapWithFunc{"a": {1, 2}, "b": {3, 4}}, 107 | SliceWithFunc: dummySliceWithFunc{"a", "b", "c", "a"}, 108 | } 109 | 110 | var fooFailureParameters = map[string]interface{}{ 111 | "foo": foo, 112 | "fooptr": &foo, 113 | } 114 | 115 | var decimalEqualityFunc = func(x, y interface{}) bool { 116 | v1, ok1 := x.(decimal.Decimal) 117 | v2, ok2 := y.(decimal.Decimal) 118 | 119 | if !ok1 || !ok2 { 120 | return false 121 | } 122 | 123 | return v1.Equal(v2) 124 | } 125 | 126 | type dummyMapWithFunc map[string][]int 127 | 128 | func (m dummyMapWithFunc) Sum(key string) int { 129 | values, ok := m[key] 130 | if !ok { 131 | return -1 132 | } 133 | 134 | sum := 0 135 | for _, v := range values { 136 | sum += v 137 | } 138 | 139 | return sum 140 | } 141 | 142 | type dummySliceWithFunc []string 143 | 144 | func (m dummySliceWithFunc) Sum(key string) int { 145 | sum := 0 146 | for _, v := range m { 147 | if v == key { 148 | sum += 1 149 | } 150 | } 151 | 152 | return sum 153 | } 154 | -------------------------------------------------------------------------------- /language.go: -------------------------------------------------------------------------------- 1 | package gval 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "text/scanner" 7 | "unicode" 8 | 9 | "github.com/shopspring/decimal" 10 | ) 11 | 12 | // Language is an expression language 13 | type Language struct { 14 | prefixes map[interface{}]extension 15 | operators map[string]operator 16 | operatorSymbols map[rune]struct{} 17 | init extension 18 | def extension 19 | selector func(Evaluables) Evaluable 20 | } 21 | 22 | // NewLanguage returns the union of given Languages as new Language. 23 | func NewLanguage(bases ...Language) Language { 24 | l := newLanguage() 25 | for _, base := range bases { 26 | for i, e := range base.prefixes { 27 | l.prefixes[i] = e 28 | } 29 | for i, e := range base.operators { 30 | l.operators[i] = e.merge(l.operators[i]) 31 | l.operators[i].initiate(i) 32 | } 33 | for i := range base.operatorSymbols { 34 | l.operatorSymbols[i] = struct{}{} 35 | } 36 | if base.init != nil { 37 | l.init = base.init 38 | } 39 | if base.def != nil { 40 | l.def = base.def 41 | } 42 | if base.selector != nil { 43 | l.selector = base.selector 44 | } 45 | } 46 | return l 47 | } 48 | 49 | func newLanguage() Language { 50 | return Language{ 51 | prefixes: map[interface{}]extension{}, 52 | operators: map[string]operator{}, 53 | operatorSymbols: map[rune]struct{}{}, 54 | } 55 | } 56 | 57 | // NewEvaluable returns an Evaluable for given expression in the specified language 58 | func (l Language) NewEvaluable(expression string) (Evaluable, error) { 59 | return l.NewEvaluableWithContext(context.Background(), expression) 60 | } 61 | 62 | // NewEvaluableWithContext returns an Evaluable for given expression in the specified language using context 63 | func (l Language) NewEvaluableWithContext(c context.Context, expression string) (Evaluable, error) { 64 | p := newParser(expression, l) 65 | 66 | eval, err := p.parse(c) 67 | if err == nil && p.isCamouflaged() && p.lastScan != scanner.EOF { 68 | err = p.camouflage 69 | } 70 | if err != nil { 71 | pos := p.scanner.Pos() 72 | return nil, fmt.Errorf("parsing error: %s - %d:%d %w", p.scanner.Position, pos.Line, pos.Column, err) 73 | } 74 | 75 | return eval, nil 76 | } 77 | 78 | // Evaluate given parameter with given expression 79 | func (l Language) Evaluate(expression string, parameter interface{}) (interface{}, error) { 80 | return l.EvaluateWithContext(context.Background(), expression, parameter) 81 | } 82 | 83 | // Evaluate given parameter with given expression using context 84 | func (l Language) EvaluateWithContext(c context.Context, expression string, parameter interface{}) (interface{}, error) { 85 | eval, err := l.NewEvaluableWithContext(c, expression) 86 | if err != nil { 87 | return nil, err 88 | } 89 | v, err := eval(c, parameter) 90 | if err != nil { 91 | return nil, fmt.Errorf("can not evaluate %s: %w", expression, err) 92 | } 93 | return v, nil 94 | } 95 | 96 | // Function returns a Language with given function. 97 | // Function has no conversion for input types. 98 | // 99 | // If the function returns an error it must be the last return parameter. 100 | // 101 | // If the function has (without the error) more then one return parameter, 102 | // it returns them as []interface{}. 103 | func Function(name string, function interface{}) Language { 104 | l := newLanguage() 105 | l.prefixes[name] = func(c context.Context, p *Parser) (eval Evaluable, err error) { 106 | args := []Evaluable{} 107 | scan := p.Scan() 108 | switch scan { 109 | case '(': 110 | args, err = p.parseArguments(c) 111 | if err != nil { 112 | return nil, err 113 | } 114 | default: 115 | p.Camouflage("function call", '(') 116 | } 117 | return p.callFunc(toFunc(function), args...), nil 118 | } 119 | return l 120 | } 121 | 122 | // Constant returns a Language with given constant 123 | func Constant(name string, value interface{}) Language { 124 | l := newLanguage() 125 | l.prefixes[l.makePrefixKey(name)] = func(c context.Context, p *Parser) (eval Evaluable, err error) { 126 | return p.Const(value), nil 127 | } 128 | return l 129 | } 130 | 131 | // PrefixExtension extends a Language 132 | func PrefixExtension(r rune, ext func(context.Context, *Parser) (Evaluable, error)) Language { 133 | l := newLanguage() 134 | l.prefixes[r] = ext 135 | return l 136 | } 137 | 138 | // Init is a language that does no parsing, but invokes the given function when 139 | // parsing starts. It is incumbent upon the function to call ParseExpression to 140 | // continue parsing. 141 | // 142 | // This function can be used to customize the parser settings, such as 143 | // whitespace or ident behavior. 144 | func Init(ext func(context.Context, *Parser) (Evaluable, error)) Language { 145 | l := newLanguage() 146 | l.init = ext 147 | return l 148 | } 149 | 150 | // DefaultExtension is a language that runs the given function if no other 151 | // prefix matches. 152 | func DefaultExtension(ext func(context.Context, *Parser) (Evaluable, error)) Language { 153 | l := newLanguage() 154 | l.def = ext 155 | return l 156 | } 157 | 158 | // PrefixMetaPrefix chooses a Prefix to be executed 159 | func PrefixMetaPrefix(r rune, ext func(context.Context, *Parser) (call string, alternative func() (Evaluable, error), err error)) Language { 160 | l := newLanguage() 161 | l.prefixes[r] = func(c context.Context, p *Parser) (Evaluable, error) { 162 | call, alternative, err := ext(c, p) 163 | if err != nil { 164 | return nil, err 165 | } 166 | if prefix, ok := p.prefixes[l.makePrefixKey(call)]; ok { 167 | return prefix(c, p) 168 | } 169 | return alternative() 170 | } 171 | return l 172 | } 173 | 174 | // PrefixOperator returns a Language with given prefix 175 | func PrefixOperator(name string, e Evaluable) Language { 176 | l := newLanguage() 177 | l.prefixes[l.makePrefixKey(name)] = func(c context.Context, p *Parser) (Evaluable, error) { 178 | eval, err := p.ParseNextExpression(c) 179 | if err != nil { 180 | return nil, err 181 | } 182 | prefix := func(c context.Context, v interface{}) (interface{}, error) { 183 | a, err := eval(c, v) 184 | if err != nil { 185 | return nil, err 186 | } 187 | return e(c, a) 188 | } 189 | if eval.IsConst() { 190 | v, err := prefix(c, nil) 191 | if err != nil { 192 | return nil, err 193 | } 194 | prefix = p.Const(v) 195 | } 196 | return prefix, nil 197 | } 198 | return l 199 | } 200 | 201 | // PostfixOperator extends a Language. 202 | func PostfixOperator(name string, ext func(context.Context, *Parser, Evaluable) (Evaluable, error)) Language { 203 | l := newLanguage() 204 | l.operators[l.makeInfixKey(name)] = postfix{ 205 | f: func(c context.Context, p *Parser, eval Evaluable, pre operatorPrecedence) (Evaluable, error) { 206 | return ext(c, p, eval) 207 | }, 208 | } 209 | return l 210 | } 211 | 212 | // InfixOperator for two arbitrary values. 213 | func InfixOperator(name string, f func(a, b interface{}) (interface{}, error)) Language { 214 | return newLanguageOperator(name, &infix{arbitrary: f}) 215 | } 216 | 217 | // InfixShortCircuit operator is called after the left operand is evaluated. 218 | func InfixShortCircuit(name string, f func(a interface{}) (interface{}, bool)) Language { 219 | return newLanguageOperator(name, &infix{shortCircuit: f}) 220 | } 221 | 222 | // InfixTextOperator for two text values. 223 | func InfixTextOperator(name string, f func(a, b string) (interface{}, error)) Language { 224 | return newLanguageOperator(name, &infix{text: f}) 225 | } 226 | 227 | // InfixNumberOperator for two number values. 228 | func InfixNumberOperator(name string, f func(a, b float64) (interface{}, error)) Language { 229 | return newLanguageOperator(name, &infix{number: f}) 230 | } 231 | 232 | // InfixDecimalOperator for two decimal values. 233 | func InfixDecimalOperator(name string, f func(a, b decimal.Decimal) (interface{}, error)) Language { 234 | return newLanguageOperator(name, &infix{decimal: f}) 235 | } 236 | 237 | // InfixBoolOperator for two bool values. 238 | func InfixBoolOperator(name string, f func(a, b bool) (interface{}, error)) Language { 239 | return newLanguageOperator(name, &infix{boolean: f}) 240 | } 241 | 242 | // Precedence of operator. The Operator with higher operatorPrecedence is evaluated first. 243 | func Precedence(name string, operatorPrecendence uint8) Language { 244 | return newLanguageOperator(name, operatorPrecedence(operatorPrecendence)) 245 | } 246 | 247 | // InfixEvalOperator operates on the raw operands. 248 | // Therefore it cannot be combined with operators for other operand types. 249 | func InfixEvalOperator(name string, f func(a, b Evaluable) (Evaluable, error)) Language { 250 | return newLanguageOperator(name, directInfix{infixBuilder: f}) 251 | } 252 | 253 | func newLanguageOperator(name string, op operator) Language { 254 | op.initiate(name) 255 | l := newLanguage() 256 | l.operators[l.makeInfixKey(name)] = op 257 | return l 258 | } 259 | 260 | func (l *Language) makePrefixKey(key string) interface{} { 261 | runes := []rune(key) 262 | if len(runes) == 1 && !unicode.IsLetter(runes[0]) { 263 | return runes[0] 264 | } 265 | return key 266 | } 267 | 268 | func (l *Language) makeInfixKey(key string) string { 269 | for _, r := range key { 270 | l.operatorSymbols[r] = struct{}{} 271 | } 272 | return key 273 | } 274 | 275 | // VariableSelector returns a Language which uses given variable selector. 276 | // It must be combined with a Language that uses the vatiable selector. E.g. gval.Base(). 277 | func VariableSelector(selector func(path Evaluables) Evaluable) Language { 278 | l := newLanguage() 279 | l.selector = selector 280 | return l 281 | } 282 | -------------------------------------------------------------------------------- /operator.go: -------------------------------------------------------------------------------- 1 | package gval 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/shopspring/decimal" 11 | ) 12 | 13 | type stage struct { 14 | Evaluable 15 | infixBuilder 16 | operatorPrecedence 17 | } 18 | 19 | type stageStack []stage //operatorPrecedence in stacktStage is continuously, monotone ascending 20 | 21 | func (s *stageStack) push(b stage) error { 22 | for len(*s) > 0 && s.peek().operatorPrecedence >= b.operatorPrecedence { 23 | a := s.pop() 24 | eval, err := a.infixBuilder(a.Evaluable, b.Evaluable) 25 | if err != nil { 26 | return err 27 | } 28 | if a.IsConst() && b.IsConst() { 29 | v, err := eval(nil, nil) 30 | if err != nil { 31 | return err 32 | } 33 | b.Evaluable = constant(v) 34 | continue 35 | } 36 | b.Evaluable = eval 37 | } 38 | *s = append(*s, b) 39 | return nil 40 | } 41 | 42 | func (s *stageStack) peek() stage { 43 | return (*s)[len(*s)-1] 44 | } 45 | 46 | func (s *stageStack) pop() stage { 47 | a := s.peek() 48 | (*s) = (*s)[:len(*s)-1] 49 | return a 50 | } 51 | 52 | type infixBuilder func(a, b Evaluable) (Evaluable, error) 53 | 54 | func (l Language) isSymbolOperation(r rune) bool { 55 | _, in := l.operatorSymbols[r] 56 | return in 57 | } 58 | 59 | func (l Language) isOperatorPrefix(op string) bool { 60 | for k := range l.operators { 61 | if strings.HasPrefix(k, op) { 62 | return true 63 | } 64 | } 65 | return false 66 | } 67 | 68 | func (op *infix) initiate(name string) { 69 | f := func(a, b interface{}) (interface{}, error) { 70 | return nil, fmt.Errorf("invalid operation (%T) %s (%T)", a, name, b) 71 | } 72 | if op.arbitrary != nil { 73 | f = op.arbitrary 74 | } 75 | for _, typeConvertion := range []bool{true, false} { 76 | if op.text != nil && (!typeConvertion || op.arbitrary == nil) { 77 | f = getStringOpFunc(op.text, f, typeConvertion) 78 | } 79 | if op.boolean != nil { 80 | f = getBoolOpFunc(op.boolean, f, typeConvertion) 81 | } 82 | if op.number != nil { 83 | f = getFloatOpFunc(op.number, f, typeConvertion) 84 | } 85 | if op.decimal != nil { 86 | f = getDecimalOpFunc(op.decimal, f, typeConvertion) 87 | } 88 | } 89 | if op.shortCircuit == nil { 90 | op.builder = func(a, b Evaluable) (Evaluable, error) { 91 | return func(c context.Context, x interface{}) (interface{}, error) { 92 | a, err := a(c, x) 93 | if err != nil { 94 | return nil, err 95 | } 96 | b, err := b(c, x) 97 | if err != nil { 98 | return nil, err 99 | } 100 | return f(a, b) 101 | }, nil 102 | } 103 | return 104 | } 105 | shortF := op.shortCircuit 106 | op.builder = func(a, b Evaluable) (Evaluable, error) { 107 | return func(c context.Context, x interface{}) (interface{}, error) { 108 | a, err := a(c, x) 109 | if err != nil { 110 | return nil, err 111 | } 112 | if r, ok := shortF(a); ok { 113 | return r, nil 114 | } 115 | b, err := b(c, x) 116 | if err != nil { 117 | return nil, err 118 | } 119 | return f(a, b) 120 | }, nil 121 | } 122 | } 123 | 124 | type opFunc func(a, b interface{}) (interface{}, error) 125 | 126 | func getStringOpFunc(s func(a, b string) (interface{}, error), f opFunc, typeConversion bool) opFunc { 127 | if typeConversion { 128 | return func(a, b interface{}) (interface{}, error) { 129 | if a != nil && b != nil { 130 | return s(fmt.Sprintf("%v", a), fmt.Sprintf("%v", b)) 131 | } 132 | return f(a, b) 133 | } 134 | } 135 | return func(a, b interface{}) (interface{}, error) { 136 | s1, k := a.(string) 137 | s2, l := b.(string) 138 | if k && l { 139 | return s(s1, s2) 140 | } 141 | return f(a, b) 142 | } 143 | } 144 | func convertToBool(o interface{}) (bool, bool) { 145 | if b, ok := o.(bool); ok { 146 | return b, true 147 | } 148 | v := reflect.ValueOf(o) 149 | 150 | if v.Kind() == reflect.Func { 151 | if vt := v.Type(); vt.NumIn() == 0 && vt.NumOut() == 1 { 152 | retType := vt.Out(0) 153 | 154 | if retType.Kind() == reflect.Bool { 155 | funcResults := v.Call([]reflect.Value{}) 156 | v = funcResults[0] 157 | o = v.Interface() 158 | } 159 | } 160 | } 161 | 162 | for o != nil && v.Kind() == reflect.Ptr { 163 | v = v.Elem() 164 | if !v.IsValid() { 165 | return false, false 166 | } 167 | o = v.Interface() 168 | } 169 | 170 | if o == false || o == nil || o == "false" || o == "FALSE" { 171 | return false, true 172 | } 173 | if o == true || o == "true" || o == "TRUE" { 174 | return true, true 175 | } 176 | if f, ok := convertToFloat(o); ok { 177 | return f != 0., true 178 | } 179 | return false, false 180 | } 181 | func getBoolOpFunc(o func(a, b bool) (interface{}, error), f opFunc, typeConversion bool) opFunc { 182 | if typeConversion { 183 | return func(a, b interface{}) (interface{}, error) { 184 | x, k := convertToBool(a) 185 | y, l := convertToBool(b) 186 | if k && l { 187 | return o(x, y) 188 | } 189 | return f(a, b) 190 | } 191 | } 192 | return func(a, b interface{}) (interface{}, error) { 193 | x, k := a.(bool) 194 | y, l := b.(bool) 195 | if k && l { 196 | return o(x, y) 197 | } 198 | return f(a, b) 199 | } 200 | } 201 | func convertToFloat(o interface{}) (float64, bool) { 202 | if i, ok := o.(float64); ok { 203 | return i, true 204 | } 205 | v := reflect.ValueOf(o) 206 | for o != nil && v.Kind() == reflect.Ptr { 207 | v = v.Elem() 208 | if !v.IsValid() { 209 | return 0, false 210 | } 211 | o = v.Interface() 212 | } 213 | switch v.Kind() { 214 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 215 | return float64(v.Int()), true 216 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 217 | return float64(v.Uint()), true 218 | case reflect.Float32, reflect.Float64: 219 | return v.Float(), true 220 | } 221 | if s, ok := o.(string); ok { 222 | f, err := strconv.ParseFloat(s, 64) 223 | if err == nil { 224 | return f, true 225 | } 226 | } 227 | return 0, false 228 | } 229 | func getFloatOpFunc(o func(a, b float64) (interface{}, error), f opFunc, typeConversion bool) opFunc { 230 | if typeConversion { 231 | return func(a, b interface{}) (interface{}, error) { 232 | x, k := convertToFloat(a) 233 | y, l := convertToFloat(b) 234 | if k && l { 235 | return o(x, y) 236 | } 237 | 238 | return f(a, b) 239 | } 240 | } 241 | return func(a, b interface{}) (interface{}, error) { 242 | x, k := a.(float64) 243 | y, l := b.(float64) 244 | if k && l { 245 | return o(x, y) 246 | } 247 | 248 | return f(a, b) 249 | } 250 | } 251 | func convertToDecimal(o interface{}) (decimal.Decimal, bool) { 252 | if i, ok := o.(decimal.Decimal); ok { 253 | return i, true 254 | } 255 | if i, ok := o.(float64); ok { 256 | return decimal.NewFromFloat(i), true 257 | } 258 | v := reflect.ValueOf(o) 259 | for o != nil && v.Kind() == reflect.Ptr { 260 | v = v.Elem() 261 | if !v.IsValid() { 262 | return decimal.Zero, false 263 | } 264 | o = v.Interface() 265 | } 266 | switch v.Kind() { 267 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 268 | return decimal.NewFromInt(v.Int()), true 269 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 270 | return decimal.NewFromFloat(float64(v.Uint())), true 271 | case reflect.Float32, reflect.Float64: 272 | return decimal.NewFromFloat(v.Float()), true 273 | } 274 | if s, ok := o.(string); ok { 275 | f, err := strconv.ParseFloat(s, 64) 276 | if err == nil { 277 | return decimal.NewFromFloat(f), true 278 | } 279 | } 280 | return decimal.Zero, false 281 | } 282 | func getDecimalOpFunc(o func(a, b decimal.Decimal) (interface{}, error), f opFunc, typeConversion bool) opFunc { 283 | if typeConversion { 284 | return func(a, b interface{}) (interface{}, error) { 285 | x, k := convertToDecimal(a) 286 | y, l := convertToDecimal(b) 287 | if k && l { 288 | return o(x, y) 289 | } 290 | 291 | return f(a, b) 292 | } 293 | } 294 | return func(a, b interface{}) (interface{}, error) { 295 | x, k := a.(decimal.Decimal) 296 | y, l := b.(decimal.Decimal) 297 | if k && l { 298 | return o(x, y) 299 | } 300 | 301 | return f(a, b) 302 | } 303 | } 304 | 305 | type operator interface { 306 | merge(operator) operator 307 | precedence() operatorPrecedence 308 | initiate(name string) 309 | } 310 | 311 | type operatorPrecedence uint8 312 | 313 | func (pre operatorPrecedence) merge(op operator) operator { 314 | if op, ok := op.(operatorPrecedence); ok { 315 | if op > pre { 316 | return op 317 | } 318 | return pre 319 | } 320 | if op == nil { 321 | return pre 322 | } 323 | return op.merge(pre) 324 | } 325 | 326 | func (pre operatorPrecedence) precedence() operatorPrecedence { 327 | return pre 328 | } 329 | 330 | func (pre operatorPrecedence) initiate(name string) {} 331 | 332 | type infix struct { 333 | operatorPrecedence 334 | number func(a, b float64) (interface{}, error) 335 | decimal func(a, b decimal.Decimal) (interface{}, error) 336 | boolean func(a, b bool) (interface{}, error) 337 | text func(a, b string) (interface{}, error) 338 | arbitrary func(a, b interface{}) (interface{}, error) 339 | shortCircuit func(a interface{}) (interface{}, bool) 340 | builder infixBuilder 341 | } 342 | 343 | func (op infix) merge(op2 operator) operator { 344 | switch op2 := op2.(type) { 345 | case *infix: 346 | if op.number == nil { 347 | op.number = op2.number 348 | } 349 | if op.decimal == nil { 350 | op.decimal = op2.decimal 351 | } 352 | if op.boolean == nil { 353 | op.boolean = op2.boolean 354 | } 355 | if op.text == nil { 356 | op.text = op2.text 357 | } 358 | if op.arbitrary == nil { 359 | op.arbitrary = op2.arbitrary 360 | } 361 | if op.shortCircuit == nil { 362 | op.shortCircuit = op2.shortCircuit 363 | } 364 | } 365 | if op2 != nil && op2.precedence() > op.operatorPrecedence { 366 | op.operatorPrecedence = op2.precedence() 367 | } 368 | return &op 369 | } 370 | 371 | type directInfix struct { 372 | operatorPrecedence 373 | infixBuilder 374 | } 375 | 376 | func (op directInfix) merge(op2 operator) operator { 377 | switch op2 := op2.(type) { 378 | case operatorPrecedence: 379 | op.operatorPrecedence = op2 380 | } 381 | if op2 != nil && op2.precedence() > op.operatorPrecedence { 382 | op.operatorPrecedence = op2.precedence() 383 | } 384 | return op 385 | } 386 | 387 | type extension func(context.Context, *Parser) (Evaluable, error) 388 | 389 | type postfix struct { 390 | operatorPrecedence 391 | f func(context.Context, *Parser, Evaluable, operatorPrecedence) (Evaluable, error) 392 | } 393 | 394 | func (op postfix) merge(op2 operator) operator { 395 | switch op2 := op2.(type) { 396 | case postfix: 397 | if op2.f != nil { 398 | op.f = op2.f 399 | } 400 | } 401 | if op2 != nil && op2.precedence() > op.operatorPrecedence { 402 | op.operatorPrecedence = op2.precedence() 403 | } 404 | return op 405 | } 406 | -------------------------------------------------------------------------------- /operator_test.go: -------------------------------------------------------------------------------- 1 | package gval 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func Test_Infix(t *testing.T) { 11 | type subTest struct { 12 | name string 13 | a interface{} 14 | b interface{} 15 | wantRet interface{} 16 | } 17 | tests := []struct { 18 | name string 19 | infix 20 | subTests []subTest 21 | }{ 22 | { 23 | "number operator", 24 | infix{ 25 | number: func(a, b float64) (interface{}, error) { return a * b, nil }, 26 | }, 27 | []subTest{ 28 | {"float64 arguments", 7., 3., 21.}, 29 | {"int arguments", 7, 3, 21.}, 30 | {"string arguments", "7", "3.", 21.}, 31 | }, 32 | }, 33 | { 34 | "number and string operator", 35 | infix{ 36 | number: func(a, b float64) (interface{}, error) { return a + b, nil }, 37 | text: func(a, b string) (interface{}, error) { return fmt.Sprintf("%v%v", a, b), nil }, 38 | }, 39 | 40 | []subTest{ 41 | {"float64 arguments", 7., 3., 10.}, 42 | {"int arguments", 7, 3, 10.}, 43 | {"number string arguments", "7", "3.", "73."}, 44 | {"string arguments", "hello ", "world", "hello world"}, 45 | }, 46 | }, 47 | { 48 | "bool operator", 49 | infix{ 50 | shortCircuit: func(a interface{}) (interface{}, bool) { return false, a == false }, 51 | boolean: func(a, b bool) (interface{}, error) { return a && b, nil }, 52 | }, 53 | 54 | []subTest{ 55 | {"bool arguments", false, true, false}, 56 | {"number arguments", 0, true, false}, 57 | {"lower string arguments", "false", "true", false}, 58 | {"upper string arguments", "TRUE", "FALSE", false}, 59 | {"shortCircuit", false, "not a boolean", false}, 60 | }, 61 | }, 62 | { 63 | "bool, number, text and interface operator", 64 | infix{ 65 | number: func(a, b float64) (interface{}, error) { return a == b, nil }, 66 | boolean: func(a, b bool) (interface{}, error) { return a == b, nil }, 67 | text: func(a, b string) (interface{}, error) { return a == b, nil }, 68 | arbitrary: func(a, b interface{}) (interface{}, error) { return a == b, nil }, 69 | }, 70 | 71 | []subTest{ 72 | {"number string and int arguments", "7", 7, true}, 73 | {"bool string and bool arguments", "true", true, true}, 74 | {"string arguments", "hello", "hello", true}, 75 | {"upper string arguments", "TRUE", "FALSE", false}, 76 | }, 77 | }, 78 | } 79 | for _, tt := range tests { 80 | t.Run(tt.name, func(t *testing.T) { 81 | tt.infix.initiate("<" + tt.name + ">") 82 | builder := tt.infix.builder 83 | for _, tt := range tt.subTests { 84 | t.Run(tt.name, func(t *testing.T) { 85 | eval, err := builder(constant(tt.a), constant(tt.b)) 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | 90 | got, err := eval(context.Background(), nil) 91 | if err != nil { 92 | t.Fatal(err) 93 | } 94 | 95 | if !reflect.DeepEqual(got, tt.wantRet) { 96 | t.Fatalf("binaryOperator() eval() = %v, want %v", got, tt.wantRet) 97 | } 98 | }) 99 | } 100 | }) 101 | } 102 | } 103 | 104 | func Test_stageStack_push(t *testing.T) { 105 | p := (*Parser)(nil) 106 | tests := []struct { 107 | name string 108 | pres []operatorPrecedence 109 | expect string 110 | }{ 111 | { 112 | "flat", 113 | []operatorPrecedence{1, 1, 1, 1}, 114 | "((((AB)C)D)E)", 115 | }, 116 | { 117 | "asc", 118 | []operatorPrecedence{1, 2, 3, 4}, 119 | "(A(B(C(DE))))", 120 | }, 121 | { 122 | "desc", 123 | []operatorPrecedence{4, 3, 2, 1}, 124 | "((((AB)C)D)E)", 125 | }, 126 | { 127 | "mixed", 128 | []operatorPrecedence{1, 2, 1, 1}, 129 | "(((A(BC))D)E)", 130 | }, 131 | } 132 | for _, tt := range tests { 133 | t.Run(tt.name, func(t *testing.T) { 134 | X := int('A') 135 | 136 | op := func(a, b Evaluable) (Evaluable, error) { 137 | return func(c context.Context, o interface{}) (interface{}, error) { 138 | aa, _ := a.EvalString(c, nil) 139 | bb, _ := b.EvalString(c, nil) 140 | s := "(" + aa + bb + ")" 141 | return s, nil 142 | }, nil 143 | } 144 | stack := stageStack{} 145 | for _, pre := range tt.pres { 146 | if err := stack.push(stage{p.Const(string(rune(X))), op, pre}); err != nil { 147 | t.Fatal(err) 148 | } 149 | X++ 150 | } 151 | 152 | if err := stack.push(stage{p.Const(string(rune(X))), nil, 0}); err != nil { 153 | t.Fatal(err) 154 | } 155 | 156 | if len(stack) != 1 { 157 | t.Fatalf("stack must hold exactly one element") 158 | } 159 | 160 | got, _ := stack[0].EvalString(context.Background(), nil) 161 | if got != tt.expect { 162 | t.Fatalf("got %s but expected %s", got, tt.expect) 163 | } 164 | }) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package gval 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | "text/scanner" 9 | 10 | "github.com/shopspring/decimal" 11 | ) 12 | 13 | // ParseExpression scans an expression into an Evaluable. 14 | func (p *Parser) ParseExpression(c context.Context) (eval Evaluable, err error) { 15 | stack := stageStack{} 16 | for { 17 | eval, err = p.ParseNextExpression(c) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | if stage, err := p.parseOperator(c, &stack, eval); err != nil { 23 | return nil, err 24 | } else if err = stack.push(stage); err != nil { 25 | return nil, err 26 | } 27 | 28 | if stack.peek().infixBuilder == nil { 29 | return stack.pop().Evaluable, nil 30 | } 31 | } 32 | } 33 | 34 | // ParseNextExpression scans the expression ignoring following operators 35 | func (p *Parser) ParseNextExpression(c context.Context) (eval Evaluable, err error) { 36 | scan := p.Scan() 37 | ex, ok := p.prefixes[scan] 38 | if !ok { 39 | if scan != scanner.EOF && p.def != nil { 40 | return p.def(c, p) 41 | } 42 | return nil, p.Expected("extensions") 43 | } 44 | return ex(c, p) 45 | } 46 | 47 | // ParseSublanguage sets the next language for this parser to parse and calls 48 | // its initialization function, usually ParseExpression. 49 | func (p *Parser) ParseSublanguage(c context.Context, l Language) (Evaluable, error) { 50 | if p.isCamouflaged() { 51 | panic("can not ParseSublanguage() on camouflaged Parser") 52 | } 53 | curLang := p.Language 54 | curWhitespace := p.scanner.Whitespace 55 | curMode := p.scanner.Mode 56 | curIsIdentRune := p.scanner.IsIdentRune 57 | 58 | p.Language = l 59 | p.resetScannerProperties() 60 | 61 | defer func() { 62 | p.Language = curLang 63 | p.scanner.Whitespace = curWhitespace 64 | p.scanner.Mode = curMode 65 | p.scanner.IsIdentRune = curIsIdentRune 66 | }() 67 | 68 | return p.parse(c) 69 | } 70 | 71 | func (p *Parser) parse(c context.Context) (Evaluable, error) { 72 | if p.init != nil { 73 | return p.init(c, p) 74 | } 75 | 76 | return p.ParseExpression(c) 77 | } 78 | 79 | func parseString(c context.Context, p *Parser) (Evaluable, error) { 80 | s, err := strconv.Unquote(p.TokenText()) 81 | if err != nil { 82 | return nil, fmt.Errorf("could not parse string: %w", err) 83 | } 84 | return p.Const(s), nil 85 | } 86 | 87 | func parseNumber(c context.Context, p *Parser) (Evaluable, error) { 88 | n, err := strconv.ParseFloat(p.TokenText(), 64) 89 | if err != nil { 90 | return nil, err 91 | } 92 | return p.Const(n), nil 93 | } 94 | 95 | func parseDecimal(c context.Context, p *Parser) (Evaluable, error) { 96 | n, err := strconv.ParseFloat(p.TokenText(), 64) 97 | if err != nil { 98 | return nil, err 99 | } 100 | return p.Const(decimal.NewFromFloat(n)), nil 101 | } 102 | 103 | func parseParentheses(c context.Context, p *Parser) (Evaluable, error) { 104 | eval, err := p.ParseExpression(c) 105 | if err != nil { 106 | return nil, err 107 | } 108 | switch p.Scan() { 109 | case ')': 110 | return eval, nil 111 | default: 112 | return nil, p.Expected("parentheses", ')') 113 | } 114 | } 115 | 116 | func (p *Parser) parseOperator(c context.Context, stack *stageStack, eval Evaluable) (st stage, err error) { 117 | for { 118 | scan := p.Scan() 119 | op := p.TokenText() 120 | mustOp := false 121 | if p.isSymbolOperation(scan) { 122 | scan = p.Peek() 123 | for p.isSymbolOperation(scan) && p.isOperatorPrefix(op+string(scan)) { 124 | mustOp = true 125 | op += string(scan) 126 | p.Next() 127 | scan = p.Peek() 128 | } 129 | } else if scan != scanner.Ident { 130 | p.Camouflage("operator") 131 | return stage{Evaluable: eval}, nil 132 | } 133 | switch operator := p.operators[op].(type) { 134 | case *infix: 135 | return stage{ 136 | Evaluable: eval, 137 | infixBuilder: operator.builder, 138 | operatorPrecedence: operator.operatorPrecedence, 139 | }, nil 140 | case directInfix: 141 | return stage{ 142 | Evaluable: eval, 143 | infixBuilder: operator.infixBuilder, 144 | operatorPrecedence: operator.operatorPrecedence, 145 | }, nil 146 | case postfix: 147 | if err = stack.push(stage{ 148 | operatorPrecedence: operator.operatorPrecedence, 149 | Evaluable: eval, 150 | }); err != nil { 151 | return stage{}, err 152 | } 153 | eval, err = operator.f(c, p, stack.pop().Evaluable, operator.operatorPrecedence) 154 | if err != nil { 155 | return 156 | } 157 | continue 158 | } 159 | 160 | if !mustOp { 161 | p.Camouflage("operator") 162 | return stage{Evaluable: eval}, nil 163 | } 164 | return stage{}, fmt.Errorf("unknown operator %s", op) 165 | } 166 | } 167 | 168 | func parseIdent(c context.Context, p *Parser) (call string, alternative func() (Evaluable, error), err error) { 169 | token := p.TokenText() 170 | return token, 171 | func() (Evaluable, error) { 172 | fullname := token 173 | 174 | keys := []Evaluable{p.Const(token)} 175 | for { 176 | scan := p.Scan() 177 | switch scan { 178 | case '.': 179 | scan = p.Scan() 180 | switch scan { 181 | case scanner.Ident: 182 | token = p.TokenText() 183 | keys = append(keys, p.Const(token)) 184 | default: 185 | return nil, p.Expected("field", scanner.Ident) 186 | } 187 | case '(': 188 | args, err := p.parseArguments(c) 189 | if err != nil { 190 | return nil, err 191 | } 192 | return p.callEvaluable(fullname, p.Var(keys...), args...), nil 193 | case '[': 194 | key, err := p.ParseExpression(c) 195 | if err != nil { 196 | return nil, err 197 | } 198 | switch p.Scan() { 199 | case ']': 200 | keys = append(keys, key) 201 | default: 202 | return nil, p.Expected("array key", ']') 203 | } 204 | default: 205 | p.Camouflage("variable", '.', '(', '[') 206 | return p.Var(keys...), nil 207 | } 208 | } 209 | }, nil 210 | 211 | } 212 | 213 | func (p *Parser) parseArguments(c context.Context) (args []Evaluable, err error) { 214 | if p.Scan() == ')' { 215 | return 216 | } 217 | p.Camouflage("scan arguments", ')') 218 | for { 219 | arg, err := p.ParseExpression(c) 220 | args = append(args, arg) 221 | if err != nil { 222 | return nil, err 223 | } 224 | switch p.Scan() { 225 | case ')': 226 | return args, nil 227 | case ',': 228 | default: 229 | return nil, p.Expected("arguments", ')', ',') 230 | } 231 | } 232 | } 233 | 234 | func inArray(a, b interface{}) (interface{}, error) { 235 | col, ok := b.([]interface{}) 236 | if !ok { 237 | return nil, fmt.Errorf("expected type []interface{} for in operator but got %T", b) 238 | } 239 | for _, value := range col { 240 | if reflect.DeepEqual(a, value) { 241 | return true, nil 242 | } 243 | } 244 | return false, nil 245 | } 246 | 247 | func parseIf(c context.Context, p *Parser, e Evaluable) (Evaluable, error) { 248 | a, err := p.ParseExpression(c) 249 | if err != nil { 250 | return nil, err 251 | } 252 | b := p.Const(nil) 253 | switch p.Scan() { 254 | case ':': 255 | b, err = p.ParseExpression(c) 256 | if err != nil { 257 | return nil, err 258 | } 259 | case scanner.EOF: 260 | default: 261 | return nil, p.Expected("<> ? <> : <>", ':', scanner.EOF) 262 | } 263 | return func(c context.Context, v interface{}) (interface{}, error) { 264 | x, err := e(c, v) 265 | if err != nil { 266 | return nil, err 267 | } 268 | if valX := reflect.ValueOf(x); x == nil || valX.IsZero() { 269 | return b(c, v) 270 | } 271 | return a(c, v) 272 | }, nil 273 | } 274 | 275 | func parseJSONArray(c context.Context, p *Parser) (Evaluable, error) { 276 | evals := []Evaluable{} 277 | for { 278 | switch p.Scan() { 279 | default: 280 | p.Camouflage("array", ',', ']') 281 | eval, err := p.ParseExpression(c) 282 | if err != nil { 283 | return nil, err 284 | } 285 | evals = append(evals, eval) 286 | case ',': 287 | case ']': 288 | return func(c context.Context, v interface{}) (interface{}, error) { 289 | vs := make([]interface{}, len(evals)) 290 | for i, e := range evals { 291 | eval, err := e(c, v) 292 | if err != nil { 293 | return nil, err 294 | } 295 | vs[i] = eval 296 | } 297 | 298 | return vs, nil 299 | }, nil 300 | } 301 | } 302 | } 303 | 304 | func parseJSONObject(c context.Context, p *Parser) (Evaluable, error) { 305 | type kv struct { 306 | key Evaluable 307 | value Evaluable 308 | } 309 | evals := []kv{} 310 | for { 311 | switch p.Scan() { 312 | default: 313 | p.Camouflage("object", ',', '}') 314 | key, err := p.ParseExpression(c) 315 | if err != nil { 316 | return nil, err 317 | } 318 | if p.Scan() != ':' { 319 | if err != nil { 320 | return nil, p.Expected("object", ':') 321 | } 322 | } 323 | value, err := p.ParseExpression(c) 324 | if err != nil { 325 | return nil, err 326 | } 327 | evals = append(evals, kv{key, value}) 328 | case ',': 329 | case '}': 330 | return func(c context.Context, v interface{}) (interface{}, error) { 331 | vs := map[string]interface{}{} 332 | for _, e := range evals { 333 | value, err := e.value(c, v) 334 | if err != nil { 335 | return nil, err 336 | } 337 | key, err := e.key.EvalString(c, v) 338 | if err != nil { 339 | return nil, err 340 | } 341 | vs[key] = value 342 | } 343 | return vs, nil 344 | }, nil 345 | } 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package gval 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "text/scanner" 8 | "unicode" 9 | ) 10 | 11 | // Parser parses expressions in a Language into an Evaluable 12 | type Parser struct { 13 | scanner scanner.Scanner 14 | Language 15 | lastScan rune 16 | camouflage error 17 | } 18 | 19 | func newParser(expression string, l Language) *Parser { 20 | sc := scanner.Scanner{} 21 | sc.Init(strings.NewReader(expression)) 22 | sc.Error = func(*scanner.Scanner, string) {} 23 | sc.Filename = expression + "\t" 24 | p := &Parser{scanner: sc, Language: l} 25 | p.resetScannerProperties() 26 | return p 27 | } 28 | 29 | func (p *Parser) resetScannerProperties() { 30 | p.scanner.Whitespace = scanner.GoWhitespace 31 | p.scanner.Mode = scanner.GoTokens 32 | p.scanner.IsIdentRune = func(r rune, pos int) bool { 33 | return unicode.IsLetter(r) || r == '_' || (pos > 0 && unicode.IsDigit(r)) 34 | } 35 | } 36 | 37 | // SetWhitespace sets the behavior of the whitespace matcher. The given 38 | // characters must be less than or equal to 0x20 (' '). 39 | func (p *Parser) SetWhitespace(chars ...rune) { 40 | var mask uint64 41 | for _, char := range chars { 42 | mask |= 1 << uint(char) 43 | } 44 | 45 | p.scanner.Whitespace = mask 46 | } 47 | 48 | // SetMode sets the tokens that the underlying scanner will match. 49 | func (p *Parser) SetMode(mode uint) { 50 | p.scanner.Mode = mode 51 | } 52 | 53 | // SetIsIdentRuneFunc sets the function that matches ident characters in the 54 | // underlying scanner. 55 | func (p *Parser) SetIsIdentRuneFunc(fn func(ch rune, i int) bool) { 56 | p.scanner.IsIdentRune = fn 57 | } 58 | 59 | // Scan reads the next token or Unicode character from source and returns it. 60 | // It only recognizes tokens t for which the respective Mode bit (1<<-t) is set. 61 | // It returns scanner.EOF at the end of the source. 62 | func (p *Parser) Scan() rune { 63 | if p.isCamouflaged() { 64 | p.camouflage = nil 65 | return p.lastScan 66 | } 67 | p.camouflage = nil 68 | p.lastScan = p.scanner.Scan() 69 | return p.lastScan 70 | } 71 | 72 | func (p *Parser) isCamouflaged() bool { 73 | return p.camouflage != nil && p.camouflage != errCamouflageAfterNext 74 | } 75 | 76 | // Camouflage rewind the last Scan(). The Parser holds the camouflage error until 77 | // the next Scan() 78 | // Do not call Rewind() on a camouflaged Parser 79 | func (p *Parser) Camouflage(unit string, expected ...rune) { 80 | if p.isCamouflaged() { 81 | panic(fmt.Errorf("can only Camouflage() after Scan(): %w", p.camouflage)) 82 | } 83 | p.camouflage = p.Expected(unit, expected...) 84 | } 85 | 86 | // Peek returns the next Unicode character in the source without advancing 87 | // the scanner. It returns EOF if the scanner's position is at the last 88 | // character of the source. 89 | // Do not call Peek() on a camouflaged Parser 90 | func (p *Parser) Peek() rune { 91 | if p.isCamouflaged() { 92 | panic("can not Peek() on camouflaged Parser") 93 | } 94 | return p.scanner.Peek() 95 | } 96 | 97 | var errCamouflageAfterNext = fmt.Errorf("Camouflage() after Next()") 98 | 99 | // Next reads and returns the next Unicode character. 100 | // It returns EOF at the end of the source. 101 | // Do not call Next() on a camouflaged Parser 102 | func (p *Parser) Next() rune { 103 | if p.isCamouflaged() { 104 | panic("can not Next() on camouflaged Parser") 105 | } 106 | p.camouflage = errCamouflageAfterNext 107 | return p.scanner.Next() 108 | } 109 | 110 | // TokenText returns the string corresponding to the most recently scanned token. 111 | // Valid after calling Scan(). 112 | func (p *Parser) TokenText() string { 113 | return p.scanner.TokenText() 114 | } 115 | 116 | // Expected returns an error signaling an unexpected Scan() result 117 | func (p *Parser) Expected(unit string, expected ...rune) error { 118 | return unexpectedRune{unit, expected, p.lastScan} 119 | } 120 | 121 | type unexpectedRune struct { 122 | unit string 123 | expected []rune 124 | got rune 125 | } 126 | 127 | func (err unexpectedRune) Error() string { 128 | exp := bytes.Buffer{} 129 | runes := err.expected 130 | switch len(runes) { 131 | default: 132 | for _, r := range runes[:len(runes)-2] { 133 | exp.WriteString(scanner.TokenString(r)) 134 | exp.WriteString(", ") 135 | } 136 | fallthrough 137 | case 2: 138 | exp.WriteString(scanner.TokenString(runes[len(runes)-2])) 139 | exp.WriteString(" or ") 140 | fallthrough 141 | case 1: 142 | exp.WriteString(scanner.TokenString(runes[len(runes)-1])) 143 | case 0: 144 | return fmt.Sprintf("unexpected %s while scanning %s", scanner.TokenString(err.got), err.unit) 145 | } 146 | return fmt.Sprintf("unexpected %s while scanning %s expected %s", scanner.TokenString(err.got), err.unit, exp.String()) 147 | } 148 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package gval 2 | 3 | import ( 4 | "testing" 5 | "text/scanner" 6 | "unicode" 7 | ) 8 | 9 | func TestParser_Scan(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | input string 13 | Language 14 | do func(p *Parser) 15 | wantScan rune 16 | wantToken string 17 | wantPanic bool 18 | }{ 19 | { 20 | name: "camouflage", 21 | input: "$abc", 22 | do: func(p *Parser) { 23 | p.Scan() 24 | p.Camouflage("test") 25 | }, 26 | wantScan: '$', 27 | wantToken: "$", 28 | }, 29 | { 30 | name: "camouflage with next", 31 | input: "$abc", 32 | do: func(p *Parser) { 33 | p.Scan() 34 | p.Camouflage("test") 35 | p.Next() 36 | }, 37 | wantPanic: true, 38 | }, 39 | { 40 | name: "camouflage scan camouflage", 41 | input: "$abc", 42 | do: func(p *Parser) { 43 | p.Scan() 44 | p.Camouflage("test") 45 | p.Scan() 46 | p.Camouflage("test2") 47 | }, 48 | wantScan: '$', 49 | wantToken: "$", 50 | }, 51 | { 52 | name: "camouflage with peek", 53 | input: "$abc", 54 | do: func(p *Parser) { 55 | p.Scan() 56 | p.Camouflage("test") 57 | p.Peek() 58 | }, 59 | wantPanic: true, 60 | }, 61 | { 62 | name: "next and peek", 63 | input: "$#abc", 64 | do: func(p *Parser) { 65 | p.Scan() 66 | p.Next() 67 | p.Peek() 68 | }, 69 | wantScan: scanner.Ident, 70 | wantToken: "abc", 71 | }, 72 | { 73 | name: "scan token camouflage token", 74 | input: "abc", 75 | do: func(p *Parser) { 76 | p.Scan() 77 | p.TokenText() 78 | p.Camouflage("test") 79 | }, 80 | wantScan: scanner.Ident, 81 | wantToken: "abc", 82 | }, 83 | { 84 | name: "scan token peek camouflage token", 85 | input: "abc", 86 | do: func(p *Parser) { 87 | p.Scan() 88 | p.TokenText() 89 | p.Peek() 90 | p.Camouflage("test") 91 | }, 92 | wantScan: scanner.Ident, 93 | wantToken: "abc", 94 | }, 95 | { 96 | name: "tokenize all whitespace", 97 | input: "foo\tbar\nbaz", 98 | do: func(p *Parser) { 99 | p.SetWhitespace() 100 | p.Scan() 101 | }, 102 | wantScan: '\t', 103 | wantToken: "\t", 104 | }, 105 | { 106 | name: "custom ident", 107 | input: "$#foo", 108 | do: func(p *Parser) { 109 | p.SetIsIdentRuneFunc(func(ch rune, i int) bool { return unicode.IsLetter(ch) || ch == '#' }) 110 | p.Scan() 111 | }, 112 | wantScan: scanner.Ident, 113 | wantToken: "#foo", 114 | }, 115 | { 116 | name: "do not scan idents", 117 | input: "abc", 118 | do: func(p *Parser) { 119 | p.SetMode(scanner.GoTokens ^ scanner.ScanIdents) 120 | p.Scan() 121 | }, 122 | wantScan: 'b', 123 | wantToken: "b", 124 | }, 125 | } 126 | for _, tt := range tests { 127 | t.Run(tt.name, func(t *testing.T) { 128 | defer func() { 129 | err := recover() 130 | if err != nil && !tt.wantPanic { 131 | t.Fatalf("unexpected panic: %v", err) 132 | } 133 | }() 134 | 135 | p := newParser(tt.input, tt.Language) 136 | tt.do(p) 137 | if tt.wantPanic { 138 | return 139 | } 140 | scan := p.Scan() 141 | token := p.TokenText() 142 | 143 | if scan != tt.wantScan || token != tt.wantToken { 144 | t.Errorf("Parser.Scan() = %v (%v), want %v (%v)", scan, token, tt.wantScan, tt.wantToken) 145 | } 146 | }) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /prtg-batmin-gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaesslerAG/gval/8faea0debb7972676e853d4effe31f85587d3157/prtg-batmin-gopher.png -------------------------------------------------------------------------------- /random_test.go: -------------------------------------------------------------------------------- 1 | package gval 2 | 3 | // Courtesy of abrander 4 | // ref: https://gist.github.com/abrander/fa05ae9b181b48ffe7afb12c961b6e90 5 | import ( 6 | "fmt" 7 | "math/rand" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | var ( 13 | hello = "hello" 14 | empty struct{} 15 | empty2 *string 16 | 17 | values = []interface{}{ 18 | -1, 19 | 0, 20 | 12, 21 | 13, 22 | "", 23 | "hello", 24 | &hello, 25 | nil, 26 | "nil", 27 | empty, 28 | empty2, 29 | true, 30 | false, 31 | time.Now(), 32 | rune('r'), 33 | int64(34), 34 | time.Duration(0), 35 | "true", 36 | "false", 37 | "\ntrue\n", 38 | "\nfalse\n", 39 | "12", 40 | "nil", 41 | "arg1", 42 | "arg2", 43 | int(12), 44 | int32(12), 45 | int64(12), 46 | complex(1.0, 1.0), 47 | []byte{0, 0, 0}, 48 | []int{0, 0, 0}, 49 | []string{}, 50 | "[]", 51 | "{}", 52 | "\"\"", 53 | "\"12\"", 54 | "\"hello\"", 55 | ".*", 56 | "==", 57 | "!=", 58 | ">", 59 | ">=", 60 | "<", 61 | "<=", 62 | "=~", 63 | "!~", 64 | "in", 65 | "&&", 66 | "||", 67 | "^", 68 | "&", 69 | "|", 70 | ">>", 71 | "<<", 72 | "+", 73 | "-", 74 | "*", 75 | "/", 76 | "%", 77 | "**", 78 | "-", 79 | "!", 80 | "~", 81 | "?", 82 | ":", 83 | "??", 84 | "+", 85 | "-", 86 | "*", 87 | "/", 88 | "%", 89 | "**", 90 | "&", 91 | "|", 92 | "^", 93 | ">>", 94 | "<<", 95 | ",", 96 | "(", 97 | ")", 98 | "[", 99 | "]", 100 | "\n", 101 | "\000", 102 | } 103 | ) 104 | 105 | const SEED = 1487873697990155515 106 | 107 | func BenchmarkRandom(bench *testing.B) { 108 | rand.Seed(SEED) 109 | for i := 0; i < bench.N; i++ { 110 | num := rand.Intn(3) + 2 111 | expression := "" 112 | 113 | for n := 0; n < num; n++ { 114 | expression += fmt.Sprintf(" %s", getRandom(values)) 115 | } 116 | 117 | Evaluate(expression, nil) 118 | } 119 | } 120 | 121 | func getRandom(haystack []interface{}) interface{} { 122 | i := rand.Intn(len(haystack)) 123 | return haystack[i] 124 | } 125 | --------------------------------------------------------------------------------