Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

functions: add Replace, and RegexpReplace from Terraform #45

Merged
merged 6 commits into from
Mar 9, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions cty/function/stdlib/string_replace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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()

azr marked this conversation as resolved.
Show resolved Hide resolved
// We search/replace using a regexp if the string is surrounded
// in forward slashes.
if len(substr) > 1 && substr[0] == '/' && substr[len(substr)-1] == '/' {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I'm sorry I didn't catch this on the initial read yesterday)

Now that we have a separate function name as a more "normal" way to select between plain string or regex-based replacement, I'd prefer to drop this special case and just expect the whole string to be a valid regular expression. That will then be consistent with the behavior of the existing Regex and RegexAll functions already in this package. I think if leading and trailing slashes are present then they should be interpreted as a literal part of the given pattern, not as special markers.

The rest of this looks great! Thanks for the updates.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome !

Copy link
Contributor Author

@azr azr Mar 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great thanks for reviewing, this is updated and I think this is good to go noew 🙂

substr = substr[1 : len(substr)-1]
}
azr marked this conversation as resolved.
Show resolved Hide resolved

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})
}
101 changes: 101 additions & 0 deletions cty/function/stdlib/string_replace_test.go
Original file line number Diff line number Diff line change
@@ -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/"),
azr marked this conversation as resolved.
Show resolved Hide resolved
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")
}
}