Skip to content

Commit

Permalink
function/stdlib: New "Replace" and "RegexpReplace" functions
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
azr authored Mar 9, 2020
1 parent 50eb240 commit 66ce4ba
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 0 deletions.
80 changes: 80 additions & 0 deletions cty/function/stdlib/string_replace.go
Original file line number Diff line number Diff line change
@@ -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})
}
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"),
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")
}
}

0 comments on commit 66ce4ba

Please sign in to comment.