Repository: compose-spec/compose-go
Pull request #76 (2020-09-29 01:46:42)

Support nested variable expansion
Signed-off-by: MarilynFranklin <marilyn.j.franklin@gmail.com>

This is my first pass at implementing nested variable expansion for consistency with [changes proposed here](https://github.com/docker/compose/pull/7795)

I ran into an `initialization loop` error and ended up removing `DefaultSubstituteFuncs` to fix the issue. I suspect there may be a better way to solve that problem, but I'm pretty new to go!



ulyssessouza: @MarilynFranklin Could you please rebase the PR? It's conflicting on `template/template.go`

Patch:
diff --git a/template/template.go b/template/template.go
index 26e9b36..ecb7358 100644
--- a/template/template.go
+++ b/template/template.go
@@ -26,7 +26,7 @@ import (
 
 var delimiter = "\\$"
 var substitutionNamed = "[_a-z][_a-z0-9]*"
-var substitutionBraced = "[_a-z][_a-z0-9]*(?::?[-?][^}]*)?"
+var substitutionBraced = "[_a-z][_a-z0-9]*(?::?[-?](.*}|[^}]*))?"
 
 var patternString = fmt.Sprintf(
 	"%s(?i:(?P<escaped>%s)|(?P<named>%s)|{(?P<braced>%s)}|(?P<invalid>))",
@@ -35,14 +35,6 @@ var patternString = fmt.Sprintf(
 
 var defaultPattern = regexp.MustCompile(patternString)
 
-// DefaultSubstituteFuncs contains the default SubstituteFunc used by the docker cli
-var DefaultSubstituteFuncs = []SubstituteFunc{
-	softDefault,
-	hardDefault,
-	requiredNonEmpty,
-	required,
-}
-
 // InvalidTemplateError is returned when a variable template is not in a valid
 // format
 type InvalidTemplateError struct {
@@ -67,6 +59,14 @@ 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) {
+	if len(subsFuncs) == 0 {
+		subsFuncs = []SubstituteFunc{
+			softDefault,
+			hardDefault,
+			requiredNonEmpty,
+			required,
+		}
+	}
 	var err error
 	result := pattern.ReplaceAllStringFunc(template, func(substring string) string {
 		matches := pattern.FindStringSubmatch(substring)
@@ -116,7 +116,7 @@ func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, su
 
 // Substitute variables in the string with their values
 func Substitute(template string, mapping Mapping) (string, error) {
-	return SubstituteWith(template, mapping, defaultPattern, DefaultSubstituteFuncs...)
+	return SubstituteWith(template, mapping, defaultPattern)
 }
 
 // ExtractVariables returns a map of all the variables defined in the specified
@@ -215,6 +215,10 @@ func softDefault(substitution string, mapping Mapping) (string, bool, error) {
 		return "", false, nil
 	}
 	name, defaultValue := partition(substitution, sep)
+	defaultValue, err := Substitute(defaultValue, mapping)
+	if err != nil {
+		return "", false, err
+	}
 	value, ok := mapping(name)
 	if !ok || value == "" {
 		return defaultValue, true, nil
@@ -229,6 +233,10 @@ func hardDefault(substitution string, mapping Mapping) (string, bool, error) {
 		return "", false, nil
 	}
 	name, defaultValue := partition(substitution, sep)
+	defaultValue, err := Substitute(defaultValue, mapping)
+	if err != nil {
+		return "", false, err
+	}
 	value, ok := mapping(name)
 	if !ok {
 		return defaultValue, true, nil
@@ -249,6 +257,10 @@ func withRequired(substitution string, mapping Mapping, sep string, valid func(s
 		return "", false, nil
 	}
 	name, errorMessage := partition(substitution, sep)
+	errorMessage, err := Substitute(errorMessage, mapping)
+	if err != nil {
+		return "", false, err
+	}
 	value, ok := mapping(name)
 	if !ok || !valid(value) {
 		return "", true, &InvalidTemplateError{


Test patch:
diff --git a/template/template_test.go b/template/template_test.go
index 3c6b61e..ef8f107 100644
--- a/template/template_test.go
+++ b/template/template_test.go
@@ -119,6 +119,44 @@ func TestNonAlphanumericDefault(t *testing.T) {
 	assert.Check(t, is.Equal("ok /non:-alphanumeric", result))
 }
 
+func TestDefaultsWithNestedExpansion(t *testing.T) {
+	testCases := []struct {
+		template string
+		expected string
+	}{
+		{
+			template: "ok ${UNSET_VAR-$FOO}",
+			expected: "ok first",
+		},
+		{
+			template: "ok ${UNSET_VAR-${FOO}}",
+			expected: "ok first",
+		},
+		{
+			template: "ok ${UNSET_VAR-${FOO} ${FOO}}",
+			expected: "ok first first",
+		},
+		{
+			template: "ok ${BAR:-$FOO}",
+			expected: "ok first",
+		},
+		{
+			template: "ok ${BAR:-${FOO}}",
+			expected: "ok first",
+		},
+		{
+			template: "ok ${BAR:-${FOO} ${FOO}}",
+			expected: "ok first first",
+		},
+	}
+
+	for _, tc := range testCases {
+		result, err := Substitute(tc.template, defaultMapping)
+		assert.NilError(t, err)
+		assert.Check(t, is.Equal(tc.expected, result))
+	}
+}
+
 func TestMandatoryVariableErrors(t *testing.T) {
 	testCases := []struct {
 		template      string
@@ -153,6 +191,28 @@ func TestMandatoryVariableErrors(t *testing.T) {
 	}
 }
 
+func TestMandatoryVariableErrorsWithNestedExpansion(t *testing.T) {
+	testCases := []struct {
+		template      string
+		expectedError string
+	}{
+		{
+			template:      "not ok ${UNSET_VAR:?Mandatory Variable ${FOO}}",
+			expectedError: "required variable UNSET_VAR is missing a value: Mandatory Variable first",
+		},
+		{
+			template:      "not ok ${UNSET_VAR?Mandatory Variable ${FOO}}",
+			expectedError: "required variable UNSET_VAR is missing a value: Mandatory Variable first",
+		},
+	}
+
+	for _, tc := range testCases {
+		_, err := Substitute(tc.template, defaultMapping)
+		assert.ErrorContains(t, err, tc.expectedError)
+		assert.ErrorType(t, err, reflect.TypeOf(&InvalidTemplateError{}))
+	}
+}
+
 func TestDefaultsForMandatoryVariables(t *testing.T) {
 	testCases := []struct {
 		template string