gval

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 24, 2017 License: BSD-3-Clause Imports: 11 Imported by: 0

README

Gval

Build Status Godoc

Gval (Go EVALuate) provides support for evaluating arbitrary expressions, in particular Go-like expressions.

Evaluate

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

It can easily be extended with custom functions or operators:

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:

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:

Maps and Arrays

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 JSON Path:

Jsonpath is also suitable for accessing array elements.

Accessors

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

It also works if the parameter is a struct directly Hello + World() or if the fields are nested foo.Hello + foo.World()

This may be convenient but note that using accessors 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

  • Modifiers: + - / * & | ^ ** % >> <<
  • Comparators: > >= < <= == != =~ !~
  • Logical ops: || &&
  • Numeric constants, as 64-bit floating point (12345.678)
  • String constants (double quotes: "foobar")
  • Date function 'Date(x)', using any permutation of RFC3339, ISO8601, ruby date, or unix date
  • Boolean constants: true false
  • Parentheses to control order of evaluation ( )
  • Json Arrays : [1, 2, "foo"]
  • Json Objects : {"a":1, "b":2, "c":"foo"}
  • Prefixes: ! - ~
  • Ternary conditional: ? :
  • Null coalescence: ??

See Godoc for gval.Language details.

Customize

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

For details see Godoc.

Performance

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. 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.

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=..

For a very rough idea of performance, here are the results from a benchmark run on a Dell Latitude E7470 Win 10 i5-6300U.

BenchmarkGval/const_evaluation-4                               500000000                 3.57 ns/op
BenchmarkGval/const_parsing-4                                    1000000              1144 ns/op
BenchmarkGval/single_parameter_evaluation-4                     10000000               165 ns/op
BenchmarkGval/single_parameter_parsing-4                         1000000              1648 ns/op
BenchmarkGval/parameter_evaluation-4                             5000000               352 ns/op
BenchmarkGval/parameter_parsing-4                                 500000              2773 ns/op
BenchmarkGval/common_evaluation-4                                3000000               434 ns/op
BenchmarkGval/common_parsing-4                                    300000              4419 ns/op
BenchmarkGval/complex_evaluation-4                             100000000                11.6 ns/op
BenchmarkGval/complex_parsing-4                                   100000             17936 ns/op
BenchmarkGval/literal_evaluation-4                             300000000                 3.84 ns/op
BenchmarkGval/literal_parsing-4                                   500000              2559 ns/op
BenchmarkGval/modifier_evaluation-4                            500000000                 3.54 ns/op
BenchmarkGval/modifier_parsing-4                                  500000              3755 ns/op
BenchmarkGval/regex_evaluation-4                                   50000             21347 ns/op
BenchmarkGval/regex_parsing-4                                     200000              6480 ns/op
BenchmarkGval/constant_regex_evaluation-4                        1000000              1000 ns/op
BenchmarkGval/constant_regex_parsing-4                            200000              9417 ns/op
BenchmarkGval/accessors_evaluation-4                             3000000               417 ns/op
BenchmarkGval/accessors_parsing-4                                1000000              1778 ns/op
BenchmarkGval/accessors_method_evaluation-4                      1000000              1931 ns/op
BenchmarkGval/accessors_method_parsing-4                         1000000              1729 ns/op
BenchmarkGval/accessors_method_parameter_evaluation-4            1000000              2162 ns/op
BenchmarkGval/accessors_method_parameter_parsing-4                500000              2618 ns/op
BenchmarkGval/nested_accessors_evaluation-4                      2000000               681 ns/op
BenchmarkGval/nested_accessors_parsing-4                         1000000              2115 ns/op
BenchmarkRandom-4                                                 500000              3631 ns/op
ok

API Breaks

The library is designed for API stability but the classification of gvals sub languages is not final yet. Releases will explicitly state when an API break happens, and if they do not specify an API break it should be safe to upgrade.

Missing Features

  • Expression Formatter
  • SQL Expression
  • Examples for Language

Documentation

Overview

Package gval provides a generic expression language with concrete language instances of several basic languages. In gval base language, an Operator involves either unicode letters or unicode punctuations and unicode symbols.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Evaluate

func Evaluate(expression string, parameter interface{}, opts ...Language) (interface{}, error)

Evaluate given parameter with given expression in gval full language

Example (Accessor)
package main

import (
	"fmt"
	"os"

	"github.com/PaesslerAG/gval"
)

type exampleType struct {
	Hello string
}

func (e exampleType) World() string {
	return "wolrd"
}

