Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extension library for set-based tests over lists #689

Merged
merged 2 commits into from
May 2, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions ext/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ go_library(
"math.go",
"native.go",
"protos.go",
"sets.go",
"strings.go",
],
importpath = "github.com/google/cel-go/ext",
Expand Down Expand Up @@ -43,6 +44,7 @@ go_test(
"math_test.go",
"native_test.go",
"protos_test.go",
"sets_test.go",
"strings_test.go",
],
embed = [
Expand Down
62 changes: 58 additions & 4 deletions ext/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ Example:

## Math

Math returns a cel.EnvOption to configure namespaced math helper macros and
functions.
Math helper macros and functions.

Note, all macros use the 'math' namespace; however, at the time of macro
expansion the namespace looks just like any other identifier. If you are
Expand Down Expand Up @@ -96,8 +95,7 @@ Examples:

## Protos

Protos returns a cel.EnvOption to configure extended macros and functions for
proto manipulation.
Protos configure extended macros and functions for proto manipulation.

Note, all macros use the 'proto' namespace; however, at the time of macro
expansion the namespace looks just like any other identifier. If you are
Expand Down Expand Up @@ -127,6 +125,62 @@ Example:

proto.hasExt(msg, google.expr.proto2.test.int32_ext) // returns true || false

## Sets

Sets provides set relationship tests.

There is no set type within CEL, and while one may be introduced in the
future, there are cases where a `list` type is known to behave like a set.
For such cases, this library provides some basic functionality for
determining set containment, equivalence, and intersection.

### Sets.Contains

Returns whether the first list argument contains all elements in the second
list argument. The list may contain elements of any type and standard CEL
equality is used to determine whether a value exists in both lists. If the
second list is empty, the result will always return true.

sets.contains(list(T), list(T)) -> bool

Examples:

sets.contains([], []) // true
sets.contains([], [1]) // false
sets.contains([1, 2, 3, 4], [2, 3]) // true
sets.contains([1, 2.0, 3u], [1.0, 2u, 3]) // true

### Sets.Equivalent

Returns whether the first and second list are set equivalent. Lists are set
equivalent if for every item in the first list, there is an element in the
second which is equal. The lists may not be of the same size as they do not
guarantee the elements within them are unique, so size does not factor into
the computation.

sets.equivalent(list(T), list(T)) -> bool

Examples:

sets.equivalent([], []) // true
sets.equivalent([1], [1, 1]) // true
sets.equivalent([1], [1u, 1.0]) // true
sets.equivalent([1, 2, 3], [3u, 2.0, 1]) // true

### Sets.Intersects

Returns whether the first list has at least one element whose value is equal
to an element in the second list. If either list is empty, the result will
be false.

sets.intersects(list(T), list(T)) -> bool

Examples:

sets.intersects([1], []) // false
sets.intersects([1], [1, 2]) // true
sets.intersects([[1], [2, 3]], [[1, 2], [2, 3.0]]) // true

## Strings

Extended functions for string manipulation. As a general note, all indices are
Expand Down
138 changes: 138 additions & 0 deletions ext/sets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Copyright 2023 Google LLC
//
// 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 ext

import (
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
)

// Sets returns a cel.EnvOption to configure namespaced set relationship
// functions.
//
// There is no set type within CEL, and while one may be introduced in the
// future, there are cases where a `list` type is known to behave like a set.
// For such cases, this library provides some basic functionality for
// determining set containment, equivalence, and intersection.
//
// # Sets.Contains
//
// Returns whether the first list argument contains all elements in the second
// list argument. The list may contain elements of any type and standard CEL
// equality is used to determine whether a value exists in both lists. If the
// second list is empty, the result will always return true.
//
// sets.contains(list(T), list(T)) -> bool
//
// Examples:
//
// sets.contains([], []) // true
// sets.contains([], [1]) // false
// sets.contains([1, 2, 3, 4], [2, 3]) // true
// sets.contains([1, 2.0, 3u], [1.0, 2u, 3]) // true
//
// # Sets.Equivalent
//
// Returns whether the first and second list are set equivalent. Lists are set
// equivalent if for every item in the first list, there is an element in the
// second which is equal. The lists may not be of the same size as they do not
// guarantee the elements within them are unique, so size does not factor into
// the computation.
//
// Examples:
//
// sets.equivalent([], []) // true
// sets.equivalent([1], [1, 1]) // true
// sets.equivalent([1], [1u, 1.0]) // true
// sets.equivalent([1, 2, 3], [3u, 2.0, 1]) // true
//
// # Sets.Intersects
//
// Returns whether the first list has at least one element whose value is equal
// to an element in the second list. If either list is empty, the result will
// be false.
//
// Examples:
//
// sets.intersects([1], []) // false
// sets.intersects([1], [1, 2]) // true
// sets.intersects([[1], [2, 3]], [[1, 2], [2, 3.0]]) // true
func Sets() cel.EnvOption {
return cel.Lib(setsLib{})
}

type setsLib struct{}

// LibraryName implements the SingletonLibrary interface method.
func (setsLib) LibraryName() string {
return "cel.lib.ext.sets"
}

// CompileOptions implements the Library interface method.
func (setsLib) CompileOptions() []cel.EnvOption {
listType := cel.ListType(cel.TypeParamType("T"))
return []cel.EnvOption{
cel.Function("sets.contains",
cel.Overload("list_sets_contains_list", []*cel.Type{listType, listType}, cel.BoolType,
cel.BinaryBinding(setsContains))),
cel.Function("sets.equivalent",
cel.Overload("list_sets_equivalent_list", []*cel.Type{listType, listType}, cel.BoolType,
cel.BinaryBinding(setsEquivalent))),
cel.Function("sets.intersects",
cel.Overload("list_sets_intersects_list", []*cel.Type{listType, listType}, cel.BoolType,
cel.BinaryBinding(setsIntersects))),
}
}

// ProgramOptions implements the Library interface method.
func (setsLib) ProgramOptions() []cel.ProgramOption {
return []cel.ProgramOption{}
}

func setsIntersects(listA, listB ref.Val) ref.Val {
lA := listA.(traits.Lister)
lB := listB.(traits.Lister)
it := lA.Iterator()
for it.HasNext() == types.True {
exists := lB.Contains(it.Next())
if exists == types.True {
return types.True
}
}
return types.False
}

func setsContains(list, sublist ref.Val) ref.Val {
l := list.(traits.Lister)
sub := sublist.(traits.Lister)
it := sub.Iterator()
for it.HasNext() == types.True {
exists := l.Contains(it.Next())
if exists != types.True {
return exists
}
}
return types.True
}

func setsEquivalent(listA, listB ref.Val) ref.Val {
aContainsB := setsContains(listA, listB)
if aContainsB != types.True {
return aContainsB
}
return setsContains(listB, listA)
}
116 changes: 116 additions & 0 deletions ext/sets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2023 Google LLC
//
// 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 ext

import (
"fmt"
"testing"

"github.com/google/cel-go/cel"
)

func TestSets(t *testing.T) {
setsTests := []struct {
expr string
}{
// set containment
{expr: `sets.contains([], [])`},
{expr: `sets.contains([1], [])`},
{expr: `sets.contains([1], [1])`},
{expr: `sets.contains([1], [1, 1])`},
{expr: `sets.contains([1, 1], [1])`},
{expr: `sets.contains([2, 1], [1])`},
{expr: `sets.contains([1, 2, 3, 4], [2, 3])`},
{expr: `sets.contains([1], [1.0, 1])`},
{expr: `sets.contains([1, 2], [2u, 2.0])`},
{expr: `sets.contains([1, 2u], [2, 2.0])`},
{expr: `sets.contains([1, 2.0, 3u], [1.0, 2u, 3])`},
{expr: `sets.contains([[1], [2, 3]], [[2, 3.0]])`},
{expr: `!sets.contains([1], [2])`},
{expr: `!sets.contains([1], [1, 2])`},
{expr: `!sets.contains([1], ["1", 1])`},
{expr: `!sets.contains([1], [1.1, 1u])`},
// set equivalence
{expr: `sets.equivalent([], [])`},
{expr: `sets.equivalent([1], [1])`},
{expr: `sets.equivalent([1], [1, 1])`},
{expr: `sets.equivalent([1, 1], [1])`},
{expr: `sets.equivalent([1], [1u, 1.0])`},
{expr: `sets.equivalent([1], [1u, 1.0])`},
{expr: `sets.equivalent([1, 2, 3], [3u, 2.0, 1])`},
{expr: `sets.equivalent([[1.0], [2, 3]], [[1], [2, 3.0]])`},
{expr: `!sets.equivalent([2, 1], [1])`},
{expr: `!sets.equivalent([1], [1, 2])`},
{expr: `!sets.equivalent([1, 2], [2u, 2, 2.0])`},
{expr: `!sets.equivalent([1, 2], [1u, 2, 2.3])`},
// set intersection
{expr: `sets.intersects([1], [1])`},
{expr: `sets.intersects([1], [1, 1])`},
{expr: `sets.intersects([1, 1], [1])`},
{expr: `sets.intersects([2, 1], [1])`},
{expr: `sets.intersects([1], [1, 2])`},
{expr: `sets.intersects([1], [1.0, 2])`},
{expr: `sets.intersects([1, 2], [2u, 2, 2.0])`},
{expr: `sets.intersects([1, 2], [1u, 2, 2.3])`},
{expr: `sets.intersects([[1], [2, 3]], [[1, 2], [2, 3.0]])`},
{expr: `!sets.intersects([], [])`},
{expr: `!sets.intersects([1], [])`},
{expr: `!sets.intersects([1], [2])`},
{expr: `!sets.intersects([1], ["1", 2])`},
{expr: `!sets.intersects([1], [1.1, 2u])`},
}

env := testSetsEnv(t)
for i, tst := range setsTests {
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 err != nil {
t.Fatalf("prg.Eval() failed: %v", err)
}
if out.Value() != true {
t.Errorf("prg.Eval() got %v, wanted true for expr: %s", out.Value(), tc.expr)
}
}
})
}
}

func testSetsEnv(t *testing.T, opts ...cel.EnvOption) *cel.Env {
t.Helper()
baseOpts := []cel.EnvOption{Sets()}
env, err := cel.NewEnv(append(baseOpts, opts...)...)
if err != nil {
t.Fatalf("cel.NewEnv(Sets()) failed: %v", err)
}
return env
}