Skip to content

Commit 54745ed

Browse files
poldsathegaul
andauthored
feat: provide a custom isSorted function (#59)
* feat: provide a custom `isSorted` function for determining whether a slice is sorted ascending. Inject the function into the playground, and fix the test that needs to verify sort order. fixes #5 * remove superfluous sort --------- Co-authored-by: Jasmin Bakalovic <[email protected]>
1 parent 47fd3ee commit 54745ed

File tree

9 files changed

+473
-9
lines changed

9 files changed

+473
-9
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,24 @@ CEL standard library and CEL Playground.
1919

2020
Take a look at [all the environment options](eval/eval.go#L31).
2121

22+
### Playground Methods
23+
24+
The following custom methods are available in the playground:
25+
26+
#### isSorted(array)
27+
28+
Returns whether the list is sorted in ascending order.
29+
```expr
30+
isSorted([1, 2, 3]) == true
31+
isSorted([1, 3, 2]) == false
32+
isSorted(["apple", "banana", "cherry"]) == true
33+
```
34+
This custom function is importable in your own Expr code by importing github.com/polds/expr-playground/functions and
35+
adding `functions.IsSorted()` to your environment. The library supports sorting on types that satisfy the
36+
`sort.Interface` interface.
37+
38+
39+
2240
## Development
2341

2442
Build the Wasm binary:

cmd/server/main.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
// Copyright 2023 Undistro Authors
2-
// Modifications Fork and conversion to Expr Copyright 2024 Peter Olds <[email protected]>
32
//
43
// Licensed under the Apache License, Version 2.0 (the "License");
54
// you may not use this file except in compliance with the License.

eval/eval.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222

2323
"github.com/expr-lang/expr"
2424
"github.com/expr-lang/expr/vm"
25+
"github.com/polds/expr-playground/functions"
2526
)
2627

2728
type RunResponse struct {
@@ -31,6 +32,8 @@ type RunResponse struct {
3132

3233
var exprEnvOptions = []expr.Option{
3334
expr.AsAny(),
35+
// Inject a custom isSorted function into the environment.
36+
functions.IsSorted(),
3437

3538
// Provide a constant timestamp to the expression environment.
3639
expr.DisableBuiltin("now"),
@@ -41,8 +44,8 @@ var exprEnvOptions = []expr.Option{
4144

4245
// Eval evaluates the expr expression against the given input.
4346
func Eval(exp string, input map[string]any) (string, error) {
44-
exprEnvOptions = append(exprEnvOptions, expr.Env(input))
45-
program, err := expr.Compile(exp, exprEnvOptions...)
47+
localOpts := append([]expr.Option{expr.Env(input)}, exprEnvOptions...)
48+
program, err := expr.Compile(exp, localOpts...)
4649
if err != nil {
4750
return "", fmt.Errorf("failed to compile the Expr expression: %w", err)
4851
}

eval/eval_test.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright 2023 Undistro Authors
2+
// Modifications Fork and conversion to Expr Copyright 2024 Peter Olds <[email protected]>
23
//
34
// Licensed under the Apache License, Version 2.0 (the "License");
45
// you may not use this file except in compliance with the License.
@@ -73,11 +74,8 @@ func TestEval(t *testing.T) {
7374
},
7475
{
7576
name: "list",
76-
// For some reason object.items == sort(object.items) is false here, but the playground evaluates it as true.
77-
// Needs further investigation.
78-
exp: `object.items == sort(object.items) && sum(object.items) == 6 && sort(object.items)[-1] == 3 && findIndex(object.items, # == 1) == 0`,
77+
exp: `isSorted(object.items) && sum(object.items) == 6 && object.items[-1] == 3 && findIndex(object.items, # == 1) == 0`,
7978
want: true,
80-
skip: true, // https://github.com/polds/expr-playground/issues/5
8179
},
8280
{
8381
name: "optional",
@@ -183,14 +181,14 @@ func TestEval(t *testing.T) {
183181
skip: true, // https://github.com/polds/expr-playground/issues/20
184182
},
185183
}
184+
186185
for _, tt := range tests {
187186
t.Run(tt.name, func(t *testing.T) {
188187
if tt.skip {
189188
t.Skip("Skipping broken test due to CEL -> Expr migration.")
190189
}
191190

192191
got, err := Eval(tt.exp, input)
193-
194192
if (err != nil) != tt.wantErr {
195193
t.Errorf("Eval() got error = %v, wantErr %t", err, tt.wantErr)
196194
return

examples.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ examples:
1717
- name: "default"
1818
expr: |
1919
// Welcome to the Expr Playground!
20-
// Expr Playground is an interactive WebAssembly powered environment to explore and experiment with the Expr-lang.
20+
// Expr Playground is an interactive WebAssembly powered environment to explore and experiment with Expr-lang.
2121
//
2222
// - Write your Expr expression here
2323
// - Use the area on the side for input data, in YAML or JSON format
2424
// - Press 'Run' to evaluate your Expr expression against the input data
2525
// - Explore our collection of examples for inspiration
26+
//
27+
// See the README on Github for information about what custom functions are available in the context.
2628
2729
account.balance >= transaction.withdrawal
2830
|| (account.overdraftProtection

functions/doc.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2024 Peter Olds <[email protected]>
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package functions provides custom importable functions for Expr runtimes that may be injected into your Expr
16+
// runtime environments.
17+
package functions

functions/is_sorted.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright 2024 Peter Olds <[email protected]>
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package functions
16+
17+
import (
18+
"cmp"
19+
"fmt"
20+
"reflect"
21+
"slices"
22+
"sort"
23+
24+
"github.com/expr-lang/expr"
25+
)
26+
27+
// IsSorted provides the isSorted function as an Expr function. It will verify that the provided type
28+
// is sorted ascending. It supports the following types:
29+
// - Injected types that support the sort.Interface
30+
// - []int
31+
// - []float64
32+
// - []string
33+
//
34+
// Usage:
35+
//
36+
// // Inject into your environment.
37+
// _, err := expr.Compile(`foo`, expr.Env(nil), functions.ExprIsSorted())
38+
//
39+
// Expression:
40+
//
41+
// isSorted([1, 2, 3])
42+
// isSorted(["a", "b", "c"])
43+
// isSorted([1.0, 2.0, 3.0])
44+
// isSorted(myCustomType) // myCustomType must implement sort.Interface
45+
func IsSorted() expr.Option {
46+
return expr.Function("isSorted", func(params ...any) (any, error) {
47+
if len(params) != 1 {
48+
return false, fmt.Errorf("expected one parameter, got %d", len(params))
49+
}
50+
return isSorted(params[0])
51+
},
52+
new(func(sort.Interface) (bool, error)),
53+
new(func([]any) (bool, error)),
54+
new(func([]int) (bool, error)),
55+
new(func([]float64) (bool, error)),
56+
new(func([]string) (bool, error)),
57+
)
58+
}
59+
60+
// isSorted attempts to determine if v is sortable, first by determine if it satisfies the sort.Interface interface,
61+
// then by checking if it is a slice of a sortable type. If the type is a slice of type []any pass it to the
62+
// isSliceSorted method which builds a new slice of the correct type and validates that it is sorted.
63+
func isSorted(v any) (any, error) {
64+
if v == nil {
65+
return false, nil
66+
}
67+
68+
switch t := v.(type) {
69+
case sort.Interface:
70+
return sort.IsSorted(t), nil
71+
72+
// There are cases where Expr is passing around an []any instead of a []int, []float64, or []string.
73+
// This logic will attempt to do its own sorting to determine if the slice is sorted.
74+
case []any:
75+
return isSliceSorted(t)
76+
case []int:
77+
return slices.IsSorted(t), nil
78+
case []float64:
79+
return slices.IsSorted(t), nil
80+
case []string:
81+
return slices.IsSorted(t), nil
82+
}
83+
return false, fmt.Errorf("type %s is not sortable", reflect.TypeOf(v))
84+
}
85+
86+
func convertTo[E cmp.Ordered](x any) (E, error) {
87+
var r E
88+
v, ok := x.(E)
89+
if !ok {
90+
return r, fmt.Errorf("mis-typed slice, expected %T, got %T", r, x)
91+
}
92+
return v, nil
93+
}
94+
95+
func less[E cmp.Ordered](vv []any) (bool, error) {
96+
for i := len(vv) - 1; i > 0; i-- {
97+
l, err := convertTo[E](vv[i-1])
98+
if err != nil {
99+
return false, err
100+
}
101+
h, err := convertTo[E](vv[i])
102+
if err != nil {
103+
return false, err
104+
}
105+
if cmp.Less(h, l) {
106+
return false, nil
107+
}
108+
}
109+
return true, nil
110+
}
111+
112+
// isSliceSorted attempts to determine if v is a slice of a sortable type.
113+
// Instead of building a slice it just walks the slice and validates that it is sorted. The first unsorted element
114+
// causes the function to return false.
115+
// Expr only supports int, float, and string types.
116+
func isSliceSorted(vv []any) (bool, error) {
117+
// We have to peek the first element to determine the type of the slice.
118+
switch t := vv[0].(type) {
119+
case int:
120+
return less[int](vv)
121+
case float64:
122+
return less[float64](vv)
123+
case string:
124+
return less[string](vv)
125+
default:
126+
return false, fmt.Errorf("unsupported type %T", t)
127+
}
128+
}

0 commit comments

Comments
 (0)