func main() {

	value, err := gval.Evaluate(`foo.Hello + foo.World()`,
		map[string]interface{}{
			"foo": exampleType{Hello: "hello "},
		})
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	fmt.Print(value)

	// Output
	// hello world!
}
Output:

Example (Arithmetic)
package main

import (
	"fmt"
	"os"

	"github.com/PaesslerAG/gval"
)

func main() {

	value, err := gval.Evaluate("(requests_made * requests_succeeded / 100) >= 90",
		map[string]interface{}{
			"requests_made":      100,
			"requests_succeeded": 80,
		})
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	fmt.Print(value)

	// Output
	// false
}
Output:

Example (Basic)
package main

import (
	"fmt"
	"os"

	"github.com/PaesslerAG/gval"
)

func main() {

	value, err := gval.Evaluate("10 > 0", nil)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	fmt.Print(value)

	// Output
	// true
}
Output:

Example (DateComparison)
package main

import (
	"fmt"
	"os"
	"time"

	"github.com/PaesslerAG/gval"
)

func main() {

	value, err := gval.Evaluate("date(`2014-01-02`) > date(`2014-01-01 23:59:59`)",
		nil,
		// define Date comparison because it is not part expression language gval
		gval.InfixOperator(">", func(a, b interface{}) (interface{}, error) {
			date1, ok1 := a.(time.Time)
			date2, ok2 := b.(time.Time)

			if ok1 && ok2 {
				return date1.Before(date2), nil
			}
			return nil, fmt.Errorf("unexpected operands types (%T) > (%T)", a, b)
		}),
	)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	fmt.Print(value)

	// Output
	// true
}
Output:

Example (Encoding)
package main

import (
	"fmt"
	"os"

	"github.com/PaesslerAG/gval"
)

func main() {

	value, err := gval.Evaluate(`(7 < "47" == true ? "hello world!\n\u263a")`+" + ` more text`",
		nil,
		gval.Function("strlen", func(args ...interface{}) (interface{}, error) {
			length := len(args[0].(string))
			return (float64)(length), nil
		}))
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	fmt.Print(value)

	// Output
	// hello world!
	// ☺ more text
}
Output:

Example (FlatAccessor)
package main

import (
	"fmt"
	"os"

	"github.com/PaesslerAG/gval"
)

type exampleType struct {
	Hello string
}

func (e exampleType) World() string {
	return "wolrd"
}

func main() {

	value, err := gval.Evaluate(`Hello + World()`,
		exampleType{Hello: "hello "},
	)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	fmt.Print(value)

	// Output
	// hello world!
}
Output:

Example (Float64)
package main

import (
	"fmt"
	"os"

	"github.com/PaesslerAG/gval"
)

func main() {

	value, err := gval.Evaluate("(mem_used / total_mem) * 100",
		map[string]interface{}{
			"total_mem": 1024,
			"mem_used":  512,
		})
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	fmt.Print(value)

	// Output
	// 50
}
Output:

Example (Jsonpath)
package main

import (
	"fmt"
	"os"

	"github.com/PaesslerAG/gval"
	"github.com/PaesslerAG/jsonpath"
)

func main() {

	value, err := gval.Evaluate(`$["response-time"]`,
		map[string]interface{}{
			"response-time": 100,
		},
		jsonpath.Language(),
	)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	fmt.Print(value)

	// Output
	// 100
}
Output:

Example (NestedAccessor)
package main

import (
	"fmt"
	"os"

	"github.com/PaesslerAG/gval"
)

type exampleType struct {
	Hello string
}

func (e exampleType) World() string {
	return "wolrd"
}

func main() {

	value, err := gval.Evaluate(`foo.Hello + foo.World()`,
		map[string]interface{}{
			"foo": struct{ bar exampleType }{
				bar: exampleType{Hello: "hello "},
			},
		})
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	fmt.Print(value)

	// Output
	// hello world!
}
Output:

Example (NestedParameter)
package main

import (
	"fmt"
	"os"

	"github.com/PaesslerAG/gval"
)

func main() {

	value, err := gval.Evaluate("foo.bar > 0", map[string]interface{}{
		"foo": map[string]interface{}{"bar": -1.},
	})
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	fmt.Print(value)

	// Output
	// false
}
Output:

Example (Parameter)
package main

import (
	"fmt"
	"os"

	"github.com/PaesslerAG/gval"
)

func main() {

	value, err := gval.Evaluate("foo > 0", map[string]interface{}{
		"foo": -1.,
	})
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	fmt.Print(value)

	// Output
	// false
}
Output:

Example (String)
package main

import (
	"fmt"
	"os"

	"github.com/PaesslerAG/gval"
)

