Skip to content

Commit

Permalink
Add string translation to CEL interceptor.
Browse files Browse the repository at this point in the history
This adds a new "translate" function to the CEL interceptor library that
allows using a regular expression to translate replacement characters to
specified strings.
  • Loading branch information
bigkevmcd committed Jan 31, 2024
1 parent 0127ca1 commit 7788cec
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 2 deletions.
16 changes: 15 additions & 1 deletion docs/cel_expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,21 @@ which can be accessed by indexing.
<pre>[1, 2, 3, 4, 5].last() == 5</pre>
</td>
</tr>

<tr>
<th>
translate()
</th>
<td>
<pre>&lt;string&gt;.translatet(string, string) -> &lt;string&gt;</pre>
</td>
<td>
Uses a regular expression to replace characters from the source string with characters from the replacements.
</td>
<td>
<pre>"This is $an Invalid5String ".translate("[^a-z0-9]+", "") == "hisisannvalid5tring"</pre><br />
<pre>"This is $an Invalid5String ".translate("[^a-z0-9]+", "ABC") == "ABChisABCisABCanABCnvalid5ABCtring"</pre>
</td>
</tr>
</table>

## Troubleshooting CEL expressions
Expand Down
25 changes: 24 additions & 1 deletion pkg/interceptors/cel/cel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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 rnot eturn correct extensions (-wantMsg+got): %v", diff)
}
}
})
Expand Down Expand Up @@ -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 {
Expand Down
39 changes: 39 additions & 0 deletions pkg/interceptors/cel/triggers.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"net/http"
"net/url"
"reflect"
"regexp"
"strings"

"github.com/google/cel-go/cel"
Expand Down Expand Up @@ -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.
//
// <string>.translate(string, 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})
Expand Down Expand Up @@ -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))),
}
}

Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 7788cec

Please sign in to comment.