blob: f75ee503c66e3108ac490e70c0855d11bfe716dc [file] [edit]
// 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 (
"fmt"
"io/ioutil"
"log"
"testing"
"github.com/google/cel-go/common/operators"
"github.com/google/cel-go/common/overloads"
"github.com/golang/protobuf/proto"
"github.com/google/cel-go/checker/decls"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
"github.com/google/cel-go/interpreter/functions"
descpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
)
func Example() {
// Create the CEL environment with declarations for the input attributes and
// the desired extension functions. In many cases the desired functionality will
// be present in a built-in function.
decls := Declarations(
// Identifiers used within this expression.
decls.NewIdent("i", decls.String, nil),
decls.NewIdent("you", decls.String, nil),
// Function to generate a greeting from one person to another.
// i.greet(you)
decls.NewFunction("greet",
decls.NewInstanceOverload("greet_string_string",
[]*exprpb.Type{decls.String, decls.String},
decls.String)))
e, err := NewEnv(decls)
if err != nil {
log.Fatalf("environment creation error: %s\n", err)
}
// Parse and check the expression.
p, iss := e.Parse("i.greet(you)")
if iss != nil && iss.Err() != nil {
log.Fatalln(iss.Err())
}
c, iss := e.Check(p)
if iss != nil && iss.Err() != nil {
log.Fatalln(iss.Err())
}
// Create the program.
funcs := Functions(
&functions.Overload{
Operator: "greet",
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
return types.String(
fmt.Sprintf("Hello %s! Nice to meet you, I'm %s.\n", rhs, lhs))
}})
prg, err := e.Program(c, funcs)
if err != nil {
log.Fatalf("program creation error: %s\n", err)
}
// Evaluate the program against some inputs. Note: the details return is not used.
out, _, err := prg.Eval(map[string]interface{}{
// Native values are converted to CEL values under the covers.
"i": "CEL",
// Values may also be lazily supplied.
"you": func() ref.Val { return types.String("world") },
})
if err != nil {
log.Fatalf("runtime error: %s\n", err)
}
fmt.Println(out)
// Output:Hello world! Nice to meet you, I'm CEL.
}
func Test_ExampleWithBuiltins(t *testing.T) {
// Variables used within this expression environment.
decls := Declarations(
decls.NewIdent("i", decls.String, nil),
decls.NewIdent("you", decls.String, nil))
env, err := NewEnv(decls)
if err != nil {
t.Fatalf("environment creation error: %s\n", err)
}
// Parse and type-check the expression.
p, iss := env.Parse(`"Hello " + you + "! I'm " + i + "."`)
if iss != nil && iss.Err() != nil {
t.Fatal(iss.Err())
}
c, iss := env.Check(p)
if iss != nil && iss.Err() != nil {
t.Fatal(iss.Err())
}
// Create the program, and evaluate it against some input.
prg, err := env.Program(c)
if err != nil {
t.Fatalf("program creation error: %s\n", err)
}
// If the Eval() call were provided with cel.EvalOptions(OptTrackState) the details response
// (2nd return) would be non-nil.
out, _, err := prg.Eval(map[string]interface{}{
"i": "CEL",
"you": "world"})
if err != nil {
t.Fatalf("runtime error: %s\n", err)
}
// Hello world! I'm CEL.
if out.Equal(types.String("Hello world! I'm CEL.")) != types.True {
t.Errorf(`Got '%v', wanted "Hello world! I'm CEL."`, out.Value())
}
}
func Test_DisableStandardEnv(t *testing.T) {
e, _ := NewEnv(
ClearBuiltIns(),
Declarations(decls.NewIdent("a.b.c", decls.Bool, nil)))
t.Run("err", func(t *testing.T) {
p, _ := e.Parse("a.b.c == true")
_, iss := e.Check(p)
if iss == nil || iss.Err() == nil {
t.Error("Got successful check, expected error for missing operator '_==_'")
}
})
t.Run("ok", func(t *testing.T) {
p, _ := e.Parse("a.b.c")
c, _ := e.Check(p)
prg, _ := e.Program(c)
out, _, _ := prg.Eval(map[string]interface{}{"a.b.c": true})
if out != types.True {
t.Errorf("Got '%v', wanted 'true'", out.Value())
}
})
}
func Test_HomogeneousAggregateLiterals(t *testing.T) {
e, _ := NewEnv(
ClearBuiltIns(),
Declarations(
decls.NewIdent("name", decls.String, nil),
decls.NewFunction(
operators.In,
decls.NewOverload(overloads.InList, []*exprpb.Type{
decls.String, decls.NewListType(decls.String),
}, decls.Bool),
decls.NewOverload(overloads.InMap, []*exprpb.Type{
decls.String, decls.NewMapType(decls.String, decls.Bool),
}, decls.Bool))),
HomogeneousAggregateLiterals())
t.Run("err_list", func(t *testing.T) {
p, _ := e.Parse("name in ['hello', 0]")
_, iss := e.Check(p)
if iss == nil || iss.Err() == nil {
t.Error("Got successful check, expected error for mixed list entry types.")
}
})
t.Run("err_map_key", func(t *testing.T) {
p, _ := e.Parse("name in {'hello':'world', 1:'!'}")
_, iss := e.Check(p)
if iss == nil || iss.Err() == nil {
t.Error("Got successful check, expected error for mixed map key types.")
}
})
t.Run("err_map_val", func(t *testing.T) {
p, _ := e.Parse("name in {'hello':'world', 'goodbye':true}")
_, iss := e.Check(p)
if iss == nil || iss.Err() == nil {
t.Error("Got successful check, expected error for mixed map value types.")
}
})
funcs := Functions(&functions.Overload{
Operator: operators.In,
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
if rhs.Type().HasTrait(traits.ContainerType) {
return rhs.(traits.Container).Contains(lhs)
}
return types.ValOrErr(rhs, "no such overload")
},
})
t.Run("ok_list", func(t *testing.T) {
p, _ := e.Parse("name in ['hello', 'world']")
c, iss := e.Check(p)
if iss != nil && iss.Err() != nil {
t.Fatalf("Got issue: %v, expected successful check.", iss.Err())
}
prg, _ := e.Program(c, funcs)
out, _, err := prg.Eval(map[string]interface{}{"name": "world"})
if err != nil {
t.Fatalf("Got err: %v, wanted result", err)
}
if out != types.True {
t.Errorf("Got '%v', wanted 'true'", out)
}
})
t.Run("ok_map", func(t *testing.T) {
p, _ := e.Parse("name in {'hello': false, 'world': true}")
c, iss := e.Check(p)
if iss != nil && iss.Err() != nil {
t.Fatalf("Got issue: %v, expected successful check.", iss.Err())
}
prg, _ := e.Program(c, funcs)
out, _, err := prg.Eval(map[string]interface{}{"name": "world"})
if err != nil {
t.Fatalf("Got err: %v, wanted result", err)
}
if out != types.True {
t.Errorf("Got '%v', wanted 'true'", out)
}
})
}
func Test_CustomTypes(t *testing.T) {
e, _ := NewEnv(
Container("google.api.expr.v1alpha1"),
Types(&exprpb.Expr{}),
Declarations(
decls.NewIdent("expr",
decls.NewObjectType("google.api.expr.v1alpha1.Expr"), nil)))
p, _ := e.Parse(`
expr == Expr{id: 2,
call_expr: Expr.Call{
function: "_==_",
args: [
Expr{id: 1, ident_expr: Expr.Ident{ name: "a" }},
Expr{id: 3, ident_expr: Expr.Ident{ name: "b" }}]
}}`)
c, _ := e.Check(p)
prg, _ := e.Program(c)
vars := map[string]interface{}{"expr": &exprpb.Expr{
Id: 2,
ExprKind: &exprpb.Expr_CallExpr{
CallExpr: &exprpb.Expr_Call{
Function: "_==_",
Args: []*exprpb.Expr{
&exprpb.Expr{
Id: 1,
ExprKind: &exprpb.Expr_IdentExpr{
IdentExpr: &exprpb.Expr_Ident{Name: "a"},
},
},
&exprpb.Expr{
Id: 3,
ExprKind: &exprpb.Expr_IdentExpr{
IdentExpr: &exprpb.Expr_Ident{Name: "b"},
},
},
},
},
},
}}
out, _, _ := prg.Eval(vars)
if out != types.True {
t.Errorf("Got '%v', wanted 'true'", out.Value())
}
}
func Test_TypeIsolation(t *testing.T) {
b, err := ioutil.ReadFile("testdata/team.fds")
if err != nil {
t.Fatal("Can't read fds file: ", err)
}
var fds descpb.FileDescriptorSet
if err = proto.Unmarshal(b, &fds); err != nil {
t.Fatal("Can't unmarshal descriptor data: ", err)
}
e, err := NewEnv(
TypeDescs(&fds),
Declarations(
decls.NewIdent("myteam",
decls.NewObjectType("cel.testdata.Team"),
nil)))
if err != nil {
t.Fatal("Can't create env: ", err)
}
src := "myteam.members[0].name == 'Cyclops'"
p, _ := e.Parse(src)
_, iss := e.Check(p)
if iss != nil && iss.Err() != nil {
t.Error(iss.Err())
}
// Ensure that isolated types don't leak through.
e2, _ := NewEnv(
Declarations(
decls.NewIdent("myteam",
decls.NewObjectType("cel.testdata.Team"),
nil)))
p2, _ := e2.Parse(src)
_, iss = e2.Check(p2)
if iss == nil || iss.Err() == nil {
t.Errorf("Wanted check failure for unknown message.")
}
}
func Test_GlobalVars(t *testing.T) {
mapStrDyn := decls.NewMapType(decls.String, decls.Dyn)
e, _ := NewEnv(
Declarations(
decls.NewIdent("attrs", mapStrDyn, nil),
decls.NewIdent("default", decls.Dyn, nil),
decls.NewFunction(
"get",
decls.NewInstanceOverload(
"get_map",
[]*exprpb.Type{mapStrDyn, decls.String, decls.Dyn},
decls.Dyn))))
p, _ := e.Parse(`attrs.get("first", attrs.get("second", default))`)
c, _ := e.Check(p)
// Create the program.
funcs := Functions(
&functions.Overload{
Operator: "get",
Function: func(args ...ref.Val) ref.Val {
if len(args) != 3 {
return types.NewErr("invalid arguments to 'get'")
}
attrs, ok := args[0].(traits.Mapper)
if !ok {
return types.NewErr(
"invalid operand of type '%v' to obj.get(key, def)",
args[0].Type())
}
key, ok := args[1].(types.String)
if !ok {
return types.NewErr(
"invalid key of type '%v' to obj.get(key, def)",
args[1].Type())
}
defVal := args[2]
if attrs.Contains(key) == types.True {
return attrs.Get(key)
}
return defVal
}})
// Global variables can be configured as a ProgramOption and optionally overridden on Eval.
prg, _ := e.Program(c, funcs, Globals(map[string]interface{}{
"default": "third",
}))
t.Run("global_default", func(t *testing.T) {
vars := map[string]interface{}{
"attrs": map[string]interface{}{}}
out, _, _ := prg.Eval(vars)
if out.Equal(types.String("third")) != types.True {
t.Errorf("Got '%v', expected 'third'.", out.Value())
}
})
t.Run("attrs_alt", func(t *testing.T) {
vars := map[string]interface{}{
"attrs": map[string]interface{}{"second": "yep"}}
out, _, _ := prg.Eval(vars)
if out.Equal(types.String("yep")) != types.True {
t.Errorf("Got '%v', expected 'yep'.", out.Value())
}
})
t.Run("local_default", func(t *testing.T) {
vars := map[string]interface{}{
"attrs": map[string]interface{}{},
"default": "fourth"}
out, _, _ := prg.Eval(vars)
if out.Equal(types.String("fourth")) != types.True {
t.Errorf("Got '%v', expected 'fourth'.", out.Value())
}
})
}
func Test_EvalOptions(t *testing.T) {
e, _ := NewEnv(
Declarations(
decls.NewIdent("k", decls.String, nil),
decls.NewIdent("v", decls.Bool, nil)))
p, _ := e.Parse(`{k: true}[k] || v != false`)
c, _ := e.Check(p)
prg, err := e.Program(c, EvalOptions(OptExhaustiveEval))
if err != nil {
t.Fatalf("program creation error: %s\n", err)
}
out, details, err := prg.Eval(
map[string]interface{}{
"k": "key",
"v": true})
if err != nil {
t.Fatalf("runtime error: %s\n", err)
}
if out != types.True {
t.Errorf("Got '%v', expected 'true'", out.Value())
}
// Test to see whether 'v != false' was resolved to a value.
// With short-circuiting it normally wouldn't be.
s := details.State()
lhsVal, found := s.Value(p.Expr().GetCallExpr().GetArgs()[0].Id)
if !found {
t.Error("Got not found, wanted evaluation of left hand side expression.")
return
}
if lhsVal != types.True {
t.Errorf("Got '%v', expected 'true'", lhsVal)
}
rhsVal, found := s.Value(p.Expr().GetCallExpr().GetArgs()[1].Id)
if !found {
t.Error("Got not found, wanted evaluation of right hand side expression.")
return
}
if rhsVal != types.True {
t.Errorf("Got '%v', expected 'true'", rhsVal)
}
}
func Benchmark_EvalOptions(b *testing.B) {
e, _ := NewEnv(
Declarations(
decls.NewIdent("ai", decls.Int, nil),
decls.NewIdent("ar", decls.NewMapType(decls.String, decls.String), nil),
),
)
past, _ := e.Parse("ai == 20 || ar['foo'] == 'bar'")
cast, _ := e.Check(past)
vars := map[string]interface{}{
"ai": 2,
"ar": map[string]string{
"foo": "bar",
},
}
opts := map[string]EvalOption{
"track-state": OptTrackState,
"exhaustive-eval": OptExhaustiveEval,
"fold-constants": OptFoldConstants,
}
for k, opt := range opts {
b.Run(k, func(bb *testing.B) {
prg, _ := e.Program(cast, EvalOptions(opt))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < bb.N; i++ {
prg.Eval(vars)
}
})
}
}