From 66ce4ba9a1a8db3d8d2b02b15042befdab73efda Mon Sep 17 00:00:00 2001 From: Adrien Delorme Date: Tue, 10 Mar 2020 00:37:10 +0100 Subject: [PATCH] function/stdlib: New "Replace" and "RegexpReplace" functions These are inspired by (but not fully compatible with) the single "replace" function in HashiCorp Terraform. Here we prefer to have two separate functions for selecting regular expression or plain string matching, where as the Terraform function uses special syntax in the matching string to activate regex mode. --- cty/function/stdlib/string_replace.go | 80 ++++++++++++++++ cty/function/stdlib/string_replace_test.go | 101 +++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 cty/function/stdlib/string_replace.go create mode 100644 cty/function/stdlib/string_replace_test.go diff --git a/cty/function/stdlib/string_replace.go b/cty/function/stdlib/string_replace.go new file mode 100644 index 00000000..b40e5d6f --- /dev/null +++ b/cty/function/stdlib/string_replace.go @@ -0,0 +1,80 @@ +package stdlib + +import ( + "regexp" + "strings" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" +) + +// ReplaceFunc is a function that searches a given string for another given +// substring, and replaces each occurence with a given replacement string. +// The substr argument is a simple string. +var ReplaceFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + { + Name: "substr", + Type: cty.String, + }, + { + Name: "replace", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + str := args[0].AsString() + substr := args[1].AsString() + replace := args[2].AsString() + + return cty.StringVal(strings.Replace(str, substr, replace, -1)), nil + }, +}) + +// RegexpReplaceFunc is a function that searches a given string for another +// given substring, and replaces each occurence with a given replacement +// string. The substr argument must be a valid regular expression. +var RegexpReplaceFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + { + Name: "substr", + Type: cty.String, + }, + { + Name: "replace", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + str := args[0].AsString() + substr := args[1].AsString() + replace := args[2].AsString() + + re, err := regexp.Compile(substr) + if err != nil { + return cty.UnknownVal(cty.String), err + } + + return cty.StringVal(re.ReplaceAllString(str, replace)), nil + }, +}) + +// Replace searches a given string for another given substring, +// and replaces all occurrences with a given replacement string. +func Replace(str, substr, replace cty.Value) (cty.Value, error) { + return ReplaceFunc.Call([]cty.Value{str, substr, replace}) +} + +func RegexpReplace(str, substr, replace cty.Value) (cty.Value, error) { + return RegexpReplaceFunc.Call([]cty.Value{str, substr, replace}) +} diff --git a/cty/function/stdlib/string_replace_test.go b/cty/function/stdlib/string_replace_test.go new file mode 100644 index 00000000..0e2664ec --- /dev/null +++ b/cty/function/stdlib/string_replace_test.go @@ -0,0 +1,101 @@ +package stdlib + +import ( + "testing" + + "github.com/zclconf/go-cty/cty" +) + +func TestReplace(t *testing.T) { + tests := []struct { + Input cty.Value + Substr, Replace cty.Value + Want cty.Value + }{ + { + cty.StringVal("hello"), + cty.StringVal("l"), + cty.StringVal(""), + cty.StringVal("heo"), + }, + { + cty.StringVal("😸😸😸😾😾😾"), + cty.StringVal("😾"), + cty.StringVal("😸"), + cty.StringVal("😸😸😸😸😸😸"), + }, + { + cty.StringVal("😸😸😸😸😸😾"), + cty.StringVal("😾"), + cty.StringVal("😸"), + cty.StringVal("😸😸😸😸😸😸"), + }, + } + + for _, test := range tests { + t.Run(test.Input.GoString()+"_replace", func(t *testing.T) { + got, err := Replace(test.Input, test.Substr, test.Replace) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !got.RawEquals(test.Want) { + t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + t.Run(test.Input.GoString()+"_regex_replace", func(t *testing.T) { + got, err := Replace(test.Input, test.Substr, test.Replace) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !got.RawEquals(test.Want) { + t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} + +func TestRegexReplace(t *testing.T) { + tests := []struct { + Input cty.Value + Substr, Replace cty.Value + Want cty.Value + }{ + { + cty.StringVal("-ab-axxb-"), + cty.StringVal("a(x*)b"), + cty.StringVal("T"), + cty.StringVal("-T-T-"), + }, + { + cty.StringVal("-ab-axxb-"), + cty.StringVal("a(x*)b"), + cty.StringVal("${1}W"), + cty.StringVal("-W-xxW-"), + }, + } + + for _, test := range tests { + t.Run(test.Input.GoString(), func(t *testing.T) { + got, err := RegexpReplace(test.Input, test.Substr, test.Replace) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !got.RawEquals(test.Want) { + t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} + +func TestRegexReplace_invalid_regex(t *testing.T) { + _, err := RegexpReplace(cty.StringVal(""), cty.StringVal("("), cty.StringVal("")) + if err == nil { + t.Fatal("expected an error") + } +}