diff --git a/custom_cel/cel.go b/custom_cel/cel.go index 7664ba2..0924456 100644 --- a/custom_cel/cel.go +++ b/custom_cel/cel.go @@ -15,8 +15,9 @@ import ( // of a given cTTL. func BuildCELOptions(cTTL *cleanerv1alpha1.ConditionalTTL) []cel.EnvOption { r := []cel.EnvOption{ - ext.Strings(), // helper string functions - Lists(), // custom VTEX helper for list functions + ext.Strings(), // helper string functions + ext.Bindings(), // helper binding functions + Lists(), // custom VTEX helper for list functions cel.Variable("time", cel.TimestampType), } for _, t := range cTTL.Spec.Targets { diff --git a/custom_cel/lists.go b/custom_cel/lists.go index 329e929..06ce483 100644 --- a/custom_cel/lists.go +++ b/custom_cel/lists.go @@ -8,33 +8,38 @@ import ( "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/common/types/traits" - "github.com/google/cel-go/ext" "github.com/google/cel-go/parser" "k8s.io/apiserver/pkg/cel/library" "sort" - "strings" -) - -const ( - AscendingOrder = "asc" - DescendingOrder = "desc" ) // Lists returns a cel.EnvOption to configure extended functions Lists manipulation. +// // # SortBy // -// Returns a new sorted list by the field and order defined (ascending or descending). +// Returns a new sorted list by the field defined. // It supports all types that implements the base traits.Comparer interface. // -// .sort_by(obj, obj.field) -> +// .sort_by(obj, obj.field) ==> // // Examples: // -// [3,1,2].sort_by(i, i) // returns [1,2,3] +// [2,3,1].sort_by(i,i) ==> [1,2,3] +// +// [{Name: "c", Age: 10}, {Name: "a", Age: 30}, {Name: "b", Age: 1}].sort_by(obj, obj.age) ==> [{Name: "b", Age: 1}, {Name: "c", Age: 10}, {Name: "a", Age: 30}] +// +// # ReverseList // -// [3,1,2].sort_by(i, i, "desc") // returns [3,2,1] +// Returns a new list in reverse order. +// It supports all types that implements the base traits.Comparer interface // -// [{Name: "c", Age: 10}, {Name: "a", Age: 30}, {Name: "b", Age: 1}].sort_by(obj, obj.age) // returns [{Name: "b", Age: 1}, {Name: "c", Age: 10}, {Name: "a", Age: 30}] +// .reverse_list() ==> +// +// # Examples +// +// [1,2,3].reverse_list() ==> [3,2,1] +// +// ["x", "y", "z"].reverse_list() ==> ["z", "y", "x"] func Lists() cel.EnvOption { return cel.Lib(listsLib{}) } @@ -45,12 +50,9 @@ type listsLib struct{} func (u listsLib) CompileOptions() []cel.EnvOption { dynListType := cel.ListType(cel.DynType) sortByMacro := parser.NewReceiverMacro("sort_by", 2, makeSortBy) - sortByMacroWithOrder := parser.NewReceiverMacro("sort_by", 3, makeSortBy) return []cel.EnvOption{ - ext.Strings(), library.Lists(), cel.Macros(sortByMacro), - cel.Macros(sortByMacroWithOrder), cel.Function( "pair", cel.Overload( @@ -63,10 +65,19 @@ func (u listsLib) CompileOptions() []cel.EnvOption { cel.Function( "sort", cel.Overload( - "sort_by_order", - []*cel.Type{dynListType, cel.DynType}, + "sort_list", + []*cel.Type{dynListType}, dynListType, - cel.BinaryBinding(sortByOrder), + cel.UnaryBinding(makeSort), + ), + ), + cel.Function( + "reverse_list", + cel.MemberOverload( + "reverse_list_id", + []*cel.Type{cel.ListType(cel.DynType)}, + cel.ListType(cel.DynType), + cel.UnaryBinding(makeReverse), ), ), } @@ -97,17 +108,12 @@ func makePair(order ref.Val, value ref.Val) ref.Val { }) } -func sortByOrder(itemsVal ref.Val, orderVal ref.Val) ref.Val { +func makeSort(itemsVal ref.Val) ref.Val { items, ok := itemsVal.(traits.Lister) if !ok { return types.ValOrErr(itemsVal, "unable to convert to traits.Lister") } - order, ok := orderVal.Value().(string) - if !ok { - return types.ValOrErr(orderVal, "unable to convert to ref.Val string") - } - pairs := make([]pair, 0, items.Size().Value().(int64)) index := 0 for it := items.Iterator(); it.HasNext().(types.Bool); { @@ -123,38 +129,9 @@ func sortByOrder(itemsVal ref.Val, orderVal ref.Val) ref.Val { index++ } - ascSort := func(i, j int) bool { - cmp := pairs[i].order.(traits.Comparer) - switch cmp.Compare(pairs[j].order) { - case types.IntNegOne: - return true - case types.IntOne: - return false - default: // IntZero means equal - return false - } - } - - descSort := func(i, j int) bool { - cmp := pairs[i].order.(traits.Comparer) - switch cmp.Compare(pairs[j].order) { - case types.IntNegOne: - return false - case types.IntOne: - return true - default: // IntZero means equal - return false - } - } - - switch strings.ToLower(order) { - case AscendingOrder: - sort.Slice(pairs, ascSort) - case DescendingOrder: - sort.Slice(pairs, descSort) - default: - return types.NewErr("unknown order: %s", order) - } + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].order.(traits.Comparer).Compare(pairs[j].order) == types.IntNegOne + }) var ordered []interface{} for _, v := range pairs { @@ -171,13 +148,6 @@ func extractIdent(e ast.Expr) (string, bool) { return "", false } -func extractOrder(args []ast.Expr) ref.Val { - if len(args) == 3 { - return args[2].AsLiteral() - } - return types.String("asc") -} - func makeSortBy(eh parser.ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) { v, found := extractIdent(args[0]) if !found { @@ -193,14 +163,12 @@ func makeSortBy(eh parser.ExprHelper, target ast.Expr, args []ast.Expr) (ast.Exp eh.NewCall("pair", fn, args[0]), )) - order := extractOrder(args) - /* This comprehension is expanded to: __result__ = [] # init expr for $v in $target: __result__ += [pair(fn(v), v)] # step expr - return sort(__result__, "asc") # result expr + return sort(__result__) # result expr */ mapped := eh.NewComprehension( target, @@ -212,9 +180,27 @@ func makeSortBy(eh parser.ExprHelper, target ast.Expr, args []ast.Expr) (ast.Exp eh.NewCall( "sort", eh.NewAccuIdent(), - eh.NewLiteral(types.DefaultTypeAdapter.NativeToValue(order)), ), ) return mapped, nil } + +func makeReverse(itemsVal ref.Val) ref.Val { + items, ok := itemsVal.(traits.Lister) + if !ok { + return types.ValOrErr(itemsVal, "unable to convert to traits.Lister") + } + + orderedItems := make([]ref.Val, 0, items.Size().Value().(int64)) + for it := items.Iterator(); it.HasNext().(types.Bool); { + orderedItems = append(orderedItems, it.Next()) + } + + for i := len(orderedItems)/2 - 1; i >= 0; i-- { + opp := len(orderedItems) - 1 - i + orderedItems[i], orderedItems[opp] = orderedItems[opp], orderedItems[i] + } + + return types.NewDynamicList(types.DefaultTypeAdapter, orderedItems) +} diff --git a/custom_cel/lists_test.go b/custom_cel/lists_test.go index e2c1ac8..118679c 100644 --- a/custom_cel/lists_test.go +++ b/custom_cel/lists_test.go @@ -9,35 +9,164 @@ import ( "time" ) -func Test_sortByOrder(t *testing.T) { - varName := "objects" +var varName = "objects" - now := time.Now() - first := now.Add(-(time.Duration(24) * time.Hour * 3)).Format(time.RFC3339Nano) - second := now.Add(-(time.Duration(24) * time.Hour * 2)).Format(time.RFC3339Nano) - third := now.Add(-(time.Duration(24) * time.Hour * 1)).Format(time.RFC3339Nano) +func Test_sort(t *testing.T) { + first, second, third := getDates() + + testCases := map[string]struct { + condition string + list any + wantList ref.Val + }{ + "sort timestamp list": { + condition: `objects.sort_by(i, i)`, + list: []time.Time{second, first, third}, + wantList: types.NewDynamicList( + types.DefaultTypeAdapter, + []types.Timestamp{ + {Time: first}, + {Time: second}, + {Time: third}, + }), + }, + + "sort duration list": { + condition: `objects.sort_by(i, i)`, + list: []time.Duration{ + time.Duration(second.Unix()), + time.Duration(first.Unix()), + time.Duration(third.Unix()), + }, + wantList: types.NewDynamicList( + types.DefaultTypeAdapter, + []types.Duration{ + {Duration: time.Duration(first.Unix())}, + {Duration: time.Duration(second.Unix())}, + {Duration: time.Duration(third.Unix())}, + }), + }, + + "sort int list": { + condition: `[2,1,3].sort_by(i,i)`, + wantList: types.NewDynamicList(types.DefaultTypeAdapter, []types.Int{1, 2, 3}), + }, + + "sort uint list": { + condition: `[uint(2), uint(1), uint(3)].sort_by(i,i)`, + wantList: types.NewDynamicList(types.DefaultTypeAdapter, []types.Uint{1, 2, 3}), + }, + + "sort double list": { + condition: `[double(2), double(1), double(3)].sort_by(i,i)`, + wantList: types.NewDynamicList(types.DefaultTypeAdapter, []types.Double{1, 2, 3}), + }, + + "sort bytes list": { + condition: `[bytes("c"), bytes("a"), bytes("b")].sort_by(i,i)`, + wantList: types.NewDynamicList(types.DefaultTypeAdapter, []types.Bytes{[]byte("a"), []byte("b"), []byte("c")}), + }, + + "sort boolean list": { + condition: `[true, false, true].sort_by(i,i)`, + wantList: types.NewDynamicList(types.DefaultTypeAdapter, []types.Bool{false, true, true}), + }, + + "sort string list": { + condition: `["c", "a", "b"].sort_by(i,i)`, + wantList: types.NewDynamicList(types.DefaultTypeAdapter, []types.String{"a", "b", "c"}), + }, + + "sort unstructured list by timestamp": { + condition: `objects.items.sort_by(o, o.metadata.creationTimestamp)`, + list: generateUnorderedUl(t, first.Format(time.RFC3339Nano), second.Format(time.RFC3339Nano), third.Format(time.RFC3339Nano)), + wantList: types.NewDynamicList(types.DefaultTypeAdapter, generateOrderedSlice(t, first.Format(time.RFC3339Nano), second.Format(time.RFC3339Nano), third.Format(time.RFC3339Nano))), + }, + } + + for description, tc := range testCases { + t.Run(description, func(t *testing.T) { + prg := setupProgram(t, varName, tc.condition) + + gotList, _, gotErr := prg.Eval(map[string]interface{}{ + varName: tc.list, + }) + + if gotErr != nil { + t.Fatalf("eval error: %s", gotErr) + } + + if gotList.Equal(tc.wantList) != types.True { + t.Errorf("\ngot=%v\nwant=%v", gotList, tc.wantList) + } + }) + } +} + +func Test_reverse(t *testing.T) { + first, second, third := getDates() testCases := map[string]struct { condition string - ul map[string]interface{} - wantUlDyn ref.Val + list any + wantList ref.Val }{ - "sort unstructured in ascending order by default": { - condition: `objects.items.sort_by(v, v.metadata.creationTimestamp)`, - ul: generateUnorderedUl(t, first, second, third), - wantUlDyn: types.NewDynamicList(types.DefaultTypeAdapter, generateOrderedSlice(t, first, second, third)), + "reverse timestamp list": { + condition: `objects.reverse_list()`, + list: []time.Time{first, second, third}, + wantList: types.NewDynamicList( + types.DefaultTypeAdapter, + []types.Timestamp{ + {Time: third}, + {Time: second}, + {Time: first}, + }), }, - "sort unstructured in descending order": { - condition: `objects.items.sort_by(v, v.metadata.creationTimestamp, "desc")`, - ul: generateUnorderedUl(t, first, second, third), - wantUlDyn: types.NewDynamicList(types.DefaultTypeAdapter, generateOrderedSlice(t, third, second, first)), + "reverse duration list": { + condition: `objects.reverse_list()`, + list: []time.Duration{ + time.Duration(first.Unix()), + time.Duration(second.Unix()), + time.Duration(third.Unix()), + }, + wantList: types.NewDynamicList( + types.DefaultTypeAdapter, + []types.Duration{ + {Duration: time.Duration(third.Unix())}, + {Duration: time.Duration(second.Unix())}, + {Duration: time.Duration(first.Unix())}, + }), }, - "sort unstructured in ascending order": { - condition: `objects.items.sort_by(v, v.metadata.creationTimestamp, "asc")`, - ul: generateUnorderedUl(t, first, second, third), - wantUlDyn: types.NewDynamicList(types.DefaultTypeAdapter, generateOrderedSlice(t, first, second, third)), + "reverse int list": { + condition: `[3,2,1].reverse_list()`, + wantList: types.NewDynamicList(types.DefaultTypeAdapter, []types.Int{1, 2, 3}), + }, + + "reverse uint list": { + condition: `[uint(3), uint(2), uint(1)].reverse_list()`, + wantList: types.NewDynamicList(types.DefaultTypeAdapter, []types.Uint{1, 2, 3}), + }, + + "reverse double list": { + condition: `[double(3), double(2), double(1)].reverse_list()`, + wantList: types.NewDynamicList(types.DefaultTypeAdapter, []types.Double{1, 2, 3}), + }, + + "reverse bytes list": { + condition: `[bytes("c"), bytes("b"), bytes("a")].reverse_list()`, + wantList: types.NewDynamicList(types.DefaultTypeAdapter, []types.Bytes{[]byte("a"), []byte("b"), []byte("c")}), + }, + + "reverse boolean list": { + condition: `[true, true, false].reverse_list()`, + wantList: types.NewDynamicList(types.DefaultTypeAdapter, []types.Bool{false, true, true}), + }, + + "reverse string list": { + condition: `["c", "b", "a"].reverse_list()`, + wantList: types.NewDynamicList(types.DefaultTypeAdapter, []types.String{"a", "b", "c"}), }, } @@ -45,16 +174,16 @@ func Test_sortByOrder(t *testing.T) { t.Run(description, func(t *testing.T) { prg := setupProgram(t, varName, tc.condition) - gotUlDyn, _, gotErr := prg.Eval(map[string]interface{}{ - varName: tc.ul, + gotList, _, gotErr := prg.Eval(map[string]interface{}{ + varName: tc.list, }) if gotErr != nil { t.Fatalf("eval error: %s", gotErr) } - if gotUlDyn.Equal(tc.wantUlDyn) != types.True { - t.Errorf("\ngot=%v\nwant=%v", gotUlDyn, tc.wantUlDyn) + if gotList.Equal(tc.wantList) != types.True { + t.Errorf("\ngot=%v\nwant=%v", gotList, tc.wantList) } }) } @@ -141,3 +270,11 @@ func generateOrderedSlice(t *testing.T, first, second, third string) []map[strin ) return orderedItems } + +func getDates() (time.Time, time.Time, time.Time) { + now := time.Now() + first := now.Add(-(time.Duration(24) * time.Hour * 3)) + second := now.Add(-(time.Duration(24) * time.Hour * 2)) + third := now.Add(-(time.Duration(24) * time.Hour * 1)) + return first, second, third +}