diff --git a/input/v1beta1/resources_transforms.go b/input/v1beta1/resources_transforms.go index 632822b..47fc4de 100644 --- a/input/v1beta1/resources_transforms.go +++ b/input/v1beta1/resources_transforms.go @@ -208,6 +208,7 @@ const ( StringTransformTypeTrimPrefix StringTransformType = "TrimPrefix" StringTransformTypeTrimSuffix StringTransformType = "TrimSuffix" StringTransformTypeRegexp StringTransformType = "Regexp" + StringTransformTypeReplace StringTransformType = "Replace" ) // StringConversionType converts a string. @@ -257,6 +258,10 @@ type StringTransform struct { // Extract a match from the input using a regular expression. // +optional Regexp *StringTransformRegexp `json:"regexp,omitempty"` + + // Search/Replace applied to the input string. + // +optional + Replace *StringTransformReplace `json:"replace,omitempty"` } // A StringTransformRegexp extracts a match from the input using a regular @@ -271,6 +276,15 @@ type StringTransformRegexp struct { Group *int `json:"group,omitempty"` } +// A StringTransformReplace replaces the search string with the replacement string. +type StringTransformReplace struct { + // The Search string to match. + Search string `json:"search"` + + // The Replace string replaces all occurrences of the search string. + Replace string `json:"replace"` +} + // TransformIOType defines the type of a ConvertTransform. type TransformIOType string diff --git a/input/v1beta1/zz_generated.deepcopy.go b/input/v1beta1/zz_generated.deepcopy.go index bd97bd7..dc74301 100644 --- a/input/v1beta1/zz_generated.deepcopy.go +++ b/input/v1beta1/zz_generated.deepcopy.go @@ -537,6 +537,11 @@ func (in *StringTransform) DeepCopyInto(out *StringTransform) { *out = new(StringTransformRegexp) (*in).DeepCopyInto(*out) } + if in.Replace != nil { + in, out := &in.Replace, &out.Replace + *out = new(StringTransformReplace) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StringTransform. @@ -569,6 +574,21 @@ func (in *StringTransformRegexp) DeepCopy() *StringTransformRegexp { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StringTransformReplace) DeepCopyInto(out *StringTransformReplace) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StringTransformReplace. +func (in *StringTransformReplace) DeepCopy() *StringTransformReplace { + if in == nil { + return nil + } + out := new(StringTransformReplace) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Transform) DeepCopyInto(out *Transform) { *out = *in diff --git a/package/input/pt.fn.crossplane.io_resources.yaml b/package/input/pt.fn.crossplane.io_resources.yaml index a077022..6c873a5 100644 --- a/package/input/pt.fn.crossplane.io_resources.yaml +++ b/package/input/pt.fn.crossplane.io_resources.yaml @@ -334,6 +334,20 @@ spec: required: - match type: object + replace: + description: Search/Replace applied to the input string. + properties: + replace: + description: The Replace string replaces all occurrences + of the search string. + type: string + search: + description: The Search string to match. + type: string + required: + - replace + - search + type: object trim: description: Trim the prefix or suffix from the input type: string @@ -690,6 +704,21 @@ spec: required: - match type: object + replace: + description: Search/Replace applied to the input + string. + properties: + replace: + description: The Replace string replaces all + occurrences of the search string. + type: string + search: + description: The Search string to match. + type: string + required: + - replace + - search + type: object trim: description: Trim the prefix or suffix from the input @@ -1108,6 +1137,21 @@ spec: required: - match type: object + replace: + description: Search/Replace applied to the input + string. + properties: + replace: + description: The Replace string replaces all + occurrences of the search string. + type: string + search: + description: The Search string to match. + type: string + required: + - replace + - search + type: object trim: description: Trim the prefix or suffix from the input diff --git a/transforms.go b/transforms.go index c1a20c5..4900eca 100644 --- a/transforms.go +++ b/transforms.go @@ -52,6 +52,7 @@ const ( errStringTransformTypeRegexp = "string transform of type %s regexp is not set" errStringTransformTypeRegexpFailed = "could not compile regexp" errStringTransformTypeRegexpNoMatch = "regexp %q had no matches for group %d" + errStringTransformTypeReplace = "string transform of type %s replace is not set" errStringConvertTypeFailed = "type %s is not supported for string convert" errDecodeString = "string is not valid base64" @@ -265,7 +266,7 @@ func unmarshalJSON(j extv1.JSON, output *any) error { } // ResolveString resolves a String transform. -func ResolveString(t *v1beta1.StringTransform, input any) (string, error) { +func ResolveString(t *v1beta1.StringTransform, input any) (string, error) { //nolint:gocyclo // This is a long but simple switch. switch t.Type { case v1beta1.StringTransformTypeFormat: if t.Format == nil { @@ -287,6 +288,11 @@ func ResolveString(t *v1beta1.StringTransform, input any) (string, error) { return "", errors.Errorf(errStringTransformTypeRegexp, string(t.Type)) } return stringRegexpTransform(input, *t.Regexp) + case v1beta1.StringTransformTypeReplace: + if t.Replace == nil { + return "", errors.Errorf(errStringTransformTypeReplace, string(t.Type)) + } + return stringReplaceTransform(input, *t.Replace), nil default: return "", errors.Errorf(errStringTransformTypeFailed, string(t.Type)) } @@ -368,6 +374,11 @@ func stringRegexpTransform(input any, r v1beta1.StringTransformRegexp) (string, return groups[g], nil } +func stringReplaceTransform(input any, r v1beta1.StringTransformReplace) string { + str := fmt.Sprintf("%v", input) + return strings.ReplaceAll(str, r.Search, r.Replace) +} + // ResolveConvert resolves a Convert transform by looking up the appropriate // conversion function for the given input type and invoking it. func ResolveConvert(t *v1beta1.ConvertTransform, input any) (any, error) { diff --git a/transforms_test.go b/transforms_test.go index be328e1..71ba78e 100644 --- a/transforms_test.go +++ b/transforms_test.go @@ -639,6 +639,7 @@ func TestStringResolve(t *testing.T) { convert *v1beta1.StringConversionType trim *string regexp *v1beta1.StringTransformRegexp + replace *v1beta1.StringTransformReplace i any } type want struct { @@ -1003,6 +1004,45 @@ func TestStringResolve(t *testing.T) { err: errors.Wrap(errors.New("json: unsupported type: func()"), errMarshalJSON), }, }, + "ReplaceFound": { + args: args{ + stype: v1beta1.StringTransformTypeReplace, + replace: &v1beta1.StringTransformReplace{ + Search: "Cr", + Replace: "B", + }, + i: "Crossplane", + }, + want: want{ + o: "Bossplane", + }, + }, + "ReplaceNotFound": { + args: args{ + stype: v1beta1.StringTransformTypeReplace, + replace: &v1beta1.StringTransformReplace{ + Search: "xx", + Replace: "zz", + }, + i: "Crossplane", + }, + want: want{ + o: "Crossplane", + }, + }, + "ReplaceRemove": { + args: args{ + stype: v1beta1.StringTransformTypeReplace, + replace: &v1beta1.StringTransformReplace{ + Search: "ss", + Replace: "", + }, + i: "Crossplane", + }, + want: want{ + o: "Croplane", + }, + }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { @@ -1012,6 +1052,7 @@ func TestStringResolve(t *testing.T) { Convert: tc.convert, Trim: tc.trim, Regexp: tc.regexp, + Replace: tc.replace, } got, err := ResolveString(tr, tc.i) diff --git a/validate.go b/validate.go index 6165f3a..dd7e1cb 100644 --- a/validate.go +++ b/validate.go @@ -382,6 +382,13 @@ func ValidateStringTransform(s *v1beta1.StringTransform) *field.Error { //nolint if _, err := regexp.Compile(s.Regexp.Match); err != nil { return field.Invalid(field.NewPath("regexp", "match"), s.Regexp.Match, "invalid regexp") } + case v1beta1.StringTransformTypeReplace: + if s.Replace == nil { + return field.Required(field.NewPath("replace"), "replace transform requires a replace") + } + if s.Replace.Search == "" { + return field.Required(field.NewPath("replace", "search"), "replace transform requires a search") + } default: return field.Invalid(field.NewPath("type"), s.Type, "unknown string transform type") }