Skip to content

Commit 500553e

Browse files
authored
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
1 parent 8774c44 commit 500553e

File tree

4 files changed

+514
-0
lines changed

4 files changed

+514
-0
lines changed

patcher/value/bench_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package value
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
"github.com/expr-lang/expr"
9+
"github.com/expr-lang/expr/vm"
10+
)
11+
12+
func Benchmark_valueAdd(b *testing.B) {
13+
env := make(map[string]any)
14+
env["ValueOne"] = &customInt{1}
15+
env["ValueTwo"] = &customInt{2}
16+
17+
program, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), ValueGetter)
18+
require.NoError(b, err)
19+
20+
var out any
21+
v := vm.VM{}
22+
23+
b.ResetTimer()
24+
for n := 0; n < b.N; n++ {
25+
out, err = v.Run(program, env)
26+
}
27+
b.StopTimer()
28+
29+
require.NoError(b, err)
30+
require.Equal(b, 3, out.(int))
31+
}
32+
33+
func Benchmark_valueUntypedAdd(b *testing.B) {
34+
env := make(map[string]any)
35+
env["ValueOne"] = &customUntypedInt{1}
36+
env["ValueTwo"] = &customUntypedInt{2}
37+
38+
program, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), ValueGetter)
39+
require.NoError(b, err)
40+
41+
var out any
42+
v := vm.VM{}
43+
44+
b.ResetTimer()
45+
for n := 0; n < b.N; n++ {
46+
out, err = v.Run(program, env)
47+
}
48+
b.StopTimer()
49+
50+
require.NoError(b, err)
51+
require.Equal(b, 3, out.(int))
52+
}
53+
54+
func Benchmark_valueTypedAdd(b *testing.B) {
55+
env := make(map[string]any)
56+
env["ValueOne"] = &customTypedInt{1}
57+
env["ValueTwo"] = &customTypedInt{2}
58+
59+
program, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), ValueGetter)
60+
require.NoError(b, err)
61+
62+
var out any
63+
v := vm.VM{}
64+
65+
b.ResetTimer()
66+
for n := 0; n < b.N; n++ {
67+
out, err = v.Run(program, env)
68+
}
69+
b.StopTimer()
70+
71+
require.NoError(b, err)
72+
require.Equal(b, 3, out.(int))
73+
}

