From 2059366fe60fb4905f48495e8fbbd47d7936ef73 Mon Sep 17 00:00:00 2001 From: Rohan Patnaik Date: Thu, 11 Jun 2026 14:36:15 +0530 Subject: [PATCH 1/2] feat(ext): add json encoder --- ext/encoders.go | 34 ++++++++++++++++++++++++++++++++++ ext/encoders_test.go | 3 +++ 2 files changed, 37 insertions(+) diff --git a/ext/encoders.go b/ext/encoders.go index 731c3d095..2d1528b6b 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,16 @@ import ( // Examples: // // base64.encode(b'hello') // return b'aGVsbG8=' +// +// # JSON.Encode +// +// 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 { @@ -89,6 +102,11 @@ func (*encoderLib) CompileOptions() []cel.EnvOption { b := bytes.(types.Bytes) return stringOrError(base64EncodeBytes([]byte(b))) }))), + 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)) + }))), } } @@ -110,3 +128,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 be6f764ee..9b496bb9b 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()) From fcc259e9057eec86be3e475b67651ceed3cc7d04 Mon Sep 17 00:00:00 2001 From: Rohan Patnaik <22250758+rohan-patnaik@users.noreply.github.com> Date: Tue, 16 Jun 2026 06:18:37 +0530 Subject: [PATCH 2/2] fix(ext): version json encoder --- ext/encoders.go | 21 ++++++++++++++------- ext/encoders_test.go | 16 +++++++++++++++- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/ext/encoders.go b/ext/encoders.go index 2d1528b6b..10beea4f1 100644 --- a/ext/encoders.go +++ b/ext/encoders.go @@ -54,6 +54,8 @@ import ( // // # JSON.Encode // +// Introduced at version: 1 +// // Encodes a CEL value to a JSON string. // // json.encode() -> @@ -88,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 { @@ -102,12 +104,17 @@ func (*encoderLib) CompileOptions() []cel.EnvOption { b := bytes.(types.Bytes) return stringOrError(base64EncodeBytes([]byte(b))) }))), - 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)) - }))), } + 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 { diff --git a/ext/encoders_test.go b/ext/encoders_test.go index 9b496bb9b..1ad09e3a4 100644 --- a/ext/encoders_test.go +++ b/ext/encoders_test.go @@ -91,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()) + } }