diff --git a/docs/cel_expressions.md b/docs/cel_expressions.md index 0cc2f9a71..73a764405 100644 --- a/docs/cel_expressions.md +++ b/docs/cel_expressions.md @@ -470,7 +470,21 @@ which can be accessed by indexing.
[1, 2, 3, 4, 5].last() == 5
- + + + translate() + + +
<string>.translate(string, string) -> <string>
+ + + Uses a regular expression to replace characters from the source string with characters from the replacements. + + +
"This is $an Invalid5String ".translate("[^a-z0-9]+", "") == "hisisannvalid5tring"

+
"This is $an Invalid5String ".translate("[^a-z0-9]+", "ABC") == "ABChisABCisABCanABCnvalid5ABCtring"
+ + ## Troubleshooting CEL expressions diff --git a/pkg/interceptors/cel/cel_test.go b/pkg/interceptors/cel/cel_test.go index 482fe5f10..63cb7c0c9 100644 --- a/pkg/interceptors/cel/cel_test.go +++ b/pkg/interceptors/cel/cel_test.go @@ -215,6 +215,19 @@ func TestInterceptor_Process(t *testing.T) { "two": "default", "three": "default", }, + }, { + name: "string replacement with regexp", + CEL: &InterceptorParams{ + Overlays: []Overlay{ + {Key: "replaced1", Expression: `body.value.lowerAscii().translate("[^a-z0-9]+", "")`}, + {Key: "replaced2", Expression: `body.value.lowerAscii().translate("[^a-z0-9]+", "ABC")`}, + }, + }, + body: json.RawMessage(`{"value":"This is $an Invalid5String"}`), + wantExtensions: map[string]interface{}{ + "replaced1": "thisisaninvalid5string", + "replaced2": "thisABCisABCanABCinvalid5string", + }, }, { name: "filters and overlays can access passed in extensions", CEL: &InterceptorParams{ @@ -278,7 +291,7 @@ func TestInterceptor_Process(t *testing.T) { if tt.wantExtensions != nil { got := res.Extensions if diff := cmp.Diff(tt.wantExtensions, got); diff != "" { - rt.Fatalf("cel.Process() did return correct extensions (-wantMsg+got): %v", diff) + rt.Fatalf("cel.Process() did not return correct extensions (-wantMsg+got): %v", diff) } } }) @@ -343,6 +356,16 @@ func TestInterceptor_Process_Error(t *testing.T) { body: []byte(`{"value":"test"}`), wantCode: codes.InvalidArgument, wantMsg: `expression "test.value" check failed: ERROR:.*undeclared reference to 'test'`, + }, { + name: "unable to parse regexp in translate", + CEL: &InterceptorParams{ + Overlays: []Overlay{ + {Key: "converted", Expression: `body.value.translate("[^a-z0-9+", "")`}, + }, + }, + body: []byte(`{"value":"testing"}`), + wantCode: codes.InvalidArgument, + wantMsg: "failed to parse regular expression for translation: error parsing regexp: missing closing ]", }, } for _, tt := range tests { diff --git a/pkg/interceptors/cel/triggers.go b/pkg/interceptors/cel/triggers.go index 9b3736371..5c05bc487 100644 --- a/pkg/interceptors/cel/triggers.go +++ b/pkg/interceptors/cel/triggers.go @@ -22,6 +22,7 @@ import ( "net/http" "net/url" "reflect" + "regexp" "strings" "github.com/google/cel-go/cel" @@ -145,6 +146,16 @@ import ( // // body.jsonObjectOrList.marshalJSON() +// translate +// +// translate returns a copy of src, replacing matches of the with the replacement string repl. Inside repl, $ signs are interpreted as in Expand, so for instance $1 represents the text of the first submatch. +// +// .translate(string, string) -> +// +// Examples: +// +// "this is $aN INvalid5string ".replace("[^a-z0-9]+", "") == "thisisaninvalid5string" + // Triggers creates and returns a new cel.Lib with the triggers extensions. func Triggers(ctx context.Context, ns string, sg interceptors.SecretGetter) cel.EnvOption { return cel.Lib(triggersLib{ctx: ctx, defaultNS: ns, secretGetter: sg}) @@ -194,6 +205,9 @@ func (t triggersLib) CompileOptions() []cel.EnvOption { cel.Function("first", cel.MemberOverload("first_list", []*cel.Type{listStrDyn}, cel.DynType, cel.UnaryBinding(listFirst))), + cel.Function("translate", + cel.MemberOverload("translate_string_string", []*cel.Type{cel.StringType, cel.StringType, cel.StringType}, cel.StringType, + cel.FunctionBinding(translateString))), } } @@ -276,6 +290,7 @@ func parseJSONString(val ref.Val) ref.Val { if err != nil { return types.NewErr("failed to create a new registry in parseJSON: %w", err) } + return types.NewDynamicMap(r, decodedVal) } @@ -353,6 +368,30 @@ func listFirst(val ref.Val) ref.Val { return l.Get(types.Int(0)) } +func translateString(vals ...ref.Val) ref.Val { + regstr, ok := vals[1].(types.String) + if !ok { + return types.ValOrErr(regstr, "unexpected type '%v' used in translate", vals[1].Type()) + } + + src, ok := vals[0].(types.String) + if !ok { + return types.ValOrErr(src, "unexpected type '%v' used in translate", vals[0].Type()) + } + + repl, ok := vals[2].(types.String) + if !ok { + return types.ValOrErr(repl, "unexpected type '%v' used in translate", vals[2].Type()) + } + + re, err := regexp.Compile(string(regstr)) + if err != nil { + return types.NewErr("failed to parse regular expression for translation: %w", err) + } + + return types.String(re.ReplaceAllString(string(src), string(repl))) +} + func max(x, y types.Int) types.Int { switch x.Compare(y) { case types.IntNegOne: