Add builtin functions

This commit is contained in:
Anton Medvedev
2023-08-14 15:37:06 +02:00
parent 29e49696f6
commit 719f30d3ad
10 changed files with 488 additions and 28 deletions
+323
View File
@@ -1,8 +1,14 @@
package builtin
import (
"encoding/base64"
"encoding/json"
"fmt"
"reflect"
"strings"
"time"
"github.com/antonmedv/expr/vm/runtime"
)
type Function struct {
@@ -97,4 +103,321 @@ var Functions = []*Function{
Builtin1: String,
Types: types(new(func(any interface{}) string)),
},
{
Name: "trim",
Func: func(args ...interface{}) (interface{}, error) {
if len(args) == 1 {
return strings.TrimSpace(args[0].(string)), nil
} else if len(args) == 2 {
return strings.Trim(args[0].(string), args[1].(string)), nil
} else {
return nil, fmt.Errorf("invalid number of arguments for trim (expected 1 or 2, got %d)", len(args))
}
},
Types: types(
strings.TrimSpace,
strings.Trim,
),
},
{
Name: "trimPrefix",
Func: func(args ...interface{}) (interface{}, error) {
return strings.TrimPrefix(args[0].(string), args[1].(string)), nil
},
Types: types(strings.TrimPrefix),
},
{
Name: "trimSuffix",
Func: func(args ...interface{}) (interface{}, error) {
return strings.TrimSuffix(args[0].(string), args[1].(string)), nil
},
Types: types(strings.TrimSuffix),
},
{
Name: "upper",
Builtin1: func(arg interface{}) interface{} {
return strings.ToUpper(arg.(string))
},
Types: types(strings.ToUpper),
},
{
Name: "lower",
Builtin1: func(arg interface{}) interface{} {
return strings.ToLower(arg.(string))
},
Types: types(strings.ToLower),
},
{
Name: "split",
Func: func(args ...interface{}) (interface{}, error) {
return strings.Split(args[0].(string), args[1].(string)), nil
},
Types: types(strings.Split),
},
{
Name: "splitN",
Func: func(args ...interface{}) (interface{}, error) {
return strings.SplitN(args[0].(string), args[1].(string), runtime.ToInt(args[2])), nil
},
Types: types(strings.SplitN),
},
{
Name: "splitAfter",
Func: func(args ...interface{}) (interface{}, error) {
return strings.SplitAfter(args[0].(string), args[1].(string)), nil
},
Types: types(strings.SplitAfter),
},
{
Name: "splitAfterN",
Func: func(args ...interface{}) (interface{}, error) {
return strings.SplitAfterN(args[0].(string), args[1].(string), runtime.ToInt(args[2])), nil
},
Types: types(strings.SplitAfterN),
},
{
Name: "replace",
Func: func(args ...interface{}) (interface{}, error) {
if len(args) == 4 {
return strings.Replace(args[0].(string), args[1].(string), args[2].(string), runtime.ToInt(args[3])), nil
} else if len(args) == 3 {
return strings.ReplaceAll(args[0].(string), args[1].(string), args[2].(string)), nil
} else {
return nil, fmt.Errorf("invalid number of arguments for replace (expected 3 or 4, got %d)", len(args))
}
},
Types: types(
strings.Replace,
strings.ReplaceAll,
),
},
{
Name: "repeat",
Func: func(args ...interface{}) (interface{}, error) {
return strings.Repeat(args[0].(string), runtime.ToInt(args[1])), nil
},
Types: types(strings.Repeat),
},
{
Name: "join",
Func: func(args ...interface{}) (interface{}, error) {
glue := ""
if len(args) == 2 {
glue = args[1].(string)
}
switch args[0].(type) {
case []string:
return strings.Join(args[0].([]string), glue), nil
case []interface{}:
var s []string
for _, arg := range args[0].([]interface{}) {
s = append(s, arg.(string))
}
return strings.Join(s, glue), nil
}
return nil, fmt.Errorf("invalid argument for join (type %s)", reflect.TypeOf(args[0]))
},
Types: types(
strings.Join,
new(func([]interface{}, string) string),
new(func([]string) string),
new(func([]interface{}) string),
),
},
{
Name: "indexOf",
Func: func(args ...interface{}) (interface{}, error) {
return strings.Index(args[0].(string), args[1].(string)), nil
},
Types: types(strings.Index),
},
{
Name: "lastIndexOf",
Func: func(args ...interface{}) (interface{}, error) {
return strings.LastIndex(args[0].(string), args[1].(string)), nil
},
Types: types(strings.LastIndex),
},
{
Name: "hasPrefix",
Func: func(args ...interface{}) (interface{}, error) {
return strings.HasPrefix(args[0].(string), args[1].(string)), nil
},
Types: types(strings.HasPrefix),
},
{
Name: "hasSuffix",
Func: func(args ...interface{}) (interface{}, error) {
return strings.HasSuffix(args[0].(string), args[1].(string)), nil
},
Types: types(strings.HasSuffix),
},
{
Name: "max",
Func: Max,
Validate: func(args []reflect.Type) (reflect.Type, error) {
if len(args) == 0 {
return anyType, fmt.Errorf("not enough arguments to call max")
}
for _, arg := range args {
switch kind(arg) {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
default:
return anyType, fmt.Errorf("invalid argument for max (type %s)", arg)
}
}
return args[0], nil
},
},
{
Name: "min",
Func: Min,
Validate: func(args []reflect.Type) (reflect.Type, error) {
if len(args) == 0 {
return anyType, fmt.Errorf("not enough arguments to call min")
}
for _, arg := range args {
switch kind(arg) {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
default:
return anyType, fmt.Errorf("invalid argument for min (type %s)", arg)
}
}
return args[0], nil
},
},
{
Name: "toJSON",
Func: func(args ...interface{}) (interface{}, error) {
b, err := json.MarshalIndent(args[0], "", " ")
if err != nil {
return nil, err
}
return string(b), nil
},
Types: types(new(func(interface{}) string)),
},
{
Name: "fromJSON",
Func: func(args ...interface{}) (interface{}, error) {
var v interface{}
err := json.Unmarshal([]byte(args[0].(string)), &v)
if err != nil {
return nil, err
}
return v, nil
},
Types: types(new(func(string) interface{})),
},
{
Name: "toBase64",
Func: func(args ...interface{}) (interface{}, error) {
return base64.StdEncoding.EncodeToString([]byte(args[0].(string))), nil
},
Types: types(new(func(string) string)),
},
{
Name: "fromBase64",
Func: func(args ...interface{}) (interface{}, error) {
b, err := base64.StdEncoding.DecodeString(args[0].(string))
if err != nil {
return nil, err
}
return string(b), nil
},
Types: types(new(func(string) string)),
},
{
Name: "now",
Func: func(args ...interface{}) (interface{}, error) {
return time.Now(), nil
},
Types: types(new(func() time.Time)),
},
{
Name: "duration",
Func: func(args ...interface{}) (interface{}, error) {
return time.ParseDuration(args[0].(string))
},
Types: types(time.ParseDuration),
},
{
Name: "date",
Func: func(args ...interface{}) (interface{}, error) {
date := args[0].(string)
if len(args) == 2 {
layout := args[1].(string)
return time.Parse(layout, date)
}
if len(args) == 3 {
layout := args[1].(string)
timeZone := args[2].(string)
tz, err := time.LoadLocation(timeZone)
if err != nil {
return nil, err
}
t, err := time.ParseInLocation(layout, date, tz)
if err != nil {
return nil, err
}
return t, nil
}
layouts := []string{
time.DateOnly,
time.TimeOnly,
time.DateTime,
time.RFC3339,
time.RFC822,
time.RFC850,
time.RFC1123,
}
for _, layout := range layouts {
t, err := time.Parse(layout, date)
if err == nil {
return t, nil
}
}
return nil, fmt.Errorf("invalid date %s", date)
},
Types: types(
new(func(string) time.Time),
new(func(string, string) time.Time),
new(func(string, string, string) time.Time),
),
},
{
Name: "first",
Func: func(args ...interface{}) (interface{}, error) {
return runtime.Fetch(args[0], 0), nil
},
Types: types(new(func([]interface{}) interface{})),
},
{
Name: "last",
Func: func(args ...interface{}) (interface{}, error) {
return runtime.Fetch(args[0], -1), nil
},
},
{
Name: "get",
Func: func(args ...interface{}) (out interface{}, err error) {
defer func() {
if r := recover(); r != nil {
return
}
}()
return runtime.Fetch(args[0], args[1]), nil
},
Validate: func(args []reflect.Type) (reflect.Type, error) {
if len(args) != 2 {
return anyType, fmt.Errorf("invalid number of arguments for get (expected 2, got %d)", len(args))
}
switch kind(args[0]) {
case reflect.Map, reflect.Struct, reflect.Slice, reflect.Array, reflect.Interface:
return anyType, nil
}
return anyType, fmt.Errorf("cannot get %s from %s", args[1], args[0])
},
},
}
+62 -1
View File
@@ -2,6 +2,7 @@ package builtin_test
import (
"testing"
"time"
"github.com/antonmedv/expr"
"github.com/stretchr/testify/assert"
@@ -26,12 +27,63 @@ var tests = []struct {
{`string(5)`, "5"},
{`string(5.5)`, "5.5"},
{`string("5.5")`, "5.5"},
{`trim(" foo ")`, "foo"},
{`trim("__foo___", "_")`, "foo"},
{`trimPrefix("prefix_foo", "prefix_")`, "foo"},
{`trimSuffix("foo_suffix", "_suffix")`, "foo"},
{`upper("foo")`, "FOO"},
{`lower("FOO")`, "foo"},
{`split("foo,bar,baz", ",")`, []string{"foo", "bar", "baz"}},
{`splitN("foo,bar,baz", ",", 2)`, []string{"foo", "bar,baz"}},
{`splitAfter("foo,bar,baz", ",")`, []string{"foo,", "bar,", "baz"}},
{`splitAfterN("foo,bar,baz", ",", 2)`, []string{"foo,", "bar,baz"}},
{`replace("foo,bar,baz", ",", ";")`, "foo;bar;baz"},
{`replace("foo,bar,baz,goo", ",", ";", 2)`, "foo;bar;baz,goo"},
{`repeat("foo", 3)`, "foofoofoo"},
{`join(ArrayOfString, ",")`, "foo,bar,baz"},
{`join(ArrayOfString)`, "foobarbaz"},
{`join(["foo", "bar", "baz"], ",")`, "foo,bar,baz"},
{`join(["foo", "bar", "baz"])`, "foobarbaz"},
{`indexOf("foo,bar,baz", ",")`, 3},
{`lastIndexOf("foo,bar,baz", ",")`, 7},
{`hasPrefix("foo,bar,baz", "foo")`, true},
{`hasSuffix("foo,bar,baz", "baz")`, true},
{`max(1, 2, 3)`, 3},
{`max(1.5, 2.5, 3.5)`, 3.5},
{`min(1, 2, 3)`, 1},
{`min(1.5, 2.5, 3.5)`, 1.5},
{`toJSON({foo: 1, bar: 2})`, "{\n \"bar\": 2,\n \"foo\": 1\n}"},
{`fromJSON("[1, 2, 3]")`, []interface{}{1.0, 2.0, 3.0}},
{`toBase64("hello")`, "aGVsbG8="},
{`fromBase64("aGVsbG8=")`, "hello"},
{`now().Format("2006-01-02T15:04:05Z")`, time.Now().Format("2006-01-02T15:04:05Z")},
{`duration("1h")`, time.Hour},
{`date("2006-01-02T15:04:05Z")`, time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC)},
{`date("2006.01.02", "2006.01.02")`, time.Date(2006, 1, 2, 0, 0, 0, 0, time.UTC)},
{`first(ArrayOfString)`, "foo"},
{`first(ArrayOfInt)`, 1},
{`first(ArrayOfAny)`, 1},
{`last(ArrayOfString)`, "baz"},
{`last(ArrayOfInt)`, 3},
{`last(ArrayOfAny)`, true},
{`get(ArrayOfString, 1)`, "bar"},
{`get(ArrayOfString, 99)`, nil},
{`get(ArrayOfInt, 1)`, 2},
{`get(ArrayOfInt, -1)`, 3},
{`get(ArrayOfAny, 1)`, "2"},
{`get({foo: 1, bar: 2}, "foo")`, 1},
{`get({foo: 1, bar: 2}, "unknown")`, nil},
}
func TestBuiltin(t *testing.T) {
env := map[string]interface{}{
"ArrayOfString": []string{"foo", "bar", "baz"},
"ArrayOfInt": []int{1, 2, 3},
"ArrayOfAny": []interface{}{1, "2", true},
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
out, err := expr.Eval(test.input, nil)
out, err := expr.Eval(test.input, env)
assert.NoError(t, err)
assert.Equal(t, test.want, out)
})
@@ -52,6 +104,15 @@ var errorTests = []struct {
{`float()`, `invalid number of arguments for float (expected 1, got 0)`},
{`float(1, 2)`, `invalid number of arguments for float (expected 1, got 2)`},
{`string(1, 2)`, `too many arguments to call string`},
{`trim()`, `not enough arguments to call trim`},
{`max()`, `not enough arguments to call max`},
{`max(1, "2")`, `invalid argument for max (type string)`},
{`min()`, `not enough arguments to call min`},
{`min(1, "2")`, `invalid argument for min (type string)`},
{`duration("error")`, `invalid duration`},
{`date("error")`, `invalid date`},
{`get()`, `invalid number of arguments for get (expected 2, got 0)`},
{`get(1, 2)`, `cannot get int from int`},
}
func TestBuiltinErrors(t *testing.T) {
+22
View File
@@ -4,6 +4,8 @@ import (
"fmt"
"reflect"
"strconv"
"github.com/antonmedv/expr/vm/runtime"
)
func Len(x interface{}) interface{} {
@@ -171,3 +173,23 @@ func Float(x interface{}) interface{} {
func String(arg interface{}) interface{} {
return fmt.Sprintf("%v", arg)
}
func Max(args ...interface{}) (interface{}, error) {
var max interface{}
for _, arg := range args {
if max == nil || runtime.Less(max, arg) {
max = arg
}
}
return max, nil
}
func Min(args ...interface{}) (interface{}, error) {
var min interface{}
for _, arg := range args {
if min == nil || runtime.More(min, arg) {
min = arg
}
}
return min, nil
}
+30 -19
View File
@@ -127,16 +127,34 @@ func (c *compiler) addConstant(constant interface{}) int {
return p
}
func (c *compiler) addFunction(node *ast.CallNode) int {
if node.Func == nil {
// emitFunction adds builtin.Function.Func to the program.Functions and emits call opcode.
func (c *compiler) emitFunction(fn *builtin.Function, argsLen int) {
switch argsLen {
case 0:
c.emit(OpCall0, c.addFunction(fn))
case 1:
c.emit(OpCall1, c.addFunction(fn))
case 2:
c.emit(OpCall2, c.addFunction(fn))
case 3:
c.emit(OpCall3, c.addFunction(fn))
default:
c.emit(OpLoadFunc, c.addFunction(fn))
c.emit(OpCallN, argsLen)
}
}
// addFunction adds builtin.Function.Func to the program.Functions and returns its index.
func (c *compiler) addFunction(fn *builtin.Function) int {
if fn == nil {
panic("function is nil")
}
if p, ok := c.functionsIndex[node.Func.Name]; ok {
if p, ok := c.functionsIndex[fn.Name]; ok {
return p
}
p := len(c.functions)
c.functions = append(c.functions, node.Func.Func)
c.functionsIndex[node.Func.Name] = p
c.functions = append(c.functions, fn.Func)
c.functionsIndex[fn.Name] = p
return p
}
@@ -564,19 +582,7 @@ func (c *compiler) CallNode(node *ast.CallNode) {
c.compile(arg)
}
if node.Func != nil {
switch len(node.Arguments) {
case 0:
c.emit(OpCall0, c.addFunction(node))
case 1:
c.emit(OpCall1, c.addFunction(node))
case 2:
c.emit(OpCall2, c.addFunction(node))
case 3:
c.emit(OpCall3, c.addFunction(node))
default:
c.emit(OpLoadFunc, c.addFunction(node))
c.emit(OpCallN, len(node.Arguments))
}
c.emitFunction(node.Func, len(node.Arguments))
return
}
c.compile(node.Callee)
@@ -691,10 +697,15 @@ func (c *compiler) BuiltinNode(node *ast.BuiltinNode) {
}
if id, ok := builtin.Index[node.Name]; ok {
f := builtin.Functions[id]
for _, arg := range node.Arguments {
c.compile(arg)
}
c.emit(OpCallBuiltin, id)
if f.Builtin1 != nil {
c.emit(OpCallBuiltin1, id)
} else if f.Func != nil {
c.emitFunction(f, len(node.Arguments))
}
return
}
+3 -3
View File
@@ -119,15 +119,15 @@ func TestOptimize_const_range(t *testing.T) {
}
func TestOptimize_const_expr(t *testing.T) {
tree, err := parser.Parse(`upper("hello")`)
tree, err := parser.Parse(`toUpper("hello")`)
require.NoError(t, err)
env := map[string]interface{}{
"upper": strings.ToUpper,
"toUpper": strings.ToUpper,
}
config := conf.New(env)
config.ConstExpr("upper")
config.ConstExpr("toUpper")
err = optimizer.Optimize(&tree.Node, config)
require.NoError(t, err)
+43
View File
@@ -5,6 +5,7 @@ import (
"testing"
"time"
"github.com/antonmedv/expr"
"github.com/antonmedv/expr/checker"
"github.com/antonmedv/expr/compiler"
"github.com/antonmedv/expr/conf"
@@ -100,3 +101,45 @@ func TestTime(t *testing.T) {
})
}
}
func TestTime_duration(t *testing.T) {
env := map[string]interface{}{
"date": time.Date(2000, time.Month(1), 1, 0, 0, 0, 0, time.UTC),
}
program, err := expr.Compile(`now() - duration("1h") < now() && date + duration("24h") < now()`, expr.Env(env))
require.NoError(t, err)
output, err := expr.Run(program, env)
require.NoError(t, err)
require.Equal(t, true, output)
}
func TestTime_date(t *testing.T) {
var tests = []struct {
input string
want time.Time
}{
{
`'2017-10-23' | date()`,
time.Date(2017, 10, 23, 0, 0, 0, 0, time.UTC),
},
{
`'24.11.1987 20:30' | date("02.01.2006 15:04", "Europe/Zurich")`,
time.Date(1987, 11, 24, 20, 30, 0, 0, time.FixedZone("Europe/Zurich", 3600)),
},
{
`'24.11.1987 20:30 MSK' | date("02.01.2006 15:04 MST", "Europe/Zurich")`,
time.Date(1987, 11, 24, 20, 30, 0, 0, time.FixedZone("MSK", 0)),
},
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
program, err := expr.Compile(test.input)
require.NoError(t, err)
output, err := expr.Run(program, nil)
require.NoError(t, err)
require.Truef(t, test.want.Equal(output.(time.Time)), "want %v, got %v", test.want, output)
})
}
}
+1 -1
View File
@@ -57,7 +57,7 @@ const (
OpCallN
OpCallFast
OpCallTyped
OpCallBuiltin
OpCallBuiltin1
OpArray
OpMap
OpLen
+2 -2
View File
@@ -232,8 +232,8 @@ func (program *Program) Disassemble() string {
signature := reflect.TypeOf(FuncTypes[arg]).Elem().String()
_, _ = fmt.Fprintf(w, "%v\t%v\t<%v>\t%v\n", pp, "OpCallTyped", arg, signature)
case OpCallBuiltin:
builtin("OpCallBuiltin")
case OpCallBuiltin1:
builtin("OpCallBuiltin1")
case OpArray:
code("OpArray")
+1 -1
View File
@@ -264,7 +264,7 @@ func In(needle interface{}, array interface{}) bool {
panic(fmt.Sprintf(`operator "in"" not defined on %T`, array))
}
func Len(a interface{}) interface{} {
func Len(a interface{}) int {
v := reflect.ValueOf(a)
switch v.Kind() {
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
+1 -1
View File
@@ -381,7 +381,7 @@ func (vm *VM) Run(program *Program, env interface{}) (_ interface{}, err error)
out := vm.call(fn, arg)
vm.push(out)
case OpCallBuiltin:
case OpCallBuiltin1:
vm.push(builtin.Functions[arg].Builtin1(vm.pop()))
case OpArray: