goel

package module
v0.0.0-...-6c5a2a3 Latest Latest
Warning

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

Go to latest
Published: Sep 11, 2020 License: Apache-2.0 Imports: 7 Imported by: 0

README

Site Go Lang Version Go Doc Go Report Card codecov CircleCI

Go EL

GoEL is an expression language parser that parses a go expression and allows the execution of that expression with a context. It currently supports the following operations:

  • Binary operators: + - * / == != && || . %
  • Relation operators: < > <= >=
  • Unary operators: - ! +
  • literals: string, int, float64. Note: rune literals are treated as strings.
  • Types: string, int, float64, bool, struct types and interfaces
  • Function calls to both globally defined functions and functions attached to types.
  • inner expressions (e.g. a[x])
  • map expressions (e.g. m["foo"])
  • type assertion (e.g. foo.(string)
  • slice expressions on slices (e.g. a[x:y:m])

Not supported (in priority order):

  1. variadic function calls
  2. add operators: ^ |
  3. type conversions
  4. mul operators: << >> & &^
  5. unary operators: ^

As time goes by, most of these expressions will be accepted. Here is a list of expressions that I doubt will ever be allowed:

  • function literals
  • composite literals
  • unary operators: * & <-
  • slice expressions on arrays (e.g. a[x:y])

Getting Started

Contexts

There are two contexts that are used when evaluating expressions: The parsing context and the execution context. Each are used to provide variables and functions to the two phases of execution.

Parsing Context

This context should contain entries with string keys that are the type of the referenced object. For instance, the type of a structure must be passed to enable the parser to understand the type of the variables and know which are defined. Variables referenced in the expression that do not have a type in the context, will result in an error stating that the variable is not defined.

Execution Context

The execution context contains the actual values or functions associated with the names used as keys.

Function return values

If a function has multiple return values, it will return an []interface{} containing the values instead. For the most part it is not well supported for a function to return multiple values. This may change in the future.

Functions with Errors

One exception to the above rule is when a function returns an error as its last output. In that case, all the previous outputs will be treated as described above but the error value will be checked against nil. If the value is not nil, the evaluation will end and return the error.

Example

package goel_test

import (
	"context"
	"fmt"
	"github.com/homedepot/goel"
	"go/parser"
	"reflect"
)

func ExampleCompile() {
	pctx := context.Background()
	ectx := context.Background()
	exp, _ := parser.ParseExpr("5 + 3")
	cexp := goel.NewCompiledExpression(pctx, exp)
	result, _ := cexp.Execute(ectx)
	fmt.Printf("%v\n", result)
	sum := func(x, y int) int {
		return x + y
	}

	pctx = context.WithValue(pctx, "sum", reflect.TypeOf(sum))
	ectx = context.WithValue(ectx, "sum", reflect.ValueOf(sum))
	exp, _ = parser.ParseExpr("sum(5,3)")
	cexp = goel.NewCompiledExpression(pctx, exp)
	result, _ = cexp.Execute(ectx)
	fmt.Printf("%v\n", result)

	x := 5
	y := 3
	pctx = context.WithValue(pctx, "x", reflect.TypeOf(x))
	ectx = context.WithValue(ectx, "x", reflect.ValueOf(x))
	pctx = context.WithValue(pctx, "y", reflect.TypeOf(y))
	ectx = context.WithValue(ectx, "y", reflect.ValueOf(y))
	exp, _ = parser.ParseExpr("sum(x,y)")
	cexp = goel.NewCompiledExpression(pctx, exp)
	result, _ = cexp.Execute(ectx)
	fmt.Printf("%v\n", result)
	// Output:
	// 8
	// 8
	// 8
}

For more details on how to use this API, see the go doc and the pages site

Maintainers

Dana H. P'Simer

License

This project is released under the Apache 2.0 Open Source License. Please see our LICENSE file for details.

Contributing

Please see our Contributing document for details on contributing.

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var (

	// StringType is a reflect.Type for strings
	StringType = reflect.TypeOf("")
	// IntType is a reflect.Type for int
	IntType = reflect.TypeOf(0)
	// DoubleType is a reflect.Type for float64
	DoubleType = reflect.TypeOf(1.0)
	// BoolType is a reflect.Type for bool
	BoolType = reflect.TypeOf(true)
	// ErrorType is a reflect.Type for error
	ErrorType = reflect.TypeOf((*error)(nil)).Elem()
	// TypeType is a reflect.Type for reflect.Type
	TypeType = reflect.TypeOf(reflect.TypeOf(IntType))
	// InterfaceType is a reflect.Type for interface{}
	InterfaceType = reflect.TypeOf(&intr).Elem()
)

Functions

This section is empty.

Types

type CompiledExpression

type CompiledExpression interface {
	// Execute will execute the expression with the given execution context and return the result or an error.
	Execute(executionContext context.Context) (interface{}, error)
	// ReturnType will return the type the expression is expected to return or an error if the expression did not
	// compile successfully
	ReturnType() (reflect.Type, error)
	// Error returns any building error that may have occurred.
	Error() error
}

CompiledExpression represents a expression that has been compiled from a source string.

func NewCompiledExpression

func NewCompiledExpression(parseContext context.Context, exp ast.Expr) CompiledExpression

NewCompiledExpression takes a parsing context and an expression AST and creates an executable CompiledExpression.

Example
package main

import (
	"context"
	"fmt"
	"github.com/homedepot/goel"
	"github.com/pkg/errors"
	"go/parser"
	"net/http"
	"reflect"
	"regexp"
)

func main() {
	pctx := context.Background()
	ectx := context.Background()
	exp, _ := parser.ParseExpr("5 + 3")
	cexp := goel.NewCompiledExpression(pctx, exp)
	result, _ := cexp.Execute(ectx)
	fmt.Printf("%v\n", result)
	sum := func(x, y int) int {
		return x + y
	}

	pctx = context.WithValue(pctx, "sum", reflect.TypeOf(sum))
	ectx = context.WithValue(ectx, "sum", reflect.ValueOf(sum))
	exp, _ = parser.ParseExpr("sum(5,3)")
	cexp = goel.NewCompiledExpression(pctx, exp)
	result, _ = cexp.Execute(ectx)
	fmt.Printf("%v\n", result)

	x := 5
	y := 3
	pctx = context.WithValue(pctx, "x", reflect.TypeOf(x))
	ectx = context.WithValue(ectx, "x", reflect.ValueOf(x))
	pctx = context.WithValue(pctx, "y", reflect.TypeOf(y))
	ectx = context.WithValue(ectx, "y", reflect.ValueOf(y))
	exp, _ = parser.ParseExpr("sum(x,y)")
	cexp = goel.NewCompiledExpression(pctx, exp)
	result, _ = cexp.Execute(ectx)
	fmt.Printf("%v\n", result)
}

type test struct {
	name                   string
	expression             string
	expectedValue          reflect.Value
	expectedParsingError   error
	expectedBuildingError  error
	expectedExecutionError error
	parsingContext         map[string]interface{}
	executionContext       map[string]interface{}
}

var testRequest *http.Request
var tests []test
var x int
var c chan int

type testStruct struct {
	X, Y int
	Name string
}

func (ts testStruct) GetName() string {
	return ts.Name
}

func (ts *testStruct) SetName(newName string) string {
	oldName := ts.Name
	ts.Name = newName
	return oldName
}

type NameGetter interface {
	GetName() string
}

func bar() interface{} {
	return "bar"
}

var ts = testStruct{1, 2, "Joe"}
var ng NameGetter = &ts
var ngType = reflect.TypeOf(&ng).Elem()

func init() {
	var testArray [6]int
	testArray[0] = 1
	testArray[1] = 2
	testArray[2] = 4
	testArray[3] = 8
	testArray[4] = 16
	testArray[5] = 32
	var err error
	testRequest, err = http.NewRequest("GET", "http://localhost/foobar", nil)
	if err != nil {
		panic(err.Error())
	}
	testRequest.Header.Add("Content-Type", "application/json")
	tests = []test{
		{
			name:          "boolean literal true",
			expression:    "true",
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "boolean literal true",
			expression:    "false",
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "boolean not true",
			expression:    "!true",
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "boolean not false",
			expression:    "!false",
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "simple integer literal",
			expression:    "5",
			expectedValue: reflect.ValueOf(5),
		},
		{
			name:          "simple integer literal negation",
			expression:    "-5",
			expectedValue: reflect.ValueOf(-5),
		},
		{
			name:          "simple double literal negation",
			expression:    "-5.7",
			expectedValue: reflect.ValueOf(-5.7),
		},
		{
			name:                  "type mismatch negation",
			expression:            `-"5.7"`,
			expectedBuildingError: errors.New("1: unsupported unary expression: -string"),
		},
		{
			name:                  "unsupported unary operator (bitwise complement)",
			expression:            "^5",
			expectedBuildingError: errors.New("1: unsupported unary expression: ^int"),
		},
		{
			name:          "simple integer literal add",
			expression:    "+5",
			expectedValue: reflect.ValueOf(5),
		},
		{
			name:          "simple double literal add",
			expression:    "+5.7",
			expectedValue: reflect.ValueOf(5.7),
		},
		{
			name:                  "type mismatch negation",
			expression:            `+"5.7"`,
			expectedBuildingError: errors.New("1: unsupported unary expression: +string"),
		},
		{
			name:                  "unsupported unary operator (pointer deref)",
			expression:            "*x",
			expectedBuildingError: errors.New("1: unknown expression type"),
			parsingContext: map[string]interface{}{
				"x": reflect.TypeOf(&x),
			},
			executionContext: map[string]interface{}{
				"x": reflect.ValueOf(&x),
			},
		},
		{
			name:                  "unsupported unary operator (pointer to)",
			expression:            "&x",
			expectedBuildingError: errors.New("1: unsupported unary expression: &int"),
			parsingContext: map[string]interface{}{
				"x": reflect.TypeOf(x),
			},
			executionContext: map[string]interface{}{
				"x": reflect.ValueOf(x),
			},
		},
		{
			name:                  "unsupported unary operator (channel input)",
			expression:            "<-c",
			expectedBuildingError: errors.New("1: unsupported unary expression: <-"),
			parsingContext: map[string]interface{}{
				"c": reflect.TypeOf(c),
			},
			executionContext: map[string]interface{}{
				"c": reflect.ValueOf(c),
			},
		},
		{
			name:          "simple float literal",
			expression:    "5.6",
			expectedValue: reflect.ValueOf(5.6),
		},
		{
			name:          "simple string literal",
			expression:    `"fubar"`,
			expectedValue: reflect.ValueOf("fubar"),
		},
		{
			name:                 "invalid string literal",
			expression:           `"fubar`,
			expectedParsingError: errors.Errorf("1:1: string literal not terminated"),
		},
		{
			name:          "simple char literal",
			expression:    `'f'`,
			expectedValue: reflect.ValueOf("f"),
		},
		{
			name:                  "unsupported operator (xor)",
			expression:            "5 ^ 2",
			expectedBuildingError: errors.Errorf("3: unsupported binary operation ^"),
		},
		{
			name:          "less than false",
			expression:    "5 < 2",
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "less than false equal",
			expression:    "5 < 5",
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "less than true",
			expression:    "2 < 5",
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "less than or equal false",
			expression:    "5 <= 2",
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "less than or equal true equal",
			expression:    "2 <= 2",
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "less than or equal true less than",
			expression:    "2 <= 5",
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "less than false (float)",
			expression:    "5.0 < 2.0",
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "less than false equal (float)",
			expression:    "5.0 < 5.0",
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "less than true (float)",
			expression:    "2.0 < 5.0",
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "less than or equal false (float)",
			expression:    "5.0 <= 2.0",
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "less than or equal true equal (float)",
			expression:    "2.0 <= 2.0",
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "less than or equal true less than (float)",
			expression:    "2.0 <= 5.0",
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "less than false (string)",
			expression:    `"5.0" < "2.0"`,
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "less than false equal (string)",
			expression:    `"5.0" < "5.0"`,
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "less than true (string)",
			expression:    `"2.0" < "5.0"`,
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "less than or equal false (string)",
			expression:    `"5.0" <= "2.0"`,
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "less than or equal true equal (string)",
			expression:    `"2.0" <= "2.0"`,
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "less than or equal true less than (string)",
			expression:    `"2.0" <= "5.0"`,
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "greater than false",
			expression:    "2 > 5",
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "greater than false equal",
			expression:    "5 > 5",
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "greater than true",
			expression:    "5 > 2",
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "greater than false (float)",
			expression:    "2.0 > 5.0",
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "greater than false equal (float)",
			expression:    "5.0 > 5.0",
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "greater than true (float)",
			expression:    "5.0 > 2.0",
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "greater than false (string)",
			expression:    `"a" > "b"`,
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "greater than false equal (string)",
			expression:    `"b" > "b"`,
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "greater than true (string)",
			expression:    `"b" > "a"`,
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "greater than or equal false",
			expression:    "2 >= 5",
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "greater than or equal true equal",
			expression:    "2 >= 2",
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "greater than or equal true less than",
			expression:    "5 >= 2",
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "greater than or equal false (float)",
			expression:    "2.0 >= 5.0",
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "greater than or equal true equal (float)",
			expression:    "2.0 >= 2.0",
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "greater than or equal true less than (float)",
			expression:    "5.0 >= 2.0",
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "greater than or equal false (string)",
			expression:    `"2" >= "5"`,
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "greater than or equal true equal (string)",
			expression:    `"2" >= "2"`,
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "greater than or equal true less than (string)",
			expression:    `"5" >= "2"`,
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:                  "unsupported operator (bitwise or)",
			expression:            "5 | 2",
			expectedBuildingError: errors.Errorf("3: unsupported binary operation |"),
		},
		{
			name:          "modulo",
			expression:    "5 % 2",
			expectedValue: reflect.ValueOf(1),
		},
		{
			name:                  "unsupported operator (shift left)",
			expression:            "5 << 2",
			expectedBuildingError: errors.Errorf("3: unsupported binary operation <<"),
		},
		{
			name:                  "unsupported operator (shift right)",
			expression:            "5 >> 2",
			expectedBuildingError: errors.Errorf("3: unsupported binary operation >>"),
		},
		{
			name:                  "unsupported operator (bitwise and)",
			expression:            "5 & 2",
			expectedBuildingError: errors.Errorf("3: unsupported binary operation &"),
		},
		{
			name:                  "unsupported operator (bit clear)",
			expression:            "5 &^ 2",
			expectedBuildingError: errors.Errorf("3: unsupported binary operation &^"),
		},
		{
			name:          "simple literal addition",
			expression:    "5 + 2",
			expectedValue: reflect.ValueOf(7),
		},
		{
			name:          "simple literal addition (float)",
			expression:    "5.0 + 2.0",
			expectedValue: reflect.ValueOf(7.0),
		},
		{
			name:                  "type mismatch literal addition",
			expression:            "'f' + 2",
			expectedBuildingError: errors.Errorf("5: type mismatch in binary expression"),
		},
		{
			name:                  "type mismatch literal subtraction",
			expression:            "3.14 - 2",
			expectedBuildingError: errors.Errorf("6: type mismatch in binary expression"),
		},
		{
			name:                  "type mismatch literal multiplication",
			expression:            "6.7 * 2",
			expectedBuildingError: errors.Errorf("5: type mismatch in binary expression"),
		},
		{
			name:                  "type mismatch literal division",
			expression:            "3.5 / 2",
			expectedBuildingError: errors.Errorf("5: type mismatch in binary expression"),
		},
		{
			name:                  "unsupported type subtraction",
			expression:            "'f' - 2",
			expectedBuildingError: errors.Errorf("5: type mismatch in binary expression"),
		},
		{
			name:                  "unsupported type  multiplication",
			expression:            "'f' * 2",
			expectedBuildingError: errors.Errorf("5: type mismatch in binary expression"),
		},
		{
			name:                  "type mismatch literal division",
			expression:            "'f' / 2",
			expectedBuildingError: errors.Errorf("5: type mismatch in binary expression"),
		},
		{
			name:          "string literal addition",
			expression:    `"foo" + "bar"`,
			expectedValue: reflect.ValueOf("foobar"),
		},
		{
			name:          "simple literal subtraction",
			expression:    "5.3 - 2.7",
			expectedValue: reflect.ValueOf(2.6),
		},
		{
			name:          "simple literal multiplication",
			expression:    "5.3 * 2.7",
			expectedValue: reflect.ValueOf(14.31),
		},
		{
			name:          "simple literal division",
			expression:    "5.3 / 2.7",
			expectedValue: reflect.ValueOf(1.962962),
		},
		{
			name:          "simple literal division (int)",
			expression:    "5 / 2",
			expectedValue: reflect.ValueOf(2),
		},
		{
			name:          "simple literal equality",
			expression:    "5 == 5",
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "simple literal inequality",
			expression:    "5 != 3",
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "simple literal equality negation",
			expression:    "5 == 3",
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "simple literal inequality negation",
			expression:    "5 != 5",
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "simple literal string equality",
			expression:    `"foo" == "foo"`,
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "simple literal string inequality",
			expression:    `"foo" != "bar"`,
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "simple literal string equality negation",
			expression:    `"foo" == "bar"`,
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "simple literal string inequality negation",
			expression:    `"foo" != "foo"`,
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "logical AND (true)",
			expression:    `5 == 5 && "foo" != "bar"`,
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "logical AND (false)",
			expression:    `5 == 5 && "foo" != "foo"`,
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "logical OR (true)",
			expression:    `5 == 5 || "foo" == "bar"`,
			expectedValue: reflect.ValueOf(true),
		},
		{
			name:          "logical OR (false)",
			expression:    `5 == 4 || "foo" != "foo"`,
			expectedValue: reflect.ValueOf(false),
		},
		{
			name:          "parenthesized literal expression",
			expression:    "(5 + 2) * 3",
			expectedValue: reflect.ValueOf(21),
		},
		{
			name:                   "binary expression left execution error",
			expression:             `returnsError() == 6`,
			expectedExecutionError: errors.New("Boo!"),
			parsingContext: map[string]interface{}{
				"returnsError": reflect.TypeOf(returnsError),
			},
			executionContext: map[string]interface{}{
				"returnsError": reflect.ValueOf(returnsError),
			},
		},
		{
			name:                   "binary expression right execution error",
			expression:             `6 == returnsError()`,
			expectedExecutionError: errors.New("Boo!"),
			parsingContext: map[string]interface{}{
				"returnsError": reflect.TypeOf(returnsError),
			},
			executionContext: map[string]interface{}{
				"returnsError": reflect.ValueOf(returnsError),
			},
		},
		{
			name:                  "binary expression left build error",
			expression:            `*3 == 6`,
			expectedBuildingError: errors.New("1: unknown expression type"),
		},
		{
			name:                  "binary expression right build error",
			expression:            `6 == *3`,
			expectedBuildingError: errors.New("6: unknown expression type"),
		},
		{
			name:                  "binary expression unsupported type",
			expression:            `x == y`,
			expectedBuildingError: errors.New("3: unsupported binary expression type: *http.Request"),
			parsingContext: map[string]interface{}{
				"x": reflect.TypeOf(testRequest),
				"y": reflect.TypeOf(testRequest),
			},
			executionContext: map[string]interface{}{
				"x": reflect.ValueOf(testRequest),
				"y": reflect.ValueOf(testRequest),
			},
		},
		{
			name:                  "add expression unsupported type (bool)",
			expression:            `true + true`,
			expectedBuildingError: errors.New("1: unsupported type bool"),
		},
		{
			name:                  "sub expression unsupported type (bool)",
			expression:            `true - true`,
			expectedBuildingError: errors.New("1: unsupported type bool"),
		},
		{
			name:                  "mul expression unsupported type (bool)",
			expression:            `true * true`,
			expectedBuildingError: errors.New("1: unsupported type bool"),
		},
		{
			name:                  "quo expression unsupported type (bool)",
			expression:            `true / true`,
			expectedBuildingError: errors.New("1: unsupported type bool"),
		},
		{
			name:                  "gtr expression unsupported type (bool)",
			expression:            `true > true`,
			expectedBuildingError: errors.New("1: unsupported type bool"),
		},
		{
			name:                  "geq expression unsupported type (bool)",
			expression:            `true >= true`,
			expectedBuildingError: errors.New("1: unsupported type bool"),
		},
		{
			name:                  "ltr expression unsupported type (bool)",
			expression:            `true < true`,
			expectedBuildingError: errors.New("1: unsupported type bool"),
		},
		{
			name:                  "leq expression unsupported type (bool)",
			expression:            `true <= true`,
			expectedBuildingError: errors.New("1: unsupported type bool"),
		},
		{
			name:                  "rem expression unsupported type (bool)",
			expression:            `true % true`,
			expectedBuildingError: errors.New("1: unsupported type bool"),
		},
		{
			name:                  "LAND expression unsupported type (int)",
			expression:            `5 && 1`,
			expectedBuildingError: errors.New("1: unsupported type int"),
		},
		{
			name:                  "LOR expression unsupported type (int)",
			expression:            `5 || 1`,
			expectedBuildingError: errors.New("1: unsupported type int"),
		},
		{
			name:          "simple variable addition",
			expression:    "5 + x",
			expectedValue: reflect.ValueOf(7),
			parsingContext: map[string]interface{}{
				"x": goel.IntType,
			},
			executionContext: map[string]interface{}{
				"x": reflect.ValueOf(2),
			},
		},
		{
			name:          "simple variable subtraction",
			expression:    "y - x",
			expectedValue: reflect.ValueOf(3),
			parsingContext: map[string]interface{}{
				"x": goel.IntType,
				"y": goel.IntType,
			},
			executionContext: map[string]interface{}{
				"x": reflect.ValueOf(2),
				"y": reflect.ValueOf(5),
			},
		},
		{
			name:          "function invocation",
			expression:    `matches("[0-9]{3}", x)`,
			expectedValue: reflect.ValueOf(true),
			parsingContext: map[string]interface{}{
				"x":       goel.StringType,
				"matches": reflect.TypeOf(matchesRegex),
			},
			executionContext: map[string]interface{}{
				"x":       reflect.ValueOf("321"),
				"matches": reflect.ValueOf(matchesRegex),
			},
		},
		{
			name:          "function invocation w/multiple return values",
			expression:    `returnsMultiple(false)`,
			expectedValue: reflect.ValueOf([]interface{}{5, 3.5}),
			parsingContext: map[string]interface{}{
				"x":               goel.StringType,
				"returnsMultiple": reflect.TypeOf(returnsMultiple),
			},
			executionContext: map[string]interface{}{
				"x":               reflect.ValueOf("321"),
				"returnsMultiple": reflect.ValueOf(returnsMultiple),
			},
		},
		{
			name:                   "function invocation w/multiple return values returns error",
			expression:             `returnsMultiple(true)`,
			expectedExecutionError: errors.New("Boo!"),
			parsingContext: map[string]interface{}{
				"x":               goel.StringType,
				"returnsMultiple": reflect.TypeOf(returnsMultiple),
			},
			executionContext: map[string]interface{}{
				"x":               reflect.ValueOf("321"),
				"returnsMultiple": reflect.ValueOf(returnsMultiple),
			},
		},
		{
			name:                  "function invocation too few arguments",
			expression:            `matches("[0-9]{3}")`,
			expectedBuildingError: errors.New("19: too few parameters to function call, expected 2, found 1"),
			parsingContext: map[string]interface{}{
				"matches": reflect.TypeOf(matchesRegex),
			},
			executionContext: map[string]interface{}{
				"matches": reflect.ValueOf(matchesRegex),
			},
		},
		{
			name:                  "function invocation too many arguments",
			expression:            `matches("[0-9]{3}", x, 15)`,
			expectedBuildingError: errors.New("26: too many parameters to function call, expected 2, found 3"),
			parsingContext: map[string]interface{}{
				"x":       goel.StringType,
				"matches": reflect.TypeOf(matchesRegex),
			},
			executionContext: map[string]interface{}{
				"x":       reflect.ValueOf("321"),
				"matches": reflect.ValueOf(matchesRegex),
			},
		},
		{
			name:                  "function invocation argument invalid",
			expression:            `matches("[0-9]{3}", "foo" * "bar")`,
			expectedBuildingError: errors.New("21: unsupported type string"),
			parsingContext: map[string]interface{}{
				"x":       goel.StringType,
				"matches": reflect.TypeOf(matchesRegex),
			},
			executionContext: map[string]interface{}{
				"x":       reflect.ValueOf("321"),
				"matches": reflect.ValueOf(matchesRegex),
			},
		},
		{
			name:                  "function invocation not defined",
			expression:            `matches("[a-z]{3}", x)`,
			expectedBuildingError: errors.New("1: unknown identifier: matches"),
			parsingContext: map[string]interface{}{
				"x": goel.StringType,
			},
			executionContext: map[string]interface{}{
				"x": reflect.ValueOf("321"),
			},
		},
		{
			name:                   "function invocation not in execution context",
			expression:             `matches("[a-z]{3}", x)`,
			expectedExecutionError: errors.New("1: undefined identifier: matches"),
			parsingContext: map[string]interface{}{
				"x":       goel.StringType,
				"matches": reflect.TypeOf(matchesRegex),
			},
			executionContext: map[string]interface{}{
				"x": reflect.ValueOf("321"),
			},
		},
		{
			name:                   "second order function invocation returns nil",
			expression:             `matches()("[a-z]{3}", x)`,
			expectedExecutionError: errors.New("1: not a function"),
			parsingContext: map[string]interface{}{
				"x":       goel.StringType,
				"matches": reflect.TypeOf(returnsNilFunction),
			},
			executionContext: map[string]interface{}{
				"x":       reflect.ValueOf("321"),
				"matches": reflect.ValueOf(returnsNilFunction),
			},
		},
		{
			name:                   "function invocation returns error",
			expression:             `returnsError()`,
			expectedExecutionError: errors.New("Boo!"),
			parsingContext: map[string]interface{}{
				"returnsError": reflect.TypeOf(returnsError),
			},
			executionContext: map[string]interface{}{
				"returnsError": reflect.ValueOf(returnsError),
			},
		},
		{
			name:                  "member access, selectee not found",
			expression:            "req.method",
			expectedBuildingError: errors.New("1: unknown identifier: req"),
		},
		{
			name:          "pointer to struct member access",
			expression:    "req.Method",
			expectedValue: reflect.ValueOf("GET"),
			parsingContext: map[string]interface{}{
				"req": reflect.TypeOf(testRequest),
			},
			executionContext: map[string]interface{}{
				"req": reflect.ValueOf(testRequest),
			},
		},
		{
			name:          "pointer to struct member Call",
			expression:    `req.Header.Get("Content-Type")`,
			expectedValue: reflect.ValueOf("application/json"),
			parsingContext: map[string]interface{}{
				"req": reflect.TypeOf(testRequest),
			},
			executionContext: map[string]interface{}{
				"req": reflect.ValueOf(testRequest),
			},
		},
		{
			name:          "struct member access",
			expression:    "req.Method",
			expectedValue: reflect.ValueOf("GET"),
			parsingContext: map[string]interface{}{
				"req": reflect.TypeOf(*testRequest),
			},
			executionContext: map[string]interface{}{
				"req": reflect.ValueOf(*testRequest),
			},
		},
		{
			name:          "struct member Call",
			expression:    `req.Header.Get("Content-Type")`,
			expectedValue: reflect.ValueOf("application/json"),
			parsingContext: map[string]interface{}{
				"req": reflect.TypeOf(*testRequest),
			},
			executionContext: map[string]interface{}{
				"req": reflect.ValueOf(*testRequest),
			},
		},
		{
			name:          "pointer to struct member Call comparison",
			expression:    `req.Header.Get("Content-Type") == "application/json"`,
			expectedValue: reflect.ValueOf(true),
			parsingContext: map[string]interface{}{
				"req": reflect.TypeOf(testRequest),
			},
			executionContext: map[string]interface{}{
				"req": reflect.ValueOf(testRequest),
			},
		},
		{
			name:          "Can access non interface methods even on interface typed variable GetName",
			expression:    `ts.GetName()`,
			expectedValue: reflect.ValueOf("Joe"),
			parsingContext: map[string]interface{}{
				"ts": reflect.TypeOf(ng),
			},
			executionContext: map[string]interface{}{
				"ts": reflect.ValueOf(ng),
			},
		},
		{
			name:          "Can access non interface methods even on interface typed variable SetName",
			expression:    `ts.SetName("Jill")`,
			expectedValue: reflect.ValueOf("Joe"),
			parsingContext: map[string]interface{}{
				"ts": reflect.TypeOf(&ts),
			},
			executionContext: map[string]interface{}{
				"ts": reflect.ValueOf(&testStruct{0, 0, "Joe"}),
			},
		},
		{
			name:          "Restrict access to interface only (success)",
			expression:    `ts.GetName()`,
			expectedValue: reflect.ValueOf("Joe"),
			parsingContext: map[string]interface{}{
				"ts": ngType,
			},
			executionContext: map[string]interface{}{
				"ts": reflect.ValueOf(ng),
			},
		},
		{
			name:                  "Restrict access to interface only (fail)",
			expression:            `ts.SetName("Jill")`,
			expectedBuildingError: errors.New("4: unknown selector SetName for goel_test.NameGetter"),
			parsingContext: map[string]interface{}{
				"ts": ngType,
			},
			executionContext: map[string]interface{}{
				"ts": reflect.ValueOf(ng),
			},
		},
		{
			name:                 "simple invalid literal",
			expression:           "5x",
			expectedParsingError: errors.Errorf("1:2: expected 'EOF', found x"),
		},
		{
			name:                  "simple invalid identifier",
			expression:            "x",
			expectedBuildingError: errors.Errorf("1: unknown identifier: x"),
		},
		{
			name:                  "simple invalid selector",
			expression:            "x.Foo",
			expectedBuildingError: errors.Errorf("3: unknown selector Foo for int"),
			parsingContext: map[string]interface{}{
				"x": goel.IntType,
			},
			executionContext: map[string]interface{}{
				"x": reflect.ValueOf(2),
			},
		},
		{
			name:                   "type assertion failure",
			expression:             "x.(string)",
			expectedExecutionError: errors.Errorf("4: int is not assignable to string."),
			parsingContext: map[string]interface{}{
				"x": goel.IntType,
			},
			executionContext: map[string]interface{}{
				"x": reflect.ValueOf(2),
			},
		},
		{
			name:          "type assertion success",
			expression:    "bar().(string)",
			expectedValue: reflect.ValueOf("bar"),
			parsingContext: map[string]interface{}{
				"bar": reflect.TypeOf(bar),
			},
			executionContext: map[string]interface{}{
				"bar": reflect.ValueOf(bar),
			},
		},
		{
			name:          "type assertion success custom type",
			expression:    "request().(Request).Header.Get(\"Content-Type\")",
			expectedValue: reflect.ValueOf("application/json"),
			parsingContext: map[string]interface{}{
				"request": reflect.TypeOf(returnsRequestAsInterface),
				"Request": reflect.TypeOf(testRequest),
			},
			executionContext: map[string]interface{}{
				"request": reflect.ValueOf(returnsRequestAsInterface),
				"Request": reflect.TypeOf(testRequest),
			},
		},
		{
			name:       "type mismatch in call",
			expression: `matches("[0-9]{3}", x)`,
			parsingContext: map[string]interface{}{
				"x":       goel.IntType,
				"matches": reflect.TypeOf(matchesRegex),
			},
			executionContext: map[string]interface{}{
				"x":       reflect.ValueOf(321),
				"matches": reflect.ValueOf(matchesRegex),
			},
			expectedBuildingError: errors.Errorf("21: type mismatch in argument 1"),
		},
		{
			name:                  "type mismatch in operator",
			expression:            `req.Header.Get("Content-Type") == 37`,
			expectedBuildingError: errors.Errorf("32: type mismatch in binary expression"),
			parsingContext: map[string]interface{}{
				"req": reflect.TypeOf(testRequest),
			},
			executionContext: map[string]interface{}{
				"req": reflect.ValueOf(testRequest),
			},
		},
		{
			name:                  "unknown expression (type conversion)",
			expression:            "float64(x)",
			expectedBuildingError: errors.Errorf("1: unknown identifier: float64"),
			parsingContext: map[string]interface{}{
				"x": goel.IntType,
			},
			executionContext: map[string]interface{}{
				"x": reflect.ValueOf(2),
			},
		},
		{
			name:                  "unknown expression (function literal)",
			expression:            "func(i int)(x)",
			expectedBuildingError: errors.Errorf("1: unknown expression type"),
			parsingContext: map[string]interface{}{
				"x": goel.IntType,
			},
			executionContext: map[string]interface{}{
				"x": reflect.ValueOf(2),
			},
		},
		{
			name:          "unknown expression (inner expression)",
			expression:    "a[0]",
			expectedValue: reflect.ValueOf(5),
			parsingContext: map[string]interface{}{
				"a": reflect.TypeOf([]int{}),
			},
			executionContext: map[string]interface{}{
				"a": reflect.ValueOf([]int{5}),
			},
		},
		{
			name:          "slice expression on string",
			expression:    "a[0:1]",
			expectedValue: reflect.ValueOf("a"),
			parsingContext: map[string]interface{}{
				"a": goel.StringType,
			},
			executionContext: map[string]interface{}{
				"a": reflect.ValueOf("abcdef"),
			},
		},
		{
			name:                  "slice3 expression on string not allowed",
			expression:            "a[0:1:2]",
			expectedBuildingError: errors.New("1: type mismatch expected a slice but found string"),
			parsingContext: map[string]interface{}{
				"a": goel.StringType,
			},
			executionContext: map[string]interface{}{
				"a": reflect.ValueOf("abcdef"),
			},
		},
		{
			name:                  "slice expression on array",
			expression:            "a[0:1]",
			expectedBuildingError: errors.New("1: type mismatch expected a slice or string but found [6]int"),
			parsingContext: map[string]interface{}{
				"a": reflect.TypeOf(&testArray).Elem(),
			},
			executionContext: map[string]interface{}{
				"a": reflect.ValueOf(&testArray).Elem(),
			},
		},
		{
			name:          "slice expression",
			expression:    "a[0:1]",
			expectedValue: reflect.ValueOf([]int{1}),
			parsingContext: map[string]interface{}{
				"a": reflect.TypeOf([]int{}),
			},
			executionContext: map[string]interface{}{
				"a": reflect.ValueOf([]int{1, 2, 4, 8, 16, 32}),
			},
		},
		{
			name:          "slice expression no lower",
			expression:    "a[:1]",
			expectedValue: reflect.ValueOf([]int{1}),
			parsingContext: map[string]interface{}{
				"a": reflect.TypeOf([]int{}),
			},
			executionContext: map[string]interface{}{
				"a": reflect.ValueOf([]int{1, 2, 4, 8, 16, 32}),
			},
		},
		{
			name:          "slice expression no high",
			expression:    "a[5:]",
			expectedValue: reflect.ValueOf([]int{32}),
			parsingContext: map[string]interface{}{
				"a": reflect.TypeOf([]int{}),
			},
			executionContext: map[string]interface{}{
				"a": reflect.ValueOf([]int{1, 2, 4, 8, 16, 32}),
			},
		},
		{
			name:          "slice expression w/max",
			expression:    "a[0:2:3]",
			expectedValue: reflect.ValueOf([]int{1, 2}),
			parsingContext: map[string]interface{}{
				"a": reflect.TypeOf([]int{}),
			},
			executionContext: map[string]interface{}{
				"a": reflect.ValueOf([]int{1, 2, 4, 8, 16, 32}),
			},
		},
		{
			name:                 "invalid slice expression no max",
			expression:           "a[0:2:]",
			expectedParsingError: errors.New("1:6: 3rd index required in 3-index slice"),
			parsingContext: map[string]interface{}{
				"a": reflect.TypeOf([]int{}),
			},
			executionContext: map[string]interface{}{
				"a": reflect.ValueOf([]int{1, 2, 4, 8, 16, 32}),
			},
		},
		{
			name:                   "invalid slice expression low greater than high",
			expression:             "a[3:2]",
			expectedExecutionError: errors.New("5: index out of range: 2"),
			parsingContext: map[string]interface{}{
				"a": reflect.TypeOf([]int{}),
			},
			executionContext: map[string]interface{}{
				"a": reflect.ValueOf([]int{1, 2, 4, 8, 16, 32}),
			},
		},
		{
			name:                   "invalid slice expression max less than than high",
			expression:             "a[2:3:1]",
			expectedExecutionError: errors.New("7: index out of range: 1"),
			parsingContext: map[string]interface{}{
				"a": reflect.TypeOf([]int{}),
			},
			executionContext: map[string]interface{}{
				"a": reflect.ValueOf([]int{1, 2, 4, 8, 16, 32}),
			},
		},
		{
			name:                   "invalid slice expression low greater than or equal to length",
			expression:             "a[6:]",
			expectedExecutionError: errors.New("3: index out of range: 6"),
			parsingContext: map[string]interface{}{
				"a": reflect.TypeOf([]int{}),
			},
			executionContext: map[string]interface{}{
				"a": reflect.ValueOf([]int{1, 2, 4, 8, 16, 32}),
			},
		},
		{
			name:                   "invalid slice expression high greater than or equal to length",
			expression:             "a[:7]",
			expectedExecutionError: errors.New("4: index out of range: 7"),
			parsingContext: map[string]interface{}{
				"a": reflect.TypeOf([]int{}),
			},
			executionContext: map[string]interface{}{
				"a": reflect.ValueOf([]int{1, 2, 4, 8, 16, 32}),
			},
		},
		{
			name:                   "invalid slice expression max greater than to capacity",
			expression:             "a[:6:7]",
			expectedExecutionError: errors.New("6: index out of range: 7"),
			parsingContext: map[string]interface{}{
				"a": reflect.TypeOf([]int{}),
			},
			executionContext: map[string]interface{}{
				"a": reflect.ValueOf([]int{1, 2, 4, 8, 16, 32}),
			},
		},
		{
			name:          "map expression not nil",
			expression:    `m["foo"]`,
			expectedValue: reflect.ValueOf(5),
			parsingContext: map[string]interface{}{
				"m": reflect.TypeOf(map[string]int{}),
			},
			executionContext: map[string]interface{}{
				"m": reflect.ValueOf(map[string]int{"foo": 5, "bar": 6}),
			},
		},
		{
			name:          "map expression nil",
			expression:    `m["snafu"]`,
			expectedValue: reflect.ValueOf(0),
			parsingContext: map[string]interface{}{
				"m": reflect.TypeOf(map[string]int{}),
			},
			executionContext: map[string]interface{}{
				"m": reflect.ValueOf(map[string]int{"foo": 5, "bar": 6}),
			},
		},
		{
			name:                  "unknown expression (variadic function call)",
			expression:            "f(1,2,3)",
			expectedBuildingError: errors.Errorf("2: variadic functions are not supported."),
			parsingContext: map[string]interface{}{
				"f": reflect.TypeOf(variadicSum),
			},
			executionContext: map[string]interface{}{
				"f": reflect.ValueOf(variadicSum),
			},
		},
	}
}

func returnsNilFunction() func(regex, str string) bool {
	return nil
}

func variadicSum(values ...int) int {
	sum := 0
	for _, v := range values {
		sum += v
	}
	return sum
}

func returnsRequestAsInterface() interface{} {
	return testRequest
}

func returnsError() (int, error) {
	return 0, errors.New("Boo!")
}

func matchesRegex(regex, x string) (bool, error) {
	return regexp.MatchString(regex, x)
}

func returnsMultiple(returnError bool) (int, float64, error) {
	if returnError {
		return 0, 0, errors.New("Boo!")
	} else {
		return 5, 3.5, nil
	}
}
Output:

8
8
8

Jump to

Keyboard shortcuts

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