func main() {

	value, err := gval.Evaluate(`http_response_body == "service is ok"`,
		map[string]interface{}{
			"http_response_body": "service is ok",
		})
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	fmt.Print(value)

	// Output
	// false
}
Output:

Example (Strlen)
package main

import (
	"fmt"
	"os"

	"github.com/PaesslerAG/gval"
)

func main() {

	value, err := gval.Evaluate(`strlen("someReallyLongInputString") <= 16`,
		nil,
		gval.Function("strlen", func(args ...interface{}) (interface{}, error) {
			length := len(args[0].(string))
			return (float64)(length), nil
		}))
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	fmt.Print(value)

	// Output
	// false
}
Output:

Types

type Evaluable

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

Evaluable evaluates given parameter

Example
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/PaesslerAG/gval"
)

func main() {
	eval, err := gval.Full(gval.Constant("maximum_time", 53)).
		NewEvaluable("response_time <= maximum_time")
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	for i := 50; i < 55; i++ {
		value, err := eval(context.Background(), map[string]interface{}{
			"response_time": i,
		})
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}

		fmt.Println(value)
	}

	// Output
	// true
	// true
	// true
	// false
	// false
}
Output:

func (Evaluable) EvalBool

func (e Evaluable) EvalBool(c context.Context, parameter interface{}) (bool, error)

EvalBool evaluates given parameter to a bool

func (Evaluable) EvalFloat64

func (e Evaluable) EvalFloat64(c context.Context, parameter interface{}) (float64, error)

EvalFloat64 evaluates given parameter to an int

func (Evaluable) EvalInt

func (e Evaluable) EvalInt(c context.Context, parameter interface{}) (int, error)

EvalInt evaluates given parameter to an int

func (Evaluable) EvalString

func (e Evaluable) EvalString(c context.Context, parameter interface{}) (string, error)

EvalString evaluates given parameter to a string

func (Evaluable) IsConst

func (e Evaluable) IsConst() bool

IsConst returns if the Evaluable is a Parser.Const() value

type Language

type Language struct {
	// contains filtered or unexported fields
}

Language is an expression language

Example
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/PaesslerAG/gval"
)

func main() {
	lang := gval.NewLanguage(gval.JSON(), gval.Arithmetic(),
		//pipe operator
		gval.PostfixOperator("|", func(c context.Context, p *gval.Parser, pre gval.Evaluable) (gval.Evaluable, error) {
			post, err := p.ParseExpression(c)
			if err != nil {
				return nil, err
			}
			return func(c context.Context, v interface{}) (interface{}, error) {
				v, err := pre(c, v)
				if err != nil {
					return nil, err
				}
				return post(c, v)
			}, nil
		}))

	eval, err := lang.NewEvaluable(`{"foobar": 50} | foobar + 100`)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	value, err := eval(context.Background(), nil)

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

	fmt.Println(value)

	// Output
	// 150
}
Output:

func Arithmetic

func Arithmetic() Language

Arithmetic contains base, plus(+), minus(-), divide(/), power(**), negative(-) and numerical order (<=,<,>,>=)

Arithmetic operators expect float64 operands. Called with unfitting input, they try to convert the input to float64. They can parse strings and convert any type of int or float.

func Base

func Base() Language

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 Bitmask

func Bitmask() Language

Bitmask contains base, bitwise and(&), bitwise or(|) and bitwise not(^).

Bitmask operators expect float64 operands. Called with unfitting input they try to convert the input to float64. They can parse strings and convert any type of int or float.

func Constant

func Constant(name string, value interface{}) Language

Constant returns a Language with given constant

func Full

func Full(extensions ...Language) Language

Full is the union of Arithmetic, Bitmask, Text, PropositionalLogic, and Json

Operator in: a in b is true iff value a is an element of array b
Operator ??: a ?? b returns a if a is not false or nil, otherwise n
Operator ?: a ? b : c returns b if bool a is true, otherwise b

Function Date: Date(a) parses string a. a must match RFC3339, ISO8601, ruby date, or unix date

func Function

func Function(name string, function interface{}) Language

Function returns a Language with given function. Function has no conversion for input types.

If the function returns an error it must be the last return parameter.

If the function has (without the error) more then one return parameter, it returns them as []interface{}.

func InfixBoolOperator

func InfixBoolOperator(name string, f func(a, b bool) (interface{}, error)) Language

InfixBoolOperator for two bool values.

func InfixEvalOperator

func InfixEvalOperator(name string, f func(a, b Evaluable) (Evaluable, error)) Language

