| // Copyright 2025 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 env provides a representation of a CEL environment. |
| package env |
| |
| import ( |
| "errors" |
| "fmt" |
| "math" |
| "strconv" |
| "strings" |
| |
| "github.com/google/cel-go/common/decls" |
| "github.com/google/cel-go/common/types" |
| ) |
| |
| // NewConfig creates an instance of a YAML serializable CEL environment configuration. |
| func NewConfig(name string) *Config { |
| return &Config{ |
| Name: name, |
| } |
| } |
| |
| // Config represents a serializable form of the CEL environment configuration. |
| // |
| // Note: custom validations, feature flags, and performance tuning parameters are not (yet) |
| // considered part of the core CEL environment configuration and should be managed separately |
| // until a common convention for such settings is developed. |
| type Config struct { |
| Name string `yaml:"name,omitempty"` |
| Description string `yaml:"description,omitempty"` |
| Container string `yaml:"container,omitempty"` |
| Imports []*Import `yaml:"imports,omitempty"` |
| StdLib *LibrarySubset `yaml:"stdlib,omitempty"` |
| Extensions []*Extension `yaml:"extensions,omitempty"` |
| ContextVariable *ContextVariable `yaml:"context_variable,omitempty"` |
| Variables []*Variable `yaml:"variables,omitempty"` |
| Functions []*Function `yaml:"functions,omitempty"` |
| Validators []*Validator `yaml:"validators,omitempty"` |
| Features []*Feature `yaml:"features,omitempty"` |
| Limits []*Limit `yaml:"limits,omitempty"` |
| } |
| |
| // Validate validates the whole configuration is well-formed. |
| func (c *Config) Validate() error { |
| if c == nil { |
| return nil |
| } |
| var errs []error |
| for _, imp := range c.Imports { |
| if err := imp.Validate(); err != nil { |
| errs = append(errs, err) |
| } |
| } |
| if err := c.StdLib.Validate(); err != nil { |
| errs = append(errs, err) |
| } |
| for _, ext := range c.Extensions { |
| if err := ext.Validate(); err != nil { |
| errs = append(errs, err) |
| } |
| } |
| if err := c.ContextVariable.Validate(); err != nil { |
| errs = append(errs, err) |
| } |
| if c.ContextVariable != nil && len(c.Variables) != 0 { |
| errs = append(errs, errors.New("invalid config: either context variable or variables may be set, but not both")) |
| } |
| for _, v := range c.Variables { |
| if err := v.Validate(); err != nil { |
| errs = append(errs, err) |
| } |
| } |
| for _, fn := range c.Functions { |
| if err := fn.Validate(); err != nil { |
| errs = append(errs, err) |
| } |
| } |
| for _, feat := range c.Features { |
| if err := feat.Validate(); err != nil { |
| errs = append(errs, err) |
| } |
| } |
| for _, limit := range c.Limits { |
| if err := limit.Validate(); err != nil { |
| errs = append(errs, err) |
| } |
| } |
| for _, val := range c.Validators { |
| if err := val.Validate(); err != nil { |
| errs = append(errs, err) |
| } |
| } |
| return errors.Join(errs...) |
| } |
| |
| // SetContainer configures the container name for this configuration. |
| func (c *Config) SetContainer(container string) *Config { |
| c.Container = container |
| return c |
| } |
| |
| // AddVariableDecls adds one or more variables to the config, converting them to serializable values first. |
| // |
| // VariableDecl inputs are expected to be well-formed. |
| func (c *Config) AddVariableDecls(vars ...*decls.VariableDecl) *Config { |
| convVars := make([]*Variable, len(vars)) |
| for i, v := range vars { |
| if v == nil { |
| continue |
| } |
| cv := NewVariable(v.Name(), SerializeTypeDesc(v.Type())) |
| cv.Description = v.Description() |
| convVars[i] = cv |
| } |
| return c.AddVariables(convVars...) |
| } |
| |
| // AddVariables adds one or more variables to the config. |
| func (c *Config) AddVariables(vars ...*Variable) *Config { |
| c.Variables = append(c.Variables, vars...) |
| return c |
| } |
| |
| // SetContextVariable configures the ContextVariable for this configuration. |
| func (c *Config) SetContextVariable(ctx *ContextVariable) *Config { |
| c.ContextVariable = ctx |
| return c |
| } |
| |
| // AddFunctionDecls adds one or more functions to the config, converting them to serializable values first. |
| // |
| // FunctionDecl inputs are expected to be well-formed. |
| func (c *Config) AddFunctionDecls(funcs ...*decls.FunctionDecl) *Config { |
| convFuncs := make([]*Function, len(funcs)) |
| for i, fn := range funcs { |
| if fn == nil { |
| continue |
| } |
| overloads := make([]*Overload, 0, len(fn.OverloadDecls())) |
| for _, o := range fn.OverloadDecls() { |
| overloadID := o.ID() |
| args := make([]*TypeDesc, 0, len(o.ArgTypes())) |
| for _, a := range o.ArgTypes() { |
| args = append(args, SerializeTypeDesc(a)) |
| } |
| ret := SerializeTypeDesc(o.ResultType()) |
| var overload *Overload |
| if o.IsMemberFunction() { |
| overload = NewMemberOverload(overloadID, args[0], args[1:], ret) |
| } else { |
| overload = NewOverload(overloadID, args, ret) |
| } |
| exampleCount := len(o.Examples()) |
| if exampleCount > 0 { |
| overload.Examples = o.Examples() |
| } |
| overloads = append(overloads, overload) |
| } |
| cf := NewFunction(fn.Name(), overloads...) |
| cf.Description = fn.Description() |
| convFuncs[i] = cf |
| } |
| return c.AddFunctions(convFuncs...) |
| } |
| |
| // AddFunctions adds one or more functions to the config. |
| func (c *Config) AddFunctions(funcs ...*Function) *Config { |
| c.Functions = append(c.Functions, funcs...) |
| return c |
| } |
| |
| // SetStdLib configures the LibrarySubset for the standard library. |
| func (c *Config) SetStdLib(subset *LibrarySubset) *Config { |
| c.StdLib = subset |
| return c |
| } |
| |
| // AddImports appends a set of imports to the config. |
| func (c *Config) AddImports(imps ...*Import) *Config { |
| c.Imports = append(c.Imports, imps...) |
| return c |
| } |
| |
| // AddExtensions appends a set of extensions to the config. |
| func (c *Config) AddExtensions(exts ...*Extension) *Config { |
| c.Extensions = append(c.Extensions, exts...) |
| return c |
| } |
| |
| // AddValidators appends one or more validators to the config. |
| func (c *Config) AddValidators(vals ...*Validator) *Config { |
| c.Validators = append(c.Validators, vals...) |
| return c |
| } |
| |
| // AddFeatures appends one or more features to the config. |
| func (c *Config) AddFeatures(feats ...*Feature) *Config { |
| c.Features = append(c.Features, feats...) |
| return c |
| } |
| |
| // AddLimits appends one or more limits to the config. |
| func (c *Config) AddLimits(limits ...*Limit) *Config { |
| c.Limits = append(c.Limits, limits...) |
| return c |
| } |
| |
| // NewImport returns a serializable import value from the qualified type name. |
| func NewImport(name string) *Import { |
| return &Import{Name: name} |
| } |
| |
| // Import represents a type name that will be appreviated by its simple name using |
| // the cel.Abbrevs() option. |
| type Import struct { |
| Name string `yaml:"name"` |
| } |
| |
| // Validate validates the import configuration is well-formed. |
| func (imp *Import) Validate() error { |
| if imp == nil { |
| return errors.New("invalid import: nil") |
| } |
| if imp.Name == "" { |
| return errors.New("invalid import: missing type name") |
| } |
| return nil |
| } |
| |
| // NewVariable returns a serializable variable from a name and type definition |
| func NewVariable(name string, t *TypeDesc) *Variable { |
| return NewVariableWithDoc(name, t, "") |
| } |
| |
| // NewVariableWithDoc returns a serializable variable from a name, type definition, and doc string. |
| func NewVariableWithDoc(name string, t *TypeDesc, doc string) *Variable { |
| return &Variable{Name: name, TypeDesc: t, Description: doc} |
| } |
| |
| // Variable represents a typed variable declaration which will be published via the |
| // cel.VariableDecls() option. |
| type Variable struct { |
| Name string `yaml:"name"` |
| Description string `yaml:"description,omitempty"` |
| |
| // Type represents the type declaration for the variable. |
| // |
| // When serialized, 'type' is used for shorthand specifier string. |
| // |
| // Use GetType() for getting the effective type. |
| Type *TypeDesc `yaml:"type,omitempty"` |
| |
| // TypeDesc is an embedded set of fields allowing for the specification of the Variable type. |
| *TypeDesc `yaml:",inline"` |
| } |
| |
| // Validate validates the variable configuration is well-formed. |
| func (v *Variable) Validate() error { |
| if v == nil { |
| return errors.New("invalid variable: nil") |
| } |
| if v.Name == "" { |
| return errors.New("invalid variable: missing variable name") |
| } |
| if err := v.GetType().Validate(); err != nil { |
| return fmt.Errorf("invalid variable %q: %w", v.Name, err) |
| } |
| if v.GetType().IsTypeParam { |
| return fmt.Errorf("invalid variable %q: variables cannot be type parameters", v.Name) |
| } |
| return nil |
| } |
| |
| // GetType returns the variable type description. |
| // |
| // Note, if both the embedded TypeDesc and the field Type are non-nil, the embedded TypeDesc will |
| // take precedence. |
| func (v *Variable) GetType() *TypeDesc { |
| if v == nil { |
| return nil |
| } |
| if v.TypeDesc != nil { |
| return v.TypeDesc |
| } |
| if v.Type != nil { |
| return v.Type |
| } |
| return nil |
| } |
| |
| // AsCELVariable converts the serializable form of the Variable into a CEL environment declaration. |
| func (v *Variable) AsCELVariable(tp types.Provider) (*decls.VariableDecl, error) { |
| if err := v.Validate(); err != nil { |
| return nil, err |
| } |
| t, err := v.GetType().AsCELType(tp) |
| if err != nil { |
| return nil, fmt.Errorf("invalid variable %q: %w", v.Name, err) |
| } |
| return decls.NewVariableWithDoc(v.Name, t, v.Description), nil |
| } |
| |
| // NewContextVariable returns a serializable context variable with a specific type name. |
| func NewContextVariable(typeName string) *ContextVariable { |
| return &ContextVariable{TypeName: typeName} |
| } |
| |
| // ContextVariable represents a structured message whose fields are to be treated as the top-level |
| // variable identifiers within CEL expressions. |
| type ContextVariable struct { |
| // TypeName represents the fully qualified typename of the context variable. |
| // Currently, only protobuf types are supported. |
| TypeName string `yaml:"type_name"` |
| } |
| |
| // Validate validates the context-variable configuration is well-formed. |
| func (ctx *ContextVariable) Validate() error { |
| if ctx == nil { |
| return nil |
| } |
| if ctx.TypeName == "" { |
| return errors.New("invalid context variable: missing type name") |
| } |
| return nil |
| } |
| |
| // NewFunction creates a serializable function and overload set. |
| func NewFunction(name string, overloads ...*Overload) *Function { |
| return &Function{Name: name, Overloads: overloads} |
| } |
| |
| // NewFunctionWithDoc creates a serializable function and overload set. |
| func NewFunctionWithDoc(name, doc string, overloads ...*Overload) *Function { |
| return &Function{Name: name, Description: doc, Overloads: overloads} |
| } |
| |
| // Function represents the serializable format of a function and its overloads. |
| type Function struct { |
| Name string `yaml:"name"` |
| Description string `yaml:"description,omitempty"` |
| Overloads []*Overload `yaml:"overloads,omitempty"` |
| } |
| |
| // Validate validates the function configuration is well-formed. |
| func (fn *Function) Validate() error { |
| if fn == nil { |
| return errors.New("invalid function: nil") |
| } |
| if fn.Name == "" { |
| return errors.New("invalid function: missing function name") |
| } |
| if len(fn.Overloads) == 0 { |
| return fmt.Errorf("invalid function %q: missing overloads", fn.Name) |
| } |
| var errs []error |
| for _, o := range fn.Overloads { |
| if err := o.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("invalid function %q: %w", fn.Name, err)) |
| } |
| } |
| return errors.Join(errs...) |
| } |
| |
| // AsCELFunction converts the serializable form of the Function into CEL environment declaration. |
| func (fn *Function) AsCELFunction(tp types.Provider) (*decls.FunctionDecl, error) { |
| if err := fn.Validate(); err != nil { |
| return nil, err |
| } |
| opts := make([]decls.FunctionOpt, 0, len(fn.Overloads)+1) |
| for _, o := range fn.Overloads { |
| opt, err := o.AsFunctionOption(tp) |
| opts = append(opts, opt) |
| if err != nil { |
| return nil, fmt.Errorf("invalid function %q: %w", fn.Name, err) |
| } |
| } |
| if len(fn.Description) != 0 { |
| opts = append(opts, decls.FunctionDocs(fn.Description)) |
| } |
| return decls.NewFunction(fn.Name, opts...) |
| } |
| |
| // NewOverload returns a new serializable representation of a global overload. |
| func NewOverload(id string, args []*TypeDesc, ret *TypeDesc, examples ...string) *Overload { |
| return &Overload{ID: id, Args: args, Return: ret, Examples: examples} |
| } |
| |
| // NewMemberOverload returns a new serializable representation of a member (receiver) overload. |
| func NewMemberOverload(id string, target *TypeDesc, args []*TypeDesc, ret *TypeDesc, examples ...string) *Overload { |
| return &Overload{ID: id, Target: target, Args: args, Return: ret, Examples: examples} |
| } |
| |
| // Overload represents the serializable format of a function overload. |
| type Overload struct { |
| ID string `yaml:"id"` |
| Examples []string `yaml:"examples,omitempty"` |
| Target *TypeDesc `yaml:"target,omitempty"` |
| Args []*TypeDesc `yaml:"args,omitempty"` |
| Return *TypeDesc `yaml:"return,omitempty"` |
| } |
| |
| // Validate validates the overload configuration is well-formed. |
| func (od *Overload) Validate() error { |
| if od == nil { |
| return errors.New("invalid overload: nil") |
| } |
| if od.ID == "" { |
| return errors.New("invalid overload: missing overload id") |
| } |
| var errs []error |
| if od.Target != nil { |
| if err := od.Target.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("invalid overload %q target: %w", od.ID, err)) |
| } |
| } |
| for i, arg := range od.Args { |
| if err := arg.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("invalid overload %q arg[%d]: %w", od.ID, i, err)) |
| } |
| } |
| if err := od.Return.Validate(); err != nil { |
| errs = append(errs, fmt.Errorf("invalid overload %q return: %w", od.ID, err)) |
| } |
| return errors.Join(errs...) |
| } |
| |
| // AsFunctionOption converts the serializable form of the Overload into a function declaration option. |
| func (od *Overload) AsFunctionOption(tp types.Provider) (decls.FunctionOpt, error) { |
| if err := od.Validate(); err != nil { |
| return nil, err |
| } |
| args := make([]*types.Type, len(od.Args)) |
| var err error |
| var errs []error |
| for i, a := range od.Args { |
| args[i], err = a.AsCELType(tp) |
| if err != nil { |
| errs = append(errs, err) |
| } |
| } |
| result, err := od.Return.AsCELType(tp) |
| if err != nil { |
| errs = append(errs, err) |
| } |
| if od.Target != nil { |
| t, err := od.Target.AsCELType(tp) |
| if err != nil { |
| return nil, errors.Join(append(errs, err)...) |
| } |
| args = append([]*types.Type{t}, args...) |
| return decls.MemberOverload(od.ID, args, result), nil |
| } |
| if len(errs) != 0 { |
| return nil, errors.Join(errs...) |
| } |
| return decls.Overload(od.ID, args, result, decls.OverloadExamples(od.Examples...)), nil |
| } |
| |
| // NewExtension creates a serializable Extension from a name and version string. |
| func NewExtension(name string, version uint32) *Extension { |
| versionString := "latest" |
| if version < math.MaxUint32 { |
| versionString = strconv.FormatUint(uint64(version), 10) |
| } |
| return &Extension{ |
| Name: name, |
| Version: versionString, |
| } |
| } |
| |
| // Extension represents a named and optionally versioned extension library configured in the environment. |
| type Extension struct { |
| // Name is either the LibraryName() or some short-hand simple identifier which is understood by the config-handler. |
| Name string `yaml:"name"` |
| |
| // Version may either be an unsigned long value or the string 'latest'. If empty, the value is treated as '0'. |
| Version string `yaml:"version,omitempty"` |
| } |
| |
| // Validate validates the extension configuration is well-formed. |
| func (e *Extension) Validate() error { |
| _, err := e.VersionNumber() |
| return err |
| } |
| |
| // VersionNumber returns the parsed version string, or an error if the version cannot be parsed. |
| func (e *Extension) VersionNumber() (uint32, error) { |
| if e == nil { |
| return 0, fmt.Errorf("invalid extension: nil") |
| } |
| if e.Name == "" { |
| return 0, fmt.Errorf("invalid extension: missing name") |
| } |
| if e.Version == "latest" { |
| return math.MaxUint32, nil |
| } |
| if e.Version == "" { |
| return 0, nil |
| } |
| ver, err := strconv.ParseUint(e.Version, 10, 32) |
| if err != nil { |
| return 0, fmt.Errorf("invalid extension %q version: %w", e.Name, err) |
| } |
| return uint32(ver), nil |
| } |
| |
| // NewLibrarySubset returns an empty library subsetting config which permits all library features. |
| func NewLibrarySubset() *LibrarySubset { |
| return &LibrarySubset{} |
| } |
| |
| // LibrarySubset indicates a subset of the macros and function supported by a subsettable library. |
| type LibrarySubset struct { |
| // Disabled indicates whether the library has been disabled, typically only used for |
| // default-enabled libraries like stdlib. |
| Disabled bool `yaml:"disabled,omitempty"` |
| |
| // DisableMacros disables macros for the given library. |
| DisableMacros bool `yaml:"disable_macros,omitempty"` |
| |
| // IncludeMacros specifies a set of macro function names to include in the subset. |
| IncludeMacros []string `yaml:"include_macros,omitempty"` |
| |
| // ExcludeMacros specifies a set of macro function names to exclude from the subset. |
| // Note: if IncludeMacros is non-empty, then ExcludeFunctions is ignored. |
| ExcludeMacros []string `yaml:"exclude_macros,omitempty"` |
| |
| // IncludeFunctions specifies a set of functions to include in the subset. |
| // |
| // Note: the overloads specified in the subset need only specify their ID. |
| // Note: if IncludeFunctions is non-empty, then ExcludeFunctions is ignored. |
| IncludeFunctions []*Function `yaml:"include_functions,omitempty"` |
| |
| // ExcludeFunctions specifies the set of functions to exclude from the subset. |
| // |
| // Note: the overloads specified in the subset need only specify their ID. |
| ExcludeFunctions []*Function `yaml:"exclude_functions,omitempty"` |
| } |
| |
| // Validate validates the library configuration is well-formed. |
| // |
| // For example, setting both the IncludeMacros and ExcludeMacros together could be confusing |
| // and create a broken expectation, likewise for IncludeFunctions and ExcludeFunctions. |
| func (lib *LibrarySubset) Validate() error { |
| if lib == nil { |
| return nil |
| } |
| var errs []error |
| if len(lib.IncludeMacros) != 0 && len(lib.ExcludeMacros) != 0 { |
| errs = append(errs, errors.New("invalid subset: cannot both include and exclude macros")) |
| } |
| if len(lib.IncludeFunctions) != 0 && len(lib.ExcludeFunctions) != 0 { |
| errs = append(errs, errors.New("invalid subset: cannot both include and exclude functions")) |
| } |
| return errors.Join(errs...) |
| } |
| |
| // SubsetFunction produces a function declaration which matches the supported subset, or nil |
| // if the function is not supported by the LibrarySubset. |
| // |
| // For IncludeFunctions, if the function does not specify a set of overloads to include, the |
| // whole function definition is included. If overloads are set, then a new function which |
| // includes only the specified overloads is produced. |
| // |
| // For ExcludeFunctions, if the function does not specify a set of overloads to exclude, the |
| // whole function definition is excluded. If overloads are set, then a new function which |
| // includes only the permitted overloads is produced. |
| func (lib *LibrarySubset) SubsetFunction(fn *decls.FunctionDecl) (*decls.FunctionDecl, bool) { |
| // When lib is null, it should indicate that all values are included in the subset. |
| if lib == nil { |
| return fn, true |
| } |
| if lib.Disabled { |
| return nil, false |
| } |
| if len(lib.IncludeFunctions) != 0 { |
| for _, include := range lib.IncludeFunctions { |
| if include.Name != fn.Name() { |
| continue |
| } |
| if len(include.Overloads) == 0 { |
| return fn, true |
| } |
| overloadIDs := make([]string, len(include.Overloads)) |
| for i, o := range include.Overloads { |
| overloadIDs[i] = o.ID |
| } |
| return fn.Subset(decls.IncludeOverloads(overloadIDs...)), true |
| } |
| return nil, false |
| } |
| if len(lib.ExcludeFunctions) != 0 { |
| for _, exclude := range lib.ExcludeFunctions { |
| if exclude.Name != fn.Name() { |
| continue |
| } |
| if len(exclude.Overloads) == 0 { |
| return nil, false |
| } |
| overloadIDs := make([]string, len(exclude.Overloads)) |
| for i, o := range exclude.Overloads { |
| overloadIDs[i] = o.ID |
| } |
| return fn.Subset(decls.ExcludeOverloads(overloadIDs...)), true |
| } |
| return fn, true |
| } |
| return fn, true |
| } |
| |
| // SubsetMacro indicates whether the macro function should be included in the library subset. |
| func (lib *LibrarySubset) SubsetMacro(macroFunction string) bool { |
| // When lib is null, it should indicate that all values are included in the subset. |
| if lib == nil { |
| return true |
| } |
| if lib.Disabled || lib.DisableMacros { |
| return false |
| } |
| if len(lib.IncludeMacros) != 0 { |
| for _, name := range lib.IncludeMacros { |
| if name == macroFunction { |
| return true |
| } |
| } |
| return false |
| } |
| if len(lib.ExcludeMacros) != 0 { |
| for _, name := range lib.ExcludeMacros { |
| if name == macroFunction { |
| return false |
| } |
| } |
| return true |
| } |
| return true |
| } |
| |
| // SetDisabled disables or enables the library. |
| func (lib *LibrarySubset) SetDisabled(value bool) *LibrarySubset { |
| lib.Disabled = value |
| return lib |
| } |
| |
| // SetDisableMacros disables the macros for the library. |
| func (lib *LibrarySubset) SetDisableMacros(value bool) *LibrarySubset { |
| lib.DisableMacros = value |
| return lib |
| } |
| |
| // AddIncludedMacros allow-lists one or more macros by function name. |
| // |
| // Note, this option will override any excluded macros. |
| func (lib *LibrarySubset) AddIncludedMacros(macros ...string) *LibrarySubset { |
| lib.IncludeMacros = append(lib.IncludeMacros, macros...) |
| return lib |
| } |
| |
| // AddExcludedMacros deny-lists one or more macros by function name. |
| func (lib *LibrarySubset) AddExcludedMacros(macros ...string) *LibrarySubset { |
| lib.ExcludeMacros = append(lib.ExcludeMacros, macros...) |
| return lib |
| } |
| |
| // AddIncludedFunctions allow-lists one or more functions from the subset. |
| // |
| // Note, this option will override any excluded functions. |
| func (lib *LibrarySubset) AddIncludedFunctions(funcs ...*Function) *LibrarySubset { |
| lib.IncludeFunctions = append(lib.IncludeFunctions, funcs...) |
| return lib |
| } |
| |
| // AddExcludedFunctions deny-lists one or more functions from the subset. |
| func (lib *LibrarySubset) AddExcludedFunctions(funcs ...*Function) *LibrarySubset { |
| lib.ExcludeFunctions = append(lib.ExcludeFunctions, funcs...) |
| return lib |
| } |
| |
| // NewValidator returns a named Validator instance. |
| func NewValidator(name string) *Validator { |
| return &Validator{Name: name} |
| } |
| |
| // Validator represents a named validator with an optional map-based configuration object. |
| // |
| // Note: the map-keys must directly correspond to the internal representation of the original |
| // validator, and should only use primitive scalar types as values at this time. |
| type Validator struct { |
| Name string `yaml:"name"` |
| Config map[string]any `yaml:"config,omitempty"` |
| } |
| |
| // Validate validates the configuration of the validator object. |
| func (v *Validator) Validate() error { |
| if v == nil { |
| return errors.New("invalid validator: nil") |
| } |
| if v.Name == "" { |
| return errors.New("invalid validator: missing name") |
| } |
| return nil |
| } |
| |
| // SetConfig sets the set of map key-value pairs associated with this validator's configuration. |
| func (v *Validator) SetConfig(config map[string]any) *Validator { |
| v.Config = config |
| return v |
| } |
| |
| // ConfigValue retrieves the value associated with the config key name, if one exists. |
| func (v *Validator) ConfigValue(name string) (any, bool) { |
| if v == nil { |
| return nil, false |
| } |
| value, found := v.Config[name] |
| return value, found |
| } |
| |
| // NewFeature creates a new feature flag with a boolean enablement flag. |
| func NewFeature(name string, enabled bool) *Feature { |
| return &Feature{Name: name, Enabled: enabled} |
| } |
| |
| // Feature represents a named boolean feature flag supported by CEL. |
| type Feature struct { |
| Name string `yaml:"name"` |
| Enabled bool `yaml:"enabled"` |
| } |
| |
| // Validate validates whether the feature is well-configured. |
| func (feat *Feature) Validate() error { |
| if feat == nil { |
| return errors.New("invalid feature: nil") |
| } |
| if feat.Name == "" { |
| return errors.New("invalid feature: missing name") |
| } |
| return nil |
| } |
| |
| // Limit represents a named limit in the CEL environment. This is used to control |
| // the complexity tolerated before failing parsing, type checking, or planning. |
| type Limit struct { |
| Name string `yaml:"name"` |
| Value int `yaml:"value"` |
| } |
| |
| // NewLimit creates a new limit. |
| func NewLimit(name string, value int) *Limit { |
| return &Limit{name, value} |
| } |
| |
| // Validate validates a limit. |
| func (l *Limit) Validate() error { |
| if l == nil { |
| return errors.New("invalid limit: nil") |
| } |
| if l.Name == "" { |
| return errors.New("invalid limit: missing name") |
| } |
| return nil |
| } |
| |
| // NewTypeDesc describes a simple or complex type with parameters. |
| func NewTypeDesc(typeName string, params ...*TypeDesc) *TypeDesc { |
| return &TypeDesc{TypeName: typeName, Params: params} |
| } |
| |
| // NewTypeParam describe a type-param type. |
| func NewTypeParam(paramName string) *TypeDesc { |
| return &TypeDesc{TypeName: paramName, IsTypeParam: true} |
| } |
| |
| // TypeDesc represents the serializable format of a CEL *types.Type value. |
| type TypeDesc struct { |
| TypeName string `yaml:"type_name"` |
| Params []*TypeDesc `yaml:"params,omitempty"` |
| IsTypeParam bool `yaml:"is_type_param,omitempty"` |
| } |
| |
| // String implements the strings.Stringer interface method. |
| func (td *TypeDesc) String() string { |
| ps := make([]string, len(td.Params)) |
| for i, p := range td.Params { |
| ps[i] = p.String() |
| } |
| typeName := td.TypeName |
| if len(ps) != 0 { |
| typeName = fmt.Sprintf("%s(%s)", typeName, strings.Join(ps, ",")) |
| } |
| return typeName |
| } |
| |
| // Validate validates the type configuration is well-formed. |
| func (td *TypeDesc) Validate() error { |
| if td == nil { |
| return errors.New("invalid type: nil") |
| } |
| if td.TypeName == "" { |
| return errors.New("invalid type: missing type name") |
| } |
| if td.IsTypeParam && len(td.Params) != 0 { |
| return errors.New("invalid type: param type cannot have parameters") |
| } |
| switch td.TypeName { |
| case "list": |
| if len(td.Params) != 1 { |
| return fmt.Errorf("invalid type: list expects 1 parameter, got %d", len(td.Params)) |
| } |
| return td.Params[0].Validate() |
| case "map": |
| if len(td.Params) != 2 { |
| return fmt.Errorf("invalid type: map expects 2 parameters, got %d", len(td.Params)) |
| } |
| if err := td.Params[0].Validate(); err != nil { |
| return err |
| } |
| if err := td.Params[1].Validate(); err != nil { |
| return err |
| } |
| case "optional_type": |
| if len(td.Params) != 1 { |
| return fmt.Errorf("invalid type: optional_type expects 1 parameter, got %d", len(td.Params)) |
| } |
| return td.Params[0].Validate() |
| case "type": |
| if len(td.Params) == 0 { |
| return nil |
| } |
| if len(td.Params) != 1 { |
| return fmt.Errorf("invalid type: type expects 0 or 1 parameters, got %d", len(td.Params)) |
| } |
| return td.Params[0].Validate() |
| default: |
| } |
| return nil |
| } |
| |
| func formatSpecifierImpl(td *TypeDesc, sb *strings.Builder) { |
| if td.IsTypeParam { |
| sb.WriteRune('~') |
| sb.WriteString(td.TypeName) |
| return |
| } |
| sb.WriteString(td.TypeName) |
| l := len(td.Params) |
| if l < 1 { |
| return |
| } |
| sb.WriteRune('<') |
| for i, p := range td.Params { |
| formatSpecifierImpl(p, sb) |
| if i < l-1 { |
| sb.WriteString(", ") |
| } |
| } |
| sb.WriteRune('>') |
| } |
| |
| // SpecifierFormat returns the short text representation of the type. e.g. "map<string, int>" |
| func (td *TypeDesc) SpecifierFormat() string { |
| var sb strings.Builder |
| formatSpecifierImpl(td, &sb) |
| return sb.String() |
| } |
| |
| // AsCELType converts the serializable object to a *types.Type value. |
| func (td *TypeDesc) AsCELType(tp types.Provider) (*types.Type, error) { |
| err := td.Validate() |
| if err != nil { |
| return nil, err |
| } |
| switch td.TypeName { |
| case "dyn": |
| return types.DynType, nil |
| // short aliases for WKTs |
| case "duration": |
| return types.DurationType, nil |
| case "timestamp": |
| return types.TimestampType, nil |
| case "any": |
| return types.AnyType, nil |
| case "null", "null_type": |
| return types.NullType, nil |
| case "bool_wrapper": |
| return types.NewNullableType(types.BoolType), nil |
| case "bytes_wrapper": |
| return types.NewNullableType(types.BytesType), nil |
| case "double_wrapper": |
| return types.NewNullableType(types.DoubleType), nil |
| case "int_wrapper": |
| return types.NewNullableType(types.IntType), nil |
| case "uint_wrapper": |
| return types.NewNullableType(types.UintType), nil |
| case "string_wrapper": |
| return types.NewNullableType(types.StringType), nil |
| case "map": |
| kt, err := td.Params[0].AsCELType(tp) |
| if err != nil { |
| return nil, err |
| } |
| vt, err := td.Params[1].AsCELType(tp) |
| if err != nil { |
| return nil, err |
| } |
| return types.NewMapType(kt, vt), nil |
| case "list": |
| et, err := td.Params[0].AsCELType(tp) |
| if err != nil { |
| return nil, err |
| } |
| return types.NewListType(et), nil |
| case "optional_type": |
| et, err := td.Params[0].AsCELType(tp) |
| if err != nil { |
| return nil, err |
| } |
| return types.NewOptionalType(et), nil |
| case "type": |
| if len(td.Params) == 0 { |
| return types.TypeType, nil |
| } |
| pt, err := td.Params[0].AsCELType(tp) |
| if err != nil { |
| return nil, err |
| } |
| return types.NewTypeTypeWithParam(pt), nil |
| default: |
| if td.IsTypeParam { |
| return types.NewTypeParamType(td.TypeName), nil |
| } |
| if msgType, found := tp.FindStructType(td.TypeName); found { |
| // First parameter is the type name. |
| return msgType.Parameters()[0], nil |
| } |
| t, found := tp.FindIdent(td.TypeName) |
| if !found { |
| return nil, fmt.Errorf("undefined type name: %q", td.TypeName) |
| } |
| _, ok := t.(*types.Type) |
| if ok && len(td.Params) == 0 { |
| return t.(*types.Type), nil |
| } |
| params := make([]*types.Type, len(td.Params)) |
| for i, p := range td.Params { |
| params[i], err = p.AsCELType(tp) |
| if err != nil { |
| return nil, err |
| } |
| } |
| return types.NewOpaqueType(td.TypeName, params...), nil |
| } |
| } |
| |
| // SerializeTypeDesc converts a CEL native *types.Type to a serializable TypeDesc. |
| func SerializeTypeDesc(t *types.Type) *TypeDesc { |
| typeName := t.TypeName() |
| if t.Kind() == types.TypeParamKind { |
| return NewTypeParam(typeName) |
| } |
| if t != types.NullType && t.IsAssignableType(types.NullType) { |
| if wrapperTypeName, found := wrapperTypes[t.Kind()]; found { |
| return NewTypeDesc(wrapperTypeName) |
| } |
| } |
| var params []*TypeDesc |
| for _, p := range t.Parameters() { |
| params = append(params, SerializeTypeDesc(p)) |
| } |
| // Special types, these aren't useful for describing environments. |
| switch t.Kind() { |
| case types.ErrorKind: |
| typeName = "*error*" |
| case types.UnknownKind: |
| typeName = "*unknown*" |
| case types.UnspecifiedKind: |
| typeName = "*unspecified type*" |
| } |
| return NewTypeDesc(typeName, params...) |
| } |
| |
| var wrapperTypes = map[types.Kind]string{ |
| types.BoolKind: "google.protobuf.BoolValue", |
| types.BytesKind: "google.protobuf.BytesValue", |
| types.DoubleKind: "google.protobuf.DoubleValue", |
| types.IntKind: "google.protobuf.Int64Value", |
| types.StringKind: "google.protobuf.StringValue", |
| types.UintKind: "google.protobuf.UInt64Value", |
| } |