rebase: update K8s packages to v0.32.1

Update K8s packages in go.mod to v0.32.1

Signed-off-by: Praveen M <m.praveen@ibm.com>
This commit is contained in:
Praveen M
2025-01-16 09:41:46 +05:30
committed by mergify[bot]
parent 5aef21ea4e
commit 7eb99fc6c9
2442 changed files with 273386 additions and 47788 deletions

View File

@ -24,6 +24,7 @@ go_library(
"//cel:go_default_library",
"//checker:go_default_library",
"//common/ast:go_default_library",
"//common/decls:go_default_library",
"//common/overloads:go_default_library",
"//common/operators:go_default_library",
"//common/types:go_default_library",
@ -31,6 +32,7 @@ go_library(
"//common/types/ref:go_default_library",
"//common/types/traits:go_default_library",
"//interpreter:go_default_library",
"//parser:go_default_library",
"@org_golang_google_protobuf//proto:go_default_library",
"@org_golang_google_protobuf//reflect/protoreflect:go_default_library",
"@org_golang_google_protobuf//types/known/structpb",
@ -61,8 +63,8 @@ go_test(
"//common/types/ref:go_default_library",
"//common/types/traits:go_default_library",
"//test:go_default_library",
"//test/proto2pb:go_default_library",
"//test/proto3pb:go_default_library",
"//test/proto2pb:go_default_library",
"//test/proto3pb:go_default_library",
"@org_golang_google_protobuf//proto:go_default_library",
"@org_golang_google_protobuf//types/known/wrapperspb:go_default_library",
"@org_golang_google_protobuf//encoding/protojson:go_default_library",

View File

@ -3,12 +3,12 @@
CEL extensions are a related set of constants, functions, macros, or other
features which may not be covered by the core CEL spec.
## Bindings
## Bindings
Returns a cel.EnvOption to configure support for local variable bindings
in expressions.
# Cel.Bind
### Cel.Bind
Binds a simple identifier to an initialization expression which may be used
in a subsequenct result expression. Bindings may also be nested within each
@ -19,11 +19,11 @@ other.
Examples:
cel.bind(a, 'hello',
cel.bind(b, 'world', a + b + b + a)) // "helloworldworldhello"
cel.bind(b, 'world', a + b + b + a)) // "helloworldworldhello"
// Avoid a list allocation within the exists comprehension.
cel.bind(valid_values, [a, b, c],
[d, e, f].exists(elem, elem in valid_values))
[d, e, f].exists(elem, elem in valid_values))
Local bindings are not guaranteed to be evaluated before use.
@ -100,7 +100,8 @@ argument. Simple numeric and list literals are supported as valid argument
types; however, other literals will be flagged as errors during macro
expansion. If the argument expression does not resolve to a numeric or
list(numeric) type during type-checking, or during runtime then an error
will be produced. If a list argument is empty, this too will produce an error.
will be produced. If a list argument is empty, this too will produce an
error.
math.least(<arg>, ...) -> <double|int|uint>
@ -117,6 +118,244 @@ Examples:
math.least(a, b) // check-time error if a or b is non-numeric
math.least(dyn('string')) // runtime error
### Math.BitOr
Introduced at version: 1
Performs a bitwise-OR operation over two int or uint values.
math.bitOr(<int>, <int>) -> <int>
math.bitOr(<uint>, <uint>) -> <uint>
Examples:
math.bitOr(1u, 2u) // returns 3u
math.bitOr(-2, -4) // returns -2
### Math.BitAnd
Introduced at version: 1
Performs a bitwise-AND operation over two int or uint values.
math.bitAnd(<int>, <int>) -> <int>
math.bitAnd(<uint>, <uint>) -> <uint>
Examples:
math.bitAnd(3u, 2u) // return 2u
math.bitAnd(3, 5) // returns 3
math.bitAnd(-3, -5) // returns -7
### Math.BitXor
Introduced at version: 1
math.bitXor(<int>, <int>) -> <int>
math.bitXor(<uint>, <uint>) -> <uint>
Performs a bitwise-XOR operation over two int or uint values.
Examples:
math.bitXor(3u, 5u) // returns 6u
math.bitXor(1, 3) // returns 2
### Math.BitNot
Introduced at version: 1
Function which accepts a single int or uint and performs a bitwise-NOT
ones-complement of the given binary value.
math.bitNot(<int>) -> <int>
math.bitNot(<uint>) -> <uint>
Examples
math.bitNot(1) // returns -1
math.bitNot(-1) // return 0
math.bitNot(0u) // returns 18446744073709551615u
### Math.BitShiftLeft
Introduced at version: 1
Perform a left shift of bits on the first parameter, by the amount of bits
specified in the second parameter. The first parameter is either a uint or
an int. The second parameter must be an int.
When the second parameter is 64 or greater, 0 will be always be returned
since the number of bits shifted is greater than or equal to the total bit
length of the number being shifted. Negative valued bit shifts will result
in a runtime error.
math.bitShiftLeft(<int>, <int>) -> <int>
math.bitShiftLeft(<uint>, <int>) -> <uint>
Examples
math.bitShiftLeft(1, 2) // returns 4
math.bitShiftLeft(-1, 2) // returns -4
math.bitShiftLeft(1u, 2) // return 4u
math.bitShiftLeft(1u, 200) // returns 0u
### Math.BitShiftRight
Introduced at version: 1
Perform a right shift of bits on the first parameter, by the amount of bits
specified in the second parameter. The first parameter is either a uint or
an int. The second parameter must be an int.
When the second parameter is 64 or greater, 0 will always be returned since
the number of bits shifted is greater than or equal to the total bit length
of the number being shifted. Negative valued bit shifts will result in a
runtime error.
The sign bit extension will not be preserved for this operation: vacant bits
on the left are filled with 0.
math.bitShiftRight(<int>, <int>) -> <int>
math.bitShiftRight(<uint>, <int>) -> <uint>
Examples
math.bitShiftRight(1024, 2) // returns 256
math.bitShiftRight(1024u, 2) // returns 256u
math.bitShiftRight(1024u, 64) // returns 0u
### Math.Ceil
Introduced at version: 1
Compute the ceiling of a double value.
math.ceil(<double>) -> <double>
Examples:
math.ceil(1.2) // returns 2.0
math.ceil(-1.2) // returns -1.0
### Math.Floor
Introduced at version: 1
Compute the floor of a double value.
math.floor(<double>) -> <double>
Examples:
math.floor(1.2) // returns 1.0
math.floor(-1.2) // returns -2.0
### Math.Round
Introduced at version: 1
Rounds the double value to the nearest whole number with ties rounding away
from zero, e.g. 1.5 -> 2.0, -1.5 -> -2.0.
math.round(<double>) -> <double>
Examples:
math.round(1.2) // returns 1.0
math.round(1.5) // returns 2.0
math.round(-1.5) // returns -2.0
### Math.Trunc
Introduced at version: 1
Truncates the fractional portion of the double value.
math.trunc(<double>) -> <double>
Examples:
math.trunc(-1.3) // returns -1.0
math.trunc(1.3) // returns 1.0
### Math.Abs
Introduced at version: 1
Returns the absolute value of the numeric type provided as input. If the
value is NaN, the output is NaN. If the input is int64 min, the function
will result in an overflow error.
math.abs(<double>) -> <double>
math.abs(<int>) -> <int>
math.abs(<uint>) -> <uint>
Examples:
math.abs(-1) // returns 1
math.abs(1) // returns 1
math.abs(-9223372036854775808) // overlflow error
### Math.Sign
Introduced at version: 1
Returns the sign of the numeric type, either -1, 0, 1 as an int, double, or
uint depending on the overload. For floating point values, if NaN is
provided as input, the output is also NaN. The implementation does not
differentiate between positive and negative zero.
math.sign(<double>) -> <double>
math.sign(<int>) -> <int>
math.sign(<uint>) -> <uint>
Examples:
math.sign(-42) // returns -1
math.sign(0) // returns 0
math.sign(42) // returns 1
### Math.IsInf
Introduced at version: 1
Returns true if the input double value is -Inf or +Inf.
math.isInf(<double>) -> <bool>
Examples:
math.isInf(1.0/0.0) // returns true
math.isInf(1.2) // returns false
### Math.IsNaN
Introduced at version: 1
Returns true if the input double value is NaN, false otherwise.
math.isNaN(<double>) -> <bool>
Examples:
math.isNaN(0.0/0.0) // returns true
math.isNaN(1.2) // returns false
### Math.IsFinite
Introduced at version: 1
Returns true if the value is a finite number. Equivalent in behavior to:
!math.isNaN(double) && !math.isInf(double)
math.isFinite(<double>) -> <bool>
Examples:
math.isFinite(0.0/0.0) // returns false
math.isFinite(1.2) // returns true
## Protos
Protos configure extended macros and functions for proto manipulation.
@ -154,6 +393,65 @@ Example:
Extended functions for list manipulation. As a general note, all indices are
zero-based.
### Distinct
**Introduced in version 2**
Returns the distinct elements of a list.
<list(T)>.distinct() -> <list(T)>
Examples:
[1, 2, 2, 3, 3, 3].distinct() // return [1, 2, 3]
["b", "b", "c", "a", "c"].distinct() // return ["b", "c", "a"]
[1, "b", 2, "b"].distinct() // return [1, "b", 2]
### Flatten
**Introduced in version 1**
Flattens a list recursively.
If an optional depth is provided, the list is flattened to a the specificied level.
A negative depth value will result in an error.
<list>.flatten(<list>) -> <list>
<list>.flatten(<list>, <int>) -> <list>
Examples:
[1,[2,3],[4]].flatten() // return [1, 2, 3, 4]
[1,[2,[3,4]]].flatten() // return [1, 2, [3, 4]]
[1,2,[],[],[3,4]].flatten() // return [1, 2, 3, 4]
[1,[2,[3,[4]]]].flatten(2) // return [1, 2, 3, [4]]
[1,[2,[3,[4]]]].flatten(-1) // error
### Range
**Introduced in version 2**
Returns a list of integers from 0 to n-1.
lists.range(<int>) -> <list(int)>
Examples:
lists.range(5) -> [0, 1, 2, 3, 4]
### Reverse
**Introduced in version 2**
Returns the elements of a list in reverse order.
<list(T)>.reverse() -> <list(T)>
Examples:
[5, 3, 1, 2].reverse() // return [2, 1, 3, 5]
### Slice
@ -164,7 +462,43 @@ Returns a new sub-list using the indexes provided.
Examples:
[1,2,3,4].slice(1, 3) // return [2, 3]
[1,2,3,4].slice(2, 4) // return [3 ,4]
[1,2,3,4].slice(2, 4) // return [3, 4]
### Sort
**Introduced in version 2**
Sorts a list with comparable elements. If the element type is not comparable
or the element types are not the same, the function will produce an error.
<list(T)>.sort() -> <list(T)>
T in {int, uint, double, bool, duration, timestamp, string, bytes}
Examples:
[3, 2, 1].sort() // return [1, 2, 3]
["b", "c", "a"].sort() // return ["a", "b", "c"]
[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.
<list(T)>.sortBy(<bindingName>, <keyExpr>) -> <list(T)>
keyExpr returns a value in {int, uint, double, bool, duration, timestamp, string, bytes}
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
@ -259,7 +593,8 @@ Examples:
'hello mellow'.indexOf('jello') // returns -1
'hello mellow'.indexOf('', 2) // returns 2
'hello mellow'.indexOf('ello', 2) // returns 7
'hello mellow'.indexOf('ello', 20) // error
'hello mellow'.indexOf('ello', 20) // returns -1
'hello mellow'.indexOf('ello', -1) // error
### Join
@ -273,10 +608,10 @@ elements in the resulting string.
Examples:
['hello', 'mellow'].join() // returns 'hellomellow'
['hello', 'mellow'].join(' ') // returns 'hello mellow'
[].join() // returns ''
[].join('/') // returns ''
['hello', 'mellow'].join() // returns 'hellomellow'
['hello', 'mellow'].join(' ') // returns 'hello mellow'
[].join() // returns ''
[].join('/') // returns ''
### LastIndexOf
@ -297,6 +632,7 @@ Examples:
'hello mellow'.lastIndexOf('ello') // returns 7
'hello mellow'.lastIndexOf('jello') // returns -1
'hello mellow'.lastIndexOf('ello', 6) // returns 1
'hello mellow'.lastIndexOf('ello', 20) // returns -1
'hello mellow'.lastIndexOf('ello', -1) // error
### LowerAscii
@ -427,4 +763,137 @@ It can be located in Version 3 of strings.
Examples:
'gums'.reverse() // returns 'smug'
'John Smith'.reverse() // returns 'htimS nhoJ'
'John Smith'.reverse() // returns 'htimS nhoJ'
## TwoVarComprehensions
TwoVarComprehensions introduces support for two-variable comprehensions.
The two-variable form of comprehensions looks similar to the one-variable
counterparts. Where possible, the same macro names were used and additional
macro signatures added. The notable distinction for two-variable comprehensions
is the introduction of `transformList`, `transformMap`, and `transformMapEntry`
support for list and map types rather than the more traditional `map` and
`filter` macros.
### All
Comprehension which tests whether all elements in the list or map satisfy a
given predicate. The `all` macro evaluates in a manner consistent with logical
AND and will short-circuit when encountering a `false` value.
<list>.all(indexVar, valueVar, <predicate>) -> bool
<map>.all(keyVar, valueVar, <predicate>) -> bool
Examples:
[1, 2, 3].all(i, j, i < j) // returns true
{'hello': 'world', 'taco': 'taco'}.all(k, v, k != v) // returns false
// Combines two-variable comprehension with single variable
{'h': ['hello', 'hi'], 'j': ['joke', 'jog']}
.all(k, vals, vals.all(v, v.startsWith(k))) // returns true
### Exists
Comprehension which tests whether any element in a list or map exists which
satisfies a given predicate. The `exists` macro evaluates in a manner consistent
with logical OR and will short-circuit when encountering a `true` value.
<list>.exists(indexVar, valueVar, <predicate>) -> bool
<map>.exists(keyVar, valueVar, <predicate>) -> bool
Examples:
{'greeting': 'hello', 'farewell': 'goodbye'}
.exists(k, v, k.startsWith('good') || v.endsWith('bye')) // returns true
[1, 2, 4, 8, 16].exists(i, v, v == 1024 && i == 10) // returns false
### ExistsOne
Comprehension which tests whether exactly one element in a list or map exists
which satisfies a given predicate expression. This comprehension does not
short-circuit in keeping with the one-variable exists one macro semantics.
<list>.existsOne(indexVar, valueVar, <predicate>)
<map>.existsOne(keyVar, valueVar, <predicate>)
This macro may also be used with the `exists_one` function name, for
compatibility with the one-variable macro of the same name.
Examples:
[1, 2, 1, 3, 1, 4].existsOne(i, v, i == 1 || v == 1) // returns false
[1, 1, 2, 2, 3, 3].existsOne(i, v, i == 2 && v == 2) // returns true
{'i': 0, 'j': 1, 'k': 2}.existsOne(i, v, i == 'l' || v == 1) // returns true
### TransformList
Comprehension which converts a map or a list into a list value. The output
expression of the comprehension determines the contents of the output list.
Elements in the list may optionally be filtered according to a predicate
expression, where elements that satisfy the predicate are transformed.
<list>.transformList(indexVar, valueVar, <transform>)
<list>.transformList(indexVar, valueVar, <filter>, <transform>)
<map>.transformList(keyVar, valueVar, <transform>)
<map>.transformList(keyVar, valueVar, <filter>, <transform>)
Examples:
[1, 2, 3].transformList(indexVar, valueVar,
(indexVar * valueVar) + valueVar) // returns [1, 4, 9]
[1, 2, 3].transformList(indexVar, valueVar, indexVar % 2 == 0
(indexVar * valueVar) + valueVar) // returns [1, 9]
{'greeting': 'hello', 'farewell': 'goodbye'}
.transformList(k, _, k) // returns ['greeting', 'farewell']
{'greeting': 'hello', 'farewell': 'goodbye'}
.transformList(_, v, v) // returns ['hello', 'goodbye']
### TransformMap
Comprehension which converts a map or a list into a map value. The output
expression of the comprehension determines the value of the output map entry;
however, the key remains fixed. Elements in the map may optionally be filtered
according to a predicate expression, where elements that satisfy the predicate
are transformed.
<list>.transformMap(indexVar, valueVar, <transform>)
<list>.transformMap(indexVar, valueVar, <filter>, <transform>)
<map>.transformMap(keyVar, valueVar, <transform>)
<map>.transformMap(keyVar, valueVar, <filter>, <transform>)
Examples:
[1, 2, 3].transformMap(indexVar, valueVar,
(indexVar * valueVar) + valueVar) // returns {0: 1, 1: 4, 2: 9}
[1, 2, 3].transformMap(indexVar, valueVar, indexVar % 2 == 0
(indexVar * valueVar) + valueVar) // returns {0: 1, 2: 9}
{'greeting': 'hello'}.transformMap(k, v, v + '!') // returns {'greeting': 'hello!'}
### TransformMapEntry
Comprehension which converts a map or a list into a map value; however, this
transform expects the entry expression be a map literal. If the transform
produces an entry which duplicates a key in the target map, the comprehension
will error. Note, that key equality is determined using CEL equality which
asserts that numeric values which are equal, even if they don't have the same
type will cause a key collision.
Elements in the map may optionally be filtered according to a predicate
expression, where elements that satisfy the predicate are transformed.
<list>.transformMap(indexVar, valueVar, <transform>)
<list>.transformMap(indexVar, valueVar, <filter>, <transform>)
<map>.transformMap(keyVar, valueVar, <transform>)
<map>.transformMap(keyVar, valueVar, <filter>, <transform>)
Examples:
// returns {'hello': 'greeting'}
{'greeting': 'hello'}.transformMapEntry(keyVar, valueVar, {valueVar: keyVar})
// reverse lookup, require all values in list be unique
[1, 2, 3].transformMapEntry(indexVar, valueVar, {valueVar: indexVar})
{'greeting': 'aloha', 'farewell': 'aloha'}
.transformMapEntry(keyVar, valueVar, {valueVar: keyVar}) // error, duplicate key

View File

@ -15,9 +15,19 @@
package ext
import (
"errors"
"fmt"
"math"
"strconv"
"strings"
"sync"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/ast"
"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/interpreter"
)
// Bindings returns a cel.EnvOption to configure support for local variable
@ -41,35 +51,120 @@ import (
// [d, e, f].exists(elem, elem in valid_values))
//
// Local bindings are not guaranteed to be evaluated before use.
func Bindings() cel.EnvOption {
return cel.Lib(celBindings{})
func Bindings(options ...BindingsOption) cel.EnvOption {
b := &celBindings{version: math.MaxUint32}
for _, o := range options {
b = o(b)
}
return cel.Lib(b)
}
const (
celNamespace = "cel"
bindMacro = "bind"
blockFunc = "@block"
unusedIterVar = "#unused"
)
type celBindings struct{}
// BindingsOption declares a functional operator for configuring the Bindings library behavior.
type BindingsOption func(*celBindings) *celBindings
func (celBindings) LibraryName() string {
// BindingsVersion sets the version of the bindings library to an explicit version.
func BindingsVersion(version uint32) BindingsOption {
return func(lib *celBindings) *celBindings {
lib.version = version
return lib
}
}
type celBindings struct {
version uint32
}
func (*celBindings) LibraryName() string {
return "cel.lib.ext.cel.bindings"
}
func (celBindings) CompileOptions() []cel.EnvOption {
return []cel.EnvOption{
func (lib *celBindings) CompileOptions() []cel.EnvOption {
opts := []cel.EnvOption{
cel.Macros(
// cel.bind(var, <init>, <expr>)
cel.ReceiverMacro(bindMacro, 3, celBind),
),
}
if lib.version >= 1 {
// The cel.@block signature takes a list of subexpressions and a typed expression which is
// used as the output type.
paramType := cel.TypeParamType("T")
opts = append(opts,
cel.Function("cel.@block",
cel.Overload("cel_block_list",
[]*cel.Type{cel.ListType(cel.DynType), paramType}, paramType)),
)
opts = append(opts, cel.ASTValidators(blockValidationExemption{}))
}
return opts
}
func (celBindings) ProgramOptions() []cel.ProgramOption {
func (lib *celBindings) ProgramOptions() []cel.ProgramOption {
if lib.version >= 1 {
celBlockPlan := func(i interpreter.Interpretable) (interpreter.Interpretable, error) {
call, ok := i.(interpreter.InterpretableCall)
if !ok {
return i, nil
}
switch call.Function() {
case "cel.@block":
args := call.Args()
if len(args) != 2 {
return nil, fmt.Errorf("cel.@block expects two arguments, but got %d", len(args))
}
expr := args[1]
// Non-empty block
if block, ok := args[0].(interpreter.InterpretableConstructor); ok {
slotExprs := block.InitVals()
return newDynamicBlock(slotExprs, expr), nil
}
// Constant valued block which can happen during runtime optimization.
if cons, ok := args[0].(interpreter.InterpretableConst); ok {
if cons.Value().Type() == types.ListType {
l := cons.Value().(traits.Lister)
if l.Size().Equal(types.IntZero) == types.True {
return args[1], nil
}
return newConstantBlock(l, expr), nil
}
}
return nil, errors.New("cel.@block expects a list constructor as the first argument")
default:
return i, nil
}
}
return []cel.ProgramOption{cel.CustomDecorator(celBlockPlan)}
}
return []cel.ProgramOption{}
}
type blockValidationExemption struct{}
// Name returns the name of the validator.
func (blockValidationExemption) Name() string {
return "cel.lib.ext.validate.functions.cel.block"
}
// Configure implements the ASTValidatorConfigurer interface and augments the list of functions to skip
// during homogeneous aggregate literal type-checks.
func (blockValidationExemption) Configure(config cel.MutableValidatorConfig) error {
functions := config.GetOrDefault(cel.HomogeneousAggregateLiteralExemptFunctions, []string{}).([]string)
functions = append(functions, "cel.@block")
return config.Set(cel.HomogeneousAggregateLiteralExemptFunctions, functions)
}
// Validate is a no-op as the intent is to simply disable strong type-checks for list literals during
// when they occur within cel.@block calls as the arg types have already been validated.
func (blockValidationExemption) Validate(env *cel.Env, _ cel.ValidatorConfig, a *ast.AST, iss *cel.Issues) {
}
func celBind(mef cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, *cel.Error) {
if !macroTargetMatchesNamespace(celNamespace, target) {
return nil, nil
@ -94,3 +189,148 @@ func celBind(mef cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Ex
resultExpr,
), nil
}
func newDynamicBlock(slotExprs []interpreter.Interpretable, expr interpreter.Interpretable) interpreter.Interpretable {
bs := &dynamicBlock{
slotExprs: slotExprs,
expr: expr,
}
bs.slotActivationPool = &sync.Pool{
New: func() any {
slotCount := len(slotExprs)
sa := &dynamicSlotActivation{
slotExprs: slotExprs,
slotCount: slotCount,
slotVals: make([]*slotVal, slotCount),
}
for i := 0; i < slotCount; i++ {
sa.slotVals[i] = &slotVal{}
}
return sa
},
}
return bs
}
type dynamicBlock struct {
slotExprs []interpreter.Interpretable
expr interpreter.Interpretable
slotActivationPool *sync.Pool
}
// ID implements the Interpretable interface method.
func (b *dynamicBlock) ID() int64 {
return b.expr.ID()
}
// Eval implements the Interpretable interface method.
func (b *dynamicBlock) Eval(activation interpreter.Activation) ref.Val {
sa := b.slotActivationPool.Get().(*dynamicSlotActivation)
sa.Activation = activation
defer b.clearSlots(sa)
return b.expr.Eval(sa)
}
func (b *dynamicBlock) clearSlots(sa *dynamicSlotActivation) {
sa.reset()
b.slotActivationPool.Put(sa)
}
type slotVal struct {
value *ref.Val
visited bool
}
type dynamicSlotActivation struct {
interpreter.Activation
slotExprs []interpreter.Interpretable
slotCount int
slotVals []*slotVal
}
// ResolveName implements the Activation interface method but handles variables prefixed with `@index`
// as special variables which exist within the slot-based memory of the cel.@block() where each slot
// refers to an expression which must be computed only once.
func (sa *dynamicSlotActivation) ResolveName(name string) (any, bool) {
if idx, found := matchSlot(name, sa.slotCount); found {
v := sa.slotVals[idx]
if v.visited {
// Return not found if the index expression refers to itself
if v.value == nil {
return nil, false
}
return *v.value, true
}
v.visited = true
val := sa.slotExprs[idx].Eval(sa)
v.value = &val
return val, true
}
return sa.Activation.ResolveName(name)
}
func (sa *dynamicSlotActivation) reset() {
sa.Activation = nil
for _, sv := range sa.slotVals {
sv.visited = false
sv.value = nil
}
}
func newConstantBlock(slots traits.Lister, expr interpreter.Interpretable) interpreter.Interpretable {
count := slots.Size().(types.Int)
return &constantBlock{slots: slots, slotCount: int(count), expr: expr}
}
type constantBlock struct {
slots traits.Lister
slotCount int
expr interpreter.Interpretable
}
// ID implements the interpreter.Interpretable interface method.
func (b *constantBlock) ID() int64 {
return b.expr.ID()
}
// Eval implements the interpreter.Interpretable interface method, and will proxy @index prefixed variable
// lookups into a set of constant slots determined from the plan step.
func (b *constantBlock) Eval(activation interpreter.Activation) ref.Val {
vars := constantSlotActivation{Activation: activation, slots: b.slots, slotCount: b.slotCount}
return b.expr.Eval(vars)
}
type constantSlotActivation struct {
interpreter.Activation
slots traits.Lister
slotCount int
}
// ResolveName implements Activation interface method and proxies @index prefixed lookups into the slot
// activation associated with the block scope.
func (sa constantSlotActivation) ResolveName(name string) (any, bool) {
if idx, found := matchSlot(name, sa.slotCount); found {
return sa.slots.Get(types.Int(idx)), true
}
return sa.Activation.ResolveName(name)
}
func matchSlot(name string, slotCount int) (int, bool) {
if idx, found := strings.CutPrefix(name, indexPrefix); found {
idx, err := strconv.Atoi(idx)
// Return not found if the index is not numeric
if err != nil {
return -1, false
}
// Return not found if the index is not a valid slot
if idx < 0 || idx >= slotCount {
return -1, false
}
return idx, true
}
return -1, false
}
var (
indexPrefix = "@index"
)

410
vendor/github.com/google/cel-go/ext/comprehensions.go generated vendored Normal file
View File

@ -0,0 +1,410 @@
// Copyright 2024 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"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/ast"
"github.com/google/cel-go/common/operators"
"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"
)
const (
mapInsert = "cel.@mapInsert"
mapInsertOverloadMap = "@mapInsert_map_map"
mapInsertOverloadKeyValue = "@mapInsert_map_key_value"
)
// TwoVarComprehensions introduces support for two-variable comprehensions.
//
// The two-variable form of comprehensions looks similar to the one-variable counterparts.
// Where possible, the same macro names were used and additional macro signatures added.
// The notable distinction for two-variable comprehensions is the introduction of
// `transformList`, `transformMap`, and `transformMapEntry` support for list and map types
// rather than the more traditional `map` and `filter` macros.
//
// # All
//
// Comprehension which tests whether all elements in the list or map satisfy a given
// predicate. The `all` macro evaluates in a manner consistent with logical AND and will
// short-circuit when encountering a `false` value.
//
// <list>.all(indexVar, valueVar, <predicate>) -> bool
// <map>.all(keyVar, valueVar, <predicate>) -> bool
//
// Examples:
//
// [1, 2, 3].all(i, j, i < j) // returns true
// {'hello': 'world', 'taco': 'taco'}.all(k, v, k != v) // returns false
//
// // Combines two-variable comprehension with single variable
// {'h': ['hello', 'hi'], 'j': ['joke', 'jog']}
// .all(k, vals, vals.all(v, v.startsWith(k))) // returns true
//
// # Exists
//
// Comprehension which tests whether any element in a list or map exists which satisfies
// a given predicate. The `exists` macro evaluates in a manner consistent with logical OR
// and will short-circuit when encountering a `true` value.
//
// <list>.exists(indexVar, valueVar, <predicate>) -> bool
// <map>.exists(keyVar, valueVar, <predicate>) -> bool
//
// Examples:
//
// {'greeting': 'hello', 'farewell': 'goodbye'}
// .exists(k, v, k.startsWith('good') || v.endsWith('bye')) // returns true
// [1, 2, 4, 8, 16].exists(i, v, v == 1024 && i == 10) // returns false
//
// # ExistsOne
//
// Comprehension which tests whether exactly one element in a list or map exists which
// satisfies a given predicate expression. This comprehension does not short-circuit in
// keeping with the one-variable exists one macro semantics.
//
// <list>.existsOne(indexVar, valueVar, <predicate>)
// <map>.existsOne(keyVar, valueVar, <predicate>)
//
// This macro may also be used with the `exists_one` function name, for compatibility
// with the one-variable macro of the same name.
//
// Examples:
//
// [1, 2, 1, 3, 1, 4].existsOne(i, v, i == 1 || v == 1) // returns false
// [1, 1, 2, 2, 3, 3].existsOne(i, v, i == 2 && v == 2) // returns true
// {'i': 0, 'j': 1, 'k': 2}.existsOne(i, v, i == 'l' || v == 1) // returns true
//
// # TransformList
//
// Comprehension which converts a map or a list into a list value. The output expression
// of the comprehension determines the contents of the output list. Elements in the list
// may optionally be filtered according to a predicate expression, where elements that
// satisfy the predicate are transformed.
//
// <list>.transformList(indexVar, valueVar, <transform>)
// <list>.transformList(indexVar, valueVar, <filter>, <transform>)
// <map>.transformList(keyVar, valueVar, <transform>)
// <map>.transformList(keyVar, valueVar, <filter>, <transform>)
//
// Examples:
//
// [1, 2, 3].transformList(indexVar, valueVar,
// (indexVar * valueVar) + valueVar) // returns [1, 4, 9]
// [1, 2, 3].transformList(indexVar, valueVar, indexVar % 2 == 0
// (indexVar * valueVar) + valueVar) // returns [1, 9]
// {'greeting': 'hello', 'farewell': 'goodbye'}
// .transformList(k, _, k) // returns ['greeting', 'farewell']
// {'greeting': 'hello', 'farewell': 'goodbye'}
// .transformList(_, v, v) // returns ['hello', 'goodbye']
//
// # TransformMap
//
// Comprehension which converts a map or a list into a map value. The output expression
// of the comprehension determines the value of the output map entry; however, the key
// remains fixed. Elements in the map may optionally be filtered according to a predicate
// expression, where elements that satisfy the predicate are transformed.
//
// <list>.transformMap(indexVar, valueVar, <transform>)
// <list>.transformMap(indexVar, valueVar, <filter>, <transform>)
// <map>.transformMap(keyVar, valueVar, <transform>)
// <map>.transformMap(keyVar, valueVar, <filter>, <transform>)
//
// Examples:
//
// [1, 2, 3].transformMap(indexVar, valueVar,
// (indexVar * valueVar) + valueVar) // returns {0: 1, 1: 4, 2: 9}
// [1, 2, 3].transformMap(indexVar, valueVar, indexVar % 2 == 0
// (indexVar * valueVar) + valueVar) // returns {0: 1, 2: 9}
// {'greeting': 'hello'}.transformMap(k, v, v + '!') // returns {'greeting': 'hello!'}
//
// # TransformMapEntry
//
// Comprehension which converts a map or a list into a map value; however, this transform
// expects the entry expression be a map literal. If the tranform produces an entry which
// duplicates a key in the target map, the comprehension will error. Note, that key
// equality is determined using CEL equality which asserts that numeric values which are
// equal, even if they don't have the same type will cause a key collision.
//
// Elements in the map may optionally be filtered according to a predicate expression, where
// elements that satisfy the predicate are transformed.
//
// <list>.transformMap(indexVar, valueVar, <transform>)
// <list>.transformMap(indexVar, valueVar, <filter>, <transform>)
// <map>.transformMap(keyVar, valueVar, <transform>)
// <map>.transformMap(keyVar, valueVar, <filter>, <transform>)
//
// Examples:
//
// // returns {'hello': 'greeting'}
// {'greeting': 'hello'}.transformMapEntry(keyVar, valueVar, {valueVar: keyVar})
// // reverse lookup, require all values in list be unique
// [1, 2, 3].transformMapEntry(indexVar, valueVar, {valueVar: indexVar})
//
// {'greeting': 'aloha', 'farewell': 'aloha'}
// .transformMapEntry(keyVar, valueVar, {valueVar: keyVar}) // error, duplicate key
func TwoVarComprehensions() cel.EnvOption {
return cel.Lib(compreV2Lib{})
}
type compreV2Lib struct{}
// LibraryName implements that SingletonLibrary interface method.
func (compreV2Lib) LibraryName() string {
return "cel.lib.ext.comprev2"
}
// CompileOptions implements the cel.Library interface method.
func (compreV2Lib) CompileOptions() []cel.EnvOption {
kType := cel.TypeParamType("K")
vType := cel.TypeParamType("V")
mapKVType := cel.MapType(kType, vType)
opts := []cel.EnvOption{
cel.Macros(
cel.ReceiverMacro("all", 3, quantifierAll),
cel.ReceiverMacro("exists", 3, quantifierExists),
cel.ReceiverMacro("existsOne", 3, quantifierExistsOne),
cel.ReceiverMacro("exists_one", 3, quantifierExistsOne),
cel.ReceiverMacro("transformList", 3, transformList),
cel.ReceiverMacro("transformList", 4, transformList),
cel.ReceiverMacro("transformMap", 3, transformMap),
cel.ReceiverMacro("transformMap", 4, transformMap),
cel.ReceiverMacro("transformMapEntry", 3, transformMapEntry),
cel.ReceiverMacro("transformMapEntry", 4, transformMapEntry),
),
cel.Function(mapInsert,
cel.Overload(mapInsertOverloadKeyValue, []*cel.Type{mapKVType, kType, vType}, mapKVType,
cel.FunctionBinding(func(args ...ref.Val) ref.Val {
m := args[0].(traits.Mapper)
k := args[1]
v := args[2]
return types.InsertMapKeyValue(m, k, v)
})),
cel.Overload(mapInsertOverloadMap, []*cel.Type{mapKVType, mapKVType}, mapKVType,
cel.BinaryBinding(func(targetMap, updateMap ref.Val) ref.Val {
tm := targetMap.(traits.Mapper)
um := updateMap.(traits.Mapper)
umIt := um.Iterator()
for umIt.HasNext() == types.True {
k := umIt.Next()
updateOrErr := types.InsertMapKeyValue(tm, k, um.Get(k))
if types.IsError(updateOrErr) {
return updateOrErr
}
tm = updateOrErr.(traits.Mapper)
}
return tm
})),
),
}
return opts
}
// ProgramOptions implements the cel.Library interface method
func (compreV2Lib) ProgramOptions() []cel.ProgramOption {
return []cel.ProgramOption{}
}
func quantifierAll(mef cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, *cel.Error) {
iterVar1, iterVar2, err := extractIterVars(mef, args[0], args[1])
if err != nil {
return nil, err
}
return mef.NewComprehensionTwoVar(
target,
iterVar1,
iterVar2,
parser.AccumulatorName,
/*accuInit=*/ mef.NewLiteral(types.True),
/*condition=*/ mef.NewCall(operators.NotStrictlyFalse, mef.NewAccuIdent()),
/*step=*/ mef.NewCall(operators.LogicalAnd, mef.NewAccuIdent(), args[2]),
/*result=*/ mef.NewAccuIdent(),
), nil
}
func quantifierExists(mef cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, *cel.Error) {
iterVar1, iterVar2, err := extractIterVars(mef, args[0], args[1])
if err != nil {
return nil, err
}
return mef.NewComprehensionTwoVar(
target,
iterVar1,
iterVar2,
parser.AccumulatorName,
/*accuInit=*/ mef.NewLiteral(types.False),
/*condition=*/ mef.NewCall(operators.NotStrictlyFalse, mef.NewCall(operators.LogicalNot, mef.NewAccuIdent())),
/*step=*/ mef.NewCall(operators.LogicalOr, mef.NewAccuIdent(), args[2]),
/*result=*/ mef.NewAccuIdent(),
), nil
}
func quantifierExistsOne(mef cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, *cel.Error) {
iterVar1, iterVar2, err := extractIterVars(mef, args[0], args[1])
if err != nil {
return nil, err
}
return mef.NewComprehensionTwoVar(
target,
iterVar1,
iterVar2,
parser.AccumulatorName,
/*accuInit=*/ mef.NewLiteral(types.Int(0)),
/*condition=*/ mef.NewLiteral(types.True),
/*step=*/ mef.NewCall(operators.Conditional, args[2],
mef.NewCall(operators.Add, mef.NewAccuIdent(), mef.NewLiteral(types.Int(1))),
mef.NewAccuIdent()),
/*result=*/ mef.NewCall(operators.Equals, mef.NewAccuIdent(), mef.NewLiteral(types.Int(1))),
), nil
}
func transformList(mef cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, *cel.Error) {
iterVar1, iterVar2, err := extractIterVars(mef, args[0], args[1])
if err != nil {
return nil, err
}
var transform ast.Expr
var filter ast.Expr
if len(args) == 4 {
filter = args[2]
transform = args[3]
} else {
filter = nil
transform = args[2]
}
// __result__ = __result__ + [transform]
step := mef.NewCall(operators.Add, mef.NewAccuIdent(), mef.NewList(transform))
if filter != nil {
// __result__ = (filter) ? __result__ + [transform] : __result__
step = mef.NewCall(operators.Conditional, filter, step, mef.NewAccuIdent())
}
return mef.NewComprehensionTwoVar(
target,
iterVar1,
iterVar2,
parser.AccumulatorName,
/*accuInit=*/ mef.NewList(),
/*condition=*/ mef.NewLiteral(types.True),
step,
/*result=*/ mef.NewAccuIdent(),
), nil
}
func transformMap(mef cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, *cel.Error) {
iterVar1, iterVar2, err := extractIterVars(mef, args[0], args[1])
if err != nil {
return nil, err
}
var transform ast.Expr
var filter ast.Expr
if len(args) == 4 {
filter = args[2]
transform = args[3]
} else {
filter = nil
transform = args[2]
}
// __result__ = cel.@mapInsert(__result__, iterVar1, transform)
step := mef.NewCall(mapInsert, mef.NewAccuIdent(), mef.NewIdent(iterVar1), transform)
if filter != nil {
// __result__ = (filter) ? cel.@mapInsert(__result__, iterVar1, transform) : __result__
step = mef.NewCall(operators.Conditional, filter, step, mef.NewAccuIdent())
}
return mef.NewComprehensionTwoVar(
target,
iterVar1,
iterVar2,
parser.AccumulatorName,
/*accuInit=*/ mef.NewMap(),
/*condition=*/ mef.NewLiteral(types.True),
step,
/*result=*/ mef.NewAccuIdent(),
), nil
}
func transformMapEntry(mef cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, *cel.Error) {
iterVar1, iterVar2, err := extractIterVars(mef, args[0], args[1])
if err != nil {
return nil, err
}
var transform ast.Expr
var filter ast.Expr
if len(args) == 4 {
filter = args[2]
transform = args[3]
} else {
filter = nil
transform = args[2]
}
// __result__ = cel.@mapInsert(__result__, transform)
step := mef.NewCall(mapInsert, mef.NewAccuIdent(), transform)
if filter != nil {
// __result__ = (filter) ? cel.@mapInsert(__result__, transform) : __result__
step = mef.NewCall(operators.Conditional, filter, step, mef.NewAccuIdent())
}
return mef.NewComprehensionTwoVar(
target,
iterVar1,
iterVar2,
parser.AccumulatorName,
/*accuInit=*/ mef.NewMap(),
/*condition=*/ mef.NewLiteral(types.True),
step,
/*result=*/ mef.NewAccuIdent(),
), nil
}
func extractIterVars(mef cel.MacroExprFactory, arg0, arg1 ast.Expr) (string, string, *cel.Error) {
iterVar1, err := extractIterVar(mef, arg0)
if err != nil {
return "", "", err
}
iterVar2, err := extractIterVar(mef, arg1)
if err != nil {
return "", "", err
}
if iterVar1 == iterVar2 {
return "", "", mef.NewError(arg1.ID(), fmt.Sprintf("duplicate variable name: %s", iterVar1))
}
if iterVar1 == parser.AccumulatorName {
return "", "", mef.NewError(arg0.ID(), "iteration variable overwrites accumulator variable")
}
if iterVar2 == parser.AccumulatorName {
return "", "", mef.NewError(arg1.ID(), "iteration variable overwrites accumulator variable")
}
return iterVar1, iterVar2, nil
}
func extractIterVar(mef cel.MacroExprFactory, target ast.Expr) (string, *cel.Error) {
iterVar, found := extractIdent(target)
if !found {
return "", mef.NewError(target.ID(), "argument must be a simple name")
}
return iterVar, nil
}

View File

@ -36,7 +36,7 @@ import (
// Examples:
//
// base64.decode('aGVsbG8=') // return b'hello'
// base64.decode('aGVsbG8') // error
// base64.decode('aGVsbG8') // return b'hello'
//
// # Base64.Encode
//
@ -79,7 +79,14 @@ func (encoderLib) ProgramOptions() []cel.ProgramOption {
}
func base64DecodeString(str string) ([]byte, error) {
return base64.StdEncoding.DecodeString(str)
b, err := base64.StdEncoding.DecodeString(str)
if err == nil {
return b, nil
}
if _, tryAltEncoding := err.(base64.CorruptInputError); tryAltEncoding {
return base64.RawStdEncoding.DecodeString(str)
}
return nil, err
}
func base64EncodeBytes(bytes []byte) (string, error) {

View File

@ -484,7 +484,7 @@ func matchConstantFormatStringWithListLiteralArgs(a *ast.AST) ast.ExprMatcher {
}
}
formatString := call.Target()
if formatString.Kind() != ast.LiteralKind && formatString.AsLiteral().Type() != cel.StringType {
if formatString.Kind() != ast.LiteralKind || formatString.AsLiteral().Type() != cel.StringType {
return false
}
args := call.Args()

View File

@ -50,14 +50,18 @@ func listStringOrError(strs []string, err error) ref.Val {
return types.DefaultTypeAdapter.NativeToValue(strs)
}
func macroTargetMatchesNamespace(ns string, target ast.Expr) bool {
func extractIdent(target ast.Expr) (string, bool) {
switch target.Kind() {
case ast.IdentKind:
if target.AsIdent() != ns {
return false
}
return true
return target.AsIdent(), true
default:
return false
return "", false
}
}
func macroTargetMatchesNamespace(ns string, target ast.Expr) bool {
if id, found := extractIdent(target); found {
return id == ns
}
return false
}

View File

@ -16,15 +16,70 @@ package ext
import (
"fmt"
"math"
"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{
cel.IntType,
cel.UintType,
cel.DoubleType,
cel.BoolType,
cel.DurationType,
cel.TimestampType,
cel.StringType,
cel.BytesType,
}
// Lists returns a cel.EnvOption to configure extended functions for list manipulation.
// As a general note, all indices are zero-based.
//
// # Distinct
//
// Introduced in version: 2
//
// Returns the distinct elements of a list.
//
// <list(T)>.distinct() -> <list(T)>
//
// Examples:
//
// [1, 2, 2, 3, 3, 3].distinct() // return [1, 2, 3]
// ["b", "b", "c", "a", "c"].distinct() // return ["b", "c", "a"]
// [1, "b", 2, "b"].distinct() // return [1, "b", 2]
//
// # Range
//
// Introduced in version: 2
//
// Returns a list of integers from 0 to n-1.
//
// lists.range(<int>) -> <list(int)>
//
// Examples:
//
// lists.range(5) -> [0, 1, 2, 3, 4]
//
// # Reverse
//
// Introduced in version: 2
//
// Returns the elements of a list in reverse order.
//
// <list(T)>.reverse() -> <list(T)>
//
// Examples:
//
// [5, 3, 1, 2].reverse() // return [2, 1, 3, 5]
//
// # Slice
//
// Returns a new sub-list using the indexes provided.
@ -35,21 +90,105 @@ import (
//
// [1,2,3,4].slice(1, 3) // return [2, 3]
// [1,2,3,4].slice(2, 4) // return [3 ,4]
func Lists() cel.EnvOption {
return cel.Lib(listsLib{})
//
// # Flatten
//
// Flattens a list recursively.
// If an optional depth is provided, the list is flattened to a the specificied level.
// A negative depth value will result in an error.
//
// <list>.flatten(<list>) -> <list>
// <list>.flatten(<list>, <int>) -> <list>
//
// Examples:
//
// [1,[2,3],[4]].flatten() // return [1, 2, 3, 4]
// [1,[2,[3,4]]].flatten() // return [1, 2, [3, 4]]
// [1,2,[],[],[3,4]].flatten() // return [1, 2, 3, 4]
// [1,[2,[3,[4]]]].flatten(2) // return [1, 2, 3, [4]]
// [1,[2,[3,[4]]]].flatten(-1) // error
//
// # Sort
//
// Introduced in version: 2
//
// Sorts a list with comparable elements. If the element type is not comparable
// or the element types are not the same, the function will produce an error.
//
// <list(T)>.sort() -> <list(T)>
// T in {int, uint, double, bool, duration, timestamp, string, bytes}
//
// Examples:
//
// [3, 2, 1].sort() // return [1, 2, 3]
// ["b", "c", "a"].sort() // return ["a", "b", "c"]
// [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.
// The output of the key expression must be a comparable type, otherwise the
// function will return an error.
//
// <list(T)>.sortBy(<bindingName>, <keyExpr>) -> <list(T)>
// keyExpr returns a value in {int, uint, double, bool, duration, timestamp, string, bytes}
// 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"]
func Lists(options ...ListsOption) cel.EnvOption {
l := &listsLib{
version: math.MaxUint32,
}
for _, o := range options {
l = o(l)
}
return cel.Lib(l)
}
type listsLib struct{}
type listsLib struct {
version uint32
}
// LibraryName implements the SingletonLibrary interface method.
func (listsLib) LibraryName() string {
return "cel.lib.ext.lists"
}
// ListsOption is a functional interface for configuring the strings library.
type ListsOption func(*listsLib) *listsLib
// ListsVersion configures the version of the string library.
//
// The version limits which functions are available. Only functions introduced
// below or equal to the given version included in the library. If this option
// is not set, all functions are available.
//
// See the library documentation to determine which version a function was introduced.
// If the documentation does not state which version a function was introduced, it can
// be assumed to be introduced at version 0, when the library was first created.
func ListsVersion(version uint32) ListsOption {
return func(lib *listsLib) *listsLib {
lib.version = version
return lib
}
}
// CompileOptions implements the Library interface method.
func (listsLib) CompileOptions() []cel.EnvOption {
func (lib listsLib) CompileOptions() []cel.EnvOption {
listType := cel.ListType(cel.TypeParamType("T"))
return []cel.EnvOption{
listListType := cel.ListType(listType)
listDyn := cel.ListType(cel.DynType)
opts := []cel.EnvOption{
cel.Function("slice",
cel.MemberOverload("list_slice",
[]*cel.Type{listType, cel.IntType, cel.IntType}, listType,
@ -66,6 +205,151 @@ func (listsLib) CompileOptions() []cel.EnvOption {
),
),
}
if lib.version >= 1 {
opts = append(opts,
cel.Function("flatten",
cel.MemberOverload("list_flatten",
[]*cel.Type{listListType}, listType,
cel.UnaryBinding(func(arg ref.Val) ref.Val {
list, ok := arg.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
flatList, err := flatten(list, 1)
if err != nil {
return types.WrapErr(err)
}
return types.DefaultTypeAdapter.NativeToValue(flatList)
}),
),
cel.MemberOverload("list_flatten_int",
[]*cel.Type{listDyn, types.IntType}, listDyn,
cel.BinaryBinding(func(arg1, arg2 ref.Val) ref.Val {
list, ok := arg1.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}
depth, ok := arg2.(types.Int)
if !ok {
return types.MaybeNoSuchOverloadErr(arg2)
}
flatList, err := flatten(list, int64(depth))
if err != nil {
return types.WrapErr(err)
}
return types.DefaultTypeAdapter.NativeToValue(flatList)
}),
),
// To handle the case where a variable of just `list(T)` is provided at runtime
// with a graceful failure more, disable the type guards since the implementation
// can handle lists which are already flat.
decls.DisableTypeGuards(true),
),
)
}
if lib.version >= 2 {
sortDecl := cel.Function("sort",
append(
templatedOverloads(comparableTypes, func(t *cel.Type) cel.FunctionOpt {
return cel.MemberOverload(
fmt.Sprintf("list_%s_sort", t.TypeName()),
[]*cel.Type{cel.ListType(t)}, cel.ListType(t),
)
}),
cel.SingletonUnaryBinding(
func(arg ref.Val) ref.Val {
list, ok := arg.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
sorted, err := sortList(list)
if err != nil {
return types.WrapErr(err)
}
return sorted
},
// List traits
traits.ListerType,
),
)...,
)
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 {
return cel.MemberOverload(
fmt.Sprintf("list_%s_sortByAssociatedKeys", u.TypeName()),
[]*cel.Type{listType, cel.ListType(u)}, listType,
)
}),
cel.SingletonBinaryBinding(
func(arg1 ref.Val, arg2 ref.Val) ref.Val {
list, ok := arg1.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}
keys, ok := arg2.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(arg2)
}
sorted, err := sortListByAssociatedKeys(list, keys)
if err != nil {
return types.WrapErr(err)
}
return sorted
},
// List traits
traits.ListerType,
),
)...,
))
opts = append(opts, cel.Function("lists.range",
cel.Overload("lists_range",
[]*cel.Type{cel.IntType}, cel.ListType(cel.IntType),
cel.FunctionBinding(func(args ...ref.Val) ref.Val {
n := args[0].(types.Int)
result, err := genRange(n)
if err != nil {
return types.WrapErr(err)
}
return result
}),
),
))
opts = append(opts, cel.Function("reverse",
cel.MemberOverload("list_reverse",
[]*cel.Type{listType}, listType,
cel.FunctionBinding(func(args ...ref.Val) ref.Val {
list := args[0].(traits.Lister)
result, err := reverseList(list)
if err != nil {
return types.WrapErr(err)
}
return result
}),
),
))
opts = append(opts, cel.Function("distinct",
cel.MemberOverload("list_distinct",
[]*cel.Type{listType}, listType,
cel.UnaryBinding(func(list ref.Val) ref.Val {
result, err := distinctList(list.(traits.Lister))
if err != nil {
return types.WrapErr(err)
}
return result
}),
),
))
}
return opts
}
// ProgramOptions implements the Library interface method.
@ -73,6 +357,24 @@ func (listsLib) ProgramOptions() []cel.ProgramOption {
return []cel.ProgramOption{}
}
func genRange(n types.Int) (ref.Val, error) {
var newList []ref.Val
for i := types.Int(0); i < n; i++ {
newList = append(newList, i)
}
return types.DefaultTypeAdapter.NativeToValue(newList), nil
}
func reverseList(list traits.Lister) (ref.Val, error) {
var newList []ref.Val
listLength := list.Size().(types.Int)
for i := types.Int(0); i < listLength; i++ {
val := list.Get(listLength - i - 1)
newList = append(newList, val)
}
return types.DefaultTypeAdapter.NativeToValue(newList), nil
}
func slice(list traits.Lister, start, end types.Int) (ref.Val, error) {
listLength := list.Size().(types.Int)
if start < 0 || end < 0 {
@ -92,3 +394,167 @@ func slice(list traits.Lister, start, end types.Int) (ref.Val, error) {
}
return types.DefaultTypeAdapter.NativeToValue(newList), nil
}
func flatten(list traits.Lister, depth int64) ([]ref.Val, error) {
if depth < 0 {
return nil, fmt.Errorf("level must be non-negative")
}
var newList []ref.Val
iter := list.Iterator()
for iter.HasNext() == types.True {
val := iter.Next()
nestedList, isList := val.(traits.Lister)
if !isList || depth == 0 {
newList = append(newList, val)
continue
} else {
flattenedList, err := flatten(nestedList, depth-1)
if err != nil {
return nil, err
}
newList = append(newList, flattenedList...)
}
}
return newList, nil
}
func sortList(list traits.Lister) (ref.Val, error) {
return sortListByAssociatedKeys(list, list)
}
// Internal function used for the implementation of sort() and sortBy().
//
// Sorts a list of arbitrary elements, according to the order produced by sorting
// another list of comparable elements. If the element type of the keys is not
// comparable or the element types are not the same, the function will produce an error.
//
// <list(T)>.@sortByAssociatedKeys(<list(U)>) -> <list(T)>
// U in {int, uint, double, bool, duration, timestamp, string, bytes}
//
// Example:
//
// ["foo", "bar", "baz"].@sortByAssociatedKeys([3, 1, 2]) // return ["bar", "baz", "foo"]
func sortListByAssociatedKeys(list, keys traits.Lister) (ref.Val, error) {
listLength := list.Size().(types.Int)
keysLength := keys.Size().(types.Int)
if listLength != keysLength {
return nil, fmt.Errorf(
"@sortByAssociatedKeys() expected a list of the same size as the associated keys list, but got %d and %d elements respectively",
listLength,
keysLength,
)
}
if listLength == 0 {
return list, nil
}
elem := keys.Get(types.IntZero)
if _, ok := elem.(traits.Comparer); !ok {
return nil, fmt.Errorf("list elements must be comparable")
}
sortedIndices := make([]ref.Val, 0, listLength)
for i := types.IntZero; i < listLength; i++ {
if keys.Get(i).Type() != elem.Type() {
return nil, fmt.Errorf("list elements must have the same type")
}
sortedIndices = append(sortedIndices, i)
}
sort.Slice(sortedIndices, func(i, j int) bool {
iKey := keys.Get(sortedIndices[i])
jKey := keys.Get(sortedIndices[j])
return iKey.(traits.Comparer).Compare(jKey) == types.IntNegOne
})
sorted := make([]ref.Val, 0, listLength)
for _, sortedIdx := range sortedIndices {
sorted = append(sorted, list.Get(sortedIdx))
}
return types.DefaultTypeAdapter.NativeToValue(sorted), nil
}
// sortByMacro transforms an expression like:
//
// mylistExpr.sortBy(e, -math.abs(e))
//
// into something equivalent to:
//
// 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, meh.Copy(varIdent), args)
if err != nil {
return nil, err
}
callExpr := meh.NewMemberCall("@sortByAssociatedKeys",
meh.Copy(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 {
return list, nil
}
uniqueList := make([]ref.Val, 0, listLength)
for i := types.IntZero; i < listLength; i++ {
val := list.Get(i)
seen := false
for j := types.IntZero; j < types.Int(len(uniqueList)); j++ {
if i == j {
continue
}
other := uniqueList[j]
if val.Equal(other) == types.True {
seen = true
break
}
}
if !seen {
uniqueList = append(uniqueList, val)
}
}
return types.DefaultTypeAdapter.NativeToValue(uniqueList), nil
}
func templatedOverloads(types []*cel.Type, template func(t *cel.Type) cel.FunctionOpt) []cel.FunctionOpt {
overloads := make([]cel.FunctionOpt, len(types))
for i, t := range types {
overloads[i] = template(t)
}
return overloads
}

View File

@ -16,6 +16,7 @@ package ext
import (
"fmt"
"math"
"strings"
"github.com/google/cel-go/cel"
@ -86,28 +87,312 @@ import (
// math.least('string') // parse error
// math.least(a, b) // check-time error if a or b is non-numeric
// math.least(dyn('string')) // runtime error
func Math() cel.EnvOption {
return cel.Lib(mathLib{})
//
// # Math.BitOr
//
// Introduced at version: 1
//
// Performs a bitwise-OR operation over two int or uint values.
//
// math.bitOr(<int>, <int>) -> <int>
// math.bitOr(<uint>, <uint>) -> <uint>
//
// Examples:
//
// math.bitOr(1u, 2u) // returns 3u
// math.bitOr(-2, -4) // returns -2
//
// # Math.BitAnd
//
// Introduced at version: 1
//
// Performs a bitwise-AND operation over two int or uint values.
//
// math.bitAnd(<int>, <int>) -> <int>
// math.bitAnd(<uint>, <uint>) -> <uint>
//
// Examples:
//
// math.bitAnd(3u, 2u) // return 2u
// math.bitAnd(3, 5) // returns 3
// math.bitAnd(-3, -5) // returns -7
//
// # Math.BitXor
//
// Introduced at version: 1
//
// math.bitXor(<int>, <int>) -> <int>
// math.bitXor(<uint>, <uint>) -> <uint>
//
// Performs a bitwise-XOR operation over two int or uint values.
//
// Examples:
//
// math.bitXor(3u, 5u) // returns 6u
// math.bitXor(1, 3) // returns 2
//
// # Math.BitNot
//
// Introduced at version: 1
//
// Function which accepts a single int or uint and performs a bitwise-NOT
// ones-complement of the given binary value.
//
// math.bitNot(<int>) -> <int>
// math.bitNot(<uint>) -> <uint>
//
// Examples
//
// math.bitNot(1) // returns -1
// math.bitNot(-1) // return 0
// math.bitNot(0u) // returns 18446744073709551615u
//
// # Math.BitShiftLeft
//
// Introduced at version: 1
//
// Perform a left shift of bits on the first parameter, by the amount of bits
// specified in the second parameter. The first parameter is either a uint or
// an int. The second parameter must be an int.
//
// When the second parameter is 64 or greater, 0 will be always be returned
// since the number of bits shifted is greater than or equal to the total bit
// length of the number being shifted. Negative valued bit shifts will result
// in a runtime error.
//
// math.bitShiftLeft(<int>, <int>) -> <int>
// math.bitShiftLeft(<uint>, <int>) -> <uint>
//
// Examples
//
// math.bitShiftLeft(1, 2) // returns 4
// math.bitShiftLeft(-1, 2) // returns -4
// math.bitShiftLeft(1u, 2) // return 4u
// math.bitShiftLeft(1u, 200) // returns 0u
//
// # Math.BitShiftRight
//
// Introduced at version: 1
//
// Perform a right shift of bits on the first parameter, by the amount of bits
// specified in the second parameter. The first parameter is either a uint or
// an int. The second parameter must be an int.
//
// When the second parameter is 64 or greater, 0 will always be returned since
// the number of bits shifted is greater than or equal to the total bit length
// of the number being shifted. Negative valued bit shifts will result in a
// runtime error.
//
// The sign bit extension will not be preserved for this operation: vacant bits
// on the left are filled with 0.
//
// math.bitShiftRight(<int>, <int>) -> <int>
// math.bitShiftRight(<uint>, <int>) -> <uint>
//
// Examples
//
// math.bitShiftRight(1024, 2) // returns 256
// math.bitShiftRight(1024u, 2) // returns 256u
// math.bitShiftRight(1024u, 64) // returns 0u
//
// # Math.Ceil
//
// Introduced at version: 1
//
// Compute the ceiling of a double value.
//
// math.ceil(<double>) -> <double>
//
// Examples:
//
// math.ceil(1.2) // returns 2.0
// math.ceil(-1.2) // returns -1.0
//
// # Math.Floor
//
// Introduced at version: 1
//
// Compute the floor of a double value.
//
// math.floor(<double>) -> <double>
//
// Examples:
//
// math.floor(1.2) // returns 1.0
// math.floor(-1.2) // returns -2.0
//
// # Math.Round
//
// Introduced at version: 1
//
// Rounds the double value to the nearest whole number with ties rounding away
// from zero, e.g. 1.5 -> 2.0, -1.5 -> -2.0.
//
// math.round(<double>) -> <double>
//
// Examples:
//
// math.round(1.2) // returns 1.0
// math.round(1.5) // returns 2.0
// math.round(-1.5) // returns -2.0
//
// # Math.Trunc
//
// Introduced at version: 1
//
// Truncates the fractional portion of the double value.
//
// math.trunc(<double>) -> <double>
//
// Examples:
//
// math.trunc(-1.3) // returns -1.0
// math.trunc(1.3) // returns 1.0
//
// # Math.Abs
//
// Introduced at version: 1
//
// Returns the absolute value of the numeric type provided as input. If the
// value is NaN, the output is NaN. If the input is int64 min, the function
// will result in an overflow error.
//
// math.abs(<double>) -> <double>
// math.abs(<int>) -> <int>
// math.abs(<uint>) -> <uint>
//
// Examples:
//
// math.abs(-1) // returns 1
// math.abs(1) // returns 1
// math.abs(-9223372036854775808) // overflow error
//
// # Math.Sign
//
// Introduced at version: 1
//
// Returns the sign of the numeric type, either -1, 0, 1 as an int, double, or
// uint depending on the overload. For floating point values, if NaN is
// provided as input, the output is also NaN. The implementation does not
// differentiate between positive and negative zero.
//
// math.sign(<double>) -> <double>
// math.sign(<int>) -> <int>
// math.sign(<uint>) -> <uint>
//
// Examples:
//
// math.sign(-42) // returns -1
// math.sign(0) // returns 0
// math.sign(42) // returns 1
//
// # Math.IsInf
//
// Introduced at version: 1
//
// Returns true if the input double value is -Inf or +Inf.
//
// math.isInf(<double>) -> <bool>
//
// Examples:
//
// math.isInf(1.0/0.0) // returns true
// math.isInf(1.2) // returns false
//
// # Math.IsNaN
//
// Introduced at version: 1
//
// Returns true if the input double value is NaN, false otherwise.
//
// math.isNaN(<double>) -> <bool>
//
// Examples:
//
// math.isNaN(0.0/0.0) // returns true
// math.isNaN(1.2) // returns false
//
// # Math.IsFinite
//
// Introduced at version: 1
//
// Returns true if the value is a finite number. Equivalent in behavior to:
// !math.isNaN(double) && !math.isInf(double)
//
// math.isFinite(<double>) -> <bool>
//
// Examples:
//
// math.isFinite(0.0/0.0) // returns false
// math.isFinite(1.2) // returns true
func Math(options ...MathOption) cel.EnvOption {
m := &mathLib{version: math.MaxUint32}
for _, o := range options {
m = o(m)
}
return cel.Lib(m)
}
const (
mathNamespace = "math"
leastMacro = "least"
greatestMacro = "greatest"
minFunc = "math.@min"
maxFunc = "math.@max"
// Min-max functions
minFunc = "math.@min"
maxFunc = "math.@max"
// Rounding functions
ceilFunc = "math.ceil"
floorFunc = "math.floor"
roundFunc = "math.round"
truncFunc = "math.trunc"
// Floating point helper functions
isInfFunc = "math.isInf"
isNanFunc = "math.isNaN"
isFiniteFunc = "math.isFinite"
// Signedness functions
absFunc = "math.abs"
signFunc = "math.sign"
// Bitwise functions
bitAndFunc = "math.bitAnd"
bitOrFunc = "math.bitOr"
bitXorFunc = "math.bitXor"
bitNotFunc = "math.bitNot"
bitShiftLeftFunc = "math.bitShiftLeft"
bitShiftRightFunc = "math.bitShiftRight"
)
type mathLib struct{}
var (
errIntOverflow = types.NewErr("integer overflow")
)
// MathOption declares a functional operator for configuring math extensions.
type MathOption func(*mathLib) *mathLib
// MathVersion sets the library version for math extensions.
func MathVersion(version uint32) MathOption {
return func(lib *mathLib) *mathLib {
lib.version = version
return lib
}
}
type mathLib struct {
version uint32
}
// LibraryName implements the SingletonLibrary interface method.
func (mathLib) LibraryName() string {
func (*mathLib) LibraryName() string {
return "cel.lib.ext.math"
}
// CompileOptions implements the Library interface method.
func (mathLib) CompileOptions() []cel.EnvOption {
return []cel.EnvOption{
func (lib *mathLib) CompileOptions() []cel.EnvOption {
opts := []cel.EnvOption{
cel.Macros(
// math.least(num, ...)
cel.ReceiverVarArgMacro(leastMacro, mathLeast),
@ -179,10 +464,95 @@ func (mathLib) CompileOptions() []cel.EnvOption {
cel.UnaryBinding(maxList)),
),
}
if lib.version >= 1 {
opts = append(opts,
// Rounding function declarations
cel.Function(ceilFunc,
cel.Overload("math_ceil_double", []*cel.Type{cel.DoubleType}, cel.DoubleType,
cel.UnaryBinding(ceil))),
cel.Function(floorFunc,
cel.Overload("math_floor_double", []*cel.Type{cel.DoubleType}, cel.DoubleType,
cel.UnaryBinding(floor))),
cel.Function(roundFunc,
cel.Overload("math_round_double", []*cel.Type{cel.DoubleType}, cel.DoubleType,
cel.UnaryBinding(round))),
cel.Function(truncFunc,
cel.Overload("math_trunc_double", []*cel.Type{cel.DoubleType}, cel.DoubleType,
cel.UnaryBinding(trunc))),
// Floating point helpers
cel.Function(isInfFunc,
cel.Overload("math_isInf_double", []*cel.Type{cel.DoubleType}, cel.BoolType,
cel.UnaryBinding(isInf))),
cel.Function(isNanFunc,
cel.Overload("math_isNaN_double", []*cel.Type{cel.DoubleType}, cel.BoolType,
cel.UnaryBinding(isNaN))),
cel.Function(isFiniteFunc,
cel.Overload("math_isFinite_double", []*cel.Type{cel.DoubleType}, cel.BoolType,
cel.UnaryBinding(isFinite))),
// Signedness functions
cel.Function(absFunc,
cel.Overload("math_abs_double", []*cel.Type{cel.DoubleType}, cel.DoubleType,
cel.UnaryBinding(absDouble)),
cel.Overload("math_abs_int", []*cel.Type{cel.IntType}, cel.IntType,
cel.UnaryBinding(absInt)),
cel.Overload("math_abs_uint", []*cel.Type{cel.UintType}, cel.UintType,
cel.UnaryBinding(identity)),
),
cel.Function(signFunc,
cel.Overload("math_sign_double", []*cel.Type{cel.DoubleType}, cel.DoubleType,
cel.UnaryBinding(sign)),
cel.Overload("math_sign_int", []*cel.Type{cel.IntType}, cel.IntType,
cel.UnaryBinding(sign)),
cel.Overload("math_sign_uint", []*cel.Type{cel.UintType}, cel.UintType,
cel.UnaryBinding(sign)),
),
// Bitwise operator declarations
cel.Function(bitAndFunc,
cel.Overload("math_bitAnd_int_int", []*cel.Type{cel.IntType, cel.IntType}, cel.IntType,
cel.BinaryBinding(bitAndPairInt)),
cel.Overload("math_bitAnd_uint_uint", []*cel.Type{cel.UintType, cel.UintType}, cel.UintType,
cel.BinaryBinding(bitAndPairUint)),
),
cel.Function(bitOrFunc,
cel.Overload("math_bitOr_int_int", []*cel.Type{cel.IntType, cel.IntType}, cel.IntType,
cel.BinaryBinding(bitOrPairInt)),
cel.Overload("math_bitOr_uint_uint", []*cel.Type{cel.UintType, cel.UintType}, cel.UintType,
cel.BinaryBinding(bitOrPairUint)),
),
cel.Function(bitXorFunc,
cel.Overload("math_bitXor_int_int", []*cel.Type{cel.IntType, cel.IntType}, cel.IntType,
cel.BinaryBinding(bitXorPairInt)),
cel.Overload("math_bitXor_uint_uint", []*cel.Type{cel.UintType, cel.UintType}, cel.UintType,
cel.BinaryBinding(bitXorPairUint)),
),
cel.Function(bitNotFunc,
cel.Overload("math_bitNot_int_int", []*cel.Type{cel.IntType}, cel.IntType,
cel.UnaryBinding(bitNotInt)),
cel.Overload("math_bitNot_uint_uint", []*cel.Type{cel.UintType}, cel.UintType,
cel.UnaryBinding(bitNotUint)),
),
cel.Function(bitShiftLeftFunc,
cel.Overload("math_bitShiftLeft_int_int", []*cel.Type{cel.IntType, cel.IntType}, cel.IntType,
cel.BinaryBinding(bitShiftLeftIntInt)),
cel.Overload("math_bitShiftLeft_uint_int", []*cel.Type{cel.UintType, cel.IntType}, cel.UintType,
cel.BinaryBinding(bitShiftLeftUintInt)),
),
cel.Function(bitShiftRightFunc,
cel.Overload("math_bitShiftRight_int_int", []*cel.Type{cel.IntType, cel.IntType}, cel.IntType,
cel.BinaryBinding(bitShiftRightIntInt)),
cel.Overload("math_bitShiftRight_uint_int", []*cel.Type{cel.UintType, cel.IntType}, cel.UintType,
cel.BinaryBinding(bitShiftRightUintInt)),
),
)
}
return opts
}
// ProgramOptions implements the Library interface method.
func (mathLib) ProgramOptions() []cel.ProgramOption {
func (*mathLib) ProgramOptions() []cel.ProgramOption {
return []cel.ProgramOption{}
}
@ -194,7 +564,7 @@ func mathLeast(meh cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.
case 0:
return nil, meh.NewError(target.ID(), "math.least() requires at least one argument")
case 1:
if isListLiteralWithValidArgs(args[0]) || isValidArgType(args[0]) {
if isListLiteralWithNumericArgs(args[0]) || isNumericArgType(args[0]) {
return meh.NewCall(minFunc, args[0]), nil
}
return nil, meh.NewError(args[0].ID(), "math.least() invalid single argument value")
@ -221,7 +591,7 @@ func mathGreatest(mef cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (a
case 0:
return nil, mef.NewError(target.ID(), "math.greatest() requires at least one argument")
case 1:
if isListLiteralWithValidArgs(args[0]) || isValidArgType(args[0]) {
if isListLiteralWithNumericArgs(args[0]) || isNumericArgType(args[0]) {
return mef.NewCall(maxFunc, args[0]), nil
}
return nil, mef.NewError(args[0].ID(), "math.greatest() invalid single argument value")
@ -244,6 +614,165 @@ func identity(val ref.Val) ref.Val {
return val
}
func ceil(val ref.Val) ref.Val {
v := val.(types.Double)
return types.Double(math.Ceil(float64(v)))
}
func floor(val ref.Val) ref.Val {
v := val.(types.Double)
return types.Double(math.Floor(float64(v)))
}
func round(val ref.Val) ref.Val {
v := val.(types.Double)
return types.Double(math.Round(float64(v)))
}
func trunc(val ref.Val) ref.Val {
v := val.(types.Double)
return types.Double(math.Trunc(float64(v)))
}
func isInf(val ref.Val) ref.Val {
v := val.(types.Double)
return types.Bool(math.IsInf(float64(v), 0))
}
func isFinite(val ref.Val) ref.Val {
v := float64(val.(types.Double))
return types.Bool(!math.IsInf(v, 0) && !math.IsNaN(v))
}
func isNaN(val ref.Val) ref.Val {
v := val.(types.Double)
return types.Bool(math.IsNaN(float64(v)))
}
func absDouble(val ref.Val) ref.Val {
v := float64(val.(types.Double))
return types.Double(math.Abs(v))
}
func absInt(val ref.Val) ref.Val {
v := int64(val.(types.Int))
if v == math.MinInt64 {
return errIntOverflow
}
if v >= 0 {
return val
}
return -types.Int(v)
}
func sign(val ref.Val) ref.Val {
switch v := val.(type) {
case types.Double:
if isNaN(v) == types.True {
return v
}
zero := types.Double(0)
if v > zero {
return types.Double(1)
}
if v < zero {
return types.Double(-1)
}
return zero
case types.Int:
return v.Compare(types.IntZero)
case types.Uint:
if v == types.Uint(0) {
return types.Uint(0)
}
return types.Uint(1)
default:
return maybeSuffixError(val, "math.sign")
}
}
func bitAndPairInt(first, second ref.Val) ref.Val {
l := first.(types.Int)
r := second.(types.Int)
return l & r
}
func bitAndPairUint(first, second ref.Val) ref.Val {
l := first.(types.Uint)
r := second.(types.Uint)
return l & r
}
func bitOrPairInt(first, second ref.Val) ref.Val {
l := first.(types.Int)
r := second.(types.Int)
return l | r
}
func bitOrPairUint(first, second ref.Val) ref.Val {
l := first.(types.Uint)
r := second.(types.Uint)
return l | r
}
func bitXorPairInt(first, second ref.Val) ref.Val {
l := first.(types.Int)
r := second.(types.Int)
return l ^ r
}
func bitXorPairUint(first, second ref.Val) ref.Val {
l := first.(types.Uint)
r := second.(types.Uint)
return l ^ r
}
func bitNotInt(value ref.Val) ref.Val {
v := value.(types.Int)
return ^v
}
func bitNotUint(value ref.Val) ref.Val {
v := value.(types.Uint)
return ^v
}
func bitShiftLeftIntInt(value, bits ref.Val) ref.Val {
v := value.(types.Int)
bs := bits.(types.Int)
if bs < types.IntZero {
return types.NewErr("math.bitShiftLeft() negative offset: %d", bs)
}
return v << bs
}
func bitShiftLeftUintInt(value, bits ref.Val) ref.Val {
v := value.(types.Uint)
bs := bits.(types.Int)
if bs < types.IntZero {
return types.NewErr("math.bitShiftLeft() negative offset: %d", bs)
}
return v << bs
}
func bitShiftRightIntInt(value, bits ref.Val) ref.Val {
v := value.(types.Int)
bs := bits.(types.Int)
if bs < types.IntZero {
return types.NewErr("math.bitShiftRight() negative offset: %d", bs)
}
return types.Int(types.Uint(v) >> bs)
}
func bitShiftRightUintInt(value, bits ref.Val) ref.Val {
v := value.(types.Uint)
bs := bits.(types.Int)
if bs < types.IntZero {
return types.NewErr("math.bitShiftRight() negative offset: %d", bs)
}
return v >> bs
}
func minPair(first, second ref.Val) ref.Val {
cmp, ok := first.(traits.Comparer)
if !ok {
@ -321,13 +850,13 @@ func checkInvalidArgs(meh cel.MacroExprFactory, funcName string, args []ast.Expr
}
func checkInvalidArgLiteral(funcName string, arg ast.Expr) error {
if !isValidArgType(arg) {
if !isNumericArgType(arg) {
return fmt.Errorf("%s simple literal arguments must be numeric", funcName)
}
return nil
}
func isValidArgType(arg ast.Expr) bool {
func isNumericArgType(arg ast.Expr) bool {
switch arg.Kind() {
case ast.LiteralKind:
c := ref.Val(arg.AsLiteral())
@ -344,7 +873,7 @@ func isValidArgType(arg ast.Expr) bool {
}
}
func isListLiteralWithValidArgs(arg ast.Expr) bool {
func isListLiteralWithNumericArgs(arg ast.Expr) bool {
switch arg.Kind() {
case ast.ListKind:
list := arg.AsList()
@ -352,7 +881,7 @@ func isListLiteralWithValidArgs(arg ast.Expr) bool {
return false
}
for _, e := range list.Elements() {
if !isValidArgType(e) {
if !isNumericArgType(e) {
return false
}
}

View File

@ -15,6 +15,7 @@
package ext
import (
"errors"
"fmt"
"reflect"
"strings"
@ -77,12 +78,45 @@ var (
// same advice holds if you are using custom type adapters and type providers. The native type
// provider composes over whichever type adapter and provider is configured in the cel.Env at
// the time that it is invoked.
func NativeTypes(refTypes ...any) cel.EnvOption {
//
// There is also the possibility to rename the fields of native structs by setting the `cel` tag
// for fields you want to override. In order to enable this feature, pass in the `EnableStructTag`
// option. Here is an example to see it in action:
//
// ```go
// package identity
//
// type Account struct {
// ID int
// OwnerName string `cel:"owner"`
// }
//
// ```
//
// The `OwnerName` field is now accessible in CEL via `owner`, e.g. `identity.Account{owner: 'bob'}`.
// In case there are duplicated field names in the struct, an error will be returned.
func NativeTypes(args ...any) cel.EnvOption {
return func(env *cel.Env) (*cel.Env, error) {
tp, err := newNativeTypeProvider(env.CELTypeAdapter(), env.CELTypeProvider(), refTypes...)
nativeTypes := make([]any, 0, len(args))
tpOptions := nativeTypeOptions{}
for _, v := range args {
switch v := v.(type) {
case NativeTypesOption:
err := v(&tpOptions)
if err != nil {
return nil, err
}
default:
nativeTypes = append(nativeTypes, v)
}
}
tp, err := newNativeTypeProvider(tpOptions, env.CELTypeAdapter(), env.CELTypeProvider(), nativeTypes...)
if err != nil {
return nil, err
}
env, err = cel.CustomTypeAdapter(tp)(env)
if err != nil {
return nil, err
@ -91,12 +125,79 @@ func NativeTypes(refTypes ...any) cel.EnvOption {
}
}
func newNativeTypeProvider(adapter types.Adapter, provider types.Provider, refTypes ...any) (*nativeTypeProvider, error) {
// NativeTypesOption is a functional interface for configuring handling of native types.
type NativeTypesOption func(*nativeTypeOptions) error
// NativeTypesFieldNameHandler is a handler for mapping a reflect.StructField to a CEL field name.
// This can be used to override the default Go struct field to CEL field name mapping.
type NativeTypesFieldNameHandler = func(field reflect.StructField) string
func fieldNameByTag(structTagToParse string) func(field reflect.StructField) string {
return func(field reflect.StructField) string {
tag, found := field.Tag.Lookup(structTagToParse)
if found {
splits := strings.Split(tag, ",")
if len(splits) > 0 {
// We make the assumption that the leftmost entry in the tag is the name.
// This seems to be true for most tags that have the concept of a name/key, such as:
// https://pkg.go.dev/encoding/xml#Marshal
// https://pkg.go.dev/encoding/json#Marshal
// https://pkg.go.dev/go.mongodb.org/mongo-driver/bson#hdr-Structs
// https://pkg.go.dev/gopkg.in/yaml.v2#Marshal
name := splits[0]
return name
}
}
return field.Name
}
}
type nativeTypeOptions struct {
// fieldNameHandler controls how CEL should perform struct field renames.
// This is most commonly used for switching to parsing based off the struct field tag,
// such as "cel" or "json".
fieldNameHandler NativeTypesFieldNameHandler
}
// ParseStructTags configures if native types field names should be overridable by CEL struct tags.
// This is equivalent to ParseStructTag("cel")
func ParseStructTags(enabled bool) NativeTypesOption {
return func(ntp *nativeTypeOptions) error {
if enabled {
ntp.fieldNameHandler = fieldNameByTag("cel")
} else {
ntp.fieldNameHandler = nil
}
return nil
}
}
// ParseStructTag configures the struct tag to parse. The 0th item in the tag is used as the name of the CEL field.
// For example:
// If the tag to parse is "cel" and the struct field has tag cel:"foo", the CEL struct field will be "foo".
// If the tag to parse is "json" and the struct field has tag json:"foo,omitempty", the CEL struct field will be "foo".
func ParseStructTag(tag string) NativeTypesOption {
return func(ntp *nativeTypeOptions) error {
ntp.fieldNameHandler = fieldNameByTag(tag)
return nil
}
}
// ParseStructField configures how to parse Go struct fields. It can be used to customize struct field parsing.
func ParseStructField(handler NativeTypesFieldNameHandler) NativeTypesOption {
return func(ntp *nativeTypeOptions) error {
ntp.fieldNameHandler = handler
return nil
}
}
func newNativeTypeProvider(tpOptions nativeTypeOptions, adapter types.Adapter, provider types.Provider, refTypes ...any) (*nativeTypeProvider, error) {
nativeTypes := make(map[string]*nativeType, len(refTypes))
for _, refType := range refTypes {
switch rt := refType.(type) {
case reflect.Type:
result, err := newNativeTypes(rt)
result, err := newNativeTypes(tpOptions.fieldNameHandler, rt)
if err != nil {
return nil, err
}
@ -104,7 +205,7 @@ func newNativeTypeProvider(adapter types.Adapter, provider types.Provider, refTy
nativeTypes[result[idx].TypeName()] = result[idx]
}
case reflect.Value:
result, err := newNativeTypes(rt.Type())
result, err := newNativeTypes(tpOptions.fieldNameHandler, rt.Type())
if err != nil {
return nil, err
}
@ -119,6 +220,7 @@ func newNativeTypeProvider(adapter types.Adapter, provider types.Provider, refTy
nativeTypes: nativeTypes,
baseAdapter: adapter,
baseProvider: provider,
options: tpOptions,
}, nil
}
@ -126,6 +228,7 @@ type nativeTypeProvider struct {
nativeTypes map[string]*nativeType
baseAdapter types.Adapter
baseProvider types.Provider
options nativeTypeOptions
}
// EnumValue proxies to the types.Provider configured at the times the NativeTypes
@ -155,6 +258,14 @@ func (tp *nativeTypeProvider) FindStructType(typeName string) (*types.Type, bool
return tp.baseProvider.FindStructType(typeName)
}
func toFieldName(fieldNameHandler NativeTypesFieldNameHandler, f reflect.StructField) string {
if fieldNameHandler == nil {
return f.Name
}
return fieldNameHandler(f)
}
// FindStructFieldNames looks up the type definition first from the native types, then from
// the backing provider type set. If found, a set of field names corresponding to the type
// will be returned.
@ -163,7 +274,7 @@ func (tp *nativeTypeProvider) FindStructFieldNames(typeName string) ([]string, b
fieldCount := t.refType.NumField()
fields := make([]string, fieldCount)
for i := 0; i < fieldCount; i++ {
fields[i] = t.refType.Field(i).Name
fields[i] = toFieldName(tp.options.fieldNameHandler, t.refType.Field(i))
}
return fields, true
}
@ -192,13 +303,13 @@ func (tp *nativeTypeProvider) FindStructFieldType(typeName, fieldName string) (*
Type: celType,
IsSet: func(obj any) bool {
refVal := reflect.Indirect(reflect.ValueOf(obj))
refField := refVal.FieldByName(fieldName)
refField := refVal.FieldByName(refField.Name)
return !refField.IsZero()
},
GetFrom: func(obj any) (any, error) {
refVal := reflect.Indirect(reflect.ValueOf(obj))
refField := refVal.FieldByName(fieldName)
return getFieldValue(tp, refField), nil
refField := refVal.FieldByName(refField.Name)
return getFieldValue(refField), nil
},
}, true
}
@ -249,6 +360,9 @@ func (tp *nativeTypeProvider) NativeToValue(val any) ref.Val {
case []byte:
return tp.baseAdapter.NativeToValue(val)
default:
if refVal.Type().Elem() == reflect.TypeOf(byte(0)) {
return tp.baseAdapter.NativeToValue(val)
}
return types.NewDynamicList(tp, val)
}
case reflect.Map:
@ -259,7 +373,7 @@ func (tp *nativeTypeProvider) NativeToValue(val any) ref.Val {
time.Time:
return tp.baseAdapter.NativeToValue(val)
default:
return newNativeObject(tp, val, rawVal)
return tp.newNativeObject(val, rawVal)
}
default:
return tp.baseAdapter.NativeToValue(val)
@ -319,13 +433,13 @@ func convertToCelType(refType reflect.Type) (*cel.Type, bool) {
return nil, false
}
func newNativeObject(adapter types.Adapter, val any, refValue reflect.Value) ref.Val {
valType, err := newNativeType(refValue.Type())
func (tp *nativeTypeProvider) newNativeObject(val any, refValue reflect.Value) ref.Val {
valType, err := newNativeType(tp.options.fieldNameHandler, refValue.Type())
if err != nil {
return types.NewErr(err.Error())
}
return &nativeObj{
Adapter: adapter,
Adapter: tp,
val: val,
valType: valType,
refValue: refValue,
@ -372,12 +486,13 @@ func (o *nativeObj) ConvertToNative(typeDesc reflect.Type) (any, error) {
if !fieldValue.IsValid() || fieldValue.IsZero() {
continue
}
fieldName := toFieldName(o.valType.fieldNameHandler, fieldType)
fieldCELVal := o.NativeToValue(fieldValue.Interface())
fieldJSONVal, err := fieldCELVal.ConvertToNative(jsonValueType)
if err != nil {
return nil, err
}
fields[fieldType.Name] = fieldJSONVal.(*structpb.Value)
fields[fieldName] = fieldJSONVal.(*structpb.Value)
}
return &structpb.Struct{Fields: fields}, nil
}
@ -469,8 +584,8 @@ func (o *nativeObj) Value() any {
return o.val
}
func newNativeTypes(rawType reflect.Type) ([]*nativeType, error) {
nt, err := newNativeType(rawType)
func newNativeTypes(fieldNameHandler NativeTypesFieldNameHandler, rawType reflect.Type) ([]*nativeType, error) {
nt, err := newNativeType(fieldNameHandler, rawType)
if err != nil {
return nil, err
}
@ -489,7 +604,7 @@ func newNativeTypes(rawType reflect.Type) ([]*nativeType, error) {
return
}
alreadySeen[t.String()] = struct{}{}
nt, ntErr := newNativeType(t)
nt, ntErr := newNativeType(fieldNameHandler, t)
if ntErr != nil {
err = ntErr
return
@ -505,7 +620,11 @@ func newNativeTypes(rawType reflect.Type) ([]*nativeType, error) {
return result, err
}
func newNativeType(rawType reflect.Type) (*nativeType, error) {
var (
errDuplicatedFieldName = errors.New("field name already exists in struct")
)
func newNativeType(fieldNameHandler NativeTypesFieldNameHandler, rawType reflect.Type) (*nativeType, error) {
refType := rawType
if refType.Kind() == reflect.Pointer {
refType = refType.Elem()
@ -513,15 +632,34 @@ func newNativeType(rawType reflect.Type) (*nativeType, error) {
if !isValidObjectType(refType) {
return nil, fmt.Errorf("unsupported reflect.Type %v, must be reflect.Struct", rawType)
}
// Since naming collisions can only happen with struct tag parsing, we only check for them if it is enabled.
if fieldNameHandler != nil {
fieldNames := make(map[string]struct{})
for idx := 0; idx < refType.NumField(); idx++ {
field := refType.Field(idx)
fieldName := toFieldName(fieldNameHandler, field)
if _, found := fieldNames[fieldName]; found {
return nil, fmt.Errorf("invalid field name `%s` in struct `%s`: %w", fieldName, refType.Name(), errDuplicatedFieldName)
} else {
fieldNames[fieldName] = struct{}{}
}
}
}
return &nativeType{
typeName: fmt.Sprintf("%s.%s", simplePkgAlias(refType.PkgPath()), refType.Name()),
refType: refType,
typeName: fmt.Sprintf("%s.%s", simplePkgAlias(refType.PkgPath()), refType.Name()),
refType: refType,
fieldNameHandler: fieldNameHandler,
}, nil
}
type nativeType struct {
typeName string
refType reflect.Type
typeName string
refType reflect.Type
fieldNameHandler NativeTypesFieldNameHandler
}
// ConvertToNative implements ref.Val.ConvertToNative.
@ -569,9 +707,26 @@ func (t *nativeType) Value() any {
return t.typeName
}
// fieldByName returns the corresponding reflect.StructField for the give name either by matching
// field tag or field name.
func (t *nativeType) fieldByName(fieldName string) (reflect.StructField, bool) {
if t.fieldNameHandler == nil {
return t.refType.FieldByName(fieldName)
}
for i := 0; i < t.refType.NumField(); i++ {
f := t.refType.Field(i)
if toFieldName(t.fieldNameHandler, f) == fieldName {
return f, true
}
}
return reflect.StructField{}, false
}
// hasField returns whether a field name has a corresponding Golang reflect.StructField
func (t *nativeType) hasField(fieldName string) (reflect.StructField, bool) {
f, found := t.refType.FieldByName(fieldName)
f, found := t.fieldByName(fieldName)
if !found || !f.IsExported() || !isSupportedType(f.Type) {
return reflect.StructField{}, false
}
@ -579,21 +734,16 @@ func (t *nativeType) hasField(fieldName string) (reflect.StructField, bool) {
}
func adaptFieldValue(adapter types.Adapter, refField reflect.Value) ref.Val {
return adapter.NativeToValue(getFieldValue(adapter, refField))
return adapter.NativeToValue(getFieldValue(refField))
}
func getFieldValue(adapter types.Adapter, refField reflect.Value) any {
func getFieldValue(refField reflect.Value) any {
if refField.IsZero() {
switch refField.Kind() {
case reflect.Array, reflect.Slice:
return types.NewDynamicList(adapter, []ref.Val{})
case reflect.Map:
return types.NewDynamicMap(adapter, map[ref.Val]ref.Val{})
case reflect.Struct:
if refField.Type() == timestampType {
return types.Timestamp{Time: time.Unix(0, 0)}
return time.Unix(0, 0)
}
return reflect.New(refField.Type()).Elem().Interface()
case reflect.Pointer:
return reflect.New(refField.Type().Elem()).Interface()
}

View File

@ -119,7 +119,8 @@ const (
// 'hello mellow'.indexOf('jello') // returns -1
// 'hello mellow'.indexOf('', 2) // returns 2
// 'hello mellow'.indexOf('ello', 2) // returns 7
// 'hello mellow'.indexOf('ello', 20) // error
// 'hello mellow'.indexOf('ello', 20) // returns -1
// 'hello mellow'.indexOf('ello', -1) // error
//
// # Join
//
@ -155,6 +156,7 @@ const (
// 'hello mellow'.lastIndexOf('ello') // returns 7
// 'hello mellow'.lastIndexOf('jello') // returns -1
// 'hello mellow'.lastIndexOf('ello', 6) // returns 1
// 'hello mellow'.lastIndexOf('ello', 20) // returns -1
// 'hello mellow'.lastIndexOf('ello', -1) // error
//
// # LowerAscii
@ -520,7 +522,7 @@ func (lib *stringLib) CompileOptions() []cel.EnvOption {
if lib.version >= 3 {
opts = append(opts,
cel.Function("reverse",
cel.MemberOverload("reverse", []*cel.Type{cel.StringType}, cel.StringType,
cel.MemberOverload("string_reverse", []*cel.Type{cel.StringType}, cel.StringType,
cel.UnaryBinding(func(str ref.Val) ref.Val {
s := str.(types.String)
return stringOrError(reverse(string(s)))
@ -561,9 +563,13 @@ func indexOfOffset(str, substr string, offset int64) (int64, error) {
off := int(offset)
runes := []rune(str)
subrunes := []rune(substr)
if off < 0 || off >= len(runes) {
if off < 0 {
return -1, fmt.Errorf("index out of range: %d", off)
}
// If the offset exceeds the length, return -1 rather than error.
if off >= len(runes) {
return -1, nil
}
for i := off; i < len(runes)-(len(subrunes)-1); i++ {
found := true
for j := 0; j < len(subrunes); j++ {
@ -594,9 +600,13 @@ func lastIndexOfOffset(str, substr string, offset int64) (int64, error) {
off := int(offset)
runes := []rune(str)
subrunes := []rune(substr)
if off < 0 || off >= len(runes) {
if off < 0 {
return -1, fmt.Errorf("index out of range: %d", off)
}
// If the offset is far greater than the length return -1
if off >= len(runes) {
return -1, nil
}
if off > len(runes)-len(subrunes) {
off = len(runes) - len(subrunes)
}