Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ CEL standard library and CEL Playground.

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

### Playground Methods

The following custom methods are available in the playground:

#### isSorted(array)

Returns whether the list is sorted in ascending order.
```expr
isSorted([1, 2, 3]) == true
isSorted([1, 3, 2]) == false
isSorted(["apple", "banana", "cherry"]) == true
```
This custom function is importable in your own Expr code by importing github.com/polds/expr-playground/functions and
adding `functions.IsSorted()` to your environment. The library supports sorting on types that satisfy the
`sort.Interface` interface.



## Development

Build the Wasm binary:
Expand Down
1 change: 0 additions & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// Copyright 2023 Undistro Authors
// Modifications Fork and conversion to Expr Copyright 2024 Peter Olds <me@polds.dev>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down
7 changes: 5 additions & 2 deletions eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

"github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm"
"github.com/polds/expr-playground/functions"
)

type RunResponse struct {
Expand All @@ -31,6 +32,8 @@ type RunResponse struct {

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

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

// Eval evaluates the expr expression against the given input.
func Eval(exp string, input map[string]any) (string, error) {
exprEnvOptions = append(exprEnvOptions, expr.Env(input))
program, err := expr.Compile(exp, exprEnvOptions...)
localOpts := append([]expr.Option{expr.Env(input)}, exprEnvOptions...)
program, err := expr.Compile(exp, localOpts...)
if err != nil {
return "", fmt.Errorf("failed to compile the Expr expression: %w", err)
}
Expand Down
8 changes: 3 additions & 5 deletions eval/eval_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright 2023 Undistro Authors
// Modifications Fork and conversion to Expr Copyright 2024 Peter Olds <me@polds.dev>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -73,11 +74,8 @@ func TestEval(t *testing.T) {
},
{
name: "list",
// For some reason object.items == sort(object.items) is false here, but the playground evaluates it as true.
// Needs further investigation.
exp: `object.items == sort(object.items) && sum(object.items) == 6 && sort(object.items)[-1] == 3 && findIndex(object.items, # == 1) == 0`,
exp: `isSorted(object.items) && sum(object.items) == 6 && object.items[-1] == 3 && findIndex(object.items, # == 1) == 0`,
want: true,
skip: true, // https://github.com/polds/expr-playground/issues/5
},
{
name: "optional",
Expand Down Expand Up @@ -183,14 +181,14 @@ func TestEval(t *testing.T) {
skip: true, // https://github.com/polds/expr-playground/issues/20
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skip {
t.Skip("Skipping broken test due to CEL -> Expr migration.")
}

got, err := Eval(tt.exp, input)

if (err != nil) != tt.wantErr {
t.Errorf("Eval() got error = %v, wantErr %t", err, tt.wantErr)
return
Expand Down
4 changes: 3 additions & 1 deletion examples.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ examples:
- name: "default"
expr: |
// Welcome to the Expr Playground!
// Expr Playground is an interactive WebAssembly powered environment to explore and experiment with the Expr-lang.
// Expr Playground is an interactive WebAssembly powered environment to explore and experiment with Expr-lang.
//
// - Write your Expr expression here
// - Use the area on the side for input data, in YAML or JSON format
// - Press 'Run' to evaluate your Expr expression against the input data
// - Explore our collection of examples for inspiration
//
// See the README on Github for information about what custom functions are available in the context.

account.balance >= transaction.withdrawal
|| (account.overdraftProtection
Expand Down
17 changes: 17 additions & 0 deletions functions/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2024 Peter Olds <me@polds.dev>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package functions provides custom importable functions for Expr runtimes that may be injected into your Expr
// runtime environments.
package functions
128 changes: 128 additions & 0 deletions functions/is_sorted.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright 2024 Peter Olds <me@polds.dev>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package functions

import (
"cmp"
"fmt"
"reflect"
"slices"
"sort"

"github.com/expr-lang/expr"
)

// IsSorted provides the isSorted function as an Expr function. It will verify that the provided type
// is sorted ascending. It supports the following types:
// - Injected types that support the sort.Interface
// - []int
// - []float64
// - []string
//
// Usage:
//
// // Inject into your environment.
// _, err := expr.Compile(`foo`, expr.Env(nil), functions.ExprIsSorted())
//
// Expression:
//
// isSorted([1, 2, 3])
// isSorted(["a", "b", "c"])
// isSorted([1.0, 2.0, 3.0])
// isSorted(myCustomType) // myCustomType must implement sort.Interface
func IsSorted() expr.Option {
return expr.Function("isSorted", func(params ...any) (any, error) {
if len(params) != 1 {
return false, fmt.Errorf("expected one parameter, got %d", len(params))
}
return isSorted(params[0])
},
new(func(sort.Interface) (bool, error)),
new(func([]any) (bool, error)),
new(func([]int) (bool, error)),
new(func([]float64) (bool, error)),
new(func([]string) (bool, error)),
)
}

// isSorted attempts to determine if v is sortable, first by determine if it satisfies the sort.Interface interface,
// 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
// isSliceSorted method which builds a new slice of the correct type and validates that it is sorted.
func isSorted(v any) (any, error) {
if v == nil {
return false, nil
}

switch t := v.(type) {
case sort.Interface:
return sort.IsSorted(t), nil

// There are cases where Expr is passing around an []any instead of a []int, []float64, or []string.
// This logic will attempt to do its own sorting to determine if the slice is sorted.
case []any:
return isSliceSorted(t)
case []int:
return slices.IsSorted(t), nil
case []float64:
return slices.IsSorted(t), nil
case []string:
return slices.IsSorted(t), nil
}
return false, fmt.Errorf("type %s is not sortable", reflect.TypeOf(v))
}

func convertTo[E cmp.Ordered](x any) (E, error) {
var r E
v, ok := x.(E)
if !ok {
return r, fmt.Errorf("mis-typed slice, expected %T, got %T", r, x)
}
return v, nil
}

func less[E cmp.Ordered](vv []any) (bool, error) {
for i := len(vv) - 1; i > 0; i-- {
l, err := convertTo[E](vv[i-1])
if err != nil {
return false, err
}
h, err := convertTo[E](vv[i])
if err != nil {
return false, err
}
if cmp.Less(h, l) {
return false, nil
}
}
return true, nil
}

// isSliceSorted attempts to determine if v is a slice of a sortable type.
// Instead of building a slice it just walks the slice and validates that it is sorted. The first unsorted element
// causes the function to return false.
// Expr only supports int, float, and string types.
func isSliceSorted(vv []any) (bool, error) {
// We have to peek the first element to determine the type of the slice.
switch t := vv[0].(type) {
case int:
return less[int](vv)
case float64:
return less[float64](vv)
case string:
return less[string](vv)
default:
return false, fmt.Errorf("unsupported type %T", t)
}
}
Loading