Skip to content

Commit

Permalink
Add sortBy() macro to lists extension.
Browse files Browse the repository at this point in the history
  • Loading branch information
seirl committed Oct 4, 2024
1 parent d3d3a07 commit b1ff30b
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 0 deletions.
16 changes: 16 additions & 0 deletions ext/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,22 @@ Examples:
[1, "b"].sort() // error
[[1, 2, 3]].sort() // error

# SortBy

**Introduced in version 2**

Sorts a list by a key value, i.e., the order is determined by the result of
an expression applied to each element of the list.

Examples:

[
Player { name: "foo", score: 0 },
Player { name: "bar", score: -10 },
Player { name: "baz", score: 1000 },
].sortBy(e, e.score).map(e, e.name)
== ["bar", "foo", "baz"]

## Sets

Sets provides set relationship tests.
Expand Down
62 changes: 62 additions & 0 deletions ext/lists.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ import (
"sort"

"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/ast"
"github.com/google/cel-go/common/decls"
"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/parser"
)

var comparableTypes = []*cel.Type{
Expand Down Expand Up @@ -126,6 +128,20 @@ var comparableTypes = []*cel.Type{
// [1, "b"].sort() // error
// [[1, 2, 3]].sort() // error
//
// # SortBy
//
// Sorts a list by a key value, i.e., the order is determined by the result of
// an expression applied to each element of the list.
//
// Examples:
//
// [
// Player { name: "foo", score: 0 },
// Player { name: "bar", score: -10 },
// Player { name: "baz", score: 1000 },
// ].sortBy(e, e.score).map(e, e.name)
// == ["bar", "foo", "baz"]
//
// # SortByAssociatedKeys
//
// Introduced in version: 2
Expand Down Expand Up @@ -274,6 +290,7 @@ func (lib listsLib) CompileOptions() []cel.EnvOption {
)...,
)
opts = append(opts, sortDecl)
opts = append(opts, cel.Macros(cel.ReceiverMacro("sortBy", 2, sortByMacro)))
opts = append(opts, cel.Function("sortByAssociatedKeys",
append(
templatedOverloads(comparableTypes, func(u *cel.Type) cel.FunctionOpt {
Expand Down Expand Up @@ -476,6 +493,51 @@ func sortListByAssociatedKeys(list traits.Lister, keys traits.Lister) (ref.Val,
return types.DefaultTypeAdapter.NativeToValue(sorted), nil
}

// sortByMacro transforms an expression like:
//
// mylistExpr.sortBy(e, -math.abs(e))
//
// into:
//
// cel.bind(
// __sortBy_input__,
// myListExpr,
// __sortBy_input__.sortByAssociatedKeys(__sortBy_input__.map(e, -math.abs(e))
// )
func sortByMacro(meh cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, *cel.Error) {
varIdent := meh.NewIdent("__sortBy_input__")
varName := varIdent.AsIdent()

targetKind := target.Kind()
if targetKind != ast.ListKind &&
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"))
}

mapCompr, err := parser.MakeMap(meh, varIdent, args)
if err != nil {
return nil, err
}
callExpr := meh.NewMemberCall("sortByAssociatedKeys",
varIdent,
mapCompr,
)

bindExpr := meh.NewComprehension(
meh.NewList(),
"#unused",
varName,
target,
meh.NewLiteral(types.False),
varIdent,
callExpr,
)

return bindExpr, nil
}

func distinctList(list traits.Lister) (ref.Val, error) {
listLength := list.Size().(types.Int)
if listLength == 0 {
Expand Down
7 changes: 7 additions & 0 deletions ext/lists_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ func TestLists(t *testing.T) {
{expr: `["a"].sortByAssociatedKeys([1]) == ["a"]`},
{expr: `["a", "b"].sortByAssociatedKeys([1]) == ["a"]`, err: "expected a list of the same size as the associated keys list, but got 2 and 1 elements respectively"},
{expr: `["pingu", "pinga", "robby"].sortByAssociatedKeys([0, 200, -1000]) == ["robby", "pingu", "pinga"]`},
{expr: `[].sortBy(e, e) == []`},
{expr: `["a"].sortBy(e, e) == ["a"]`},
{expr: `[-3, 1, -5, -2, 4].sortBy(e, -(e * e)) == [-5, 4, -3, -2, 1]`},
{expr: `[-3, 1, -5, -2, 4].map(e, e * 2).sortBy(e, -(e * e)) == [-10, 8, -6, -4, 2]`},
{expr: `range(3).sortBy(e, -e) == [2, 1, 0]`},
{expr: `["a", "b"].sortByAssociatedKeys([1]) == ["a"]`, err: "expected a list of the same size as the associated keys list, but got 2 and 1 elements respectively"},
{expr: `["pingu", "pinga", "robby"].sortByAssociatedKeys([0, 200, -1000]) == ["robby", "pingu", "pinga"]`},
{expr: `[].distinct() == []`},
{expr: `[1].distinct() == [1]`},
{expr: `[-2, 5, -2, 1, 1, 5, -2, 1].distinct() == [-2, 1, 5]`},
Expand Down

0 comments on commit b1ff30b

Please sign in to comment.