Skip to content

Commit fddae56

Browse files
Canonical environment description and stdlib subsetting (#1125)
* Canonical CEL environment with stdlib subsetting support
1 parent b7c14fa commit fddae56

30 files changed

+1398
-380
lines changed

cel/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ go_library(
2929
"//common/ast:go_default_library",
3030
"//common/containers:go_default_library",
3131
"//common/decls:go_default_library",
32+
"//common/env:go_default_library",
3233
"//common/functions:go_default_library",
3334
"//common/operators:go_default_library",
3435
"//common/overloads:go_default_library",

cel/cel_test.go

+84
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131

3232
"github.com/google/cel-go/checker"
3333
celast "github.com/google/cel-go/common/ast"
34+
"github.com/google/cel-go/common/env"
3435
"github.com/google/cel-go/common/operators"
3536
"github.com/google/cel-go/common/overloads"
3637
"github.com/google/cel-go/common/types"
@@ -359,6 +360,89 @@ func TestExtendStdlibFunction(t *testing.T) {
359360
}
360361
}
361362

363+
func TestSubsetStdLib(t *testing.T) {
364+
env, err := NewCustomEnv(StdLib(StdLibSubset(
365+
&env.LibrarySubset{
366+
IncludeMacros: []string{"has"},
367+
IncludeFunctions: []*env.Function{
368+
{Name: operators.Equals},
369+
{Name: operators.NotEquals},
370+
{Name: operators.LogicalAnd},
371+
{Name: operators.LogicalOr},
372+
{Name: operators.LogicalNot},
373+
{Name: overloads.Size, Overloads: []*env.Overload{{ID: "list_size"}}},
374+
},
375+
},
376+
)))
377+
if err != nil {
378+
t.Fatalf("StdLib() subsetting failed: %v", err)
379+
}
380+
tests := []struct {
381+
name string
382+
expr string
383+
compiles bool
384+
want ref.Val
385+
}{
386+
{
387+
name: "has macro",
388+
expr: "!has({}.a)",
389+
compiles: true,
390+
want: types.True,
391+
},
392+
{
393+
name: "not equals",
394+
expr: "has({}.a) != true",
395+
compiles: true,
396+
want: types.True,
397+
},
398+
{
399+
name: "logical operators",
400+
expr: "has({}.a) != true && has({'b': 1}.b) == true",
401+
compiles: true,
402+
want: types.True,
403+
},
404+
{
405+
name: "list size - allowed",
406+
expr: "[1, 2, 3].size()",
407+
compiles: true,
408+
want: types.Int(3),
409+
},
410+
{
411+
name: "excluded macro",
412+
expr: "[1, 2, 3].exists(i, i != 0)",
413+
compiles: false,
414+
},
415+
{
416+
name: "string size - not allowed",
417+
expr: "'hello'.size()",
418+
compiles: false,
419+
},
420+
}
421+
for _, tst := range tests {
422+
tc := tst
423+
t.Run(tc.name, func(t *testing.T) {
424+
ast, iss := env.Compile(tc.expr)
425+
if tc.compiles && iss.Err() != nil {
426+
t.Fatalf("env.Compile(%q) failed: %v", tc.expr, iss.Err())
427+
}
428+
if !tc.compiles && iss.Err() != nil {
429+
return
430+
}
431+
prg, err := env.Program(ast)
432+
if err != nil {
433+
t.Fatalf("env.Program() failed: %v", err)
434+
}
435+
out, _, err := prg.Eval(NoVars())
436+
if err != nil {
437+
t.Fatalf("prg.Eval() failed: %s", err)
438+
}
439+
if out.Equal(tc.want) != types.True {
440+
t.Errorf("prg.Eval() got %v, wanted %v", out, tc.want)
441+
}
442+
})
443+
}
444+
}
445+
362446
func TestCustomTypes(t *testing.T) {
363447
reg := types.NewEmptyRegistry()
364448
env := testEnv(t,

cel/decls.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,16 @@ func Variable(name string, t *Type) EnvOption {
148148
}
149149
}
150150

151+
// VariableDecls configures a set of fully defined cel.VariableDecl instances in the environment.
152+
func VariableDecls(vars ...*decls.VariableDecl) EnvOption {
153+
return func(e *Env) (*Env, error) {
154+
for _, v := range vars {
155+
e.variables = append(e.variables, v)
156+
}
157+
return e, nil
158+
}
159+
}
160+
151161
// Function defines a function and overloads with optional singleton or per-overload bindings.
152162
//
153163
// Using Function is roughly equivalent to calling Declarations() to declare the function signatures
@@ -209,7 +219,7 @@ func ExcludeOverloads(overloadIDs ...string) OverloadSelector {
209219
return decls.ExcludeOverloads(overloadIDs...)
210220
}
211221

212-
// FunctionDecls provides one or more fully formed function declaration to be added to the environment.
222+
// FunctionDecls provides one or more fully formed function declarations to be added to the environment.
213223
func FunctionDecls(funcs ...*decls.FunctionDecl) EnvOption {
214224
return func(e *Env) (*Env, error) {
215225
var err error

cel/library.go

+45-12
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import (
2222
"time"
2323

2424
"github.com/google/cel-go/common/ast"
25+
"github.com/google/cel-go/common/decls"
26+
"github.com/google/cel-go/common/env"
2527
"github.com/google/cel-go/common/operators"
2628
"github.com/google/cel-go/common/overloads"
2729
"github.com/google/cel-go/common/stdlib"
@@ -94,26 +96,61 @@ func Lib(l Library) EnvOption {
9496
}
9597
}
9698

99+
// StdLibOption specifies a functional option for configuring the standard CEL library.
100+
type StdLibOption func(*stdLibrary) *stdLibrary
101+
102+
// StdLibSubset configures the standard library to use a subset of its functions and macros.
103+
func StdLibSubset(subset *env.LibrarySubset) StdLibOption {
104+
return func(lib *stdLibrary) *stdLibrary {
105+
lib.subset = subset
106+
return lib
107+
}
108+
}
109+
97110
// StdLib returns an EnvOption for the standard library of CEL functions and macros.
98-
func StdLib() EnvOption {
99-
return Lib(stdLibrary{})
111+
func StdLib(opts ...StdLibOption) EnvOption {
112+
lib := &stdLibrary{}
113+
for _, o := range opts {
114+
lib = o(lib)
115+
}
116+
return Lib(lib)
100117
}
101118

102119
// stdLibrary implements the Library interface and provides functional options for the core CEL
103120
// features documented in the specification.
104-
type stdLibrary struct{}
121+
type stdLibrary struct {
122+
subset *env.LibrarySubset
123+
}
105124

106125
// LibraryName implements the SingletonLibrary interface method.
107-
func (stdLibrary) LibraryName() string {
126+
func (*stdLibrary) LibraryName() string {
108127
return "cel.lib.std"
109128
}
110129

111130
// CompileOptions returns options for the standard CEL function declarations and macros.
112-
func (stdLibrary) CompileOptions() []EnvOption {
131+
func (lib *stdLibrary) CompileOptions() []EnvOption {
132+
funcs := stdlib.Functions()
133+
macros := StandardMacros
134+
if lib.subset != nil {
135+
subMacros := []Macro{}
136+
for _, m := range macros {
137+
if lib.subset.SubsetMacro(m.Function()) {
138+
subMacros = append(subMacros, m)
139+
}
140+
}
141+
macros = subMacros
142+
subFuncs := []*decls.FunctionDecl{}
143+
for _, fn := range funcs {
144+
if f, include := lib.subset.SubsetFunction(fn); include {
145+
subFuncs = append(subFuncs, f)
146+
}
147+
}
148+
funcs = subFuncs
149+
}
113150
return []EnvOption{
114151
func(e *Env) (*Env, error) {
115152
var err error
116-
for _, fn := range stdlib.Functions() {
153+
for _, fn := range funcs {
117154
existing, found := e.functions[fn.Name()]
118155
if found {
119156
fn, err = existing.Merge(fn)
@@ -125,16 +162,12 @@ func (stdLibrary) CompileOptions() []EnvOption {
125162
}
126163
return e, nil
127164
},
128-
func(e *Env) (*Env, error) {
129-
e.variables = append(e.variables, stdlib.Types()...)
130-
return e, nil
131-
},
132-
Macros(StandardMacros...),
165+
Macros(macros...),
133166
}
134167
}
135168

136169
// ProgramOptions returns function implementations for the standard CEL functions.
137-
func (stdLibrary) ProgramOptions() []ProgramOption {
170+
func (*stdLibrary) ProgramOptions() []ProgramOption {
138171
return []ProgramOption{}
139172
}
140173

checker/checker_test.go

-3
Original file line numberDiff line numberDiff line change
@@ -2377,7 +2377,6 @@ func TestCheck(t *testing.T) {
23772377
t.Fatalf("NewEnv(cont, reg) failed: %v", err)
23782378
}
23792379
if !tc.disableStdEnv {
2380-
env.AddIdents(stdlib.Types()...)
23812380
env.AddFunctions(stdlib.Functions()...)
23822381
}
23832382
if tc.env.idents != nil {
@@ -2467,7 +2466,6 @@ func BenchmarkCheck(b *testing.B) {
24672466
b.Fatalf("NewEnv(cont, reg) failed: %v", err)
24682467
}
24692468
if !tc.disableStdEnv {
2470-
env.AddIdents(stdlib.Types()...)
24712469
env.AddFunctions(stdlib.Functions()...)
24722470
}
24732471
if tc.env.idents != nil {
@@ -2583,7 +2581,6 @@ func TestCheckErrorData(t *testing.T) {
25832581
if err != nil {
25842582
t.Fatalf("NewEnv(cont, reg) failed: %v", err)
25852583
}
2586-
env.AddIdents(stdlib.Types()...)
25872584
env.AddFunctions(stdlib.Functions()...)
25882585
_, iss = Check(ast, src, env)
25892586
if len(iss.GetErrors()) != 1 {

checker/env_test.go

+1-19
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,6 @@ import (
2626
"github.com/google/cel-go/parser"
2727
)
2828

29-
func TestOverlappingIdentifier(t *testing.T) {
30-
env := newStdEnv(t)
31-
err := env.AddIdents(decls.NewVariable("int", types.TypeType))
32-
if err == nil {
33-
t.Error("Got nil, wanted error")
34-
} else if !strings.Contains(err.Error(), "overlapping identifier") {
35-
t.Errorf("Got %v, wanted overlapping identifier error", err)
36-
}
37-
}
38-
3929
func TestOverlappingMacro(t *testing.T) {
4030
env := newStdEnv(t)
4131
hasFn, err := decls.NewFunction("has",
@@ -92,10 +82,6 @@ func BenchmarkCopyDeclarations(b *testing.B) {
9282
if err != nil {
9383
b.Fatalf("NewEnv() failed: %v", err)
9484
}
95-
err = env.AddIdents(stdlib.Types()...)
96-
if err != nil {
97-
b.Fatalf("env.AddIdents(stdlib.Types()...) failed: %v", err)
98-
}
9985
err = env.AddFunctions(stdlib.Functions()...)
10086
if err != nil {
10187
b.Fatalf("env.AddFunctions(stdlib.Functions()...) failed: %v", err)
@@ -111,13 +97,9 @@ func newStdEnv(t *testing.T) *Env {
11197
if err != nil {
11298
t.Fatalf("NewEnv() failed: %v", err)
11399
}
114-
err = env.AddIdents(stdlib.Types()...)
115-
if err != nil {
116-
t.Fatalf("env.Add(stdlib.TypeExprDecls()...) failed: %v", err)
117-
}
118100
err = env.AddFunctions(stdlib.Functions()...)
119101
if err != nil {
120-
t.Fatalf("env.Add(stdlib.FunctionExprDecls()...) failed: %v", err)
102+
t.Fatalf("env.Add(stdlib.Functions()...) failed: %v", err)
121103
}
122104
return env
123105
}

common/ast/navigable_test.go

-4
Original file line numberDiff line numberDiff line change
@@ -594,10 +594,6 @@ func newTestEnv(t testing.TB, cont *containers.Container, reg *types.Registry) *
594594
if err != nil {
595595
t.Fatalf("checker.NewEnv(%v, %v) failed: %v", cont, reg, err)
596596
}
597-
err = env.AddIdents(stdlib.Types()...)
598-
if err != nil {
599-
t.Fatalf("env.Add(stdlib.Types()...) failed: %v", err)
600-
}
601597
err = env.AddFunctions(stdlib.Functions()...)
602598
if err != nil {
603599
t.Fatalf("env.Add(stdlib.Functions()...) failed: %v", err)

common/decls/decls.go

+4
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ func (f *FunctionDecl) Merge(other *FunctionDecl) (*FunctionDecl, error) {
148148
return merged, nil
149149
}
150150

151+
// FunctionSubsetter subsets a function declaration or returns nil and false if the function
152+
// subset was empty.
153+
type FunctionSubsetter func(fn *FunctionDecl) (*FunctionDecl, bool)
154+
151155
// OverloadSelector selects an overload associated with a given function when it returns true.
152156
//
153157
// Used in combination with the Subset method.

common/env/BUILD.bazel

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
load("@io_bazel_rules_go//go:def.bzl", "go_library")
2+
3+
package(
4+
default_visibility = ["//visibility:public"],
5+
licenses = ["notice"], # Apache 2.0
6+
)
7+
8+
go_library(
9+
name = "go_default_library",
10+
srcs = [
11+
"env.go",
12+
],
13+
importpath = "github.com/google/cel-go/common/env",
14+
deps = [
15+
"//common/decls:go_default_library",
16+
"//common/types:go_default_library",
17+
],
18+
)

0 commit comments

Comments
 (0)