diff --git a/eval.go b/eval.go index 40573fbdf..8012de41a 100644 --- a/eval.go +++ b/eval.go @@ -282,6 +282,27 @@ func (n methodNode) eval(env interface{}) (interface{}, error) { return call(n.property.value, method, n.arguments, env) } +func (n builtinNode) eval(env interface{}) (interface{}, error) { + if len(n.arguments) == 0 { + return nil, fmt.Errorf("missing argument to %v", n.name) + } + if len(n.arguments) > 1 { + return nil, fmt.Errorf("too many arguments to %v: %v", n.name, n) + } + + a, err := Run(n.arguments[0], env) + if err != nil { + return nil, err + } + + switch n.name { + case "len": + return count(n.arguments[0], a) + } + + return nil, fmt.Errorf("unknown %q builtin", n.name) +} + func (n functionNode) eval(env interface{}) (interface{}, error) { fn, err := extract(env, n.name) if err != nil { diff --git a/eval_test.go b/eval_test.go index fc1ef8fee..73b4d492a 100644 --- a/eval_test.go +++ b/eval_test.go @@ -149,6 +149,16 @@ var evalTests = []evalTest{ map[string]interface{}{"foo": []rune{'a', 'b', 'c'}}, 'c', }, + { + "len(foo) == 3", + map[string]interface{}{"foo": []rune{'a', 'b', 'c'}}, + true, + }, + { + `len(foo) == 6`, + map[string]string{"foo": "foobar"}, + true, + }, { "[1, 2, 3][2/2]", nil, @@ -359,6 +369,21 @@ var evalErrorTests = []evalErrorTest{ nil, "operator in not defined on string", }, + { + "len(1)", + nil, + "invalid argument 1 (type float64) for len", + }, + { + "len(foo, bar)", + map[string]interface{}{"foo": nil, "bar": nil}, + "too many arguments to len: len(foo, bar)", + }, + { + "len()", + nil, + "missing argument to len", + }, } func TestEval(t *testing.T) { diff --git a/node.go b/node.go index e317ccf74..5abef3da2 100644 --- a/node.go +++ b/node.go @@ -1,6 +1,6 @@ package expr -// Node items of abstract syntax tree. +// Node represents items of abstract syntax tree. type Node interface{} type nilNode struct{} @@ -47,6 +47,11 @@ type methodNode struct { arguments []Node } +type builtinNode struct { + name string + arguments []Node +} + type functionNode struct { name string arguments []Node diff --git a/parser.go b/parser.go index 9524dd9ab..b3c72da5d 100644 --- a/parser.go +++ b/parser.go @@ -52,6 +52,10 @@ var binaryOperators = map[string]info{ "**": {200, right}, } +var builtins = map[string]bool{ + "len": true, +} + type parser struct { input string tokens []token @@ -284,16 +288,24 @@ func (p *parser) parsePrimaryExpression() (Node, error) { return nilNode{}, nil default: if p.current.is(punctuation, "(") { - if p.options.funcs != nil { - if _, ok := p.options.funcs[token.value]; !ok { - return nil, p.errorf("unknown func %v", token.value) + if _, ok := builtins[token.value]; ok { + arguments, err := p.parseArguments() + if err != nil { + return nil, err } + node = builtinNode{name: token.value, arguments: arguments} + } else { + if p.options.funcs != nil { + if _, ok := p.options.funcs[token.value]; !ok { + return nil, p.errorf("unknown func %v", token.value) + } + } + arguments, err := p.parseArguments() + if err != nil { + return nil, err + } + node = functionNode{name: token.value, arguments: arguments} } - arguments, err := p.parseArguments() - if err != nil { - return nil, err - } - node = functionNode{name: token.value, arguments: arguments} } else { if p.options.names != nil { if _, ok := p.options.names[token.value]; !ok { diff --git a/parser_test.go b/parser_test.go index 1c05e7976..b7f30f79a 100644 --- a/parser_test.go +++ b/parser_test.go @@ -121,6 +121,10 @@ var parseTests = []parseTest{ "{foo:1}.bar", propertyNode{mapNode{[]pairNode{{identifierNode{"foo"}, numberNode{1}}}}, identifierNode{"bar"}}, }, + { + "len(foo)", + builtinNode{"len", []Node{nameNode{"foo"}}}, + }, } type parseErrorTest struct { diff --git a/print.go b/print.go index 99c15da4d..992aac45b 100644 --- a/print.go +++ b/print.go @@ -63,6 +63,17 @@ func (n methodNode) String() string { return s + ")" } +func (n builtinNode) String() string { + s := fmt.Sprintf("%v(", n.name) + for i, a := range n.arguments { + if i != 0 { + s += ", " + } + s += fmt.Sprintf("%v", a) + } + return s + ")" +} + func (n functionNode) String() string { s := fmt.Sprintf("%v(", n.name) for i, a := range n.arguments { diff --git a/utils.go b/utils.go index b4bb8e05a..b5ea03f37 100644 --- a/utils.go +++ b/utils.go @@ -120,6 +120,21 @@ func contains(needle interface{}, array interface{}) (bool, error) { return false, nil } +func count(node Node, array interface{}) (float64, error) { + if array != nil { + value := reflect.ValueOf(array) + switch reflect.TypeOf(array).Kind() { + case reflect.Array, reflect.Slice: + return float64(value.Len()), nil + case reflect.String: + return float64(value.Len()), nil + } + return 0, fmt.Errorf("invalid argument %v (type %T) for len", node, array) + } + + return 0, nil +} + func call(name string, fn interface{}, arguments []Node, env interface{}) (interface{}, error) { in := make([]reflect.Value, 0) for _, arg := range arguments {