Skip to content

Commit

Permalink
Make Optional Optional.
Browse files Browse the repository at this point in the history
Make the lists first and last functions return optional optionals.

Signed-off-by: Kevin McDermott <[email protected]>
  • Loading branch information
bigkevmcd committed Nov 13, 2024
1 parent f8ce4d0 commit 5b59ddc
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 15 deletions.
57 changes: 49 additions & 8 deletions ext/lists.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ var comparableTypes = []*cel.Type{
// nested.elements[nested.elements.size()-1] you can rewrite this as
// nested.elements.last().value()
//
// <list(T)>.last() -> <list(T)[len()-1]>
// <list(T)>.last() -> <Optional<list(T)[len()-1]>>
//
// Examples:
//
Expand All @@ -168,7 +168,7 @@ var comparableTypes = []*cel.Type{
//
// This is syntactic sugar to complement last().
//
// <list(T)>.first() -> <list(T)[0]>
// <list(T)>.first() -> <Optional<list(T)[0]>>
//
// Examples:
//
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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"))
Expand Down Expand Up @@ -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))
}),
),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -606,14 +627,34 @@ 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
}

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

0 comments on commit 5b59ddc

Please sign in to comment.