diff --git a/cel/cel_test.go b/cel/cel_test.go index 00ccb7fb..5ed908fd 100644 --- a/cel/cel_test.go +++ b/cel/cel_test.go @@ -1992,6 +1992,20 @@ func TestOptionalValues(t *testing.T) { }, out: 42, }, + { + expr: `x.optMap(y, y + 1)`, + in: map[string]any{ + "x": types.OptionalNone, + }, + out: types.OptionalNone, + }, + { + expr: `x.optMap(y, y + 1)`, + in: map[string]any{ + "x": types.OptionalOf(types.Int(42)), + }, + out: types.OptionalOf(types.Int(43)), + }, { expr: `optional.ofNonZeroValue(z).or(optional.of(10)).value() == 42`, in: map[string]any{ @@ -2322,6 +2336,21 @@ func TestOptionalValues(t *testing.T) { } } +func TestOptionalMacroError(t *testing.T) { + env, err := NewEnv( + OptionalTypes(), + // Test variables. + Variable("x", OptionalType(IntType)), + ) + if err != nil { + t.Fatalf("NewEnv() failed: %v", err) + } + _, iss := env.Compile("x.optMap(y.z, y.z + 1)") + if iss.Err() == nil || !strings.Contains(iss.Err().Error(), "variable name must be a simple identifier") { + t.Errorf("optMap() got an unexpected result: %v", iss.Err()) + } +} + func BenchmarkOptionalValues(b *testing.B) { env, err := NewEnv( OptionalTypes(), diff --git a/cel/library.go b/cel/library.go index 072cec30..bcfd44f7 100644 --- a/cel/library.go +++ b/cel/library.go @@ -20,6 +20,7 @@ import ( "time" "github.com/google/cel-go/checker" + "github.com/google/cel-go/common" "github.com/google/cel-go/common/operators" "github.com/google/cel-go/common/overloads" "github.com/google/cel-go/common/types" @@ -28,6 +29,18 @@ import ( "github.com/google/cel-go/interpreter" "github.com/google/cel-go/interpreter/functions" "github.com/google/cel-go/parser" + + exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" +) + +const ( + optMapMacro = "optMap" + hasValueFunc = "hasValue" + optionalNoneFunc = "optional.none" + optionalOfFunc = "optional.of" + optionalOfNonZeroValueFunc = "optional.ofNonZeroValue" + valueFunc = "value" + unusedIterVar = "#unused" ) // Library provides a collection of EnvOption and ProgramOption values used to configure a CEL @@ -130,13 +143,16 @@ func (optionalLibrary) CompileOptions() []EnvOption { // Introduce the optional type. Types(types.OptionalType), + // Configure the optMap macro. + Macros(NewReceiverMacro(optMapMacro, 2, optMap)), + // Global and member functions for working with optional values. - Function("optional.of", + Function(optionalOfFunc, Overload("optional_of", []*Type{paramTypeV}, optionalTypeV, UnaryBinding(func(value ref.Val) ref.Val { return types.OptionalOf(value) }))), - Function("optional.ofNonZeroValue", + Function(optionalOfNonZeroValueFunc, Overload("optional_ofNonZeroValue", []*Type{paramTypeV}, optionalTypeV, UnaryBinding(func(value ref.Val) ref.Val { v, isZeroer := value.(traits.Zeroer) @@ -145,18 +161,18 @@ func (optionalLibrary) CompileOptions() []EnvOption { } return types.OptionalNone }))), - Function("optional.none", + Function(optionalNoneFunc, Overload("optional_none", []*Type{}, optionalTypeV, FunctionBinding(func(values ...ref.Val) ref.Val { return types.OptionalNone }))), - Function("value", + Function(valueFunc, MemberOverload("optional_value", []*Type{optionalTypeV}, paramTypeV, UnaryBinding(func(value ref.Val) ref.Val { opt := value.(*types.Optional) return opt.GetValue() }))), - Function("hasValue", + Function(hasValueFunc, MemberOverload("optional_hasValue", []*Type{optionalTypeV}, BoolType, UnaryBinding(func(value ref.Val) ref.Val { opt := value.(*types.Optional) @@ -190,6 +206,37 @@ func (optionalLibrary) CompileOptions() []EnvOption { } } +func optMap(meh MacroExprHelper, target *exprpb.Expr, args []*exprpb.Expr) (*exprpb.Expr, *common.Error) { + varIdent := args[0] + varName := "" + switch varIdent.GetExprKind().(type) { + case *exprpb.Expr_IdentExpr: + varName = varIdent.GetIdentExpr().GetName() + default: + return nil, &common.Error{ + Message: "optMap() variable name must be a simple identifier", + Location: meh.OffsetLocation(varIdent.GetId()), + } + } + mapExpr := args[1] + return meh.GlobalCall( + operators.Conditional, + meh.ReceiverCall(hasValueFunc, target), + meh.GlobalCall(optionalOfFunc, + meh.Fold( + unusedIterVar, + meh.NewList(), + varName, + meh.ReceiverCall(valueFunc, target), + meh.LiteralBool(false), + meh.Ident(varName), + mapExpr, + ), + ), + meh.GlobalCall(optionalNoneFunc), + ), nil +} + // ProgramOptions implements the Library interface method. func (optionalLibrary) ProgramOptions() []ProgramOption { return []ProgramOption{