Skip to content

Instantly share code, notes, and snippets.

@LOZORD
Created January 12, 2020 17:14
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save LOZORD/573749028b570e694658d1dd3273e74d to your computer and use it in GitHub Desktop.
Save LOZORD/573749028b570e694658d1dd3273e74d to your computer and use it in GitHub Desktop.
An example of using a native JSON-friendly Go struct as a type and input in a CEL program.
package test
import (
"bytes"
"encoding/json"
"testing"
"github.com/golang/protobuf/jsonpb"
structpb "github.com/golang/protobuf/ptypes/struct"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/checker/decls"
)
type Payload struct {
Strs []string `json:"strs"`
Data map[string]string `json:"data"`
}
type MyStruct struct {
Num int64 `json:"num"`
Str string `json:"str"`
Payload Payload `json:"payload"`
}
func TestCELStructJSON(t *testing.T) {
for _, tc := range []struct {
name string
filter string
myStruct *MyStruct
wantMatch bool
}{{
name: "simple match",
filter: `myStruct.str == "hello" && "world" in myStruct.payload.data`,
myStruct: &MyStruct{Num: 10, Str: "hello", Payload: Payload{Data: map[string]string{"world": "foobar"}}},
wantMatch: true,
}, {
name: "simple mismatch",
filter: `myStruct.num > 9000 && "banana" in myStruct.payload.strs`,
myStruct: &MyStruct{Num: 9001, Str: "blah", Payload: Payload{Strs: []string{"kiwi", "orange"}, Data: map[string]string{"mars": "goober"}}},
wantMatch: false,
}} {
t.Run(tc.name, func(t *testing.T) {
// First build the CEL program.
ds := cel.Declarations(
decls.NewIdent("myStruct", decls.NewMapType(decls.String, decls.Dyn), nil),
)
env, err := cel.NewEnv(ds)
if err != nil {
t.Fatal(err)
}
prs, iss := env.Parse(tc.filter)
if iss != nil && iss.Err() != nil {
t.Fatal(iss.Err())
}
chk, iss := env.Check(prs)
if iss != nil && iss.Err() != nil {
t.Fatal(iss.Err())
}
prg, err := env.Program(chk)
if err != nil {
t.Fatal(err)
}
// Now, get the input in the correct format (conversion: Go struct -> JSON -> structpb).
j, err := json.Marshal(tc.myStruct)
if err != nil {
t.Fatal(err)
}
var spb structpb.Struct
if err := jsonpb.Unmarshal(bytes.NewBuffer(j), &spb); err != nil {
t.Fatal(err)
}
// Now, evaluate the program and check the output.
val, _, err := prg.Eval(map[string]interface{}{"myStruct": &spb})
if err != nil {
t.Fatal(err)
}
gotMatch, ok := val.Value().(bool)
if !ok {
t.Fatalf("failed to convert %+v to bool", val)
}
if gotMatch != tc.wantMatch {
t.Errorf("expected cel(%q, %s) to be %v", tc.filter, string(j), tc.wantMatch)
}
})
}
}
@TristonianJones
Copy link

Thanks for sharing, this is great! This is definitely the simplest way to get started.

If you want to avoid the conversion costs, you can also implement the ref.TypeAdapter, ref.TypeProvider, and ref.Val interfaces much like Istio does: https://github.com/istio/istio/blob/master/mixer/pkg/lang/cel/provider.go. It's more work, but also much better performance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment