diff --git a/ext/encoders.go b/ext/encoders.go index 731c3d09..10beea4f 100644 --- a/ext/encoders.go +++ b/ext/encoders.go @@ -16,11 +16,14 @@ package ext import ( "encoding/base64" + "fmt" "math" "github.com/google/cel-go/cel" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/structpb" ) // Encoders returns a cel.EnvOption to configure extended functions for string, byte, and object @@ -48,6 +51,18 @@ import ( // Examples: // // base64.encode(b'hello') // return b'aGVsbG8=' +// +// # JSON.Encode +// +// Introduced at version: 1 +// +// Encodes a CEL value to a JSON string. +// +// json.encode() -> +// +// Examples: +// +// json.encode({'hello': 'world'}) // return '{"hello":"world"}' func Encoders(options ...EncodersOption) cel.EnvOption { l := &encoderLib{version: math.MaxUint32} for _, o := range options { @@ -75,8 +90,8 @@ func (*encoderLib) LibraryName() string { return "cel.lib.ext.encoders" } -func (*encoderLib) CompileOptions() []cel.EnvOption { - return []cel.EnvOption{ +func (lib *encoderLib) CompileOptions() []cel.EnvOption { + opts := []cel.EnvOption{ cel.Function("base64.decode", cel.Overload("base64_decode_string", []*cel.Type{cel.StringType}, cel.BytesType, cel.UnaryBinding(func(str ref.Val) ref.Val { @@ -90,6 +105,16 @@ func (*encoderLib) CompileOptions() []cel.EnvOption { return stringOrError(base64EncodeBytes([]byte(b))) }))), } + if lib.version >= 1 { + opts = append(opts, + cel.Function("json.encode", + cel.Overload("json_encode_dyn", []*cel.Type{cel.DynType}, cel.StringType, + cel.UnaryBinding(func(val ref.Val) ref.Val { + return stringOrError(jsonEncodeValue(val)) + }))), + ) + } + return opts } func (*encoderLib) ProgramOptions() []cel.ProgramOption { @@ -110,3 +135,19 @@ func base64DecodeString(str string) ([]byte, error) { func base64EncodeBytes(bytes []byte) (string, error) { return base64.StdEncoding.EncodeToString(bytes), nil } + +func jsonEncodeValue(val ref.Val) (string, error) { + native, err := val.ConvertToNative(types.JSONValueType) + if err != nil { + return "", err + } + jsonValue, ok := native.(*structpb.Value) + if !ok { + return "", fmt.Errorf("cannot convert %T to JSON value", native) + } + jsonBytes, err := protojson.Marshal(jsonValue) + if err != nil { + return "", err + } + return string(jsonBytes), nil +} diff --git a/ext/encoders_test.go b/ext/encoders_test.go index be6f764e..1ad09e3a 100644 --- a/ext/encoders_test.go +++ b/ext/encoders_test.go @@ -41,6 +41,9 @@ func TestEncoders(t *testing.T) { err: "no such overload", parseOnly: true, }, + {expr: "json.encode('hello') == '\"hello\"'"}, + {expr: "json.encode([1, 'two', true]) == '[1, \"two\", true]'"}, + {expr: "json.encode({'items': [1, 'two', false]}) == '{\"items\":[1, \"two\", false]}'"}, } env, err := cel.NewEnv(Encoders()) @@ -88,8 +91,22 @@ func TestEncoders(t *testing.T) { } func TestEncodersVersion(t *testing.T) { - _, err := cel.NewEnv(Encoders(EncodersVersion(0))) + env, err := cel.NewEnv(Encoders(EncodersVersion(0))) if err != nil { t.Fatalf("EncodersVersion(0) failed: %v", err) } + if _, iss := env.Compile("base64.encode(b'hello')"); iss.Err() != nil { + t.Fatalf("base64.encode() got %v, wanted no error", iss.Err()) + } + if _, iss := env.Compile("json.encode('hello')"); iss.Err() == nil { + t.Fatal("json.encode() got no error, wanted version-gated function to be unavailable") + } + + env, err = cel.NewEnv(Encoders(EncodersVersion(1))) + if err != nil { + t.Fatalf("EncodersVersion(1) failed: %v", err) + } + if _, iss := env.Compile("json.encode('hello')"); iss.Err() != nil { + t.Fatalf("json.encode() got %v, wanted no error", iss.Err()) + } }