diff --git a/docs/expressions.md b/docs/expressions.md index 14b6e44..8c10d83 100644 --- a/docs/expressions.md +++ b/docs/expressions.md @@ -140,7 +140,7 @@ On a CEL variable named `subject` of value: ```json [ - "one", null, "two", + "one", null, "two" ] ``` @@ -159,3 +159,17 @@ On a CEL variable named `subject` of value: - `trimPrefix(subject, "something:")` => `group:engineering@example.com` - `trimPrefix(subject, "group:")` => `engineering@example.com` +### `replace` + +Performs a RegEx replace on a string using [RE2 syntax](https://github.com/google/re2/wiki/Syntax). +If there are no pattern matches, the input string will be returned. `replace` supports group references (`$1`). + +On a CEL variable named `subject` of value: + +```json +"group:engineering@example.com" +``` + +- `replace(subject, "group:([[:alpha:]]+)@.+$", "$1")` => `engineering` +- `replace(subject, "@example\\.com", "")` => `group:engineering` +- `replace(subject, ".+@test.example.com"` => `group:engineering@example.com` (no match) diff --git a/expr/functions.go b/expr/functions.go index d5e0251..2635df6 100644 --- a/expr/functions.go +++ b/expr/functions.go @@ -2,6 +2,7 @@ package expr import ( "reflect" + "regexp" "strings" "github.com/google/cel-go/cel" @@ -28,6 +29,7 @@ func (*stdlib) CompileOptions() []cel.EnvOption { Coalesce(), First(), TrimPrefix(), + Replace(), } } @@ -128,3 +130,25 @@ func TrimPrefix() cel.EnvOption { ), ) } + +// Replace removes the given string from the front of the input. +func Replace() cel.EnvOption { + binding := func(vals ...ref.Val) ref.Val { + input, pattern, replacement := vals[0], vals[1], vals[2] + regex, err := regexp.Compile(pattern.Value().(string)) + if err != nil { + return types.WrapErr(err) + } + + return types.String(regex.ReplaceAllString(input.Value().(string), replacement.Value().(string))) + } + + return cel.Function("replace", + cel.Overload("replace_string_string_string", + []*cel.Type{cel.StringType, cel.StringType, cel.StringType}, + cel.StringType, + cel.FunctionBinding(binding), + //cel.BinaryBinding(binding), + ), + ) +} diff --git a/expr/functions_test.go b/expr/functions_test.go index 1fd6a5d..d00a1ad 100644 --- a/expr/functions_test.go +++ b/expr/functions_test.go @@ -6,7 +6,6 @@ import ( "github.com/google/cel-go/cel" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -112,4 +111,38 @@ var _ = Describe("Functions", func() { }) }) }) + + Describe("Replace", func() { + BeforeEach(func() { + src = "replace(value, r'^https://([0-9A-Za-z_\\-]+)\\.example\\.com.*$', '$1')" + }) + + When("the input is a matching string", func() { + It("Returns the replacement", func() { + out, _, err := prg.Eval(map[string]any{ + "value": "https://some-slug.example.com/test", + }) + Expect(err).NotTo(HaveOccurred()) + + result, err := out.ConvertToNative(reflect.TypeOf("")) + Expect(err).NotTo(HaveOccurred()) + + Expect(result).To(Equal("some-slug")) + }) + }) + + When("the input has no matches", func() { + It("returns the source string", func() { + out, _, err := prg.Eval(map[string]any{ + "value": "https://some-slug.incident.io/test", + }) + Expect(err).NotTo(HaveOccurred()) + + result, err := out.ConvertToNative(reflect.TypeOf("")) + Expect(err).NotTo(HaveOccurred()) + + Expect(result).To(Equal("https://some-slug.incident.io/test")) + }) + }) + }) }) diff --git a/go.mod b/go.mod index 7440701..788a0e8 100644 --- a/go.mod +++ b/go.mod @@ -22,8 +22,8 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.2 github.com/machinebox/graphql v0.2.2 github.com/manifoldco/promptui v0.9.0 - github.com/onsi/ginkgo/v2 v2.9.2 - github.com/onsi/gomega v1.27.6 + github.com/onsi/ginkgo/v2 v2.11.0 + github.com/onsi/gomega v1.27.8 github.com/pkg/errors v0.9.1 github.com/rodaine/table v1.1.0 github.com/samber/lo v1.38.1 @@ -32,7 +32,7 @@ require ( github.com/yargevad/filepathx v1.0.0 github.com/zyedidia/highlight v0.0.0-20200217010119-291680feaca1 golang.org/x/oauth2 v0.8.0 - golang.org/x/sync v0.1.0 + golang.org/x/sync v0.2.0 gopkg.in/guregu/null.v3 v3.5.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -47,7 +47,7 @@ require ( github.com/cloudflare/circl v1.1.0 // indirect github.com/getkin/kin-openapi v0.107.0 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect - github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/logr v1.2.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/swag v0.21.1 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect @@ -72,12 +72,12 @@ require ( github.com/xhit/go-str2duration/v2 v2.1.0 // indirect golang.org/x/crypto v0.7.0 // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect - golang.org/x/mod v0.9.0 // indirect + golang.org/x/mod v0.10.0 // indirect golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect + golang.org/x/sys v0.9.0 // indirect golang.org/x/term v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect - golang.org/x/tools v0.7.0 // indirect + golang.org/x/tools v0.9.3 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 // indirect google.golang.org/protobuf v1.28.1 // indirect diff --git a/go.sum b/go.sum index ea3b54a..1a0b5ce 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNV github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= @@ -131,8 +133,12 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= +github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= +github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= +github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -182,6 +188,7 @@ golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfU golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= @@ -190,6 +197,8 @@ golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -205,6 +214,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= @@ -217,6 +228,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM= +golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=