Last active
July 11, 2024 12:41
-
-
Save rkbalgi/3d15b0c40afcb34067adb890632366c0 to your computer and use it in GitHub Desktop.
Using custom types with cel-go
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 | |
//A very simple example of using user defined structures with instance functions with cel-go | |
import ( | |
"github.com/google/cel-go/cel" | |
"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" | |
"google.golang.org/genproto/googleapis/api/expr/v1alpha1" | |
"log" | |
"reflect" | |
"testing" | |
) | |
//our custom type | |
type Test struct { | |
a, b int | |
} | |
//operations supported by the custom type | |
func (t *Test) Add() int { | |
return t.a + t.b | |
} | |
func (t *Test) Subtract() int { | |
return t.a - t.b | |
} | |
//the CEL type to represent Test | |
var TestType = types.NewTypeValue("Test", traits.ReceiverType) | |
func (t Test) ConvertToNative(typeDesc reflect.Type) (interface{}, error) { | |
panic("not required") | |
} | |
func (t Test) ConvertToType(typeVal ref.Type) ref.Val { | |
panic("not required") | |
} | |
func (t Test) Equal(other ref.Val) ref.Val { | |
o, ok := other.Value().(Test) | |
if ok { | |
if o == t { | |
return types.Bool(true) | |
} else { | |
return types.Bool(false) | |
} | |
} else { | |
return types.ValOrErr(other, "%v is not of type Test", other) | |
} | |
} | |
func (t Test) Type() ref.Type { | |
return TestType | |
} | |
func (t Test) Value() interface{} { | |
return t | |
} | |
func (t Test) Receive(function string, overload string, args []ref.Val) ref.Val { | |
if function == "Add" { | |
return types.Int(t.Add()) | |
} else if function == "Subtract" { | |
return types.Int(t.Subtract()) | |
} | |
return types.ValOrErr(TestType, "no such function - %s", function) | |
} | |
func (t *Test) HasTrait(trait int) bool { | |
return trait == traits.ReceiverType | |
} | |
func (t *Test) TypeName() string { | |
return TestType.TypeName() | |
} | |
type customTypeAdapter struct { | |
} | |
func (customTypeAdapter) NativeToValue(value interface{}) ref.Val { | |
val, ok := value.(Test) | |
if ok { | |
return val | |
} else { | |
//let the default adapter handle other cases | |
return types.DefaultTypeAdapter.NativeToValue(value) | |
} | |
} | |
func TestExprEval_CelGo(t *testing.T) { | |
env, err := cel.NewEnv(cel.CustomTypeAdapter(&customTypeAdapter{}), | |
cel.Declarations( | |
decls.NewIdent("test", decls.NewObjectType("Test"), nil), | |
decls.NewFunction("MulBy3", | |
decls.NewOverload("mulby3_int", []*expr.Type{decls.Int}, decls.Int)), | |
decls.NewFunction("Add", | |
decls.NewInstanceOverload("test_add", []*expr.Type{decls.NewObjectType("Test")}, decls.Int)), | |
decls.NewFunction("Subtract", | |
decls.NewInstanceOverload("test_subtract", []*expr.Type{decls.NewObjectType("Test")}, decls.Int)))) | |
if err != nil { | |
t.Fatal(err) | |
} | |
parsed, issues := env.Parse(`test.Add()==3 && test.Subtract()==-1 && MulBy3(9)==27`) | |
if issues != nil && issues.Err() != nil { | |
log.Fatalf("parse error: %s", issues.Err()) | |
} | |
checked, issues := env.Check(parsed) | |
if issues != nil && issues.Err() != nil { | |
log.Fatalf("type-check error: %s", issues.Err()) | |
} | |
globalFunctions := cel.Functions( | |
&functions.Overload{ | |
Operator: "MulBy3", | |
Unary: func(lhs ref.Val) ref.Val { | |
return types.Int(3 * lhs.Value().(int64)) | |
}}) | |
prg, err := env.Program(checked, globalFunctions) | |
if err != nil { | |
log.Fatalf("program construction error: %s", err) | |
} | |
out, _, err := prg.Eval(map[string]interface{}{"test": Test{a: 1, b: 2}}) | |
if err != nil { | |
t.Fatal(err) | |
} else { | |
t.Log(out) | |
} | |
} |
Hi Tristan, Thank you for taking time for this useful write up. I didn't realize that there was an Err type that implements ref.Val and types.ValOrErr is absolutely handy!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Nice work! I have a couple of pointers on CEL style and conventions that you might find useful.
Overload Declarations
For overloads, a convention that works well is to specify the argument types and method in snake case. We're trying to use this pattern within CEL going forward.
<instance_type>_<method>_<arg_type>_<arg_type>
<method>_<arg_type>
Example based on the gist:
Error Values v. Panics
CEL's logical operators are capable of short-circuiting errors which makes them much more robust to errant expressions or unexpected data, as well as to unknown values (as is the case when evaluating with partial state). The error values also makes it easier to reason about the result of your program. It is strongly recommended that functions return an
types.Err
value instead of panicking.Since errors do not necessarily terminate evaluation, this does mean you can get them as input to your overloads. To check whether the input to your function is already an error and return it. For these sort of checks where it's not clear whether the input is already an error or this is a brand new instance of the error, use
types.ValOrErr
:Whenever possible, don't
panic
:)Equality testing
Currently, CEL only supports homogeneous equality. There is an open issue to support heterogeneous equality google/cel-spec#54, but until then when the types differ the result should be a
types.Err
response.Something like the following would be consistent with how the built-in CEL types behave.
I hope you find these pointers useful. Happy coding!