patcher/value/value.go

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
// 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.
2+
package value
3+
4+
import (
5+
"reflect"
6+
"time"
7+
8+
"github.com/expr-lang/expr"
9+
"github.com/expr-lang/expr/ast"
10+
"github.com/expr-lang/expr/conf"
11+
)
12+
13+
// ValueGetter is a Patcher that allows custom types to be represented as standard go values for use with expr.
14+
// It also adds the `$patcher_value_getter` function to the program for efficiently calling matching interfaces.
15+
//
16+
// The purpose of this Patcher is to make it seemless to use custom types in expressions without the need to
17+
// first convert them to standard go values. It may also facilitate using already existing structs or maps as
18+
// environments when they contain compatabile types.
19+
//
20+
// An example usage may be modeling a database record with columns that have varying data types and constraints.
21+
// In such an example you may have custom types that, beyond storing a simple value, such as an integer, may
22+
// contain metadata such as column type and if a value is specifically a NULL value.
23+
//
24+
// Use it directly as an Option to expr.Compile()
25+
var ValueGetter = func() expr.Option {
26+
vPatcher := patcher{}
27+
return func(c *conf.Config) {
28+
c.Visitors = append(c.Visitors, vPatcher)
29+
vPatcher.ApplyOptions(c)
30+
}
31+
}()
32+
33+
// A AnyValuer provides a generic function for a custom type to return standard go values.
34+
// It allows for returning a `nil` value but does not provide any type checking at expression compile.
35+
//
36+
// A custom type may implement both AnyValuer and a type specific interface to enable both
37+
// compile time checking and the ability to return a `nil` value.
38+
type AnyValuer interface {
39+
AsAny() any
40+
}
41+
42+
type IntValuer interface {
43+
AsInt() int
44+
}
45+
46+
type BoolValuer interface {
47+
AsBool() bool
48+
}
49+
50+
type Int8Valuer interface {
51+
AsInt8() int8
52+
}
53+
54+
type Int16Valuer interface {
55+
AsInt16() int16
56+
}
57+
58+
type Int32Valuer interface {
59+
AsInt32() int32
60+
}
61+
62+
type Int64Valuer interface {
63+
AsInt64() int64
64+
}
65+
66+
type UintValuer interface {
67+
AsUint() uint
68+
}
69+
70+
type Uint8Valuer interface {
71+
AsUint8() uint8
72+
}
73+
74+
type Uint16Valuer interface {
75+
AsUint16() uint16
76+
}
77+
78+
type Uint32Valuer interface {
79+
AsUint32() uint32
80+
}
81+
82+
type Uint64Valuer interface {
83+
AsUint64() uint64
84+
}
85+
86+
type Float32Valuer interface {
87+
AsFloat32() float32
88+
}
89+
90+
type Float64Valuer interface {
91+
AsFloat64() float64
92+
}
93+
94+
type StringValuer interface {
95+
AsString() string
96+
}
97+
98+
type TimeValuer interface {
99+
AsTime() time.Time
100+
}
101+
102+
type DurationValuer interface {
103+
AsDuration() time.Duration
104+
}
105+
106+
type ArrayValuer interface {
107+
AsArray() []any
108+
}
109+
110+
type MapValuer interface {
111+
AsMap() map[string]any
112+
}
113+
114+
var supportedInterfaces = []reflect.Type{
115+
reflect.TypeOf((*AnyValuer)(nil)).Elem(),
116+
reflect.TypeOf((*BoolValuer)(nil)).Elem(),
117+
reflect.TypeOf((*IntValuer)(nil)).Elem(),
118+
reflect.TypeOf((*Int8Valuer)(nil)).Elem(),
119+
reflect.TypeOf((*Int16Valuer)(nil)).Elem(),
120+
reflect.TypeOf((*Int32Valuer)(nil)).Elem(),
121+
reflect.TypeOf((*Int64Valuer)(nil)).Elem(),
122+
reflect.TypeOf((*UintValuer)(nil)).Elem(),
123+
reflect.TypeOf((*Uint8Valuer)(nil)).Elem(),
124+
reflect.TypeOf((*Uint16Valuer)(nil)).Elem(),
125+
reflect.TypeOf((*Uint32Valuer)(nil)).Elem(),
126+
reflect.TypeOf((*Uint64Valuer)(nil)).Elem(),
127+
reflect.TypeOf((*Float32Valuer)(nil)).Elem(),
128+
reflect.TypeOf((*Float64Valuer)(nil)).Elem(),
129+
reflect.TypeOf((*StringValuer)(nil)).Elem(),
130+
reflect.TypeOf((*TimeValuer)(nil)).Elem(),
131+
reflect.TypeOf((*DurationValuer)(nil)).Elem(),
132+
reflect.TypeOf((*ArrayValuer)(nil)).Elem(),
133+
reflect.TypeOf((*MapValuer)(nil)).Elem(),
134+
}
135+
136+
type patcher struct{}
137+
138+
func (patcher) Visit(node *ast.Node) {
139+
id, ok := (*node).(*ast.IdentifierNode)
140+
if !ok {
141+
return
142+
}
143+
144+
nodeType := id.Type()
145+
146+
for _, t := range supportedInterfaces {
147+
if nodeType.Implements(t) {
148+
callnode := &ast.CallNode{
149+
Callee: &ast.IdentifierNode{Value: "$patcher_value_getter"},
150+
Arguments: []ast.Node{id},
151+
}
152+
153+
ast.Patch(node, callnode)
154+
}
155+
}
156+
}
157+
158+
func (patcher) ApplyOptions(c *conf.Config) {
159+
getValueFunc(c)
160+
}
161+
162+
func getValue(params ...any) (any, error) {
163+
switch v := params[0].(type) {
164+
case AnyValuer:
165+
return v.AsAny(), nil
166+
case BoolValuer:
167+
return v.AsBool(), nil
168+
case IntValuer:
169+
return v.AsInt(), nil
170+
case Int8Valuer:
171+
return v.AsInt8(), nil
172+
case Int16Valuer:
173+
return v.AsInt16(), nil
174+
case Int32Valuer:
175+
return v.AsInt32(), nil
176+
case Int64Valuer:
177+
return v.AsInt64(), nil
178+
case UintValuer:
179+
return v.AsUint(), nil
180+
case Uint8Valuer:
181+
return v.AsUint8(), nil
182+
case Uint16Valuer:
183+
return v.AsUint16(), nil
184+
case Uint32Valuer:
185+
return v.AsUint32(), nil
186+
case Uint64Valuer:
187+
return v.AsUint64(), nil
188+
case Float32Valuer:
189+
return v.AsFloat32(), nil
190+
case Float64Valuer:
191+
return v.AsFloat64(), nil
192+
case StringValuer:
193+
return v.AsString(), nil
194+
case TimeValuer:
195+
return v.AsTime(), nil
196+
case DurationValuer:
197+
return v.AsDuration(), nil
198+
case ArrayValuer:
199+
return v.AsArray(), nil
200+
case MapValuer:
201+
return v.AsMap(), nil
202+
}
203+
204+
return params[0], nil
205+
}
206+
207+
var getValueFunc = expr.Function("$patcher_value_getter", getValue,
208+
new(func(BoolValuer) bool),
209+
new(func(IntValuer) int),
210+
new(func(Int8Valuer) int8),
211+
new(func(Int16Valuer) int16),
212+
new(func(Int32Valuer) int32),
213+
new(func(Int64Valuer) int64),
214+
new(func(UintValuer) uint),
215+
new(func(Uint8Valuer) uint8),
216+
new(func(Uint16Valuer) uint16),
217+
new(func(Uint32Valuer) uint32),
218+
new(func(Uint64Valuer) uint64),
219+
new(func(Float32Valuer) float32),
220+
new(func(Float64Valuer) float64),
221+
new(func(StringValuer) string),
222+
new(func(TimeValuer) time.Time),
223+
new(func(DurationValuer) time.Duration),
224+
new(func(ArrayValuer) []any),
225+
new(func(MapValuer) map[string]any),
226+
new(func(any) any),
227+
)

patcher/value/value_example_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package value_test
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/expr-lang/expr"
7+
"github.com/expr-lang/expr/patcher/value"
8+
"github.com/expr-lang/expr/vm"
9+
)
10+
11+
type myInt struct {
12+
Int int
13+
}
14+
15+
func (v *myInt) AsInt() int {
16+
return v.Int
17+
}
18+
19+
func (v *myInt) AsAny() any {
20+
return v.Int
21+
}
22+
23+
func ExampleAnyValuer() {
24+
env := make(map[string]any)
25+
env["ValueOne"] = &myInt{1}
26+
env["ValueTwo"] = &myInt{2}
27+
28+
program, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), value.ValueGetter)
29+
30+
if err != nil {
31+
panic(err)
32+
}
33+
34+
out, err := vm.Run(program, env)
35+
36+
if err != nil {
37+
panic(err)
38+
}
39+
40+
fmt.Println(out)
41+
// Output: 3
42+
}

0 commit comments

Comments
 (0)