From 5b59ddce62e137e09a48631602953b06f2814d1c Mon Sep 17 00:00:00 2001 From: Kevin McDermott Date: Wed, 13 Nov 2024 07:20:50 +0000 Subject: [PATCH] Make Optional Optional. Make the lists first and last functions return optional optionals. Signed-off-by: Kevin McDermott --- ext/lists.go | 57 +++++++++++++++++++++++++----- ext/lists_test.go | 89 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 131 insertions(+), 15 deletions(-) diff --git a/ext/lists.go b/ext/lists.go index e96948dc..010b1ff1 100644 --- a/ext/lists.go +++ b/ext/lists.go @@ -154,7 +154,7 @@ var comparableTypes = []*cel.Type{ // nested.elements[nested.elements.size()-1] you can rewrite this as // nested.elements.last().value() // -// .last() -> +// .last() -> > // // Examples: // @@ -168,7 +168,7 @@ var comparableTypes = []*cel.Type{ // // This is syntactic sugar to complement last(). // -// .first() -> +// .first() -> > // // Examples: // @@ -186,7 +186,8 @@ func Lists(options ...ListsOption) cel.EnvOption { } type listsLib struct { - version uint32 + version uint32 + withOptional bool } // LibraryName implements the SingletonLibrary interface method. @@ -197,7 +198,7 @@ func (listsLib) LibraryName() string { // ListsOption is a functional interface for configuring the strings library. type ListsOption func(*listsLib) *listsLib -// ListsVersion configures the version of the string library. +// ListsVersion configures the version of the lists library. // // The version limits which functions are available. Only functions introduced // below or equal to the given version included in the library. If this option @@ -213,6 +214,15 @@ func ListsVersion(version uint32) ListsOption { } } +// ListsOptionals configures the lists library to use cel.Optional values where +// appropriate. +func ListsOptionals() ListsOption { + return func(lib *listsLib) *listsLib { + lib.withOptional = true + return lib + } +} + // CompileOptions implements the Library interface method. func (lib listsLib) CompileOptions() []cel.EnvOption { listType := cel.ListType(cel.TypeParamType("T")) @@ -383,17 +393,28 @@ func (lib listsLib) CompileOptions() []cel.EnvOption { paramTypeV := cel.TypeParamType("V") optionalTypeV := cel.OptionalType(paramTypeV) + var resultType *cel.Type = cel.DynType + if lib.withOptional { + resultType = optionalTypeV + } + opts = append(opts, cel.Function("last", - cel.MemberOverload("list_last", []*cel.Type{listDyn}, optionalTypeV, + cel.MemberOverload("list_last", []*cel.Type{listDyn}, resultType, cel.UnaryBinding(func(list ref.Val) ref.Val { + if lib.withOptional { + return lastListOptional(list.(traits.Lister)) + } return lastList(list.(traits.Lister)) }), ), )) opts = append(opts, cel.Function("first", - cel.MemberOverload("list_first", []*cel.Type{listDyn}, optionalTypeV, + cel.MemberOverload("list_first", []*cel.Type{listDyn}, resultType, cel.UnaryBinding(func(list ref.Val) ref.Val { + if lib.withOptional { + return firstListOptional(list.(traits.Lister)) + } return firstList(list.(traits.Lister)) }), ), @@ -551,7 +572,7 @@ func sortByMacro(meh cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (as targetKind != ast.SelectKind && targetKind != ast.IdentKind && targetKind != ast.ComprehensionKind && targetKind != ast.CallKind { - return nil, meh.NewError(target.ID(), fmt.Sprintf("sortBy can only be applied to a list, identifier, comprehension, call or select expression")) + return nil, meh.NewError(target.ID(), "sortBy can only be applied to a list, identifier, comprehension, call or select expression") } mapCompr, err := parser.MakeMap(meh, meh.Copy(varIdent), args) @@ -606,6 +627,26 @@ func distinctList(list traits.Lister) (ref.Val, error) { func firstList(list traits.Lister) ref.Val { sz := list.Size().Value().(int64) + if sz == 0 { + return types.NullValue + } + + return list.Get(types.Int(0)) +} + +func lastList(list traits.Lister) ref.Val { + sz := list.Size().Value().(int64) + + if sz == 0 { + return types.NullValue + } + + return list.Get(types.Int(sz - 1)) +} + +func firstListOptional(list traits.Lister) ref.Val { + sz := list.Size().Value().(int64) + if sz == 0 { return types.OptionalNone } @@ -613,7 +654,7 @@ func firstList(list traits.Lister) ref.Val { return types.OptionalOf(list.Get(types.Int(0))) } -func lastList(list traits.Lister) ref.Val { +func lastListOptional(list traits.Lister) ref.Val { sz := list.Size().Value().(int64) if sz == 0 { diff --git a/ext/lists_test.go b/ext/lists_test.go index 60983618..3a320ec5 100644 --- a/ext/lists_test.go +++ b/ext/lists_test.go @@ -71,15 +71,9 @@ func TestLists(t *testing.T) { {expr: `[1, 1.0, 2].distinct() == [1, 2]`}, {expr: `[[1], [1], [2]].distinct() == [[1], [2]]`}, {expr: `[ExampleType{name: 'a'}, ExampleType{name: 'b'}, ExampleType{name: 'a'}].distinct() == [ExampleType{name: 'a'}, ExampleType{name: 'b'}]`}, - {expr: `![].first().hasValue()`}, - {expr: `[1, 2, 3].first().value() == 1`}, - {expr: `![].last().hasValue()`}, - {expr: `[1, 2, 3].last().value() == 3`}, - {expr: `'/path/to'.split('/').filter(t, t.size() > 0).first().value() == 'path'`}, - {expr: `'/path/to'.split('/').filter(t, t.size() > 0).last().value() == 'to'`}, } - env := testListsEnv(t, cel.OptionalTypes(), Strings()) + env := testListsEnv(t) for i, tst := range listsTests { tc := tst t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { @@ -119,6 +113,87 @@ func TestLists(t *testing.T) { } } +func TestListsWithOptionals(t *testing.T) { + optionalEnv := []cel.EnvOption{cel.OptionalTypes()} + optionalStringsEnv := []cel.EnvOption{cel.OptionalTypes(), Strings()} + + newOptionalEnv := func(withOptions bool, opts ...cel.EnvOption) *cel.Env { + var optionalOpts []ListsOption + if withOptions { + optionalOpts = []ListsOption{ListsOptionals()} + } + + baseOpts := []cel.EnvOption{ + Lists(optionalOpts...), + } + env, err := cel.NewEnv(append(baseOpts, opts...)...) + if err != nil { + t.Fatalf("cel.NewEnv(Lists()) failed: %v", err) + } + + return env + } + + listsTests := []struct { + expr string + withOptions bool + celOptions []cel.EnvOption + err string + }{ + {expr: `[].first() == null`}, + {expr: `['a','b','c'].first() == 'a'`}, + {expr: `[].last() == null`}, + {expr: `['a','b','c'].last() == 'c'`}, + {expr: `'/path/to'.split('/').filter(t, t.size() > 0).first() == 'path'`, celOptions: optionalStringsEnv}, + {expr: `'/path/to'.split('/').filter(t, t.size() > 0).last() == 'to'`, celOptions: optionalStringsEnv}, + {expr: `![].first().hasValue()`, celOptions: optionalEnv, withOptions: true}, + {expr: `[1, 2, 3].first().value() == 1`, celOptions: optionalEnv, withOptions: true}, + {expr: `![].last().hasValue()`, celOptions: optionalEnv, withOptions: true}, + {expr: `[1, 2, 3].last().value() == 3`, celOptions: optionalEnv, withOptions: true}, + {expr: `'/path/to'.split('/').filter(t, t.size() > 0).first().value() == 'path'`, celOptions: optionalStringsEnv, withOptions: true}, + {expr: `'/path/to'.split('/').filter(t, t.size() > 0).last().value() == 'to'`, celOptions: optionalStringsEnv, withOptions: true}, + } + + for i, tst := range listsTests { + env := newOptionalEnv(tst.withOptions, tst.celOptions...) + tc := tst + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + var asts []*cel.Ast + pAst, iss := env.Parse(tc.expr) + if iss.Err() != nil { + t.Fatalf("env.Parse(%v) failed: %v", tc.expr, iss.Err()) + } + asts = append(asts, pAst) + cAst, iss := env.Check(pAst) + if iss.Err() != nil { + t.Fatalf("env.Check(%v) failed: %v", tc.expr, iss.Err()) + } + asts = append(asts, cAst) + + for _, ast := range asts { + prg, err := env.Program(ast) + if err != nil { + t.Fatalf("env.Program() failed: %v", err) + } + out, _, err := prg.Eval(cel.NoVars()) + if tc.err != "" { + if err == nil { + t.Fatalf("got value %v, wanted error %s for expr: %s", + out.Value(), tc.err, tc.expr) + } + if !strings.Contains(err.Error(), tc.err) { + t.Errorf("got error %v, wanted error %s for expr: %s", err, tc.err, tc.expr) + } + } else if err != nil { + t.Fatal(err) + } else if out.Value() != true { + t.Errorf("got %v, wanted true for expr: %s", out.Value(), tc.expr) + } + } + }) + } +} + func testListsEnv(t *testing.T, opts ...cel.EnvOption) *cel.Env { t.Helper() baseOpts := []cel.EnvOption{