| // Copyright 2019 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 cel |
| |
| import ( |
| "errors" |
| "fmt" |
| |
| "google.golang.org/protobuf/proto" |
| "google.golang.org/protobuf/reflect/protodesc" |
| "google.golang.org/protobuf/reflect/protoreflect" |
| "google.golang.org/protobuf/reflect/protoregistry" |
| "google.golang.org/protobuf/types/dynamicpb" |
| |
| "github.com/google/cel-go/checker" |
| "github.com/google/cel-go/common/containers" |
| "github.com/google/cel-go/common/decls" |
| "github.com/google/cel-go/common/env" |
| "github.com/google/cel-go/common/functions" |
| "github.com/google/cel-go/common/types" |
| "github.com/google/cel-go/common/types/pb" |
| "github.com/google/cel-go/common/types/ref" |
| "github.com/google/cel-go/interpreter" |
| "github.com/google/cel-go/parser" |
| |
| exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" |
| descpb "google.golang.org/protobuf/types/descriptorpb" |
| ) |
| |
| // These constants beginning with "Feature" enable optional behavior in |
| // the library. See the documentation for each constant to see its |
| // effects, compatibility restrictions, and standard conformance. |
| const ( |
| _ = iota |
| |
| // Enable the tracking of function call expressions replaced by macros. |
| featureEnableMacroCallTracking |
| |
| // Enable the use of cross-type numeric comparisons at the type-checker. |
| featureCrossTypeNumericComparisons |
| |
| // Enable eager validation of declarations to ensure that Env values created |
| // with `Extend` inherit a validated list of declarations from the parent Env. |
| featureEagerlyValidateDeclarations |
| |
| // Enable the use of the default UTC timezone when a timezone is not specified |
| // on a CEL timestamp operation. This fixes the scenario where the input time |
| // is not already in UTC. |
| featureDefaultUTCTimeZone |
| |
| // Enable the serialization of logical operator ASTs as variadic calls, thus |
| // compressing the logic graph to a single call when multiple like-operator |
| // expressions occur: e.g. a && b && c && d -> call(_&&_, [a, b, c, d]) |
| featureVariadicLogicalASTs |
| |
| // Enable error generation when a presence test or optional field selection is |
| // performed on a primitive type. |
| featureEnableErrorOnBadPresenceTest |
| |
| // Enable escape syntax for field identifiers (`). |
| featureIdentEscapeSyntax |
| |
| // Enable accessing fields by JSON names within protobuf messages |
| featureJSONFieldNames |
| ) |
| |
| var featureIDsToNames = map[int]string{ |
| featureEnableMacroCallTracking: "cel.feature.macro_call_tracking", |
| featureCrossTypeNumericComparisons: "cel.feature.cross_type_numeric_comparisons", |
| featureIdentEscapeSyntax: "cel.feature.backtick_escape_syntax", |
| featureJSONFieldNames: "cel.feature.json_field_names", |
| } |
| |
| func featureNameByID(id int) (string, bool) { |
| name, found := featureIDsToNames[id] |
| return name, found |
| } |
| |
| func featureIDByName(name string) (int, bool) { |
| for id, n := range featureIDsToNames { |
| if n == name { |
| return id, true |
| } |
| } |
| return 0, false |
| } |
| |
| // limitID is used as a key for configurable limits. These are options that |
| // support exporting to YAML environment config. |
| type limitID int |
| |
| const ( |
| _ = limitID(iota) |
| // The number of recursive calls permitted in parsing. |
| limitParseRecursionDepth |
| // The number of code points permitted in an input expression string. |
| limitCodePointSize |
| // The number of attempts to recover from a parse error. |
| limitParseErrorRecovery |
| ) |
| |
| var limitIDsToNames = map[limitID]string{ |
| limitCodePointSize: "cel.limit.expression_code_points", |
| limitParseErrorRecovery: "cel.limit.parse_error_recovery", |
| limitParseRecursionDepth: "cel.limit.parse_recursion_depth", |
| } |
| |
| func limitNameByID(id limitID) (string, bool) { |
| v, ok := limitIDsToNames[id] |
| return v, ok |
| } |
| |
| func limitIDByName(name string) (limitID, bool) { |
| for k, v := range limitIDsToNames { |
| if v == name { |
| return k, true |
| } |
| } |
| return limitID(0), false |
| } |
| |
| // EnvOption is a functional interface for configuring the environment. |
| type EnvOption func(e *Env) (*Env, error) |
| |
| // ClearMacros options clears all parser macros. |
| // |
| // Clearing macros will ensure CEL expressions can only contain linear evaluation paths, as |
| // comprehensions such as `all` and `exists` are enabled only via macros. |
| func ClearMacros() EnvOption { |
| return func(e *Env) (*Env, error) { |
| e.macros = NoMacros |
| return e, nil |
| } |
| } |
| |
| // CustomTypeAdapter swaps the default types.Adapter implementation with a custom one. |
| // |
| // Note: This option must be specified before the Types and TypeDescs options when used together. |
| func CustomTypeAdapter(adapter types.Adapter) EnvOption { |
| return func(e *Env) (*Env, error) { |
| e.adapter = adapter |
| return e, nil |
| } |
| } |
| |
| // CustomTypeProvider replaces the types.Provider implementation with a custom one. |
| // |
| // The `provider` variable type may either be types.Provider or ref.TypeProvider (deprecated) |
| // |
| // Note: This option must be specified before the Types and TypeDescs options when used together. |
| func CustomTypeProvider(provider any) EnvOption { |
| return func(e *Env) (*Env, error) { |
| var err error |
| e.provider, err = maybeInteropProvider(provider) |
| return e, err |
| } |
| } |
| |
| // Declarations option extends the declaration set configured in the environment. |
| // |
| // Note: Declarations will by default be appended to the pre-existing declaration set configured |
| // for the environment. The NewEnv call builds on top of the standard CEL declarations. For a |
| // purely custom set of declarations use NewCustomEnv. |
| // |
| // Deprecated: use FunctionDecls and VariableDecls or FromConfig instead. |
| func Declarations(decls ...*exprpb.Decl) EnvOption { |
| declOpts := []EnvOption{} |
| var err error |
| var opt EnvOption |
| // Convert the declarations to `EnvOption` values ahead of time. |
| // Surface any errors in conversion when the options are applied. |
| for _, d := range decls { |
| opt, err = ExprDeclToDeclaration(d) |
| if err != nil { |
| break |
| } |
| declOpts = append(declOpts, opt) |
| } |
| return func(e *Env) (*Env, error) { |
| if err != nil { |
| return nil, err |
| } |
| for _, o := range declOpts { |
| e, err = o(e) |
| if err != nil { |
| return nil, err |
| } |
| } |
| return e, nil |
| } |
| } |
| |
| // EagerlyValidateDeclarations ensures that any collisions between configured declarations are caught |
| // at the time of the `NewEnv` call. |
| // |
| // Eagerly validating declarations is also useful for bootstrapping a base `cel.Env` value. |
| // Calls to base `Env.Extend()` will be significantly faster when declarations are eagerly validated |
| // as declarations will be collision-checked at most once and only incrementally by way of `Extend` |
| // |
| // Disabled by default as not all environments are used for type-checking. |
| func EagerlyValidateDeclarations(enabled bool) EnvOption { |
| return features(featureEagerlyValidateDeclarations, enabled) |
| } |
| |
| // HomogeneousAggregateLiterals disables mixed type list and map literal values. |
| // |
| // Note, it is still possible to have heterogeneous aggregates when provided as variables to the |
| // expression, as well as via conversion of well-known dynamic types, or with unchecked |
| // expressions. |
| func HomogeneousAggregateLiterals() EnvOption { |
| return ASTValidators(ValidateHomogeneousAggregateLiterals()) |
| } |
| |
| // variadicLogicalOperatorASTs flatten like-operator chained logical expressions into a single |
| // variadic call with N-terms. This behavior is useful when serializing to a protocol buffer as |
| // it will reduce the number of recursive calls needed to deserialize the AST later. |
| // |
| // For example, given the following expression the call graph will be rendered accordingly: |
| // |
| // expression: a && b && c && (d || e) |
| // ast: call(_&&_, [a, b, c, call(_||_, [d, e])]) |
| func variadicLogicalOperatorASTs() EnvOption { |
| return features(featureVariadicLogicalASTs, true) |
| } |
| |
| // Macros option extends the macro set configured in the environment. |
| // |
| // Note: This option must be specified after ClearMacros if used together. |
| func Macros(macros ...Macro) EnvOption { |
| return func(e *Env) (*Env, error) { |
| e.macros = append(e.macros, macros...) |
| return e, nil |
| } |
| } |
| |
| // Container sets the container for resolving variable names. Defaults to an empty container. |
| // |
| // If all references within an expression are relative to a protocol buffer package, then |
| // specifying a container of `google.type` would make it possible to write expressions such as |
| // `Expr{expression: 'a < b'}` instead of having to write `google.type.Expr{...}`. |
| func Container(name string) EnvOption { |
| return func(e *Env) (*Env, error) { |
| cont, err := e.Container.Extend(containers.Name(name)) |
| if err != nil { |
| return nil, err |
| } |
| e.Container = cont |
| return e, nil |
| } |
| } |
| |
| // Abbrevs configures a set of simple names as abbreviations for fully-qualified names. |
| // |
| // An abbreviation (abbrev for short) is a simple name that expands to a fully-qualified name. |
| // Abbreviations can be useful when working with variables, functions, and especially types from |
| // multiple namespaces: |
| // |
| // // CEL object construction |
| // qual.pkg.version.ObjTypeName{ |
| // field: alt.container.ver.FieldTypeName{value: ...} |
| // } |
| // |
| // Only one the qualified names above may be used as the CEL container, so at least one of these |
| // references must be a long qualified name within an otherwise short CEL program. Using the |
| // following abbreviations, the program becomes much simpler: |
| // |
| // // CEL Go option |
| // Abbrevs("qual.pkg.version.ObjTypeName", "alt.container.ver.FieldTypeName") |
| // // Simplified Object construction |
| // ObjTypeName{field: FieldTypeName{value: ...}} |
| // |
| // There are a few rules for the qualified names and the simple abbreviations generated from them: |
| // - Qualified names must be dot-delimited, e.g. `package.subpkg.name`. |
| // - The last element in the qualified name is the abbreviation. |
| // - Abbreviations must not collide with each other. |
| // - The abbreviation must not collide with unqualified names in use. |
| // |
| // Abbreviations are distinct from container-based references in the following important ways: |
| // - Abbreviations must expand to a fully-qualified name. |
| // - Expanded abbreviations do not participate in namespace resolution. |
| // - Abbreviation expansion is done instead of the container search for a matching identifier. |
| // - Containers follow C++ namespace resolution rules with searches from the most qualified name |
| // |
| // to the least qualified name. |
| // |
| // - Container references within the CEL program may be relative, and are resolved to fully |
| // |
| // qualified names at either type-check time or program plan time, whichever comes first. |
| // |
| // If there is ever a case where an identifier could be in both the container and as an |
| // abbreviation, the abbreviation wins as this will ensure that the meaning of a program is |
| // preserved between compilations even as the container evolves. |
| func Abbrevs(qualifiedNames ...string) EnvOption { |
| return func(e *Env) (*Env, error) { |
| cont, err := e.Container.Extend(containers.Abbrevs(qualifiedNames...)) |
| if err != nil { |
| return nil, err |
| } |
| e.Container = cont |
| return e, nil |
| } |
| } |
| |
| // protoTypeRegistry is an internal-only interface containing the minimum methods required to support |
| // custom types. It is a subset of methods from ref.TypeRegistry. |
| type protoTypeRegistry interface { |
| RegisterDescriptor(protoreflect.FileDescriptor) error |
| RegisterType(...ref.Type) error |
| } |
| |
| // Types adds one or more type declarations to the environment, allowing for construction of |
| // type-literals whose definitions are included in the common expression built-in set. |
| // |
| // The input types may either be instances of `proto.Message` or `ref.Type`. Any other type |
| // provided to this option will result in an error. |
| // |
| // Well-known protobuf types within the `google.protobuf.*` package are included in the standard |
| // environment by default. |
| // |
| // Note: This option must be specified after the CustomTypeProvider option when used together. |
| func Types(addTypes ...any) EnvOption { |
| return func(e *Env) (*Env, error) { |
| reg, isReg := e.provider.(protoTypeRegistry) |
| if !isReg { |
| return nil, fmt.Errorf("custom types not supported by provider: %T", e.provider) |
| } |
| for _, t := range addTypes { |
| switch v := t.(type) { |
| case proto.Message: |
| fdMap := pb.CollectFileDescriptorSet(v) |
| for _, fd := range fdMap { |
| err := reg.RegisterDescriptor(fd) |
| if err != nil { |
| return nil, err |
| } |
| } |
| case ref.Type: |
| err := reg.RegisterType(v) |
| if err != nil { |
| return nil, err |
| } |
| default: |
| return nil, fmt.Errorf("unsupported type: %T", t) |
| } |
| } |
| return e, nil |
| } |
| } |
| |
| // TypeDescs adds type declarations from any protoreflect.FileDescriptor, protoregistry.Files, |
| // google.protobuf.FileDescriptorProto or google.protobuf.FileDescriptorSet provided. |
| // |
| // Note that messages instantiated from these descriptors will be *dynamicpb.Message values |
| // rather than the concrete message type. |
| // |
| // TypeDescs are hermetic to a single Env object, but may be copied to other Env values via |
| // extension or by re-using the same EnvOption with another NewEnv() call. |
| func TypeDescs(descs ...any) EnvOption { |
| return func(e *Env) (*Env, error) { |
| reg, isReg := e.provider.(protoTypeRegistry) |
| if !isReg { |
| return nil, fmt.Errorf("custom types not supported by provider: %T", e.provider) |
| } |
| // Scan the input descriptors for FileDescriptorProto messages and accumulate them into a |
| // synthetic FileDescriptorSet as the FileDescriptorProto messages may refer to each other |
| // and will not resolve properly unless they are part of the same set. |
| var fds *descpb.FileDescriptorSet |
| for _, d := range descs { |
| switch f := d.(type) { |
| case *descpb.FileDescriptorProto: |
| if fds == nil { |
| fds = &descpb.FileDescriptorSet{ |
| File: []*descpb.FileDescriptorProto{}, |
| } |
| } |
| fds.File = append(fds.File, f) |
| } |
| } |
| if fds != nil { |
| if err := registerFileSet(reg, fds); err != nil { |
| return nil, err |
| } |
| } |
| for _, d := range descs { |
| switch f := d.(type) { |
| case *protoregistry.Files: |
| if err := registerFiles(reg, f); err != nil { |
| return nil, err |
| } |
| case protoreflect.FileDescriptor: |
| if err := reg.RegisterDescriptor(f); err != nil { |
| return nil, err |
| } |
| case *descpb.FileDescriptorSet: |
| if err := registerFileSet(reg, f); err != nil { |
| return nil, err |
| } |
| case *descpb.FileDescriptorProto: |
| // skip, handled as a synthetic file descriptor set. |
| default: |
| return nil, fmt.Errorf("unsupported type descriptor: %T", d) |
| } |
| } |
| return e, nil |
| } |
| } |
| |
| func registerFileSet(reg protoTypeRegistry, fileSet *descpb.FileDescriptorSet) error { |
| files, err := protodesc.NewFiles(fileSet) |
| if err != nil { |
| return fmt.Errorf("protodesc.NewFiles(%v) failed: %v", fileSet, err) |
| } |
| return registerFiles(reg, files) |
| } |
| |
| func registerFiles(reg protoTypeRegistry, files *protoregistry.Files) error { |
| var err error |
| files.RangeFiles(func(fd protoreflect.FileDescriptor) bool { |
| err = reg.RegisterDescriptor(fd) |
| return err == nil |
| }) |
| return err |
| } |
| |
| // JSONFieldNames supports accessing protocol buffer fields by json-name. |
| // |
| // Enabling JSON field name support will create a copy of the types.Registry with fields indexed |
| // by JSON name, and whether JSON name or Proto-style names are supported will be inferred from |
| // the AST extensions metadata. |
| func JSONFieldNames(enabled bool) EnvOption { |
| return features(featureJSONFieldNames, enabled) |
| } |
| |
| // ProgramOption is a functional interface for configuring evaluation bindings and behaviors. |
| type ProgramOption func(p *prog) (*prog, error) |
| |
| // CustomDecorator appends an InterpreterDecorator to the program. |
| // |
| // InterpretableDecorators can be used to inspect, alter, or replace the Program plan. |
| func CustomDecorator(dec interpreter.InterpretableDecorator) ProgramOption { |
| return func(p *prog) (*prog, error) { |
| p.plannerOptions = append(p.plannerOptions, interpreter.CustomDecorator(dec)) |
| return p, nil |
| } |
| } |
| |
| // Functions adds function overloads that extend or override the set of CEL built-ins. |
| // |
| // Deprecated: use Function() instead to declare the function, its overload signatures, |
| // and the overload implementations. |
| func Functions(funcs ...*functions.Overload) ProgramOption { |
| return func(p *prog) (*prog, error) { |
| if err := p.dispatcher.Add(funcs...); err != nil { |
| return nil, err |
| } |
| return p, nil |
| } |
| } |
| |
| // Globals sets the global variable values for a given program. These values may be shadowed by |
| // variables with the same name provided to the Eval() call. If Globals is used in a Library with |
| // a Lib EnvOption, vars may shadow variables provided by previously added libraries. |
| // |
| // The vars value may either be an `cel.Activation` instance or a `map[string]any`. |
| func Globals(vars any) ProgramOption { |
| return func(p *prog) (*prog, error) { |
| defaultVars, err := NewActivation(vars) |
| if err != nil { |
| return nil, err |
| } |
| if p.defaultVars != nil { |
| defaultVars = interpreter.NewHierarchicalActivation(p.defaultVars, defaultVars) |
| } |
| p.defaultVars = defaultVars |
| return p, nil |
| } |
| } |
| |
| // OptimizeRegex provides a way to replace the InterpretableCall for regex functions. This can be used |
| // to compile regex string constants at program creation time and report any errors and then use the |
| // compiled regex for all regex function invocations. |
| func OptimizeRegex(regexOptimizations ...*interpreter.RegexOptimization) ProgramOption { |
| return func(p *prog) (*prog, error) { |
| p.regexOptimizations = append(p.regexOptimizations, regexOptimizations...) |
| return p, nil |
| } |
| } |
| |
| // ConfigOptionFactory declares a signature which accepts a configuration element, e.g. env.Extension |
| // and optionally produces an EnvOption in response. |
| // |
| // If there are multiple ConfigOptionFactory values which could apply to the same configuration node |
| // the first one that returns an EnvOption and a `true` response will be used, and the config node |
| // will not be passed along to any other option factory. |
| // |
| // Only the *env.Extension type is provided at this time, but validators, optimizers, and other tuning |
| // parameters may be supported in the future. |
| type ConfigOptionFactory func(any) (EnvOption, bool) |
| |
| // FromConfig produces and applies a set of EnvOption values derived from an env.Config object. |
| // |
| // For configuration elements which refer to features outside of the `cel` package, an optional set of |
| // ConfigOptionFactory values may be passed in to support the conversion from static configuration to |
| // configured cel.Env value. |
| // |
| // Note: disabling the standard library will clear the EnvOptions values previously set for the |
| // environment with the exception of propagating types and adapters over to the new environment. |
| // |
| // Note: to support custom types referenced in the configuration file, you must ensure that one of |
| // the following options appears before the FromConfig option: Types, TypeDescs, or CustomTypeProvider |
| // as the type provider configured at the time when the config is processed is the one used to derive |
| // type references from the configuration. |
| func FromConfig(config *env.Config, optFactories ...ConfigOptionFactory) EnvOption { |
| return func(e *Env) (*Env, error) { |
| if err := config.Validate(); err != nil { |
| return nil, err |
| } |
| opts, err := configToEnvOptions(config, e.CELTypeProvider(), optFactories) |
| if err != nil { |
| return nil, err |
| } |
| for _, o := range opts { |
| e, err = o(e) |
| if err != nil { |
| return nil, err |
| } |
| } |
| return e, nil |
| } |
| } |
| |
| // configToEnvOptions generates a set of EnvOption values (or error) based on a config, a type provider, |
| // and an optional set of environment options. |
| func configToEnvOptions(config *env.Config, provider types.Provider, optFactories []ConfigOptionFactory) ([]EnvOption, error) { |
| envOpts := []EnvOption{} |
| // Configure the standard lib subset. |
| if config.StdLib != nil { |
| envOpts = append(envOpts, func(e *Env) (*Env, error) { |
| if e.HasLibrary("cel.lib.std") { |
| return nil, errors.New("invalid subset of stdlib: create a custom env") |
| } |
| return e, nil |
| }) |
| if !config.StdLib.Disabled { |
| envOpts = append(envOpts, StdLib(StdLibSubset(config.StdLib))) |
| } |
| } else { |
| envOpts = append(envOpts, StdLib()) |
| } |
| |
| // Configure the container |
| if config.Container != "" { |
| envOpts = append(envOpts, Container(config.Container)) |
| } |
| |
| // Configure abbreviations |
| for _, imp := range config.Imports { |
| envOpts = append(envOpts, Abbrevs(imp.Name)) |
| } |
| |
| // Configure features and common limits. |
| for _, feat := range config.Features { |
| // Note, if a feature is not found, it is skipped as it is possible the feature |
| // is not intended to be supported publicly. In the future, a refinement of |
| // to this strategy to report unrecognized features and validators should probably |
| // be covered as a standard ConfigOptionFactory |
| if id, found := featureIDByName(feat.Name); found { |
| envOpts = append(envOpts, features(id, feat.Enabled)) |
| } |
| } |
| |
| // Configure the context variable declaration |
| if config.ContextVariable != nil { |
| typeName := config.ContextVariable.TypeName |
| if _, found := provider.FindStructType(typeName); !found { |
| return nil, fmt.Errorf("invalid context proto type: %q", typeName) |
| } |
| // Attempt to instantiate the proto in order to reflect to its descriptor |
| msg := provider.NewValue(typeName, map[string]ref.Val{}) |
| pbMsg, ok := msg.Value().(proto.Message) |
| if !ok { |
| return nil, fmt.Errorf("unsupported context type: %T", msg.Value()) |
| } |
| envOpts = append(envOpts, DeclareContextProto(pbMsg.ProtoReflect().Descriptor())) |
| } |
| |
| // Configure variables |
| if len(config.Variables) != 0 { |
| vars := make([]*decls.VariableDecl, 0, len(config.Variables)) |
| for _, v := range config.Variables { |
| vDef, err := v.AsCELVariable(provider) |
| if err != nil { |
| return nil, err |
| } |
| vars = append(vars, vDef) |
| } |
| envOpts = append(envOpts, VariableDecls(vars...)) |
| } |
| |
| // Configure functions |
| if len(config.Functions) != 0 { |
| funcs := make([]*decls.FunctionDecl, 0, len(config.Functions)) |
| for _, f := range config.Functions { |
| fnDef, err := f.AsCELFunction(provider) |
| if err != nil { |
| return nil, err |
| } |
| funcs = append(funcs, fnDef) |
| } |
| envOpts = append(envOpts, FunctionDecls(funcs...)) |
| } |
| |
| for _, limit := range config.Limits { |
| if id, found := limitIDByName(limit.Name); found { |
| envOpts = append(envOpts, setLimit(id, limit.Value)) |
| } |
| } |
| |
| // Configure validators |
| for _, val := range config.Validators { |
| if fac, found := astValidatorFactories[val.Name]; found { |
| envOpts = append(envOpts, func(e *Env) (*Env, error) { |
| validator, err := fac(val) |
| if err != nil { |
| return nil, fmt.Errorf("%w", err) |
| } |
| return ASTValidators(validator)(e) |
| }) |
| } else if opt, handled := handleExtendedConfigOption(val, optFactories); handled { |
| envOpts = append(envOpts, opt) |
| } |
| // we don't error when the validator isn't found as it may be part |
| // of an extension library and enabled implicitly. |
| } |
| |
| // Configure extensions |
| for _, ext := range config.Extensions { |
| // version number has been validated by the call to `Validate` |
| ver, _ := ext.VersionNumber() |
| if ext.Name == "optional" { |
| envOpts = append(envOpts, OptionalTypes(OptionalTypesVersion(ver))) |
| } else { |
| opt, handled := handleExtendedConfigOption(ext, optFactories) |
| if !handled { |
| return nil, fmt.Errorf("unrecognized extension: %s", ext.Name) |
| } |
| envOpts = append(envOpts, opt) |
| } |
| } |
| |
| return envOpts, nil |
| } |
| |
| func handleExtendedConfigOption(conf any, optFactories []ConfigOptionFactory) (EnvOption, bool) { |
| for _, optFac := range optFactories { |
| if opt, useOption := optFac(conf); useOption { |
| return opt, true |
| } |
| } |
| return nil, false |
| } |
| |
| // EvalOption indicates an evaluation option that may affect the evaluation behavior or information |
| // in the output result. |
| type EvalOption int |
| |
| const ( |
| // OptTrackState will cause the runtime to return an immutable EvalState value in the Result. |
| OptTrackState EvalOption = 1 << iota |
| |
| // OptExhaustiveEval causes the runtime to disable short-circuits and track state. |
| OptExhaustiveEval EvalOption = 1<<iota | OptTrackState |
| |
| // OptOptimize precomputes functions and operators with constants as arguments at program |
| // creation time. It also pre-compiles regex pattern constants passed to 'matches', reports any compilation errors |
| // at program creation and uses the compiled regex pattern for all 'matches' function invocations. |
| // This flag is useful when the expression will be evaluated repeatedly against |
| // a series of different inputs. |
| OptOptimize EvalOption = 1 << iota |
| |
| // OptPartialEval enables the evaluation of a partial state where the input data that may be |
| // known to be missing, either as top-level variables, or somewhere within a variable's object |
| // member graph. |
| // |
| // By itself, OptPartialEval does not change evaluation behavior unless the input to the |
| // Program Eval() call is created via PartialVars(). |
| OptPartialEval EvalOption = 1 << iota |
| |
| // OptTrackCost enables the runtime cost calculation while validation and return cost within evalDetails |
| // cost calculation is available via func ActualCost() |
| OptTrackCost EvalOption = 1 << iota |
| |
| // OptCheckStringFormat enables compile-time checking of string.format calls for syntax/cardinality. |
| // |
| // Deprecated: use ext.StringsValidateFormatCalls() as this option is now a no-op. |
| OptCheckStringFormat EvalOption = 1 << iota |
| ) |
| |
| // EvalOptions sets one or more evaluation options which may affect the evaluation or Result. |
| func EvalOptions(opts ...EvalOption) ProgramOption { |
| return func(p *prog) (*prog, error) { |
| for _, opt := range opts { |
| p.evalOpts |= opt |
| } |
| return p, nil |
| } |
| } |
| |
| // InterruptCheckFrequency configures the number of iterations within a comprehension to evaluate |
| // before checking whether the function evaluation has been interrupted. |
| func InterruptCheckFrequency(checkFrequency uint) ProgramOption { |
| return func(p *prog) (*prog, error) { |
| p.interruptCheckFrequency = checkFrequency |
| return p, nil |
| } |
| } |
| |
| // CostEstimatorOptions configure type-check time options for estimating expression cost. |
| func CostEstimatorOptions(costOpts ...checker.CostOption) EnvOption { |
| return func(e *Env) (*Env, error) { |
| e.costOptions = append(e.costOptions, costOpts...) |
| return e, nil |
| } |
| } |
| |
| // CostTrackerOptions configures a set of options for cost-tracking. |
| // |
| // Note, CostTrackerOptions is a no-op unless CostTracking is also enabled. |
| func CostTrackerOptions(costOpts ...interpreter.CostTrackerOption) ProgramOption { |
| return func(p *prog) (*prog, error) { |
| p.costOptions = append(p.costOptions, costOpts...) |
| return p, nil |
| } |
| } |
| |
| // CostTracking enables cost tracking and registers a ActualCostEstimator that can optionally provide a runtime cost estimate for any function calls. |
| func CostTracking(costEstimator interpreter.ActualCostEstimator) ProgramOption { |
| return func(p *prog) (*prog, error) { |
| p.callCostEstimator = costEstimator |
| p.evalOpts |= OptTrackCost |
| return p, nil |
| } |
| } |
| |
| // CostLimit enables cost tracking and sets configures program evaluation to exit early with a |
| // "runtime cost limit exceeded" error if the runtime cost exceeds the costLimit. |
| // The CostLimit is a metric that corresponds to the number and estimated expense of operations |
| // performed while evaluating an expression. It is indicative of CPU usage, not memory usage. |
| func CostLimit(costLimit uint64) ProgramOption { |
| return func(p *prog) (*prog, error) { |
| p.costLimit = &costLimit |
| p.evalOpts |= OptTrackCost |
| return p, nil |
| } |
| } |
| |
| func fieldToCELType(field protoreflect.FieldDescriptor) (*Type, error) { |
| if field.Kind() == protoreflect.MessageKind || field.Kind() == protoreflect.GroupKind { |
| msgName := (string)(field.Message().FullName()) |
| return ObjectType(msgName), nil |
| } |
| if primitiveType, found := types.ProtoCELPrimitives[field.Kind()]; found { |
| return primitiveType, nil |
| } |
| if field.Kind() == protoreflect.EnumKind { |
| return IntType, nil |
| } |
| return nil, fmt.Errorf("field %s type %s not implemented", field.FullName(), field.Kind().String()) |
| } |
| |
| func fieldToVariable(field protoreflect.FieldDescriptor, jsonFieldNames bool) (*decls.VariableDecl, error) { |
| name := string(field.Name()) |
| if jsonFieldNames { |
| name = field.JSONName() |
| } |
| if field.IsMap() { |
| mapKey := field.MapKey() |
| mapValue := field.MapValue() |
| keyType, err := fieldToCELType(mapKey) |
| if err != nil { |
| return nil, err |
| } |
| valueType, err := fieldToCELType(mapValue) |
| if err != nil { |
| return nil, err |
| } |
| return decls.NewVariable(name, MapType(keyType, valueType)), nil |
| } |
| if field.IsList() { |
| elemType, err := fieldToCELType(field) |
| if err != nil { |
| return nil, err |
| } |
| return decls.NewVariable(name, ListType(elemType)), nil |
| } |
| celType, err := fieldToCELType(field) |
| if err != nil { |
| return nil, err |
| } |
| return decls.NewVariable(name, celType), nil |
| } |
| |
| // DeclareContextProto returns an option to extend CEL environment with declarations from the given context proto. |
| // Each field of the proto defines a variable of the same name in the environment. |
| // https://github.com/google/cel-spec/blob/master/doc/langdef.md#evaluation-environment |
| // |
| // If using JSONFieldNames(), ensure that the option is set before DeclareContextProto is provided. |
| func DeclareContextProto(descriptor protoreflect.MessageDescriptor) EnvOption { |
| return func(e *Env) (*Env, error) { |
| if e.contextProto != nil { |
| return nil, fmt.Errorf("context proto already declared as %q, got %q", |
| e.contextProto.FullName(), descriptor.FullName()) |
| } |
| e.contextProto = descriptor |
| fields := descriptor.Fields() |
| vars := make([]*decls.VariableDecl, 0, fields.Len()) |
| jsonFieldNames := e.HasFeature(featureJSONFieldNames) |
| for i := 0; i < fields.Len(); i++ { |
| field := fields.Get(i) |
| variable, err := fieldToVariable(field, jsonFieldNames) |
| if err != nil { |
| return nil, err |
| } |
| vars = append(vars, variable) |
| } |
| var err error |
| e, err = VariableDecls(vars...)(e) |
| if err != nil { |
| return nil, err |
| } |
| return Types(dynamicpb.NewMessage(descriptor))(e) |
| } |
| } |
| |
| // ContextProtoVars uses the fields of the input proto.Messages as top-level variables within an Activation. |
| // |
| // Consider using with `DeclareContextProto` to simplify variable type declarations and publishing when using |
| // protocol buffers. |
| // |
| // Use the types.JSONFieldNames(true) option to populate the context proto vars using the JSON field names. |
| func ContextProtoVars(ctx proto.Message, opts ...types.RegistryOption) (Activation, error) { |
| if ctx == nil || !ctx.ProtoReflect().IsValid() { |
| return interpreter.EmptyActivation(), nil |
| } |
| regOpts := []types.RegistryOption{types.ProtoTypeDefs(ctx)} |
| regOpts = append(regOpts, opts...) |
| reg, err := types.NewProtoRegistry(regOpts...) |
| if err != nil { |
| return nil, err |
| } |
| pbRef := ctx.ProtoReflect() |
| typeName := string(pbRef.Descriptor().FullName()) |
| fields := pbRef.Descriptor().Fields() |
| vars := make(map[string]any, fields.Len()) |
| for i := 0; i < fields.Len(); i++ { |
| field := fields.Get(i) |
| fieldName := field.TextName() |
| if reg.JSONFieldNames() { |
| fieldName = field.JSONName() |
| } |
| sft, found := reg.FindStructFieldType(typeName, fieldName) |
| if !found { |
| return nil, fmt.Errorf("no such field: %s", fieldName) |
| } |
| fieldVal, err := sft.GetFrom(ctx) |
| if err != nil { |
| return nil, err |
| } |
| vars[fieldName] = fieldVal |
| } |
| return NewActivation(vars) |
| } |
| |
| // EnableMacroCallTracking ensures that call expressions which are replaced by macros |
| // are tracked in the `SourceInfo` of parsed and checked expressions. |
| func EnableMacroCallTracking() EnvOption { |
| return features(featureEnableMacroCallTracking, true) |
| } |
| |
| // EnableIdentifierEscapeSyntax enables identifier escaping (`) syntax for |
| // fields. |
| func EnableIdentifierEscapeSyntax() EnvOption { |
| return features(featureIdentEscapeSyntax, true) |
| } |
| |
| // CrossTypeNumericComparisons makes it possible to compare across numeric types, e.g. double < int |
| func CrossTypeNumericComparisons(enabled bool) EnvOption { |
| return features(featureCrossTypeNumericComparisons, enabled) |
| } |
| |
| // DefaultUTCTimeZone ensures that time-based operations use the UTC timezone rather than the |
| // input time's local timezone. |
| func DefaultUTCTimeZone(enabled bool) EnvOption { |
| return features(featureDefaultUTCTimeZone, enabled) |
| } |
| |
| // features sets the given feature flags. See list of Feature constants above. |
| func features(flag int, enabled bool) EnvOption { |
| return func(e *Env) (*Env, error) { |
| e.features[flag] = enabled |
| return e, nil |
| } |
| } |
| |
| func setLimit(id limitID, limit int) EnvOption { |
| if limit < 0 { |
| limit = -1 |
| } |
| return func(e *Env) (*Env, error) { |
| e.limits[id] = limit |
| return e, nil |
| } |
| } |
| |
| // ParserRecursionLimit adjusts the AST depth the parser will tolerate. |
| // Defaults defined in the parser package. |
| func ParserRecursionLimit(limit int) EnvOption { |
| return setLimit(limitParseRecursionDepth, limit) |
| } |
| |
| // ParserErrorRecoveryLimit sets the number of attemtps the parser will take |
| // to recover after encountering an error. |
| func ParserErrorRecoveryLimit(limit int) EnvOption { |
| return setLimit(limitParseErrorRecovery, limit) |
| } |
| |
| // ParserExpressionSizeLimit adjusts the number of code points the expression parser is allowed to parse. |
| // Defaults are defined in the parser package. A negative value means unbounded. |
| func ParserExpressionSizeLimit(limit int) EnvOption { |
| return setLimit(limitCodePointSize, limit) |
| } |
| |
| // EnableHiddenAccumulatorName sets the parser to use the identifier '@result' for accumulators |
| // which is not normally accessible from CEL source. |
| func EnableHiddenAccumulatorName(enabled bool) EnvOption { |
| return func(e *Env) (*Env, error) { |
| e.prsrOpts = append(e.prsrOpts, parser.EnableHiddenAccumulatorName(enabled)) |
| return e, nil |
| } |
| } |
| |
| func maybeInteropProvider(provider any) (types.Provider, error) { |
| switch p := provider.(type) { |
| case types.Provider: |
| return p, nil |
| case ref.TypeProvider: |
| return &interopCELTypeProvider{TypeProvider: p}, nil |
| default: |
| return nil, fmt.Errorf("unsupported type provider: %T", provider) |
| } |
| } |