Repository: compose-spec/compose-go
Pull request #291 (2022-07-27 10:27:23)

Add 'default' value in case of presence of a variable


Patch:
diff --git a/template/template.go b/template/template.go
index e9fd112..daed7aa 100644
--- a/template/template.go
+++ b/template/template.go
@@ -28,7 +28,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>))",
@@ -214,9 +214,10 @@ func recurseExtract(value interface{}, pattern *regexp.Regexp) map[string]Variab
 }
 
 type Variable struct {
-	Name         string
-	DefaultValue string
-	Required     bool
+	Name          string
+	DefaultValue  string
+	PresenceValue string
+	Required      bool
 }
 
 func extractVariable(value interface{}, pattern *regexp.Regexp) ([]Variable, bool) {
@@ -240,6 +241,7 @@ func extractVariable(value interface{}, pattern *regexp.Regexp) ([]Variable, boo
 		}
 		name := val
 		var defaultValue string
+		var presenceValue string
 		var required bool
 		switch {
 		case strings.Contains(val, ":?"):
@@ -252,11 +254,16 @@ func extractVariable(value interface{}, pattern *regexp.Regexp) ([]Variable, boo
 			name, defaultValue = partition(val, ":-")
 		case strings.Contains(val, "-"):
 			name, defaultValue = partition(val, "-")
+		case strings.Contains(val, ":+"):
+			name, presenceValue = partition(val, ":+")
+		case strings.Contains(val, "+"):
+			name, presenceValue = partition(val, "+")
 		}
 		values = append(values, Variable{
-			Name:         name,
-			DefaultValue: defaultValue,
-			Required:     required,
+			Name:          name,
+			DefaultValue:  defaultValue,
+			PresenceValue: presenceValue,
+			Required:      required,
 		})
 	}
 	return values, len(values) > 0
@@ -273,11 +280,11 @@ func defaultWhenUnset(substitution string, mapping Mapping) (string, bool, error
 }
 
 func defaultWhenNotEmpty(substitution string, mapping Mapping) (string, bool, error) {
-	return "", false, nil // TODO Implement ":+"
+	return withDefaultWhenPresence(substitution, mapping, true)
 }
 
 func defaultWhenSet(substitution string, mapping Mapping) (string, bool, error) {
-	return "", false, nil // TODO Implement "+"
+	return withDefaultWhenPresence(substitution, mapping, false)
 }
 
 func requiredErrorWhenEmptyOrUnset(substitution string, mapping Mapping) (string, bool, error) {
@@ -288,6 +295,26 @@ func requiredErrorWhenUnset(substitution string, mapping Mapping) (string, bool,
 	return withRequired(substitution, mapping, "?", func(_ string) bool { return true })
 }
 
+func withDefaultWhenPresence(substitution string, mapping Mapping, notEmpty bool) (string, bool, error) {
+	sep := "+"
+	if notEmpty {
+		sep = ":+"
+	}
+	if !strings.Contains(substitution, sep) {
+		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 && (!notEmpty || (notEmpty && value != "")) {
+		return defaultValue, true, nil
+	}
+	return value, true, nil
+}
+
 func withDefaultWhenAbsence(substitution string, mapping Mapping, emptyOrUnset bool) (string, bool, error) {
 	sep := "-"
 	if emptyOrUnset {


Test patch:
diff --git a/template/template_test.go b/template/template_test.go
index a38b419..83ebe80 100644
--- a/template/template_test.go
+++ b/template/template_test.go
@@ -113,6 +113,42 @@ func TestEmptyValueWithHardDefault(t *testing.T) {
 	assert.Check(t, is.Equal("ok ", result))
 }
 
+func TestPresentValueWithUnset(t *testing.T) {
+	result, err := Substitute("ok ${UNSET_VAR:+presence_value}", defaultMapping)
+	assert.NilError(t, err)
+	assert.Check(t, is.Equal("ok ", result))
+}
+
+func TestPresentValueWithUnset2(t *testing.T) {
+	result, err := Substitute("ok ${UNSET_VAR+presence_value}", defaultMapping)
+	assert.NilError(t, err)
+	assert.Check(t, is.Equal("ok ", result))
+}
+
+func TestPresentValueWithNonEmpty(t *testing.T) {
+	result, err := Substitute("ok ${FOO:+presence_value}", defaultMapping)
+	assert.NilError(t, err)
+	assert.Check(t, is.Equal("ok presence_value", result))
+}
+
+func TestPresentValueAndNonEmptyWithNonEmpty(t *testing.T) {
+	result, err := Substitute("ok ${FOO+presence_value}", defaultMapping)
+	assert.NilError(t, err)
+	assert.Check(t, is.Equal("ok presence_value", result))
+}
+
+func TestPresentValueWithSet(t *testing.T) {
+	result, err := Substitute("ok ${BAR+presence_value}", defaultMapping)
+	assert.NilError(t, err)
+	assert.Check(t, is.Equal("ok presence_value", result))
+}
+
+func TestPresentValueAndNotEmptyWithSet(t *testing.T) {
+	result, err := Substitute("ok ${BAR:+presence_value}", defaultMapping)
+	assert.NilError(t, err)
+	assert.Check(t, is.Equal("ok ", result))
+}
+
 func TestNonAlphanumericDefault(t *testing.T) {
 	result, err := Substitute("ok ${BAR:-/non:-alphanumeric}", defaultMapping)
 	assert.NilError(t, err)
@@ -148,6 +184,14 @@ func TestDefaultsWithNestedExpansion(t *testing.T) {
 			template: "ok ${BAR:-${FOO} ${FOO}}",
 			expected: "ok first first",
 		},
+		{
+			template: "ok ${BAR+$FOO}",
+			expected: "ok first",
+		},
+		{
+			template: "ok ${BAR+$FOO ${FOO:+second}}",
+			expected: "ok first second",
+		},
 	}
 
 	for _, tc := range testCases {
@@ -398,6 +442,24 @@ func TestExtractVariables(t *testing.T) {
 				"project": {Name: "project", DefaultValue: "cli"},
 			},
 		},
+		{
+			name: "presence-value-nonEmpty",
+			dict: map[string]interface{}{
+				"foo": "${bar:+foo}",
+			},
+			expected: map[string]Variable{
+				"bar": {Name: "bar", PresenceValue: "foo"},
+			},
+		},
+		{
+			name: "presence-value",
+			dict: map[string]interface{}{
+				"foo": "${bar+foo}",
+			},
+			expected: map[string]Variable{
+				"bar": {Name: "bar", PresenceValue: "foo"},
+			},
+		},
 	}
 	for _, tc := range testCases {
 		tc := tc