Skip to content

Commit 2bbabf1

Browse files
committed
Fix variable extraction when string uses variables concatenation
Signed-off-by: Nicolas De Loof <[email protected]>
1 parent db62d1d commit 2bbabf1

File tree

4 files changed

+325
-218
lines changed

4 files changed

+325
-218
lines changed

template/template.go

Lines changed: 0 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -271,102 +271,6 @@ func Substitute(template string, mapping Mapping) (string, error) {
271271
return SubstituteWith(template, mapping, DefaultPattern)
272272
}
273273

274-
// ExtractVariables returns a map of all the variables defined in the specified
275-
// composefile (dict representation) and their default value if any.
276-
func ExtractVariables(configDict map[string]interface{}, pattern *regexp.Regexp) map[string]Variable {
277-
if pattern == nil {
278-
pattern = DefaultPattern
279-
}
280-
return recurseExtract(configDict, pattern)
281-
}
282-
283-
func recurseExtract(value interface{}, pattern *regexp.Regexp) map[string]Variable {
284-
m := map[string]Variable{}
285-
286-
switch value := value.(type) {
287-
case string:
288-
if values, is := extractVariable(value, pattern); is {
289-
for _, v := range values {
290-
m[v.Name] = v
291-
}
292-
}
293-
case map[string]interface{}:
294-
for _, elem := range value {
295-
submap := recurseExtract(elem, pattern)
296-
for key, value := range submap {
297-
m[key] = value
298-
}
299-
}
300-
301-
case []interface{}:
302-
for _, elem := range value {
303-
if values, is := extractVariable(elem, pattern); is {
304-
for _, v := range values {
305-
m[v.Name] = v
306-
}
307-
}
308-
}
309-
}
310-
311-
return m
312-
}
313-
314-
type Variable struct {
315-
Name string
316-
DefaultValue string
317-
PresenceValue string
318-
Required bool
319-
}
320-
321-
func extractVariable(value interface{}, pattern *regexp.Regexp) ([]Variable, bool) {
322-
sValue, ok := value.(string)
323-
if !ok {
324-
return []Variable{}, false
325-
}
326-
matches := pattern.FindAllStringSubmatch(sValue, -1)
327-
if len(matches) == 0 {
328-
return []Variable{}, false
329-
}
330-
values := []Variable{}
331-
for _, match := range matches {
332-
groups := matchGroups(match, pattern)
333-
if escaped := groups[groupEscaped]; escaped != "" {
334-
continue
335-
}
336-
val := groups[groupNamed]
337-
if val == "" {
338-
val = groups[groupBraced]
339-
}
340-
name := val
341-
var defaultValue string
342-
var presenceValue string
343-
var required bool
344-
switch {
345-
case strings.Contains(val, ":?"):
346-
name, _ = partition(val, ":?")
347-
required = true
348-
case strings.Contains(val, "?"):
349-
name, _ = partition(val, "?")
350-
required = true
351-
case strings.Contains(val, ":-"):
352-
name, defaultValue = partition(val, ":-")
353-
case strings.Contains(val, "-"):
354-
name, defaultValue = partition(val, "-")
355-
case strings.Contains(val, ":+"):
356-
name, presenceValue = partition(val, ":+")
357-
case strings.Contains(val, "+"):
358-
name, presenceValue = partition(val, "+")
359-
}
360-
values = append(values, Variable{
361-
Name: name,
362-
DefaultValue: defaultValue,
363-
PresenceValue: presenceValue,
364-
Required: required,
365-
})
366-
}
367-
return values, len(values) > 0
368-
}
369-
370274
// Soft default (fall back if unset or empty)
371275
func defaultWhenEmptyOrUnset(substitution string, mapping Mapping) (string, bool, error) {
372276
return withDefaultWhenAbsence(substitution, mapping, true)

template/template_test.go

Lines changed: 4 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,10 @@ func TestSubstituteWithReplacementFunc(t *testing.T) {
405405

406406
_, err = SubstituteWithOptions("ok ${NOTHERE}", defaultMapping, options...)
407407
assert.Check(t, is.ErrorContains(err, "bad choice"))
408+
409+
result, err = SubstituteWith("ok ${SUBDOMAIN:-redis}.${FOO:?}", defaultMapping, DefaultPattern)
410+
assert.NilError(t, err)
411+
assert.Check(t, is.Equal("ok redis.first", result))
408412
}
409413

410414
func TestSubstituteWithReplacementAppliedFunc(t *testing.T) {
@@ -478,128 +482,6 @@ func TestPrecedence(t *testing.T) {
478482
}
479483
}
480484

481-
func TestExtractVariables(t *testing.T) {
482-
testCases := []struct {
483-
name string
484-
dict map[string]interface{}
485-
expected map[string]Variable
486-
}{
487-
{
488-
name: "empty",
489-
dict: map[string]interface{}{},
490-
expected: map[string]Variable{},
491-
},
492-
{
493-
name: "no-variables",
494-
dict: map[string]interface{}{
495-
"foo": "bar",
496-
},
497-
expected: map[string]Variable{},
498-
},
499-
{
500-
name: "variable-without-curly-braces",
501-
dict: map[string]interface{}{
502-
"foo": "$bar",
503-
},
504-
expected: map[string]Variable{
505-
"bar": {Name: "bar"},
506-
},
507-
},
508-
{
509-
name: "variable",
510-
dict: map[string]interface{}{
511-
"foo": "${bar}",
512-
},
513-
expected: map[string]Variable{
514-
"bar": {Name: "bar", DefaultValue: ""},
515-
},
516-
},
517-
{
518-
name: "required-variable",
519-
dict: map[string]interface{}{
520-
"foo": "${bar?:foo}",
521-
},
522-
expected: map[string]Variable{
523-
"bar": {Name: "bar", DefaultValue: "", Required: true},
524-
},
525-
},
526-
{
527-
name: "required-variable2",
528-
dict: map[string]interface{}{
529-
"foo": "${bar?foo}",
530-
},
531-
expected: map[string]Variable{
532-
"bar": {Name: "bar", DefaultValue: "", Required: true},
533-
},
534-
},
535-
{
536-
name: "default-variable",
537-
dict: map[string]interface{}{
538-
"foo": "${bar:-foo}",
539-
},
540-
expected: map[string]Variable{
541-
"bar": {Name: "bar", DefaultValue: "foo"},
542-
},
543-
},
544-
{
545-
name: "default-variable2",
546-
dict: map[string]interface{}{
547-
"foo": "${bar-foo}",
548-
},
549-
expected: map[string]Variable{
550-
"bar": {Name: "bar", DefaultValue: "foo"},
551-
},
552-
},
553-
{
554-
name: "multiple-values",
555-
dict: map[string]interface{}{
556-
"foo": "${bar:-foo}",
557-
"bar": map[string]interface{}{
558-
"foo": "${fruit:-banana}",
559-
"bar": "vegetable",
560-
},
561-
"baz": []interface{}{
562-
"foo",
563-
"$docker:${project:-cli}",
564-
"$toto",
565-
},
566-
},
567-
expected: map[string]Variable{
568-
"bar": {Name: "bar", DefaultValue: "foo"},
569-
"fruit": {Name: "fruit", DefaultValue: "banana"},
570-
"toto": {Name: "toto", DefaultValue: ""},
571-
"docker": {Name: "docker", DefaultValue: ""},
572-
"project": {Name: "project", DefaultValue: "cli"},
573-
},
574-
},
575-
{
576-
name: "presence-value-nonEmpty",
577-
dict: map[string]interface{}{
578-
"foo": "${bar:+foo}",
579-
},
580-
expected: map[string]Variable{
581-
"bar": {Name: "bar", PresenceValue: "foo"},
582-
},
583-
},
584-
{
585-
name: "presence-value",
586-
dict: map[string]interface{}{
587-
"foo": "${bar+foo}",
588-
},
589-
expected: map[string]Variable{
590-
"bar": {Name: "bar", PresenceValue: "foo"},
591-
},
592-
},
593-
}
594-
for _, tc := range testCases {
595-
tc := tc
596-
t.Run(tc.name, func(t *testing.T) {
597-
actual := ExtractVariables(tc.dict, DefaultPattern)
598-
assert.Check(t, is.DeepEqual(actual, tc.expected))
599-
})
600-
}
601-
}
602-
603485
func TestSubstitutionFunctionChoice(t *testing.T) {
604486
testcases := []struct {
605487
name string

template/variables.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
Copyright 2020 The Compose Specification Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package template
18+
19+
import (
20+
"regexp"
21+
"strings"
22+
)
23+
24+
type Variable struct {
25+
Name string
26+
DefaultValue string
27+
PresenceValue string
28+
Required bool
29+
}
30+
31+
// ExtractVariables returns a map of all the variables defined in the specified
32+
// compose file (dict representation) and their default value if any.
33+
func ExtractVariables(configDict map[string]interface{}, pattern *regexp.Regexp) map[string]Variable {
34+
if pattern == nil {
35+
pattern = DefaultPattern
36+
}
37+
return recurseExtract(configDict, pattern)
38+
}
39+
40+
func recurseExtract(value interface{}, pattern *regexp.Regexp) map[string]Variable {
41+
m := map[string]Variable{}
42+
43+
switch value := value.(type) {
44+
case string:
45+
if values, is := extractVariable(value, pattern); is {
46+
for _, v := range values {
47+
m[v.Name] = v
48+
}
49+
}
50+
case map[string]interface{}:
51+
for _, elem := range value {
52+
submap := recurseExtract(elem, pattern)
53+
for key, value := range submap {
54+
m[key] = value
55+
}
56+
}
57+
58+
case []interface{}:
59+
for _, elem := range value {
60+
if values, is := extractVariable(elem, pattern); is {
61+
for _, v := range values {
62+
m[v.Name] = v
63+
}
64+
}
65+
}
66+
}
67+
68+
return m
69+
}
70+
71+
func extractVariable(value interface{}, pattern *regexp.Regexp) ([]Variable, bool) {
72+
sValue, ok := value.(string)
73+
if !ok {
74+
return []Variable{}, false
75+
}
76+
matches := pattern.FindAllStringSubmatch(sValue, -1)
77+
if len(matches) == 0 {
78+
return []Variable{}, false
79+
}
80+
values := []Variable{}
81+
for _, match := range matches {
82+
groups := matchGroups(match, pattern)
83+
if escaped := groups[groupEscaped]; escaped != "" {
84+
continue
85+
}
86+
val := groups[groupNamed]
87+
if val == "" {
88+
val = groups[groupBraced]
89+
s := match[0]
90+
i := getFirstBraceClosingIndex(s)
91+
if i > 0 {
92+
val = s[2:i]
93+
if len(s) > i {
94+
if v, b := extractVariable(s[i+1:], pattern); b {
95+
values = append(values, v...)
96+
}
97+
}
98+
}
99+
}
100+
name := val
101+
var defaultValue string
102+
var presenceValue string
103+
var required bool
104+
i := strings.IndexFunc(val, func(r rune) bool {
105+
if r >= 'a' && r <= 'z' {
106+
return false
107+
}
108+
if r >= 'A' && r <= 'Z' {
109+
return false
110+
}
111+
if r == '_' {
112+
return false
113+
}
114+
return true
115+
})
116+
117+
if i > 0 {
118+
name = val[:i]
119+
rest := val[i:]
120+
switch {
121+
case strings.HasPrefix(rest, ":?"):
122+
required = true
123+
case strings.HasPrefix(rest, "?"):
124+
required = true
125+
case strings.HasPrefix(rest, ":-"):
126+
defaultValue = rest[2:]
127+
case strings.HasPrefix(rest, "-"):
128+
defaultValue = rest[1:]
129+
case strings.HasPrefix(rest, ":+"):
130+
presenceValue = rest[2:]
131+
case strings.HasPrefix(rest, "+"):
132+
presenceValue = rest[1:]
133+
}
134+
}
135+
136+
values = append(values, Variable{
137+
Name: name,
138+
DefaultValue: defaultValue,
139+
PresenceValue: presenceValue,
140+
Required: required,
141+
})
142+
143+
if defaultValue != "" {
144+
if v, b := extractVariable(defaultValue, pattern); b {
145+
values = append(values, v...)
146+
}
147+
}
148+
if presenceValue != "" {
149+
if v, b := extractVariable(presenceValue, pattern); b {
150+
values = append(values, v...)
151+
}
152+
}
153+
}
154+
return values, len(values) > 0
155+
}

0 commit comments

Comments
 (0)