Skip to content

Commit 02dae79

Browse files
authored
Merge pull request #418 from srebhan/template_options
Add substitution with options
2 parents 0dbeb27 + e95c1cf commit 02dae79

File tree

2 files changed

+147
-56
lines changed

2 files changed

+147
-56
lines changed

template/template.go

Lines changed: 122 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package template
1818

1919
import (
20+
"errors"
2021
"fmt"
2122
"regexp"
2223
"sort"
@@ -71,77 +72,143 @@ type Mapping func(string) (string, bool)
7172
// the substitution and an error.
7273
type SubstituteFunc func(string, Mapping) (string, bool, error)
7374

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

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

86-
closingBraceIndex := getFirstBraceClosingIndex(substring)
87-
rest := ""
88-
if closingBraceIndex > -1 {
89-
rest = substring[closingBraceIndex+1:]
90-
substring = substring[0 : closingBraceIndex+1]
91-
}
86+
type Option func(*Config)
9287

93-
matches := pattern.FindStringSubmatch(substring)
94-
groups := matchGroups(matches, pattern)
95-
if escaped := groups["escaped"]; escaped != "" {
96-
return escaped
97-
}
88+
func WithPattern(pattern *regexp.Regexp) Option {
89+
return func(cfg *Config) {
90+
cfg.pattern = pattern
91+
}
92+
}
9893

99-
braced := false
100-
substitution := groups["named"]
101-
if substitution == "" {
102-
substitution = groups["braced"]
103-
braced = true
104-
}
94+
func WithSubstitutionFunction(subsFunc SubstituteFunc) Option {
95+
return func(cfg *Config) {
96+
cfg.substituteFunc = subsFunc
97+
}
98+
}
10599

106-
if substitution == "" {
107-
outerErr = &InvalidTemplateError{Template: template}
108-
if returnErr == nil {
109-
returnErr = outerErr
110-
}
111-
return ""
112-
}
100+
func WithReplacementFunction(replacementFunc ReplacementFunc) Option {
101+
return func(cfg *Config) {
102+
cfg.replacementFunc = replacementFunc
103+
}
104+
}
105+
106+
func WithoutLogging(cfg *Config) {
107+
cfg.logging = false
108+
}
113109

114-
if braced {
115-
var (
116-
value string
117-
applied bool
118-
)
119-
value, applied, outerErr = subsFunc(substitution, mapping)
120-
if outerErr != nil {
121-
if returnErr == nil {
122-
returnErr = outerErr
110+
// SubstituteWithOptions substitute variables in the string with their values.
111+
// It accepts additional options such as a custom function or pattern.
112+
func SubstituteWithOptions(template string, mapping Mapping, options ...Option) (string, error) {
113+
var returnErr error
114+
115+
cfg := &Config{
116+
pattern: defaultPattern,
117+
replacementFunc: DefaultReplacementFunc,
118+
logging: true,
119+
}
120+
for _, o := range options {
121+
o(cfg)
122+
}
123+
124+
result := cfg.pattern.ReplaceAllStringFunc(template, func(substring string) string {
125+
replacement, err := cfg.replacementFunc(substring, mapping, cfg)
126+
if err != nil {
127+
// Add the template for template errors
128+
var tmplErr *InvalidTemplateError
129+
if errors.As(err, &tmplErr) {
130+
if tmplErr.Template == "" {
131+
tmplErr.Template = template
123132
}
124-
return ""
125133
}
126-
if applied {
127-
interpolatedNested, err := SubstituteWith(rest, mapping, pattern)
128-
if err != nil {
129-
return ""
130-
}
131-
return value + interpolatedNested
134+
// Save the first error to be returned
135+
if returnErr == nil {
136+
returnErr = err
132137
}
133-
}
134138

135-
value, ok := mapping(substitution)
136-
if !ok {
137-
logrus.Warnf("The %q variable is not set. Defaulting to a blank string.", substitution)
138139
}
139-
return value
140+
return replacement
140141
})
141142

142143
return result, returnErr
143144
}
144145

146+
func DefaultReplacementFunc(substring string, mapping Mapping, cfg *Config) (string, error) {
147+
pattern := cfg.pattern
148+
subsFunc := cfg.substituteFunc
149+
if subsFunc == nil {
150+
_, subsFunc = getSubstitutionFunctionForTemplate(substring)
151+
}
152+
153+
closingBraceIndex := getFirstBraceClosingIndex(substring)
154+
rest := ""
155+
if closingBraceIndex > -1 {
156+
rest = substring[closingBraceIndex+1:]
157+
substring = substring[0 : closingBraceIndex+1]
158+
}
159+
160+
matches := pattern.FindStringSubmatch(substring)
161+
groups := matchGroups(matches, pattern)
162+
if escaped := groups["escaped"]; escaped != "" {
163+
return escaped, nil
164+
}
165+
166+
braced := false
167+
substitution := groups["named"]
168+
if substitution == "" {
169+
substitution = groups["braced"]
170+
braced = true
171+
}
172+
173+
if substitution == "" {
174+
return "", &InvalidTemplateError{}
175+
}
176+
177+
if braced {
178+
value, applied, err := subsFunc(substitution, mapping)
179+
if err != nil {
180+
return "", err
181+
}
182+
if applied {
183+
interpolatedNested, err := SubstituteWith(rest, mapping, pattern)
184+
if err != nil {
185+
return "", err
186+
}
187+
return value + interpolatedNested, nil
188+
}
189+
}
190+
191+
value, ok := mapping(substitution)
192+
if !ok && cfg.logging {
193+
logrus.Warnf("The %q variable is not set. Defaulting to a blank string.", substitution)
194+
}
195+
196+
return value, nil
197+
}
198+
199+
// SubstituteWith substitute variables in the string with their values.
200+
// It accepts additional substitute function.
201+
func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, subsFuncs ...SubstituteFunc) (string, error) {
202+
options := []Option{
203+
WithPattern(pattern),
204+
}
205+
if len(subsFuncs) > 0 {
206+
options = append(options, WithSubstitutionFunction(subsFuncs[0]))
207+
}
208+
209+
return SubstituteWithOptions(template, mapping, options...)
210+
}
211+
145212
func getSubstitutionFunctionForTemplate(template string) (string, SubstituteFunc) {
146213
interpolationMapping := []struct {
147214
string

template/template_test.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,9 +377,33 @@ func TestSubstituteWithCustomFunc(t *testing.T) {
377377
assert.Check(t, is.ErrorContains(err, "required variable"))
378378
}
379379

380+
func TestSubstituteWithReplacementFunc(t *testing.T) {
381+
options := []Option{
382+
WithReplacementFunction(func(s string, m Mapping, c *Config) (string, error) {
383+
if s == "${NOTHERE}" {
384+
return "", fmt.Errorf("bad choice: %q", s)
385+
}
386+
r, err := DefaultReplacementFunc(s, m, c)
387+
if err == nil && r != "" {
388+
return r, nil
389+
}
390+
return "foobar", nil
391+
}),
392+
}
393+
result, err := SubstituteWithOptions("ok ${FOO}", defaultMapping, options...)
394+
assert.NilError(t, err)
395+
assert.Check(t, is.Equal("ok first", result))
396+
397+
result, err = SubstituteWithOptions("ok ${BAR}", defaultMapping, options...)
398+
assert.NilError(t, err)
399+
assert.Check(t, is.Equal("ok foobar", result))
400+
401+
_, err = SubstituteWithOptions("ok ${NOTHERE}", defaultMapping, options...)
402+
assert.Check(t, is.ErrorContains(err, "bad choice"))
403+
}
404+
380405
// TestPrecedence tests is the precedence on '-' and '?' is of the first match
381406
func TestPrecedence(t *testing.T) {
382-
383407
testCases := []struct {
384408
template string
385409
expected string

0 commit comments

Comments
 (0)