Created
January 12, 2020 17:14
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
}) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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
, andref.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.