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: