Skip to content

Commit a8cc2ca

Browse files
Add versioning support for extensions
1 parent 4b73ba3 commit a8cc2ca

14 files changed

Lines changed: 281 additions & 52 deletions

ext/comprehensions.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package ext
1616

1717
import (
1818
"fmt"
19+
"math"
1920

2021
"github.com/google/cel-go/cel"
2122
"github.com/google/cel-go/common/ast"
@@ -159,19 +160,36 @@ const (
159160
//
160161
// {'greeting': 'aloha', 'farewell': 'aloha'}
161162
// .transformMapEntry(keyVar, valueVar, {valueVar: keyVar}) // error, duplicate key
162-
func TwoVarComprehensions() cel.EnvOption {
163-
return cel.Lib(compreV2Lib{})
163+
func TwoVarComprehensions(options ...TwoVarComprehensionsOption) cel.EnvOption {
164+
l := &compreV2Lib{version: math.MaxUint32}
165+
for _, o := range options {
166+
l = o(l)
167+
}
168+
return cel.Lib(l)
169+
}
170+
171+
// TwoVarComprehensionsOption declares a functional operator for configuring two-variable comprehensions.
172+
type TwoVarComprehensionsOption func(*compreV2Lib) *compreV2Lib
173+
174+
// TwoVarComprehensionsVersion sets the library version for two-variable comprehensions.
175+
func TwoVarComprehensionsVersion(version uint32) TwoVarComprehensionsOption {
176+
return func(lib *compreV2Lib) *compreV2Lib {
177+
lib.version = version
178+
return lib
179+
}
164180
}
165181

166-
type compreV2Lib struct{}
182+
type compreV2Lib struct {
183+
version uint32
184+
}
167185

168186
// LibraryName implements that SingletonLibrary interface method.
169-
func (compreV2Lib) LibraryName() string {
187+
func (*compreV2Lib) LibraryName() string {
170188
return "cel.lib.ext.comprev2"
171189
}
172190

173191
// CompileOptions implements the cel.Library interface method.
174-
func (compreV2Lib) CompileOptions() []cel.EnvOption {
192+
func (*compreV2Lib) CompileOptions() []cel.EnvOption {
175193
kType := cel.TypeParamType("K")
176194
vType := cel.TypeParamType("V")
177195
mapKVType := cel.MapType(kType, vType)
@@ -217,7 +235,7 @@ func (compreV2Lib) CompileOptions() []cel.EnvOption {
217235
}
218236

219237
// ProgramOptions implements the cel.Library interface method
220-
func (compreV2Lib) ProgramOptions() []cel.ProgramOption {
238+
func (*compreV2Lib) ProgramOptions() []cel.ProgramOption {
221239
return []cel.ProgramOption{}
222240
}
223241

ext/comprehensions_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,13 @@ func TestTwoVarComprehensionsRuntimeErrors(t *testing.T) {
345345
}
346346
}
347347

348+
func TestTwoVarComprehensionsVersion(t *testing.T) {
349+
_, err := cel.NewEnv(TwoVarComprehensions(TwoVarComprehensionsVersion(0)))
350+
if err != nil {
351+
t.Fatalf("TwoVarComprehensionVersion(0) failed: %v", err)
352+
}
353+
}
354+
348355
func testCompreEnv(t *testing.T, opts ...cel.EnvOption) *cel.Env {
349356
t.Helper()
350357
baseOpts := []cel.EnvOption{

ext/encoders.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package ext
1616

1717
import (
1818
"encoding/base64"
19+
"math"
1920

2021
"github.com/google/cel-go/cel"
2122
"github.com/google/cel-go/common/types"
@@ -47,17 +48,34 @@ import (
4748
// Examples:
4849
//
4950
// base64.encode(b'hello') // return b'aGVsbG8='
50-
func Encoders() cel.EnvOption {
51-
return cel.Lib(encoderLib{})
51+
func Encoders(options ...EncodersOption) cel.EnvOption {
52+
l := &encoderLib{version: math.MaxUint32}
53+
for _, o := range options {
54+
l = o(l)
55+
}
56+
return cel.Lib(l)
57+
}
58+
59+
// EncodersOption declares a functional operator for configuring encoder extensions.
60+
type EncodersOption func(*encoderLib) *encoderLib
61+
62+
// EncodersVersion sets the library version for encoder extensions.
63+
func EncodersVersion(version uint32) EncodersOption {
64+
return func(lib *encoderLib) *encoderLib {
65+
lib.version = version
66+
return lib
67+
}
5268
}
5369

54-
type encoderLib struct{}
70+
type encoderLib struct {
71+
version uint32
72+
}
5573

56-
func (encoderLib) LibraryName() string {
74+
func (*encoderLib) LibraryName() string {
5775
return "cel.lib.ext.encoders"
5876
}
5977

60-
func (encoderLib) CompileOptions() []cel.EnvOption {
78+
func (*encoderLib) CompileOptions() []cel.EnvOption {
6179
return []cel.EnvOption{
6280
cel.Function("base64.decode",
6381
cel.Overload("base64_decode_string", []*cel.Type{cel.StringType}, cel.BytesType,
@@ -74,7 +92,7 @@ func (encoderLib) CompileOptions() []cel.EnvOption {
7492
}
7593
}
7694

77-
func (encoderLib) ProgramOptions() []cel.ProgramOption {
95+
func (*encoderLib) ProgramOptions() []cel.ProgramOption {
7896
return []cel.ProgramOption{}
7997
}
8098

ext/encoders_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,10 @@ func TestEncoders(t *testing.T) {
8686
})
8787
}
8888
}
89+
90+
func TestEncodersVersion(t *testing.T) {
91+
_, err := cel.NewEnv(Encoders(EncodersVersion(0)))
92+
if err != nil {
93+
t.Fatalf("EncodersVersion(0) failed: %v", err)
94+
}
95+
}

ext/lists.go

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,10 @@ var comparableTypes = []*cel.Type{
145145
// == ["bar", "foo", "baz"]
146146

147147
func Lists(options ...ListsOption) cel.EnvOption {
148-
l := &listsLib{
149-
version: math.MaxUint32,
150-
}
148+
l := &listsLib{version: math.MaxUint32}
151149
for _, o := range options {
152150
l = o(l)
153151
}
154-
155152
return cel.Lib(l)
156153
}
157154

@@ -211,9 +208,10 @@ func (lib listsLib) CompileOptions() []cel.EnvOption {
211208
cel.MemberOverload("list_flatten",
212209
[]*cel.Type{listListType}, listType,
213210
cel.UnaryBinding(func(arg ref.Val) ref.Val {
211+
// double-check as type-guards disabled
214212
list, ok := arg.(traits.Lister)
215213
if !ok {
216-
return types.MaybeNoSuchOverloadErr(arg)
214+
return types.ValOrErr(arg, "no such overload: %v.flatten()", arg.Type())
217215
}
218216
flatList, err := flatten(list, 1)
219217
if err != nil {
@@ -226,13 +224,14 @@ func (lib listsLib) CompileOptions() []cel.EnvOption {
226224
cel.MemberOverload("list_flatten_int",
227225
[]*cel.Type{listDyn, types.IntType}, listDyn,
228226
cel.BinaryBinding(func(arg1, arg2 ref.Val) ref.Val {
227+
// double-check as type-guards disabled
229228
list, ok := arg1.(traits.Lister)
230229
if !ok {
231-
return types.MaybeNoSuchOverloadErr(arg1)
230+
return types.ValOrErr(arg1, "no such overload: %v.flatten(%v)", arg1.Type(), arg2.Type())
232231
}
233232
depth, ok := arg2.(types.Int)
234233
if !ok {
235-
return types.MaybeNoSuchOverloadErr(arg2)
234+
return types.ValOrErr(arg1, "no such overload: %v.flatten(%v)", arg1.Type(), arg2.Type())
236235
}
237236
flatList, err := flatten(list, int64(depth))
238237
if err != nil {
@@ -260,10 +259,8 @@ func (lib listsLib) CompileOptions() []cel.EnvOption {
260259
}),
261260
cel.SingletonUnaryBinding(
262261
func(arg ref.Val) ref.Val {
263-
list, ok := arg.(traits.Lister)
264-
if !ok {
265-
return types.MaybeNoSuchOverloadErr(arg)
266-
}
262+
// validated by type-guards
263+
list := arg.(traits.Lister)
267264
sorted, err := sortList(list)
268265
if err != nil {
269266
return types.WrapErr(err)
@@ -287,15 +284,10 @@ func (lib listsLib) CompileOptions() []cel.EnvOption {
287284
)
288285
}),
289286
cel.SingletonBinaryBinding(
290-
func(arg1 ref.Val, arg2 ref.Val) ref.Val {
291-
list, ok := arg1.(traits.Lister)
292-
if !ok {
293-
return types.MaybeNoSuchOverloadErr(arg1)
294-
}
295-
keys, ok := arg2.(traits.Lister)
296-
if !ok {
297-
return types.MaybeNoSuchOverloadErr(arg2)
298-
}
287+
func(arg1, arg2 ref.Val) ref.Val {
288+
// validated by type-guards
289+
list := arg1.(traits.Lister)
290+
keys := arg2.(traits.Lister)
299291
sorted, err := sortListByAssociatedKeys(list, keys)
300292
if err != nil {
301293
return types.WrapErr(err)
@@ -498,8 +490,9 @@ func sortByMacro(meh cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (as
498490
if targetKind != ast.ListKind &&
499491
targetKind != ast.SelectKind &&
500492
targetKind != ast.IdentKind &&
501-
targetKind != ast.ComprehensionKind && targetKind != ast.CallKind {
502-
return nil, meh.NewError(target.ID(), fmt.Sprintf("sortBy can only be applied to a list, identifier, comprehension, call or select expression"))
493+
targetKind != ast.ComprehensionKind &&
494+
targetKind != ast.CallKind {
495+
return nil, meh.NewError(target.ID(), "sortBy can only be applied to a list, identifier, comprehension, call or select expression")
503496
}
504497

505498
mapCompr, err := parser.MakeMap(meh, meh.Copy(varIdent), args)

ext/lists_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"testing"
2121

2222
"github.com/google/cel-go/cel"
23+
"github.com/google/cel-go/common/types"
2324
proto2pb "github.com/google/cel-go/test/proto2pb"
2425
)
2526

@@ -113,6 +114,118 @@ func TestLists(t *testing.T) {
113114
}
114115
}
115116

117+
func TestListsRuntimeErrors(t *testing.T) {
118+
env, err := cel.NewEnv(Lists(ListsVersion(1)))
119+
if err != nil {
120+
t.Fatalf("cel.NewEnv() failed: %v", err)
121+
}
122+
listsTests := []struct {
123+
expr string
124+
err string
125+
}{
126+
{
127+
expr: "dyn({}).flatten()",
128+
err: "no such overload",
129+
},
130+
{
131+
expr: "dyn({}).flatten(0)",
132+
err: "no such overload",
133+
},
134+
{
135+
expr: "[].flatten(-1)",
136+
err: "level must be non-negative",
137+
},
138+
{
139+
expr: "[].flatten(dyn('1'))",
140+
err: "no such overload",
141+
},
142+
}
143+
for i, tst := range listsTests {
144+
tc := tst
145+
t.Run(fmt.Sprintf("[%d]", i), func(t *testing.T) {
146+
ast, iss := env.Compile(tc.expr)
147+
if iss.Err() != nil {
148+
t.Fatalf("env.Compile(%q) failed: %v", tc.expr, iss.Err())
149+
}
150+
prg, err := env.Program(ast)
151+
if err != nil {
152+
t.Fatalf("env.Program() failed: %v", err)
153+
}
154+
_, _, err = prg.Eval(cel.NoVars())
155+
if err == nil || !strings.Contains(err.Error(), tc.err) {
156+
t.Errorf("prg.Eval() got %v, wanted %v", err, tc.err)
157+
}
158+
})
159+
}
160+
}
161+
162+
func TestListsVersion(t *testing.T) {
163+
versionCases := []struct {
164+
version uint32
165+
supportedFunctions map[string]string
166+
}{
167+
{
168+
version: 0,
169+
supportedFunctions: map[string]string{
170+
"slice": "[1, 2, 3, 4, 5].slice(2, 4) == [3, 4]",
171+
},
172+
},
173+
{
174+
version: 1,
175+
supportedFunctions: map[string]string{
176+
"flatten": "[[1, 2], [3, 4]].flatten() == [1, 2, 3, 4]",
177+
},
178+
},
179+
{
180+
version: 2,
181+
supportedFunctions: map[string]string{
182+
"distinct": "[1, 2, 2, 1].distinct() == [1, 2]",
183+
"range": "lists.range(5) == [0, 1, 2, 3, 4]",
184+
"reverse": "[1, 2, 3].reverse() == [3, 2, 1]",
185+
"sort": "[2, 1, 3].sort() == [1, 2, 3]",
186+
"sortBy": "[{'field': 'lo'}, {'field': 'hi'}].sortBy(m, m.field) == [{'field': 'hi'}, {'field': 'lo'}]",
187+
},
188+
},
189+
}
190+
for _, lib := range versionCases {
191+
env, err := cel.NewEnv(Lists(ListsVersion(lib.version)))
192+
if err != nil {
193+
t.Fatalf("cel.NewEnv(Lists(ListsVersion(%d))) failed: %v", lib.version, err)
194+
}
195+
t.Run(fmt.Sprintf("version=%d", lib.version), func(t *testing.T) {
196+
for _, tc := range versionCases {
197+
for name, expr := range tc.supportedFunctions {
198+
supported := lib.version >= tc.version
199+
t.Run(fmt.Sprintf("%s-supported=%t", name, supported), func(t *testing.T) {
200+
ast, iss := env.Compile(expr)
201+
if supported {
202+
if iss.Err() != nil {
203+
t.Errorf("unexpected error: %v", iss.Err())
204+
}
205+
} else {
206+
if iss.Err() == nil || !strings.Contains(iss.Err().Error(), "undeclared reference") {
207+
t.Errorf("got error %v, wanted error %s for expr: %s, version: %d", iss.Err(), "undeclared reference", expr, tc.version)
208+
}
209+
return
210+
}
211+
prg, err := env.Program(ast)
212+
if err != nil {
213+
t.Fatalf("env.Program() failed: %v", err)
214+
}
215+
out, _, err := prg.Eval(cel.NoVars())
216+
if err != nil {
217+
t.Fatalf("prg.Eval() failed: %v", err)
218+
}
219+
if out != types.True {
220+
t.Errorf("prg.Eval() got %v, wanted true", out)
221+
}
222+
})
223+
}
224+
}
225+
})
226+
}
227+
}
228+
116229
func testListsEnv(t *testing.T, opts ...cel.EnvOption) *cel.Env {
117230
t.Helper()
118231
baseOpts := []cel.EnvOption{

0 commit comments

Comments
 (0)