Skip to content

Commit af0bf8e

Browse files
Support for cel.Env conversion to YAML-serializable config (#1128)
* Support for cel.Env conversion to an env.Config object * Backed out change remove stdlib types as variables
1 parent fddae56 commit af0bf8e

35 files changed

+12653
-161
lines changed

cel/cel_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2234,8 +2234,7 @@ func TestDefaultUTCTimeZoneError(t *testing.T) {
22342234
|| x.getMilliseconds('Am/Ph') == 1
22352235
`, map[string]any{
22362236
"x": time.Unix(7506, 1000000).Local(),
2237-
},
2238-
)
2237+
})
22392238
if err == nil {
22402239
t.Fatalf("prg.Eval() got %v wanted error", out)
22412240
}

cel/env.go

Lines changed: 127 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ package cel
1616

1717
import (
1818
"errors"
19+
"fmt"
20+
"math"
1921
"sync"
2022

2123
"github.com/google/cel-go/checker"
@@ -24,12 +26,15 @@ import (
2426
celast "github.com/google/cel-go/common/ast"
2527
"github.com/google/cel-go/common/containers"
2628
"github.com/google/cel-go/common/decls"
29+
"github.com/google/cel-go/common/env"
30+
"github.com/google/cel-go/common/stdlib"
2731
"github.com/google/cel-go/common/types"
2832
"github.com/google/cel-go/common/types/ref"
2933
"github.com/google/cel-go/interpreter"
3034
"github.com/google/cel-go/parser"
3135

3236
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
37+
"google.golang.org/protobuf/reflect/protoreflect"
3338
)
3439

3540
// Source interface representing a user-provided expression.
@@ -127,12 +132,13 @@ type Env struct {
127132
Container *containers.Container
128133
variables []*decls.VariableDecl
129134
functions map[string]*decls.FunctionDecl
130-
macros []parser.Macro
135+
macros []Macro
136+
contextProto protoreflect.MessageDescriptor
131137
adapter types.Adapter
132138
provider types.Provider
133139
features map[int]bool
134140
appliedFeatures map[int]bool
135-
libraries map[string]bool
141+
libraries map[string]SingletonLibrary
136142
validators []ASTValidator
137143
costOptions []checker.CostOption
138144

@@ -151,6 +157,115 @@ type Env struct {
151157
progOpts []ProgramOption
152158
}
153159

160+
// ToConfig produces a YAML-serializable env.Config object from the given environment.
161+
//
162+
// The serialized configuration value is intended to represent a baseline set of config
163+
// options which could be used as input to an EnvOption to configure the majority of the
164+
// environment from a file.
165+
//
166+
// Note: validators, features, flags, and safe-guard settings are not yet supported by
167+
// the serialize method. Since optimizers are a separate construct from the environment
168+
// and the standard expression components (parse, check, evalute), they are also not
169+
// supported by the serialize method.
170+
func (e *Env) ToConfig(name string) (*env.Config, error) {
171+
conf := env.NewConfig(name)
172+
// Container settings
173+
if e.Container != containers.DefaultContainer {
174+
conf.SetContainer(e.Container.Name())
175+
}
176+
for _, typeName := range e.Container.AliasSet() {
177+
conf.AddImports(env.NewImport(typeName))
178+
}
179+
180+
libOverloads := map[string][]string{}
181+
for libName, lib := range e.libraries {
182+
// Track the options which have been configured by a library and
183+
// then diff the library version against the configured function
184+
// to detect incremental overloads or rewrites.
185+
libEnv, _ := NewCustomEnv()
186+
libEnv, _ = Lib(lib)(libEnv)
187+
for fnName, fnDecl := range libEnv.Functions() {
188+
if len(fnDecl.OverloadDecls()) == 0 {
189+
continue
190+
}
191+
overloads, exist := libOverloads[fnName]
192+
if !exist {
193+
overloads = make([]string, 0, len(fnDecl.OverloadDecls()))
194+
}
195+
for _, o := range fnDecl.OverloadDecls() {
196+
overloads = append(overloads, o.ID())
197+
}
198+
libOverloads[fnName] = overloads
199+
}
200+
subsetLib, canSubset := lib.(LibrarySubsetter)
201+
alias := ""
202+
if aliasLib, canAlias := lib.(LibraryAliaser); canAlias {
203+
alias = aliasLib.LibraryAlias()
204+
libName = alias
205+
}
206+
if libName == "stdlib" && canSubset {
207+
conf.SetStdLib(subsetLib.LibrarySubset())
208+
continue
209+
}
210+
version := uint32(math.MaxUint32)
211+
if versionLib, isVersioned := lib.(LibraryVersioner); isVersioned {
212+
version = versionLib.LibraryVersion()
213+
}
214+
conf.AddExtensions(env.NewExtension(libName, version))
215+
}
216+
217+
// If this is a custom environment without the standard env, mark the stdlib as disabled.
218+
if conf.StdLib == nil && !e.HasLibrary("cel.lib.std") {
219+
conf.SetStdLib(env.NewLibrarySubset().SetDisabled(true))
220+
}
221+
222+
// Serialize the variables
223+
vars := make([]*decls.VariableDecl, 0, len(e.Variables()))
224+
stdTypeVars := map[string]*decls.VariableDecl{}
225+
for _, v := range stdlib.Types() {
226+
stdTypeVars[v.Name()] = v
227+
}
228+
for _, v := range e.Variables() {
229+
if _, isStdType := stdTypeVars[v.Name()]; isStdType {
230+
continue
231+
}
232+
vars = append(vars, v)
233+
}
234+
if e.contextProto != nil {
235+
conf.SetContextVariable(env.NewContextVariable(string(e.contextProto.FullName())))
236+
skipVariables := map[string]bool{}
237+
fields := e.contextProto.Fields()
238+
for i := 0; i < fields.Len(); i++ {
239+
field := fields.Get(i)
240+
variable, err := fieldToVariable(field)
241+
if err != nil {
242+
return nil, fmt.Errorf("could not serialize context field variable %q, reason: %w", field.FullName(), err)
243+
}
244+
skipVariables[variable.Name()] = true
245+
}
246+
for _, v := range vars {
247+
if _, found := skipVariables[v.Name()]; !found {
248+
conf.AddVariableDecls(v)
249+
}
250+
}
251+
} else {
252+
conf.AddVariableDecls(vars...)
253+
}
254+
255+
// Serialize functions which are distinct from the ones configured by libraries.
256+
for fnName, fnDecl := range e.Functions() {
257+
if excludedOverloads, found := libOverloads[fnName]; found {
258+
if newDecl := fnDecl.Subset(decls.ExcludeOverloads(excludedOverloads...)); newDecl != nil {
259+
conf.AddFunctionDecls(newDecl)
260+
}
261+
} else {
262+
conf.AddFunctionDecls(fnDecl)
263+
}
264+
}
265+
266+
return conf, nil
267+
}
268+
154269
// NewEnv creates a program environment configured with the standard library of CEL functions and
155270
// macros. The Env value returned can parse and check any CEL program which builds upon the core
156271
// features documented in the CEL specification.
@@ -194,7 +309,7 @@ func NewCustomEnv(opts ...EnvOption) (*Env, error) {
194309
provider: registry,
195310
features: map[int]bool{},
196311
appliedFeatures: map[int]bool{},
197-
libraries: map[string]bool{},
312+
libraries: map[string]SingletonLibrary{},
198313
validators: []ASTValidator{},
199314
progOpts: []ProgramOption{},
200315
costOptions: []checker.CostOption{},
@@ -362,7 +477,7 @@ func (e *Env) Extend(opts ...EnvOption) (*Env, error) {
362477
for k, v := range e.functions {
363478
funcsCopy[k] = v
364479
}
365-
libsCopy := make(map[string]bool, len(e.libraries))
480+
libsCopy := make(map[string]SingletonLibrary, len(e.libraries))
366481
for k, v := range e.libraries {
367482
libsCopy[k] = v
368483
}
@@ -376,6 +491,7 @@ func (e *Env) Extend(opts ...EnvOption) (*Env, error) {
376491
variables: varsCopy,
377492
functions: funcsCopy,
378493
macros: macsCopy,
494+
contextProto: e.contextProto,
379495
progOpts: progOptsCopy,
380496
adapter: adapter,
381497
features: featuresCopy,
@@ -399,8 +515,8 @@ func (e *Env) HasFeature(flag int) bool {
399515

400516
// HasLibrary returns whether a specific SingletonLibrary has been configured in the environment.
401517
func (e *Env) HasLibrary(libName string) bool {
402-
configured, exists := e.libraries[libName]
403-
return exists && configured
518+
_, exists := e.libraries[libName]
519+
return exists
404520
}
405521

406522
// Libraries returns a list of SingletonLibrary that have been configured in the environment.
@@ -423,6 +539,11 @@ func (e *Env) Functions() map[string]*decls.FunctionDecl {
423539
return e.functions
424540
}
425541

542+
// Variables returns the set of variables associated with the environment.
543+
func (e *Env) Variables() []*decls.VariableDecl {
544+
return e.variables
545+
}
546+
426547
// HasValidator returns whether a specific ASTValidator has been configured in the environment.
427548
func (e *Env) HasValidator(name string) bool {
428549
for _, v := range e.validators {

cel/env_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ package cel
1616

1717
import (
1818
"fmt"
19+
"math"
1920
"reflect"
2021
"sync"
2122
"testing"
2223

2324
"github.com/google/cel-go/common"
25+
"github.com/google/cel-go/common/env"
2426
"github.com/google/cel-go/common/operators"
2527
"github.com/google/cel-go/common/types"
2628
"github.com/google/cel-go/common/types/ref"
@@ -302,6 +304,119 @@ func TestFunctions(t *testing.T) {
302304
}
303305
}
304306

307+
func TestEnvToConfig(t *testing.T) {
308+
tests := []struct {
309+
name string
310+
opts []EnvOption
311+
wantConfig *env.Config
312+
}{
313+
{
314+
name: "std env",
315+
wantConfig: env.NewConfig("std env"),
316+
},
317+
{
318+
name: "std env - container",
319+
opts: []EnvOption{
320+
Container("example.container"),
321+
},
322+
wantConfig: env.NewConfig("std env - container").SetContainer("example.container"),
323+
},
324+
{
325+
name: "std env - aliases",
326+
opts: []EnvOption{
327+
Abbrevs("example.type.name"),
328+
},
329+
wantConfig: env.NewConfig("std env - aliases").AddImports(env.NewImport("example.type.name")),
330+
},
331+
{
332+
name: "std env disabled",
333+
opts: []EnvOption{
334+
func(*Env) (*Env, error) {
335+
return NewCustomEnv()
336+
},
337+
},
338+
wantConfig: env.NewConfig("std env disabled").SetStdLib(
339+
env.NewLibrarySubset().SetDisabled(true)),
340+
},
341+
{
342+
name: "std env - with variable",
343+
opts: []EnvOption{
344+
Variable("var", IntType),
345+
},
346+
wantConfig: env.NewConfig("std env - with variable").AddVariables(env.NewVariable("var", env.NewTypeDesc("int"))),
347+
},
348+
{
349+
name: "std env - with function",
350+
opts: []EnvOption{Function("hello", Overload("hello_string", []*Type{StringType}, StringType))},
351+
wantConfig: env.NewConfig("std env - with function").AddFunctions(
352+
env.NewFunction("hello", []*env.Overload{
353+
env.NewOverload("hello_string",
354+
[]*env.TypeDesc{env.NewTypeDesc("string")}, env.NewTypeDesc("string"))},
355+
)),
356+
},
357+
{
358+
name: "optional lib",
359+
opts: []EnvOption{
360+
OptionalTypes(),
361+
},
362+
wantConfig: env.NewConfig("optional lib").AddExtensions(env.NewExtension("optional", math.MaxUint32)),
363+
},
364+
{
365+
name: "optional lib - versioned",
366+
opts: []EnvOption{
367+
OptionalTypes(OptionalTypesVersion(1)),
368+
},
369+
wantConfig: env.NewConfig("optional lib - versioned").AddExtensions(env.NewExtension("optional", 1)),
370+
},
371+
{
372+
name: "optional lib - alt last()",
373+
opts: []EnvOption{
374+
OptionalTypes(),
375+
Function("last", MemberOverload("string_last", []*Type{StringType}, StringType)),
376+
},
377+
wantConfig: env.NewConfig("optional lib - alt last()").
378+
AddExtensions(env.NewExtension("optional", math.MaxUint32)).
379+
AddFunctions(env.NewFunction("last", []*env.Overload{
380+
env.NewMemberOverload("string_last", env.NewTypeDesc("string"), []*env.TypeDesc{}, env.NewTypeDesc("string")),
381+
})),
382+
},
383+
{
384+
name: "context proto - with extra variable",
385+
opts: []EnvOption{
386+
DeclareContextProto((&proto3pb.TestAllTypes{}).ProtoReflect().Descriptor()),
387+
Variable("extra", StringType),
388+
},
389+
wantConfig: env.NewConfig("context proto - with extra variable").
390+
SetContextVariable(env.NewContextVariable("google.expr.proto3.test.TestAllTypes")).
391+
AddVariables(env.NewVariable("extra", env.NewTypeDesc("string"))),
392+
},
393+
{
394+
name: "context proto",
395+
opts: []EnvOption{
396+
DeclareContextProto((&proto3pb.TestAllTypes{}).ProtoReflect().Descriptor()),
397+
},
398+
wantConfig: env.NewConfig("context proto").SetContextVariable(env.NewContextVariable("google.expr.proto3.test.TestAllTypes")),
399+
},
400+
}
401+
402+
for _, tst := range tests {
403+
tc := tst
404+
t.Run(tc.name, func(t *testing.T) {
405+
e, err := NewEnv(tc.opts...)
406+
if err != nil {
407+
t.Fatalf("NewEnv() failed: %v", err)
408+
}
409+
gotConfig, err := e.ToConfig(tc.name)
410+
if err != nil {
411+
t.Fatalf("ToConfig() failed: %v", err)
412+
}
413+
if !reflect.DeepEqual(gotConfig, tc.wantConfig) {
414+
t.Errorf("e.Config() got %v, wanted %v", gotConfig, tc.wantConfig)
415+
}
416+
})
417+
}
418+
}
419+
305420
func BenchmarkNewCustomEnvLazy(b *testing.B) {
306421
b.ResetTimer()
307422
for i := 0; i < b.N; i++ {

0 commit comments

Comments
 (0)