Skip to content

Commit

Permalink
allow arbitrary functions as language function
Browse files Browse the repository at this point in the history
  • Loading branch information
JanErik Keller committed Oct 12, 2017
1 parent b957afc commit e7b2bbc
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 9 deletions.
4 changes: 2 additions & 2 deletions evaluable.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func variable(path ...Evaluable) Evaluable {
}
}

func (*Parser) callFunc(fun Func, args ...Evaluable) Evaluable {
func (*Parser) callFunc(fun function, args ...Evaluable) Evaluable {
return func(c context.Context, v interface{}) (ret interface{}, err error) {
a := make([]interface{}, len(args))
for i, arg := range args {
Expand Down Expand Up @@ -199,7 +199,7 @@ func (*Parser) callEvaluable(fullname string, fun Evaluable, args ...Evaluable)

switch len(r) {
case 0:
return nil, fmt.Errorf("function %s does not return any values", fullname)
return err, nil
case 1:
return r[0], err
default:
Expand Down
2 changes: 1 addition & 1 deletion example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ func ExampleEvaluate_jsonpath() {
}

func ExampleLanguage() {
lang := gval.NewLanguage(gval.Json(), gval.Arithmetic(),
lang := gval.NewLanguage(gval.JSON(), gval.Arithmetic(),
//pipe operator
gval.PostfixOperator("|", func(p *gval.Parser, pre gval.Evaluable) (gval.Evaluable, error) {
post, err := p.ParseExpression()
Expand Down
66 changes: 66 additions & 0 deletions functions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package gval

import (
"fmt"
"reflect"
)

type function func(arguments ...interface{}) (interface{}, error)

func toFunc(f interface{}) function {
if f, ok := f.(func(arguments ...interface{}) (interface{}, error)); ok {
return function(f)
}
return func(args ...interface{}) (interface{}, error) {
fun := reflect.ValueOf(f)
t := fun.Type()

variadic := t.IsVariadic()
numIn := t.NumIn()

if (!variadic && len(args) != numIn) || (variadic && len(args) < numIn-1) {
return nil, fmt.Errorf("invalid number of parameters")
}

in := make([]reflect.Value, len(args))
var inType reflect.Type
for i, arg := range args {
if !variadic || i < numIn-1 {
inType = t.In(i)
} else if i == numIn-1 {
inType = t.In(numIn - 1).Elem()
}
argVal := reflect.ValueOf(arg)
if arg == nil || !argVal.Type().AssignableTo(inType) {
return nil, fmt.Errorf("expected type %s for parameter %d but got %T",
inType.String(), i, arg)
}
in[i] = argVal
}

out := fun.Call(in)

r := make([]interface{}, len(out))
for i, e := range out {
r[i] = e.Interface()
}

err := error(nil)
errorInterface := reflect.TypeOf((*error)(nil)).Elem()
if len(r) > 0 && fun.Type().Out(len(r)-1).Implements(errorInterface) {
if r[len(r)-1] != nil {
err = r[len(r)-1].(error)
}
r = r[0 : len(r)-1]
}

switch len(r) {
case 0:
return nil, err
case 1:
return r[0], err
default:
return r, err
}
}
}
136 changes: 136 additions & 0 deletions functions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package gval

import (
"fmt"
"reflect"
"testing"
)

func Test_toFunc(t *testing.T) {
myError := fmt.Errorf("my error")
tests := []struct {
name string
function interface{}
arguments []interface{}
want interface{}
wantErr error
wantAnyErr bool
}{
{
name: "empty",
function: func() {},
},
{
name: "one arg",
function: func(a interface{}) {
if a != true {
panic("fail")
}
},
arguments: []interface{}{true},
},
{
name: "three args",
function: func(a, b, c interface{}) {
if a != 1 || b != 2 || c != 3 {
panic("fail")
}
},
arguments: []interface{}{1, 2, 3},
},
{
name: "input types",
function: func(a int, b string, c bool) {
if a != 1 || b != "2" || !c {
panic("fail")
}
},
arguments: []interface{}{1, "2", true},
},
{
name: "wronge input type int",
function: func(a int, b string, c bool) {},
arguments: []interface{}{"1", "2", true},
wantAnyErr: true,
},
{
name: "wronge input type string",
function: func(a int, b string, c bool) {},
arguments: []interface{}{1, 2, true},
wantAnyErr: true,
},
{
name: "wronge input type bool",
function: func(a int, b string, c bool) {},
arguments: []interface{}{1, "2", "true"},
wantAnyErr: true,
},
{
name: "wronge input number",
function: func(a int, b string, c bool) {},
arguments: []interface{}{1, "2"},
wantAnyErr: true,
},
{
name: "one return",
function: func() bool {
return true
},
want: true,
},
{
name: "three returns",
function: func() (bool, string, int) {
return true, "2", 3
},
want: []interface{}{true, "2", 3},
},
{
name: "error",
function: func() error {
return myError
},
wantErr: myError,
},
{
name: "none error",
function: func() error {
return nil
},
},
{
name: "one return with error",
function: func() (bool, error) {
return false, myError
},
want: false,
wantErr: myError,
},
{
name: "three returns with error",
function: func() (bool, string, int, error) {
return false, "", 0, myError
},
want: []interface{}{false, "", 0},
wantErr: myError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := toFunc(tt.function)(tt.arguments...)

if tt.wantAnyErr {
if err != nil {
return
}
t.Fatalf("toFunc()(args...) = error(nil), but wantAnyErr")
}
if err != tt.wantErr {
t.Fatalf("toFunc()(args...) = error(%v), wantErr (%v)", err, tt.wantErr)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("toFunc()(args...) = %v, want %v", got, tt.want)
}
})
}
}
13 changes: 7 additions & 6 deletions language.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,12 @@ func (l Language) Evaluate(expression string, parameter interface{}) (interface{
return eval(context.Background(), parameter)
}

// Func can be called from within an expression.
type Func func(arguments ...interface{}) (interface{}, error)

// Function returns a Language with given constant
func Function(name string, function Func) 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 Function(name string, function interface{}) Language {
l := newLanguage()
l.prefixes[name] = func(p *Parser) (eval Evaluable, err error) {
args := []Evaluable{}
Expand All @@ -79,7 +80,7 @@ func Function(name string, function Func) Language {
default:
p.Camouflage("function call", '(')
}
return p.callFunc(function, args...), nil
return p.callFunc(toFunc(function), args...), nil
}
return l
}
Expand Down

0 comments on commit e7b2bbc

Please sign in to comment.