InfixEvalOperator operates on the raw operands. Therefore it cannot be combined with operators for other operand types.

func InfixNumberOperator

func InfixNumberOperator(name string, f func(a, b float64) (interface{}, error)) Language

InfixNumberOperator for two number values.

func InfixOperator

func InfixOperator(name string, f func(a, b interface{}) (interface{}, error)) Language

InfixOperator for two arbitrary values.

func InfixShortCircuit

func InfixShortCircuit(name string, f func(a interface{}) (interface{}, bool)) Language

InfixShortCircuit operator is called after the left operand is evaluated.

func InfixTextOperator

func InfixTextOperator(name string, f func(a, b string) (interface{}, error)) Language

InfixTextOperator for two text values.

func JSON

func JSON() Language

JSON contains json objects ({string:expression,...}) and json arrays ([expression, ...])

func NewLanguage

func NewLanguage(bases ...Language) Language

NewLanguage returns the union of given Languages as new Language.

func PostfixOperator

func PostfixOperator(name string, ext func(context.Context, *Parser, Evaluable) (Evaluable, error)) Language

PostfixOperator extends a Language

func Precedence

func Precedence(name string, operatorPrecendence uint8) Language

Precedence of operator. The Operator with higher operatorPrecedence is evaluated first.

func PrefixExtension

func PrefixExtension(r rune, ext func(context.Context, *Parser) (Evaluable, error)) Language

PrefixExtension extends a Language

func PrefixMetaPrefix

func PrefixMetaPrefix(r rune, ext func(context.Context, *Parser) (call string, alternative func() (Evaluable, error), err error)) Language

PrefixMetaPrefix chooses a Prefix to be executed

func PrefixOperator

func PrefixOperator(name string, e Evaluable) Language

PrefixOperator returns a Language with given prefix

func PropositionalLogic

func PropositionalLogic() Language

PropositionalLogic contains base, not(!), and (&&), or (||) and Base.

Propositional operator expect bool operands. Called with unfitting input they try to convert the input to bool. Numbers other than 0 and the strings "TRUE" and "true" are interpreted as true. 0 and the strings "FALSE" and "false" are interpreted as false.

func Text

func Text() Language

Text contains base, lexical order on strings (<=,<,>,>=), regex match (=~) and regex not match (!~)

func (Language) Evaluate

func (l Language) Evaluate(expression string, parameter interface{}) (interface{}, error)

Evaluate given parameter with given expression

func (Language) NewEvaluable

func (l Language) NewEvaluable(expression string) (Evaluable, error)

NewEvaluable returns an Evaluable for given expression in the specified language

type Parser

type Parser struct {
	Language
	// contains filtered or unexported fields
}

Parser parses expressions in a Language into an Evaluable

func (*Parser) Camouflage

func (p *Parser) Camouflage(unit string, expected ...rune)

Camouflage rewind the last Scan(). The Parser holds the camouflage error until the next Scan() Do not call Rewind() on a camouflaged Parser

func (*Parser) Const

func (*Parser) Const(value interface{}) Evaluable

Const Evaluable represents given constant

func (*Parser) Expected

func (p *Parser) Expected(unit string, expected ...rune) error

Expected returns an error signaling an unexpected Scan() result

func (*Parser) Next

func (p *Parser) Next() rune

Next reads and returns the next Unicode character. It returns EOF at the end of the source. Do not call Next() on a camouflaged Parser

func (*Parser) ParseExpression

func (p *Parser) ParseExpression(c context.Context) (eval Evaluable, err error)

ParseExpression scans an expression into an Evaluable.

func (*Parser) ParseNextExpression

func (p *Parser) ParseNextExpression(c context.Context) (eval Evaluable, err error)

ParseNextExpression scans the expression ignoring following operators

func (*Parser) Peek

func (p *Parser) Peek() rune

Peek returns the next Unicode character in the source without advancing the scanner. It returns EOF if the scanner's position is at the last character of the source. Do not call Peek() on a camouflaged Parser

func (*Parser) Scan

func (p *Parser) Scan() rune

Scan reads the next token or Unicode character from source and returns it. It only recognizes tokens t for which the respective Mode bit (1<<-t) is set. It returns scanner.EOF at the end of the source.

func (*Parser) TokenText

func (p *Parser) TokenText() string

TokenText returns the string corresponding to the most recently scanned token. Valid after calling Scan().

func (*Parser) Var

func (*Parser) Var(path ...Evaluable) Evaluable

Var Evaluable represents value at given path. It supports:

map[string]interface{},
[]interface{} and
struct fields

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL