Patcher and interfaces for custom types to be used in expressions as standard go types (#487)

* A bultin patcher providing interfaces for custom types that can be represented as standard go types for expressions.

* Move node creation inside of loop.

* Reorder imports. Fix some formatting.

* Rename expr function from 'getExprValue' to '$patcher_value_getter'.

* Move to expr-lang

* Rename Patcher to ValueGetter

* Remove use of Expr in naming

* Rename interface functions to use 'AsType()' convention

* Document motivation and an example use case

* Add example usage to AsAny()

* Move under patcher directory

* Fix reference to old import path
This commit is contained in:
Ryan Bullock
2023-12-26 16:01:55 +01:00
committed by GitHub
parent 15a98164a2
commit 3bda06349a
4 changed files with 514 additions and 0 deletions
+73
View File
@@ -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))
}
+227
View File
@@ -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),
)
+42
View File
@@ -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
}
+172
View File
@@ -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))
}