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

Add substitution with options #418

Merged
merged 5 commits into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
177 changes: 122 additions & 55 deletions template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package template

import (
"errors"
"fmt"
"regexp"
"sort"
Expand Down Expand Up @@ -71,77 +72,143 @@ type Mapping func(string) (string, bool)
// the substitution and an error.
type SubstituteFunc func(string, Mapping) (string, bool, error)

// SubstituteWith substitute variables in the string with their values.
// It accepts additional substitute function.
func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, subsFuncs ...SubstituteFunc) (string, error) {
var outerErr error
var returnErr error
// ReplacementFunc is a user-supplied function that is apply to the matching
// substring. Returns the value as a string and an error.
type ReplacementFunc func(string, Mapping, *Config) (string, error)

result := pattern.ReplaceAllStringFunc(template, func(substring string) string {
_, subsFunc := getSubstitutionFunctionForTemplate(substring)
if len(subsFuncs) > 0 {
subsFunc = subsFuncs[0]
}
type Config struct {
pattern *regexp.Regexp
substituteFunc SubstituteFunc
replacementFunc ReplacementFunc
logging bool
}

closingBraceIndex := getFirstBraceClosingIndex(substring)
rest := ""
if closingBraceIndex > -1 {
rest = substring[closingBraceIndex+1:]
substring = substring[0 : closingBraceIndex+1]
}
type Option func(*Config)

matches := pattern.FindStringSubmatch(substring)
groups := matchGroups(matches, pattern)
if escaped := groups["escaped"]; escaped != "" {
return escaped
}
func WithPattern(pattern *regexp.Regexp) Option {
return func(cfg *Config) {
cfg.pattern = pattern
}
}

braced := false
substitution := groups["named"]
if substitution == "" {
substitution = groups["braced"]
braced = true
}
func WithSubstitutionFunction(subsFunc SubstituteFunc) Option {
return func(cfg *Config) {
cfg.substituteFunc = subsFunc
}
}

if substitution == "" {
outerErr = &InvalidTemplateError{Template: template}
if returnErr == nil {
returnErr = outerErr
}
return ""
}
func WithReplacementFunction(replacementFunc ReplacementFunc) Option {
return func(cfg *Config) {
cfg.replacementFunc = replacementFunc
}
}

func WithoutLogging(cfg *Config) {
cfg.logging = false
}

if braced {
var (
value string
applied bool
)
value, applied, outerErr = subsFunc(substitution, mapping)
if outerErr != nil {
if returnErr == nil {
returnErr = outerErr
// SubstituteWithOptions substitute variables in the string with their values.
// It accepts additional options such as a custom function or pattern.
func SubstituteWithOptions(template string, mapping Mapping, options ...Option) (string, error) {
var returnErr error

cfg := &Config{
pattern: defaultPattern,
replacementFunc: DefaultReplacementFunc,
logging: true,
}
for _, o := range options {
o(cfg)
}

result := cfg.pattern.ReplaceAllStringFunc(template, func(substring string) string {
replacement, err := cfg.replacementFunc(substring, mapping, cfg)
if err != nil {
// Add the template for template errors
var tmplErr *InvalidTemplateError
if errors.As(err, &tmplErr) {
if tmplErr.Template == "" {
tmplErr.Template = template
}
return ""
}
if applied {
interpolatedNested, err := SubstituteWith(rest, mapping, pattern)
if err != nil {
return ""
}
return value + interpolatedNested
// Save the first error to be returned
if returnErr == nil {
returnErr = err
}
}

value, ok := mapping(substitution)
if !ok {
logrus.Warnf("The %q variable is not set. Defaulting to a blank string.", substitution)
}
return value
return replacement
})

return result, returnErr
}

func DefaultReplacementFunc(substring string, mapping Mapping, cfg *Config) (string, error) {
pattern := cfg.pattern
subsFunc := cfg.substituteFunc
if subsFunc == nil {
_, subsFunc = getSubstitutionFunctionForTemplate(substring)
}

closingBraceIndex := getFirstBraceClosingIndex(substring)
rest := ""
if closingBraceIndex > -1 {
rest = substring[closingBraceIndex+1:]
substring = substring[0 : closingBraceIndex+1]
}

matches := pattern.FindStringSubmatch(substring)
groups := matchGroups(matches, pattern)
if escaped := groups["escaped"]; escaped != "" {
return escaped, nil
}

braced := false
substitution := groups["named"]
if substitution == "" {
substitution = groups["braced"]
braced = true
}

if substitution == "" {
return "", &InvalidTemplateError{}
}

if braced {
value, applied, err := subsFunc(substitution, mapping)
if err != nil {
return "", err
}
if applied {
interpolatedNested, err := SubstituteWith(rest, mapping, pattern)
if err != nil {
return "", err
}
return value + interpolatedNested, nil
}
}

value, ok := mapping(substitution)
if !ok && cfg.logging {
logrus.Warnf("The %q variable is not set. Defaulting to a blank string.", substitution)
}

return value, nil
}

// SubstituteWith substitute variables in the string with their values.
// It accepts additional substitute function.
func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, subsFuncs ...SubstituteFunc) (string, error) {
options := []Option{
WithPattern(pattern),
}
if len(subsFuncs) > 0 {
options = append(options, WithSubstitutionFunction(subsFuncs[0]))
}

return SubstituteWithOptions(template, mapping, options...)
}

func getSubstitutionFunctionForTemplate(template string) (string, SubstituteFunc) {
interpolationMapping := []struct {
string
Expand Down
26 changes: 25 additions & 1 deletion template/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,9 +377,33 @@ func TestSubstituteWithCustomFunc(t *testing.T) {
assert.Check(t, is.ErrorContains(err, "required variable"))
}

func TestSubstituteWithReplacementFunc(t *testing.T) {
options := []Option{
WithReplacementFunction(func(s string, m Mapping, c *Config) (string, error) {
if s == "${NOTHERE}" {
return "", fmt.Errorf("bad choice: %q", s)
}
r, err := DefaultReplacementFunc(s, m, c)
if err == nil && r != "" {
return r, nil
}
return "foobar", nil
}),
}
result, err := SubstituteWithOptions("ok ${FOO}", defaultMapping, options...)
assert.NilError(t, err)
assert.Check(t, is.Equal("ok first", result))

result, err = SubstituteWithOptions("ok ${BAR}", defaultMapping, options...)
assert.NilError(t, err)
assert.Check(t, is.Equal("ok foobar", result))

_, err = SubstituteWithOptions("ok ${NOTHERE}", defaultMapping, options...)
assert.Check(t, is.ErrorContains(err, "bad choice"))
}

// TestPrecedence tests is the precedence on '-' and '?' is of the first match
func TestPrecedence(t *testing.T) {

testCases := []struct {
template string
expected string
Expand Down