diff --git a/patcher/value/bench_test.go b/patcher/value/bench_test.go new file mode 100644 index 00000000..d6a1aae8 --- /dev/null +++ b/patcher/value/bench_test.go @@ -0,0 +1,73 @@ +package value + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/expr-lang/expr" + "github.com/expr-lang/expr/vm" +) + +func Benchmark_valueAdd(b *testing.B) { + env := make(map[string]any) + env["ValueOne"] = &customInt{1} + env["ValueTwo"] = &customInt{2} + + program, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), ValueGetter) + require.NoError(b, err) + + var out any + v := vm.VM{} + + b.ResetTimer() + for n := 0; n < b.N; n++ { + out, err = v.Run(program, env) + } + b.StopTimer() + + require.NoError(b, err) + require.Equal(b, 3, out.(int)) +} + +func Benchmark_valueUntypedAdd(b *testing.B) { + env := make(map[string]any) + env["ValueOne"] = &customUntypedInt{1} + env["ValueTwo"] = &customUntypedInt{2} + + program, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), ValueGetter) + require.NoError(b, err) + + var out any + v := vm.VM{} + + b.ResetTimer() + for n := 0; n < b.N; n++ { + out, err = v.Run(program, env) + } + b.StopTimer() + + require.NoError(b, err) + require.Equal(b, 3, out.(int)) +} + +func Benchmark_valueTypedAdd(b *testing.B) { + env := make(map[string]any) + env["ValueOne"] = &customTypedInt{1} + env["ValueTwo"] = &customTypedInt{2} + + program, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), ValueGetter) + require.NoError(b, err) + + var out any + v := vm.VM{} + + b.ResetTimer() + for n := 0; n < b.N; n++ { + out, err = v.Run(program, env) + } + b.StopTimer() + + require.NoError(b, err) + require.Equal(b, 3, out.(int)) +} diff --git a/patcher/value/value.go b/patcher/value/value.go new file mode 100644 index 00000000..b7e4a767 --- /dev/null +++ b/patcher/value/value.go @@ -0,0 +1,227 @@ +// Package value provides a Patcher that uses interfaces to allow custom types that can be represented as standard go values to be used more easily in expressions. +package value + +import ( + "reflect" + "time" + + "github.com/expr-lang/expr" + "github.com/expr-lang/expr/ast" + "github.com/expr-lang/expr/conf" +) + +// ValueGetter is a Patcher that allows custom types to be represented as standard go values for use with expr. +// It also adds the `$patcher_value_getter` function to the program for efficiently calling matching interfaces. +// +// The purpose of this Patcher is to make it seemless to use custom types in expressions without the need to +// first convert them to standard go values. It may also facilitate using already existing structs or maps as +// environments when they contain compatabile types. +// +// An example usage may be modeling a database record with columns that have varying data types and constraints. +// In such an example you may have custom types that, beyond storing a simple value, such as an integer, may +// contain metadata such as column type and if a value is specifically a NULL value. +// +// Use it directly as an Option to expr.Compile() +var ValueGetter = func() expr.Option { + vPatcher := patcher{} + return func(c *conf.Config) { + c.Visitors = append(c.Visitors, vPatcher) + vPatcher.ApplyOptions(c) + } +}() + +// A AnyValuer provides a generic function for a custom type to return standard go values. +// It allows for returning a `nil` value but does not provide any type checking at expression compile. +// +// A custom type may implement both AnyValuer and a type specific interface to enable both +// compile time checking and the ability to return a `nil` value. +type AnyValuer interface { + AsAny() any +} + +type IntValuer interface { + AsInt() int +} + +type BoolValuer interface { + AsBool() bool +} + +type Int8Valuer interface { + AsInt8() int8 +} + +type Int16Valuer interface { + AsInt16() int16 +} + +type Int32Valuer interface { + AsInt32() int32 +} + +type Int64Valuer interface { + AsInt64() int64 +} + +type UintValuer interface { + AsUint() uint +} + +type Uint8Valuer interface { + AsUint8() uint8 +} + +type Uint16Valuer interface { + AsUint16() uint16 +} + +type Uint32Valuer interface { + AsUint32() uint32 +} + +type Uint64Valuer interface { + AsUint64() uint64 +} + +type Float32Valuer interface { + AsFloat32() float32 +} + +type Float64Valuer interface { + AsFloat64() float64 +} + +type StringValuer interface { + AsString() string +} + +type TimeValuer interface { + AsTime() time.Time +} + +type DurationValuer interface { + AsDuration() time.Duration +} + +type ArrayValuer interface { + AsArray() []any +} + +type MapValuer interface { + AsMap() map[string]any +} + +var supportedInterfaces = []reflect.Type{ + reflect.TypeOf((*AnyValuer)(nil)).Elem(), + reflect.TypeOf((*BoolValuer)(nil)).Elem(), + reflect.TypeOf((*IntValuer)(nil)).Elem(), + reflect.TypeOf((*Int8Valuer)(nil)).Elem(), + reflect.TypeOf((*Int16Valuer)(nil)).Elem(), + reflect.TypeOf((*Int32Valuer)(nil)).Elem(), + reflect.TypeOf((*Int64Valuer)(nil)).Elem(), + reflect.TypeOf((*UintValuer)(nil)).Elem(), + reflect.TypeOf((*Uint8Valuer)(nil)).Elem(), + reflect.TypeOf((*Uint16Valuer)(nil)).Elem(), + reflect.TypeOf((*Uint32Valuer)(nil)).Elem(), + reflect.TypeOf((*Uint64Valuer)(nil)).Elem(), + reflect.TypeOf((*Float32Valuer)(nil)).Elem(), + reflect.TypeOf((*Float64Valuer)(nil)).Elem(), + reflect.TypeOf((*StringValuer)(nil)).Elem(), + reflect.TypeOf((*TimeValuer)(nil)).Elem(), + reflect.TypeOf((*DurationValuer)(nil)).Elem(), + reflect.TypeOf((*ArrayValuer)(nil)).Elem(), + reflect.TypeOf((*MapValuer)(nil)).Elem(), +} + +type patcher struct{} + +func (patcher) Visit(node *ast.Node) { + id, ok := (*node).(*ast.IdentifierNode) + if !ok { + return + } + + nodeType := id.Type() + + for _, t := range supportedInterfaces { + if nodeType.Implements(t) { + callnode := &ast.CallNode{ + Callee: &ast.IdentifierNode{Value: "$patcher_value_getter"}, + Arguments: []ast.Node{id}, + } + + ast.Patch(node, callnode) + } + } +} + +func (patcher) ApplyOptions(c *conf.Config) { + getValueFunc(c) +} + +func getValue(params ...any) (any, error) { + switch v := params[0].(type) { + case AnyValuer: + return v.AsAny(), nil + case BoolValuer: + return v.AsBool(), nil + case IntValuer: + return v.AsInt(), nil + case Int8Valuer: + return v.AsInt8(), nil + case Int16Valuer: + return v.AsInt16(), nil + case Int32Valuer: + return v.AsInt32(), nil + case Int64Valuer: + return v.AsInt64(), nil + case UintValuer: + return v.AsUint(), nil + case Uint8Valuer: + return v.AsUint8(), nil + case Uint16Valuer: + return v.AsUint16(), nil + case Uint32Valuer: + return v.AsUint32(), nil + case Uint64Valuer: + return v.AsUint64(), nil + case Float32Valuer: + return v.AsFloat32(), nil + case Float64Valuer: + return v.AsFloat64(), nil + case StringValuer: + return v.AsString(), nil + case TimeValuer: + return v.AsTime(), nil + case DurationValuer: + return v.AsDuration(), nil + case ArrayValuer: + return v.AsArray(), nil + case MapValuer: + return v.AsMap(), nil + } + + return params[0], nil +} + +var getValueFunc = expr.Function("$patcher_value_getter", getValue, + new(func(BoolValuer) bool), + new(func(IntValuer) int), + new(func(Int8Valuer) int8), + new(func(Int16Valuer) int16), + new(func(Int32Valuer) int32), + new(func(Int64Valuer) int64), + new(func(UintValuer) uint), + new(func(Uint8Valuer) uint8), + new(func(Uint16Valuer) uint16), + new(func(Uint32Valuer) uint32), + new(func(Uint64Valuer) uint64), + new(func(Float32Valuer) float32), + new(func(Float64Valuer) float64), + new(func(StringValuer) string), + new(func(TimeValuer) time.Time), + new(func(DurationValuer) time.Duration), + new(func(ArrayValuer) []any), + new(func(MapValuer) map[string]any), + new(func(any) any), +) diff --git a/patcher/value/value_example_test.go b/patcher/value/value_example_test.go new file mode 100644 index 00000000..2da1739b --- /dev/null +++ b/patcher/value/value_example_test.go @@ -0,0 +1,42 @@ +package value_test + +import ( + "fmt" + + "github.com/expr-lang/expr" + "github.com/expr-lang/expr/patcher/value" + "github.com/expr-lang/expr/vm" +) + +type myInt struct { + Int int +} + +func (v *myInt) AsInt() int { + return v.Int +} + +func (v *myInt) AsAny() any { + return v.Int +} + +func ExampleAnyValuer() { + env := make(map[string]any) + env["ValueOne"] = &myInt{1} + env["ValueTwo"] = &myInt{2} + + program, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), value.ValueGetter) + + if err != nil { + panic(err) + } + + out, err := vm.Run(program, env) + + if err != nil { + panic(err) + } + + fmt.Println(out) + // Output: 3 +} diff --git a/patcher/value/value_test.go b/patcher/value/value_test.go new file mode 100644 index 00000000..1b6c5527 --- /dev/null +++ b/patcher/value/value_test.go @@ -0,0 +1,172 @@ +package value + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/expr-lang/expr" + "github.com/expr-lang/expr/vm" +) + +type customInt struct { + Int int +} + +func (v *customInt) AsInt() int { + return v.Int +} + +func (v *customInt) AsAny() any { + return v.Int +} + +type customTypedInt struct { + Int int +} + +func (v *customTypedInt) AsInt() int { + return v.Int +} + +type customUntypedInt struct { + Int int +} + +func (v *customUntypedInt) AsAny() any { + return v.Int +} + +type customString struct { + String string +} + +func (v *customString) AsString() string { + return v.String +} + +func (v *customString) AsAny() any { + return v.String +} + +type customTypedString struct { + String string +} + +func (v *customTypedString) AsString() string { + return v.String +} + +type customUntypedString struct { + String string +} + +func (v *customUntypedString) AsAny() any { + return v.String +} + +type customTypedArray struct { + Array []any +} + +func (v *customTypedArray) AsArray() []any { + return v.Array +} + +type customTypedMap struct { + Map map[string]any +} + +func (v *customTypedMap) AsMap() map[string]any { + return v.Map +} + +func Test_valueAddInt(t *testing.T) { + env := make(map[string]any) + env["ValueOne"] = &customInt{1} + env["ValueTwo"] = &customInt{2} + + program, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), ValueGetter) + require.NoError(t, err) + + out, err := vm.Run(program, env) + + require.NoError(t, err) + require.Equal(t, 3, out.(int)) +} + +func Test_valueUntypedAddInt(t *testing.T) { + env := make(map[string]any) + env["ValueOne"] = &customUntypedInt{1} + env["ValueTwo"] = &customUntypedInt{2} + + program, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), ValueGetter) + require.NoError(t, err) + + out, err := vm.Run(program, env) + + require.NoError(t, err) + require.Equal(t, 3, out.(int)) +} + +func Test_valueTypedAddInt(t *testing.T) { + env := make(map[string]any) + env["ValueOne"] = &customTypedInt{1} + env["ValueTwo"] = &customTypedInt{2} + + program, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), ValueGetter) + require.NoError(t, err) + + out, err := vm.Run(program, env) + + require.NoError(t, err) + require.Equal(t, 3, out.(int)) +} + +func Test_valueTypedAddMismatch(t *testing.T) { + env := make(map[string]any) + env["ValueOne"] = &customTypedInt{1} + env["ValueTwo"] = &customTypedString{"test"} + + _, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), ValueGetter) + require.Error(t, err) +} + +func Test_valueUntypedAddMismatch(t *testing.T) { + env := make(map[string]any) + env["ValueOne"] = &customUntypedInt{1} + env["ValueTwo"] = &customUntypedString{"test"} + + program, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), ValueGetter) + require.NoError(t, err) + + _, err = vm.Run(program, env) + + require.Error(t, err) +} + +func Test_valueTypedArray(t *testing.T) { + env := make(map[string]any) + env["ValueOne"] = &customTypedArray{[]any{1, 2}} + + program, err := expr.Compile("ValueOne[0] + ValueOne[1]", expr.Env(env), ValueGetter) + require.NoError(t, err) + + out, err := vm.Run(program, env) + + require.NoError(t, err) + require.Equal(t, 3, out.(int)) +} + +func Test_valueTypedMap(t *testing.T) { + env := make(map[string]any) + env["ValueOne"] = &customTypedMap{map[string]any{"one": 1, "two": 2}} + + program, err := expr.Compile("ValueOne.one + ValueOne.two", expr.Env(env), ValueGetter) + require.NoError(t, err) + + out, err := vm.Run(program, env) + + require.NoError(t, err) + require.Equal(t, 3, out.(int)) +}