From 8128e0b85ab477fa1840a21a371b526298210145 Mon Sep 17 00:00:00 2001 From: Kevin McDermott Date: Wed, 20 Nov 2024 14:16:00 +0000 Subject: [PATCH] Move first and last to the optional package. Signed-off-by: Kevin McDermott --- cel/cel_test.go | 4 ++ cel/library.go | 54 +++++++++++++++++++++ ext/lists.go | 119 ++-------------------------------------------- ext/lists_test.go | 81 ------------------------------- 4 files changed, 61 insertions(+), 197 deletions(-) diff --git a/cel/cel_test.go b/cel/cel_test.go index e80a3288..98b757b9 100644 --- a/cel/cel_test.go +++ b/cel/cel_test.go @@ -2735,6 +2735,10 @@ func TestOptionalValuesEval(t *testing.T) { RepeatedString: []string{"greetings", "world"}, }, }, + {expr: `[].first()`, out: types.OptionalNone}, + {expr: `['a','b','c'].first()`, out: types.OptionalOf(types.String("a"))}, + {expr: `[].last()`, out: types.OptionalNone}, + {expr: `[1, 2, 3].last()`, out: types.OptionalOf(types.Int(3))}, } for i, tst := range tests { diff --git a/cel/library.go b/cel/library.go index be59f1b0..3dc8594b 100644 --- a/cel/library.go +++ b/cel/library.go @@ -260,6 +260,27 @@ func (stdLibrary) ProgramOptions() []ProgramOption { // be expressed with `optMap`. // // msg.?elements.optFlatMap(e, e[?0]) // return the first element if present. + +// # First +// +// Introduced in version: 2 +// +// Returns an optional with the first value from the right hand list, or +// optional.None. +// +// [1, 2, 3].first().value() == 1 + +// # Last +// +// Introduced in version: 2 +// +// Returns an optional with the last value from the right hand list, or +// optional.None. +// +// [1, 2, 3].last().value() == 3 +// +// This is syntactic sugar for msg.elements[msg.elements.size()-1]. + func OptionalTypes(opts ...OptionalTypesOption) EnvOption { lib := &optionalLib{version: math.MaxUint32} for _, opt := range opts { @@ -375,6 +396,39 @@ func (lib *optionalLib) CompileOptions() []EnvOption { if lib.version >= 1 { opts = append(opts, Macros(ReceiverMacro(optFlatMapMacro, 2, optFlatMap))) } + + if lib.version >= 2 { + opts = append(opts, Function("last", + MemberOverload("list_last", []*Type{listTypeV}, optionalTypeV, + UnaryBinding(func(v ref.Val) ref.Val { + list := v.(traits.Lister) + sz := list.Size().Value().(int64) + + if sz == 0 { + return types.OptionalNone + } + + return types.OptionalOf(list.Get(types.Int(sz - 1))) + }), + ), + )) + + opts = append(opts, Function("first", + MemberOverload("list_first", []*Type{listTypeV}, optionalTypeV, + UnaryBinding(func(v ref.Val) ref.Val { + list := v.(traits.Lister) + sz := list.Size().Value().(int64) + + if sz == 0 { + return types.OptionalNone + } + + return types.OptionalOf(list.Get(types.Int(0))) + }), + ), + )) + } + return opts } diff --git a/ext/lists.go b/ext/lists.go index 010b1ff1..d0b90ea9 100644 --- a/ext/lists.go +++ b/ext/lists.go @@ -144,36 +144,6 @@ var comparableTypes = []*cel.Type{ // ].sortBy(e, e.score).map(e, e.name) // == ["bar", "foo", "baz"] -// # Last -// -// Introduced in version: 3 -// -// Returns the last element in a list as an Optional value. -// -// This can shorten a long expression instead of -// nested.elements[nested.elements.size()-1] you can rewrite this as -// nested.elements.last().value() -// -// .last() -> > -// -// Examples: -// -// [1, 2, 3].last().value() // return 3 - -// # First -// -// Introduced in version: 3 -// -// Returns the first element in a list as an Optional value. -// -// This is syntactic sugar to complement last(). -// -// .first() -> > -// -// Examples: -// -// [1, 2, 3].first().value() // return 1 - func Lists(options ...ListsOption) cel.EnvOption { l := &listsLib{ version: math.MaxUint32, @@ -186,8 +156,7 @@ func Lists(options ...ListsOption) cel.EnvOption { } type listsLib struct { - version uint32 - withOptional bool + version uint32 } // LibraryName implements the SingletonLibrary interface method. @@ -198,7 +167,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 lists library. +// ListsVersion configures the version of the string 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 @@ -214,15 +183,6 @@ 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")) @@ -389,39 +349,6 @@ func (lib listsLib) CompileOptions() []cel.EnvOption { )) } - if lib.version >= 3 { - 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}, 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}, resultType, - cel.UnaryBinding(func(list ref.Val) ref.Val { - if lib.withOptional { - return firstListOptional(list.(traits.Lister)) - } - return firstList(list.(traits.Lister)) - }), - ), - )) - - } - return opts } @@ -572,7 +499,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(), "sortBy can only be applied to a list, identifier, comprehension, call or select expression") + return nil, meh.NewError(target.ID(), fmt.Sprintf("sortBy can only be applied to a list, identifier, comprehension, call or select expression")) } mapCompr, err := parser.MakeMap(meh, meh.Copy(varIdent), args) @@ -624,46 +551,6 @@ func distinctList(list traits.Lister) (ref.Val, error) { return types.DefaultTypeAdapter.NativeToValue(uniqueList), nil } -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 - } - - return types.OptionalOf(list.Get(types.Int(0))) -} - -func lastListOptional(list traits.Lister) ref.Val { - sz := list.Size().Value().(int64) - - if sz == 0 { - return types.OptionalNone - } - - return types.OptionalOf(list.Get(types.Int(sz - 1))) -} - func templatedOverloads(types []*cel.Type, template func(t *cel.Type) cel.FunctionOpt) []cel.FunctionOpt { overloads := make([]cel.FunctionOpt, len(types)) for i, t := range types { diff --git a/ext/lists_test.go b/ext/lists_test.go index 3a320ec5..2baff0e6 100644 --- a/ext/lists_test.go +++ b/ext/lists_test.go @@ -113,87 +113,6 @@ 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{