// Copyright 2018 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 parser import ( "fmt" "github.com/google/cel-go/common" "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" ) // MacroOpt defines a functional option for configuring macro behavior. type MacroOpt func(*macro) *macro // MacroDocs configures a list of strings into a multiline description for the macro. func MacroDocs(docs ...string) MacroOpt { return func(m *macro) *macro { m.doc = common.MultilineDescription(docs...) return m } } // MacroExamples configures a list of examples, either as a string or common.MultilineString, // into an example set to be provided with the macro Documentation() call. func MacroExamples(examples ...string) MacroOpt { return func(m *macro) *macro { m.examples = examples return m } } // NewGlobalMacro creates a Macro for a global function with the specified arg count. func NewGlobalMacro(function string, argCount int, expander MacroExpander, opts ...MacroOpt) Macro { m := ¯o{ function: function, argCount: argCount, expander: expander} for _, opt := range opts { m = opt(m) } return m } // NewReceiverMacro creates a Macro for a receiver function matching the specified arg count. func NewReceiverMacro(function string, argCount int, expander MacroExpander, opts ...MacroOpt) Macro { m := ¯o{ function: function, argCount: argCount, expander: expander, receiverStyle: true} for _, opt := range opts { m = opt(m) } return m } // NewGlobalVarArgMacro creates a Macro for a global function with a variable arg count. func NewGlobalVarArgMacro(function string, expander MacroExpander, opts ...MacroOpt) Macro { m := ¯o{ function: function, expander: expander, varArgStyle: true} for _, opt := range opts { m = opt(m) } return m } // NewReceiverVarArgMacro creates a Macro for a receiver function matching a variable arg count. func NewReceiverVarArgMacro(function string, expander MacroExpander, opts ...MacroOpt) Macro { m := ¯o{ function: function, expander: expander, receiverStyle: true, varArgStyle: true} for _, opt := range opts { m = opt(m) } return m } // Macro interface for describing the function signature to match and the MacroExpander to apply. // // Note: when a Macro should apply to multiple overloads (based on arg count) of a given function, // a Macro should be created per arg-count. type Macro interface { // Function name to match. Function() string // ArgCount for the function call. // // When the macro is a var-arg style macro, the return value will be zero, but the MacroKey // will contain a `*` where the arg count would have been. ArgCount() int // IsReceiverStyle returns true if the macro matches a receiver style call. IsReceiverStyle() bool // MacroKey returns the macro signatures accepted by this macro. // // Format: `::`. // // When the macros is a var-arg style macro, the `arg-count` value is represented as a `*`. MacroKey() string // Expander returns the MacroExpander to apply when the macro key matches the parsed call // signature. Expander() MacroExpander } // Macro type which declares the function name and arg count expected for the // macro, as well as a macro expansion function. type macro struct { function string receiverStyle bool varArgStyle bool argCount int expander MacroExpander doc string examples []string } // Function returns the macro's function name (i.e. the function whose syntax it mimics). func (m *macro) Function() string { return m.function } // ArgCount returns the number of arguments the macro expects. func (m *macro) ArgCount() int { return m.argCount } // IsReceiverStyle returns whether the macro is receiver style. func (m *macro) IsReceiverStyle() bool { return m.receiverStyle } // Expander implements the Macro interface method. func (m *macro) Expander() MacroExpander { return m.expander } // MacroKey implements the Macro interface method. func (m *macro) MacroKey() string { if m.varArgStyle { return makeVarArgMacroKey(m.function, m.receiverStyle) } return makeMacroKey(m.function, m.argCount, m.receiverStyle) } // Documentation generates documentation and examples for the macro. func (m *macro) Documentation() *common.Doc { examples := make([]*common.Doc, len(m.examples)) for i, ex := range m.examples { examples[i] = common.NewExampleDoc(ex) } return common.NewMacroDoc(m.Function(), m.doc, examples...) } func makeMacroKey(name string, args int, receiverStyle bool) string { return fmt.Sprintf("%s:%d:%v", name, args, receiverStyle) } func makeVarArgMacroKey(name string, receiverStyle bool) string { return fmt.Sprintf("%s:*:%v", name, receiverStyle) } // MacroExpander converts a call and its associated arguments into a new CEL abstract syntax tree. // // If the MacroExpander determines within the implementation that an expansion is not needed it may return // a nil Expr value to indicate a non-match. However, if an expansion is to be performed, but the arguments // are not well-formed, the result of the expansion will be an error. // // The MacroExpander accepts as arguments a MacroExprHelper as well as the arguments used in the function call // and produces as output an Expr ast node. // // Note: when the Macro.IsReceiverStyle() method returns true, the target argument will be nil. type MacroExpander func(eh ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) // ExprHelper assists with the creation of Expr values in a manner which is consistent // the internal semantics and id generation behaviors of the parser and checker libraries. type ExprHelper interface { // Copy the input expression with a brand new set of identifiers. Copy(ast.Expr) ast.Expr // Literal creates an Expr value for a scalar literal value. NewLiteral(value ref.Val) ast.Expr // NewList creates a list literal instruction with an optional set of elements. NewList(elems ...ast.Expr) ast.Expr // NewMap creates a CreateStruct instruction for a map where the map is comprised of the // optional set of key, value entries. NewMap(entries ...ast.EntryExpr) ast.Expr // NewMapEntry creates a Map Entry for the key, value pair. NewMapEntry(key ast.Expr, val ast.Expr, optional bool) ast.EntryExpr // NewStruct creates a struct literal expression with an optional set of field initializers. NewStruct(typeName string, fieldInits ...ast.EntryExpr) ast.Expr // NewStructField creates a new struct field initializer from the field name and value. NewStructField(field string, init ast.Expr, optional bool) ast.EntryExpr // NewComprehension creates a new one-variable comprehension instruction. // // - iterRange represents the expression that resolves to a list or map where the elements or // keys (respectively) will be iterated over. // - iterVar is the variable name for the list element value, or the map key, depending on the // range type. // - accuVar is the accumulation variable name, typically parser.AccumulatorName. // - accuInit is the initial expression whose value will be set for the accuVar prior to // folding. // - condition is the expression to test to determine whether to continue folding. // - step is the expression to evaluation at the conclusion of a single fold iteration. // - result is the computation to evaluate at the conclusion of the fold. // // The accuVar should not shadow variable names that you would like to reference within the // environment in the step and condition expressions. Presently, the name __result__ is commonly // used by built-in macros but this may change in the future. NewComprehension(iterRange ast.Expr, iterVar, accuVar string, accuInit, condition, step, result ast.Expr) ast.Expr // NewComprehensionTwoVar creates a new two-variable comprehension instruction. // // - iterRange represents the expression that resolves to a list or map where the elements or // keys (respectively) will be iterated over. // - iterVar is the iteration variable assigned to the list index or the map key. // - iterVar2 is the iteration variable assigned to the list element value or the map key value. // - accuVar is the accumulation variable name, typically parser.AccumulatorName. // - accuInit is the initial expression whose value will be set for the accuVar prior to // folding. // - condition is the expression to test to determine whether to continue folding. // - step is the expression to evaluation at the conclusion of a single fold iteration. // - result is the computation to evaluate at the conclusion of the fold. // // The accuVar should not shadow variable names that you would like to reference within the // environment in the step and condition expressions. Presently, the name __result__ is commonly // used by built-in macros but this may change in the future. NewComprehensionTwoVar(iterRange ast.Expr, iterVar, iterVar2, accuVar string, accuInit, condition, step, result ast.Expr) ast.Expr // NewIdent creates an identifier Expr value. NewIdent(name string) ast.Expr // NewAccuIdent returns an accumulator identifier for use with comprehension results. NewAccuIdent() ast.Expr // AccuIdentName returns the name of the accumulator identifier. AccuIdentName() string // NewCall creates a function call Expr value for a global (free) function. NewCall(function string, args ...ast.Expr) ast.Expr // NewMemberCall creates a function call Expr value for a receiver-style function. NewMemberCall(function string, target ast.Expr, args ...ast.Expr) ast.Expr // NewPresenceTest creates a Select TestOnly Expr value for modelling has() semantics. NewPresenceTest(operand ast.Expr, field string) ast.Expr // NewSelect create a field traversal Expr value. NewSelect(operand ast.Expr, field string) ast.Expr // OffsetLocation returns the Location of the expression identifier. OffsetLocation(exprID int64) common.Location // NewError associates an error message with a given expression id. NewError(exprID int64, message string) *common.Error } var ( // HasMacro expands "has(m.f)" which tests the presence of a field, avoiding the need to // specify the field as a string. HasMacro = NewGlobalMacro(operators.Has, 1, MakeHas, MacroDocs( `check a protocol buffer message for the presence of a field, or check a map`, `for the presence of a string key.`, `Only map accesses using the select notation are supported.`), MacroExamples( common.MultilineDescription( `// true if the 'address' field exists in the 'user' message`, `has(user.address)`), common.MultilineDescription( `// test whether the 'key_name' is set on the map which defines it`, `has({'key_name': 'value'}.key_name) // true`), common.MultilineDescription( `// test whether the 'id' field is set to a non-default value on the Expr{} message literal`, `has(Expr{}.id) // false`), )) // AllMacro expands "range.all(var, predicate)" into a comprehension which ensures that all // elements in the range satisfy the predicate. AllMacro = NewReceiverMacro(operators.All, 2, MakeAll, MacroDocs(`tests whether all elements in the input list or all keys in a map`, `satisfy the given predicate. The all macro behaves in a manner consistent with`, `the Logical AND operator including in how it absorbs errors and short-circuits.`), MacroExamples( `[1, 2, 3].all(x, x > 0) // true`, `[1, 2, 0].all(x, x > 0) // false`, `['apple', 'banana', 'cherry'].all(fruit, fruit.size() > 3) // true`, `[3.14, 2.71, 1.61].all(num, num < 3.0) // false`, `{'a': 1, 'b': 2, 'c': 3}.all(key, key != 'b') // false`, common.MultilineDescription( `// an empty list or map as the range will result in a trivially true result`, `[].all(x, x > 0) // true`), )) // ExistsMacro expands "range.exists(var, predicate)" into a comprehension which ensures that // some element in the range satisfies the predicate. ExistsMacro = NewReceiverMacro(operators.Exists, 2, MakeExists, MacroDocs(`tests whether any value in the list or any key in the map`, `satisfies the predicate expression. The exists macro behaves in a manner`, `consistent with the Logical OR operator including in how it absorbs errors and`, `short-circuits.`), MacroExamples( `[1, 2, 3].exists(i, i % 2 != 0) // true`, `[0, -1, 5].exists(num, num < 0) // true`, `{'x': 'foo', 'y': 'bar'}.exists(key, key.startsWith('z')) // false`, common.MultilineDescription( `// an empty list or map as the range will result in a trivially false result`, `[].exists(i, i > 0) // false`), common.MultilineDescription( `// test whether a key name equalling 'iss' exists in the map and the`, `// value contains the substring 'cel.dev'`, `// tokens = {'sub': 'me', 'iss': 'https://issuer.cel.dev'}`, `tokens.exists(k, k == 'iss' && tokens[k].contains('cel.dev'))`), )) // ExistsOneMacro expands "range.exists_one(var, predicate)", which is true if for exactly one // element in range the predicate holds. // Deprecated: Use ExistsOneMacroNew ExistsOneMacro = NewReceiverMacro(operators.ExistsOne, 2, MakeExistsOne, MacroDocs(`tests whether exactly one list element or map key satisfies`, `the predicate expression. This macro does not short-circuit in order to remain`, `consistent with logical operators being the only operators which can absorb`, `errors within CEL.`), MacroExamples( `[1, 2, 2].exists_one(i, i < 2) // true`, `{'a': 'hello', 'aa': 'hellohello'}.exists_one(k, k.startsWith('a')) // false`, `[1, 2, 3, 4].exists_one(num, num % 2 == 0) // false`, common.MultilineDescription( `// ensure exactly one key in the map ends in @acme.co`, `{'wiley@acme.co': 'coyote', 'aa@milne.co': 'bear'}.exists_one(k, k.endsWith('@acme.co')) // true`), )) // ExistsOneMacroNew expands "range.existsOne(var, predicate)", which is true if for exactly one // element in range the predicate holds. ExistsOneMacroNew = NewReceiverMacro("existsOne", 2, MakeExistsOne, MacroDocs( `tests whether exactly one list element or map key satisfies the predicate`, `expression. This macro does not short-circuit in order to remain consistent`, `with logical operators being the only operators which can absorb errors`, `within CEL.`), MacroExamples( `[1, 2, 2].existsOne(i, i < 2) // true`, `{'a': 'hello', 'aa': 'hellohello'}.existsOne(k, k.startsWith('a')) // false`, `[1, 2, 3, 4].existsOne(num, num % 2 == 0) // false`, common.MultilineDescription( `// ensure exactly one key in the map ends in @acme.co`, `{'wiley@acme.co': 'coyote', 'aa@milne.co': 'bear'}.existsOne(k, k.endsWith('@acme.co')) // true`), )) // MapMacro expands "range.map(var, function)" into a comprehension which applies the function // to each element in the range to produce a new list. MapMacro = NewReceiverMacro(operators.Map, 2, MakeMap, MacroDocs("the three-argument form of map transforms all elements in the input range."), MacroExamples( `[1, 2, 3].map(x, x * 2) // [2, 4, 6]`, `[5, 10, 15].map(x, x / 5) // [1, 2, 3]`, `['apple', 'banana'].map(fruit, fruit.upperAscii()) // ['APPLE', 'BANANA']`, common.MultilineDescription( `// Combine all map key-value pairs into a list`, `{'hi': 'you', 'howzit': 'bruv'}.map(k,`, ` k + ":" + {'hi': 'you', 'howzit': 'bruv'}[k]) // ['hi:you', 'howzit:bruv']`), )) // MapFilterMacro expands "range.map(var, predicate, function)" into a comprehension which // first filters the elements in the range by the predicate, then applies the transform function // to produce a new list. MapFilterMacro = NewReceiverMacro(operators.Map, 3, MakeMap, MacroDocs(`the four-argument form of the map transforms only elements which satisfy`, `the predicate which is equivalent to chaining the filter and three-argument`, `map macros together.`), MacroExamples( common.MultilineDescription( `// multiply only numbers divisible two, by 2`, `[1, 2, 3, 4].map(num, num % 2 == 0, num * 2) // [4, 8]`), )) // FilterMacro expands "range.filter(var, predicate)" into a comprehension which filters // elements in the range, producing a new list from the elements that satisfy the predicate. FilterMacro = NewReceiverMacro(operators.Filter, 2, MakeFilter, MacroDocs(`returns a list containing only the elements from the input list`, `that satisfy the given predicate`), MacroExamples( `[1, 2, 3].filter(x, x > 1) // [2, 3]`, `['cat', 'dog', 'bird', 'fish'].filter(pet, pet.size() == 3) // ['cat', 'dog']`, `[{'a': 10, 'b': 5, 'c': 20}].map(m, m.filter(key, m[key] > 10)) // [['c']]`, common.MultilineDescription( `// filter a list to select only emails with the @cel.dev suffix`, `['alice@buf.io', 'tristan@cel.dev'].filter(v, v.endsWith('@cel.dev')) // ['tristan@cel.dev']`), common.MultilineDescription( `// filter a map into a list, selecting only the values for keys that start with 'http-auth'`, `{'http-auth-agent': 'secret', 'user-agent': 'mozilla'}.filter(k,`, ` k.startsWith('http-auth')) // ['secret']`), )) // AllMacros includes the list of all spec-supported macros. AllMacros = []Macro{ HasMacro, AllMacro, ExistsMacro, ExistsOneMacro, ExistsOneMacroNew, MapMacro, MapFilterMacro, FilterMacro, } // NoMacros list. NoMacros = []Macro{} ) // AccumulatorName is the traditional variable name assigned to the fold accumulator variable. const AccumulatorName = "__result__" // HiddenAccumulatorName is a proposed update to the default fold accumlator variable. // @result is not normally accessible from source, preventing accidental or intentional collisions // in user expressions. const HiddenAccumulatorName = "@result" type quantifierKind int const ( quantifierAll quantifierKind = iota quantifierExists quantifierExistsOne ) // MakeAll expands the input call arguments into a comprehension that returns true if all of the // elements in the range match the predicate expressions: // .all(, ) func MakeAll(eh ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) { return makeQuantifier(quantifierAll, eh, target, args) } // MakeExists expands the input call arguments into a comprehension that returns true if any of the // elements in the range match the predicate expressions: // .exists(, ) func MakeExists(eh ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) { return makeQuantifier(quantifierExists, eh, target, args) } // MakeExistsOne expands the input call arguments into a comprehension that returns true if exactly // one of the elements in the range match the predicate expressions: // .exists_one(, ) func MakeExistsOne(eh ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) { return makeQuantifier(quantifierExistsOne, eh, target, args) } // MakeMap expands the input call arguments into a comprehension that transforms each element in the // input to produce an output list. // // There are two call patterns supported by map: // // .map(, ) // .map(, , ) // // In the second form only iterVar values which return true when provided to the predicate expression // are transformed. func MakeMap(eh ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) { v, found := extractIdent(args[0]) if !found { return nil, eh.NewError(args[0].ID(), "argument is not an identifier") } accu := eh.AccuIdentName() if v == accu || v == AccumulatorName { return nil, eh.NewError(args[0].ID(), "iteration variable overwrites accumulator variable") } var fn ast.Expr var filter ast.Expr if len(args) == 3 { filter = args[1] fn = args[2] } else { filter = nil fn = args[1] } init := eh.NewList() condition := eh.NewLiteral(types.True) step := eh.NewCall(operators.Add, eh.NewAccuIdent(), eh.NewList(fn)) if filter != nil { step = eh.NewCall(operators.Conditional, filter, step, eh.NewAccuIdent()) } return eh.NewComprehension(target, v, accu, init, condition, step, eh.NewAccuIdent()), nil } // MakeFilter expands the input call arguments into a comprehension which produces a list which contains // only elements which match the provided predicate expression: // .filter(, ) func MakeFilter(eh ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) { v, found := extractIdent(args[0]) if !found { return nil, eh.NewError(args[0].ID(), "argument is not an identifier") } accu := eh.AccuIdentName() if v == accu || v == AccumulatorName { return nil, eh.NewError(args[0].ID(), "iteration variable overwrites accumulator variable") } filter := args[1] init := eh.NewList() condition := eh.NewLiteral(types.True) step := eh.NewCall(operators.Add, eh.NewAccuIdent(), eh.NewList(args[0])) step = eh.NewCall(operators.Conditional, filter, step, eh.NewAccuIdent()) return eh.NewComprehension(target, v, accu, init, condition, step, eh.NewAccuIdent()), nil } // MakeHas expands the input call arguments into a presence test, e.g. has(.field) func MakeHas(eh ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) { if args[0].Kind() == ast.SelectKind { s := args[0].AsSelect() return eh.NewPresenceTest(s.Operand(), s.FieldName()), nil } return nil, eh.NewError(args[0].ID(), "invalid argument to has() macro") } func makeQuantifier(kind quantifierKind, eh ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) { v, found := extractIdent(args[0]) if !found { return nil, eh.NewError(args[0].ID(), "argument must be a simple name") } accu := eh.AccuIdentName() if v == accu || v == AccumulatorName { return nil, eh.NewError(args[0].ID(), "iteration variable overwrites accumulator variable") } var init ast.Expr var condition ast.Expr var step ast.Expr var result ast.Expr switch kind { case quantifierAll: init = eh.NewLiteral(types.True) condition = eh.NewCall(operators.NotStrictlyFalse, eh.NewAccuIdent()) step = eh.NewCall(operators.LogicalAnd, eh.NewAccuIdent(), args[1]) result = eh.NewAccuIdent() case quantifierExists: init = eh.NewLiteral(types.False) condition = eh.NewCall( operators.NotStrictlyFalse, eh.NewCall(operators.LogicalNot, eh.NewAccuIdent())) step = eh.NewCall(operators.LogicalOr, eh.NewAccuIdent(), args[1]) result = eh.NewAccuIdent() case quantifierExistsOne: init = eh.NewLiteral(types.Int(0)) condition = eh.NewLiteral(types.True) step = eh.NewCall(operators.Conditional, args[1], eh.NewCall(operators.Add, eh.NewAccuIdent(), eh.NewLiteral(types.Int(1))), eh.NewAccuIdent()) result = eh.NewCall(operators.Equals, eh.NewAccuIdent(), eh.NewLiteral(types.Int(1))) default: return nil, eh.NewError(args[0].ID(), fmt.Sprintf("unrecognized quantifier '%v'", kind)) } return eh.NewComprehension(target, v, accu, init, condition, step, result), nil } func extractIdent(e ast.Expr) (string, bool) { switch e.Kind() { case ast.IdentKind: return e.AsIdent(), true } return "", false }