Skip to content

Commit

Permalink
Move first and last to the optional package.
Browse files Browse the repository at this point in the history
Signed-off-by: Kevin McDermott <bigkevmcd@gmail.com>
  • Loading branch information
bigkevmcd committed Nov 20, 2024
1 parent 5b59ddc commit 8128e0b
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 197 deletions.
4 changes: 4 additions & 0 deletions cel/cel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
54 changes: 54 additions & 0 deletions cel/library.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down
119 changes: 3 additions & 116 deletions ext/lists.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
//
// <list(T)>.last() -> <Optional<list(T)[len()-1]>>
//
// 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().
//
// <list(T)>.first() -> <Optional<list(T)[0]>>
//
// Examples:
//
// [1, 2, 3].first().value() // return 1

func Lists(options ...ListsOption) cel.EnvOption {
l := &listsLib{
version: math.MaxUint32,
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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"))
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
81 changes: 0 additions & 81 deletions ext/lists_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down

0 comments on commit 8128e0b

Please sign in to comment.