Skip to content

Commit

Permalink
Add support for splitting strings to CEL expressions.
Browse files Browse the repository at this point in the history
This adds an additional CEL function for splitting strings on a separator.

e.g. split(body.ref, '/') would split the ref on '/' and return a list of strings.
  • Loading branch information
bigkevmcd committed Feb 5, 2020
1 parent d37feee commit ab31468
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 8 deletions.
15 changes: 14 additions & 1 deletion docs/cel_expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,18 @@ interceptor.
<pre>truncate(body.commit.sha, 5)</pre>
</td>
</tr>

<tr>
<th>
split
</th>
<td>
(string, string) -> string(dyn)
</td>
<td>
Splits a string on the provided separator value.
</td>
<td>
<pre>split(body.ref, '/')</pre>
</td>
</tr>
</table>
27 changes: 25 additions & 2 deletions pkg/interceptors/cel/cel.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"io/ioutil"
"net/http"
"reflect"
"strings"

structpb "github.com/golang/protobuf/ptypes/struct"
"github.com/google/cel-go/cel"
Expand Down Expand Up @@ -151,22 +152,28 @@ func embeddedFunctions() cel.ProgramOption {
&functions.Overload{
Operator: "truncate",
Binary: truncateString},
&functions.Overload{
Operator: "split",
Binary: splitString},
)

}
func makeCelEnv() (cel.Env, error) {
mapStrDyn := decls.NewMapType(decls.String, decls.Dyn)
listStr := decls.NewListType(decls.String)
return cel.NewEnv(
cel.Declarations(
decls.NewIdent("body", mapStrDyn, nil),
decls.NewIdent("header", mapStrDyn, nil),
decls.NewFunction("match",
decls.NewInstanceOverload("match_map_string_string",
[]*exprpb.Type{mapStrDyn, decls.String, decls.String}, decls.Bool)),
decls.NewFunction("split",
decls.NewOverload("split_dyn_string_dyn",
[]*exprpb.Type{decls.Dyn, decls.String}, listStr)),
decls.NewFunction("truncate",
decls.NewOverload("truncate_string_uint",
[]*exprpb.Type{decls.String, decls.Int}, decls.String))))

}

func makeEvalContext(body []byte, r *http.Request) (map[string]interface{}, error) {
Expand Down Expand Up @@ -210,7 +217,23 @@ func truncateString(lhs, rhs ref.Val) ref.Val {
return types.ValOrErr(n, "unexpected type '%v' passed to truncate", rhs.Type())
}

return types.Bytes([]byte(str[:max(n, types.Int(len(str)))]))
return types.String(str[:max(n, types.Int(len(str)))])
}

func splitString(lhs, rhs ref.Val) ref.Val {
str, ok := lhs.(types.String)
if !ok {
return types.ValOrErr(str, "unexpected type '%v' passed to splitString", lhs.Type())
}

splitStr, ok := rhs.(types.String)
if !ok {
return types.ValOrErr(str, "unexpected type '%v' passed to splitString", lhs.Type())
}

r := types.NewRegistry()
splitVals := strings.Split(string(str), string(splitStr))
return types.NewStringList(r, splitVals)
}

func max(x, y types.Int) types.Int {
Expand Down
29 changes: 24 additions & 5 deletions pkg/interceptors/cel/cel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"reflect"
"regexp"
"strings"
"testing"

"github.com/google/cel-go/common/types"
Expand Down Expand Up @@ -69,11 +70,11 @@ func TestInterceptor_ExecuteTrigger(t *testing.T) {
name: "single overlay with no filter",
CEL: &triggersv1.CELInterceptor{
Overlays: []triggersv1.CELOverlay{
{Key: "new", Expression: "body.value"},
{Key: "new", Expression: "split(body.ref, '/')[2]"},
},
},
payload: ioutil.NopCloser(bytes.NewBufferString(`{"value":"test"}`)),
want: []byte(`{"new":"test","value":"test"}`),
payload: ioutil.NopCloser(bytes.NewBufferString(`{"ref":"refs/head/master"}`)),
want: []byte(`{"new":"master","ref":"refs/head/master"}`),
},
{
name: "multiple overlays",
Expand Down Expand Up @@ -212,10 +213,13 @@ func TestInterceptor_ExecuteTrigger_Errors(t *testing.T) {

func TestExpressionEvaluation(t *testing.T) {
testSHA := "ec26c3e57ca3a959ca5aad62de7213c562f8c821"
testRef := "refs/heads/master"
jsonMap := map[string]interface{}{
"value": "testing",
"sha": testSHA,
"ref": testRef,
}
refParts := strings.Split(testRef, "/")
header := http.Header{}
evalEnv := map[string]interface{}{"body": jsonMap, "header": header}
env, err := makeCelEnv()
Expand All @@ -240,12 +244,22 @@ func TestExpressionEvaluation(t *testing.T) {
{
name: "truncate a long string",
expr: "truncate(body.sha, 7)",
want: types.Bytes("ec26c3e"),
want: types.String("ec26c3e"),
},
{
name: "truncate a string to fewer characters than it has",
expr: "truncate(body.sha, 45)",
want: types.Bytes(testSHA),
want: types.String(testSHA),
},
{
name: "split a string on a character",
expr: "split(body.ref, '/')",
want: types.NewStringList(types.NewRegistry(), refParts),
},
{
name: "extract a branch from a non refs string",
expr: "split(body.value, '/')",
want: types.NewStringList(types.NewRegistry(), []string{"testing"}),
},
}
for _, tt := range tests {
Expand Down Expand Up @@ -305,6 +319,11 @@ func TestExpressionEvaluation_Error(t *testing.T) {
expr: "body.match('testing', 'test')",
want: "failed to convert to http.Header",
},
{
name: "non-string passed to split",
expr: "split(body.value, 54)",
want: "found no matching overload for 'split'",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down

0 comments on commit ab31468

Please sign in to comment.