Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into combinator-features
Browse files Browse the repository at this point in the history
  • Loading branch information
impl committed Dec 8, 2020
2 parents 2f55be3 + c11e94e commit 73dbf83
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 31 deletions.
8 changes: 4 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ before_install:
- go get github.com/mattn/goveralls

script:
- go test -bench=. -benchmem -timeout 10m -coverprofile coverage.out
- $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN
- go test -bench=Random -benchtime 5m -timeout 30m -benchmem -coverprofile coverage.out
- go test -bench=. -benchmem -timeout 9m -coverprofile coverage.out
- $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken=$COVERALLS_TOKEN
- go test -bench=Random -benchtime 3m -timeout 9m -benchmem -coverprofile coverage.out

go: "1.11"
go: "1.15"
51 changes: 29 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Gval

[![Godoc](https://godoc.org/github.com/PaesslerAG/gval?status.png)](https://godoc.org/github.com/PaesslerAG/gval)
[![Godoc](https://pkg.go.dev/github.com/PaesslerAG/gval?status.png)](https://pkg.go.dev/github.com/PaesslerAG/gval)
[![Build Status](https://api.travis-ci.org/PaesslerAG/gval.svg?branch=master)](https://travis-ci.org/PaesslerAG/gval)
[![Coverage Status](https://coveralls.io/repos/github/PaesslerAG/gval/badge.svg?branch=master)](https://coveralls.io/github/PaesslerAG/gval?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/PaesslerAG/gval)](https://goreportcard.com/report/github.com/PaesslerAG/gval)
Expand All @@ -13,71 +13,71 @@ Gval (Go eVALuate) provides support for evaluating arbitrary expressions, in par

Gval can evaluate expressions with parameters, arimethetic, logical, and string operations:

- basic expression: [10 > 0](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--Basic)
- parameterized expression: [foo > 0](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--Parameter)
- nested parameterized expression: [foo.bar > 0](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--NestedParameter)
- arithmetic expression: [(requests_made * requests_succeeded / 100) >= 90](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--Arithmetic)
- string expression: [http_response_body == "service is ok"](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--String)
- float64 expression: [(mem_used / total_mem) * 100](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--Float64)
- basic expression: [10 > 0](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Basic)
- parameterized expression: [foo > 0](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Parameter)
- nested parameterized expression: [foo.bar > 0](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-NestedParameter)
- arithmetic expression: [(requests_made * requests_succeeded / 100) >= 90](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Arithmetic)
- string expression: [http_response_body == "service is ok"](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-String)
- float64 expression: [(mem_used / total_mem) * 100](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Float64)

It can easily be extended with custom functions or operators:

- custom date comparator: [date(\`2014-01-02\`) > date(\`2014-01-01 23:59:59\`)](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--DateComparison)
- string length: [strlen("someReallyLongInputString") <= 16](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--Strlen)
- 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)
- string length: [strlen("someReallyLongInputString") <= 16](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Strlen)

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:

- [Parsing and Evaluation](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluable)
- [Parsing and Evaluation](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluable)

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.

Strings, numbers, and booleans can be used like in Go:

- [(7 < "47" == true ? "hello world!\n\u263a") + \` more text\`](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--Encoding)
- [(7 < "47" == true ? "hello world!\n\u263a") + \` more text\`](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Encoding)

## Parameter

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.

- [foo > 0](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--Parameter)
- [foo > 0](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Parameter)

### Bracket Selector

Map and array elements and Struct Field can be accessed via `[]`.

- [foo[0]](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--Array)
- [foo["b" + "a" + "r"]](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--ExampleEvaluate_ComplexAccessor)
- [foo[0]](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Array)
- [foo["b" + "a" + "r"]](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-ExampleEvaluate_ComplexAccessor)

### Dot Selector

A nested variable with a name containing only letters and underscores can be accessed via a dot selector.

- [foo.bar > 0](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--NestedParameter)
- [foo.bar > 0](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-NestedParameter)

### Custom Selector

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):

- [$["response-time"]](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--Jsonpath)
- [$["response-time"]](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Jsonpath)

Jsonpath is also suitable for accessing array elements.

### Fields and Methods

If you have structs in your parameters, you can access their fields and methods in the usual way:

- [foo.Hello + foo.World()](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--FlatAccessor)
- [foo.Hello + foo.World()](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-FlatAccessor)

It also works if the parameter is a struct directly
[Hello + World()](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--Accessor)
[Hello + World()](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Accessor)
or if the fields are nested
[foo.Hello + foo.World()](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--NestedAccessor)
[foo.Hello + foo.World()](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-NestedAccessor)

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.

## Default Language

The default language is in serveral sub languages like text, arithmetic or propositional logic defined. See [Godoc](https://godoc.org/github.com/PaesslerAG/gval/#Gval) for details. All sub languages are merged into gval.Full which contains the following elements:
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:

- Modifiers: `+` `-` `/` `*` `&` `|` `^` `**` `%` `>>` `<<`
- Comparators: `>` `>=` `<` `<=` `==` `!=` `=~` `!~`
Expand All @@ -97,9 +97,16 @@ The default language is in serveral sub languages like text, arithmetic or propo

Gval is completly customizable. Every constant, function or operator can be defined separately and existing expression languages can be reused:

- [foo.Hello + foo.World()](https://godoc.org/github.com/PaesslerAG/gval/#example-Language)
- [foo.Hello + foo.World()](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Language)

For details see [Godoc](https://godoc.org/github.com/PaesslerAG/gval).
For details see [Godoc](https://pkg.go.dev/github.com/PaesslerAG/gval).

### Implementing custom selector

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.
Function receives next part of the path and can return any type of var that is again evaluated through standard gval procedures.

[Example Custom Selector](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-custom-selector)

### External gval Languages

Expand Down
15 changes: 14 additions & 1 deletion evaluable.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import (
"strings"
)

// Selector allows for custom variable selection from structs
//
// Return value is again handled with variable() until end of the given path
type Selector interface {
SelectGVal(c context.Context, key string) (interface{}, error)
}

// Evaluable evaluates given parameter
type Evaluable func(c context.Context, parameter interface{}) (interface{}, error)

Expand All @@ -26,7 +33,7 @@ func (e Evaluable) EvalInt(c context.Context, parameter interface{}) (int, error
return int(f), nil
}

//EvalFloat64 evaluates given parameter to an int
//EvalFloat64 evaluates given parameter to a float64
func (e Evaluable) EvalFloat64(c context.Context, parameter interface{}) (float64, error) {
v, err := e(c, parameter)
if err != nil {
Expand Down Expand Up @@ -114,6 +121,12 @@ func variable(path Evaluables) Evaluable {
}
for i, k := range keys {
switch o := v.(type) {
case Selector:
v, err = o.SelectGVal(c, k)
if err != nil {
return nil, fmt.Errorf("failed to select '%s' on %T: %w", k, o, err)
}
continue
case map[interface{}]interface{}:
v = o[k]
continue
Expand Down
101 changes: 101 additions & 0 deletions evaluable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package gval

import (
"context"
"fmt"
"reflect"
"strings"
"testing"
)

Expand Down Expand Up @@ -111,3 +114,101 @@ func TestEvaluable_EvalFloat64(t *testing.T) {
})
}
}

type testSelector struct {
str string
Map map[string]interface{}
}

func (s testSelector) SelectGVal(ctx context.Context, k string) (interface{}, error) {
if k == "str" {
return s.str, nil
}

if k == "map" {
return s.Map, nil
}

if strings.HasPrefix(k, "deep") {
return s, nil
}

return nil, fmt.Errorf("unknown-key")

}

func TestEvaluable_CustomSelector(t *testing.T) {
var (
lang = Base()
tests = []struct {
name string
expr string
params interface{}
want interface{}
wantErr bool
}{
{
"unknown",
"s.Foo",
map[string]interface{}{"s": &testSelector{}},
nil,
true,
},
{
"field directly",
"s.Str",
map[string]interface{}{"s": &testSelector{str: "test-value"}},
nil,
true,
},
{
"field via selector",
"s.str",
map[string]interface{}{"s": &testSelector{str: "test-value"}},
"test-value",
false,
},
{
"flat",
"str",
&testSelector{str: "test-value"},
"test-value",
false,
},
{
"map field",
"s.map.foo",
map[string]interface{}{"s": &testSelector{Map: map[string]interface{}{"foo": "bar"}}},
"bar",
false,
},
{
"crawl to val",
"deep.deeper.deepest.str",
&testSelector{str: "foo"},
"foo",
false,
},
{
"crawl to struct",
"deep.deeper.deepest",
&testSelector{},
testSelector{},
false,
},
}
)

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := lang.Evaluate(tt.expr, tt.params)
if (err != nil) != tt.wantErr {
t.Errorf("Evaluable.Evaluate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Evaluable.Evaluate() = %v, want %v", got, tt.want)
}
})
}
}
29 changes: 29 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -393,3 +393,32 @@ func ExampleLanguage() {
// Output:
// 150
}

type exampleCustomSelector struct{ hidden string }

var _ gval.Selector = &exampleCustomSelector{}

func (s *exampleCustomSelector) SelectGVal(ctx context.Context, k string) (interface{}, error) {
if k == "hidden" {
return s.hidden, nil
}

return nil, nil
}

func ExampleCustomSelector() {
lang := gval.Base()
value, err := lang.Evaluate(
"myStruct.hidden",
map[string]interface{}{"myStruct": &exampleCustomSelector{hidden: "hello world"}},
)

if err != nil {
fmt.Println(err)
}

fmt.Println(value)

// Output:
// hello world
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module github.com/PaesslerAG/gval

require github.com/PaesslerAG/jsonpath v0.1.0
go 1.15

go 1.13
require github.com/PaesslerAG/jsonpath v0.1.0
22 changes: 20 additions & 2 deletions gval.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ func JSON() Language {
return ljson
}

// Parentheses contains support for parentheses.
func Parentheses() Language {
return parentheses
}

// Ident contains support for variables and functions.
func Ident() Language {
return ident
}

// Base contains equal (==) and not equal (!=), perentheses and general support for variables, constants and functions
// It contains true, false, (floating point) number, string ("" or ``) and char ('') constants
func Base() Language {
Expand Down Expand Up @@ -205,6 +215,14 @@ var propositionalLogic = NewLanguage(
base,
)

var parentheses = NewLanguage(
PrefixExtension('(', parseParentheses),
)

var ident = NewLanguage(
PrefixMetaPrefix(scanner.Ident, parseIdent),
)

var base = NewLanguage(
PrefixExtension(scanner.Int, parseNumber),
PrefixExtension(scanner.Float, parseNumber),
Expand All @@ -225,7 +243,7 @@ var base = NewLanguage(

InfixOperator("==", func(a, b interface{}) (interface{}, error) { return reflect.DeepEqual(a, b), nil }),
InfixOperator("!=", func(a, b interface{}) (interface{}, error) { return !reflect.DeepEqual(a, b), nil }),
PrefixExtension('(', parseParentheses),
parentheses,

Precedence("??", 0),

Expand Down Expand Up @@ -258,5 +276,5 @@ var base = NewLanguage(

Precedence("**", 200),

PrefixMetaPrefix(scanner.Ident, parseIdent),
ident,
)

0 comments on commit 73dbf83

Please sign in to comment.