Skip to content

Commit

Permalink
Add a new ExtractVariables function to compose/template package
Browse files Browse the repository at this point in the history
It allows to get easily all the variables defined in a
composefile (the `map[string]interface{}` representation that
`loader.ParseYAML` returns at least) and their default value too.

This commit also does some small function extract on substitution
funcs to reduce a tiny bit duplication.

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
  • Loading branch information
vdemeester committed Aug 1, 2018
1 parent 7f853fe commit afb87e4
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 25 deletions.
110 changes: 85 additions & 25 deletions cli/compose/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,25 +93,91 @@ func Substitute(template string, mapping Mapping) (string, error) {
return SubstituteWith(template, mapping, pattern, DefaultSubstituteFuncs...)
}

// Soft default (fall back if unset or empty)
func softDefault(substitution string, mapping Mapping) (string, bool, error) {
if !strings.Contains(substitution, ":-") {
return "", false, nil
// ExtractVariables returns a map of all the variables defined in the specified
// composefile (dict representation) and their default value if any.
func ExtractVariables(configDict map[string]interface{}) map[string]string {
return recurseExtract(configDict)
}

func recurseExtract(value interface{}) map[string]string {
m := map[string]string{}

switch value := value.(type) {
case string:
if v, is := extractVariable(value); is {
m[v.name] = v.value
}
case map[string]interface{}:
for _, elem := range value {
submap := recurseExtract(elem)
for key, value := range submap {
m[key] = value
}
}

case []interface{}:
for _, elem := range value {
if v, is := extractVariable(elem); is {
m[v.name] = v.value
}
}
}
name, defaultValue := partition(substitution, ":-")
value, ok := mapping(name)
if !ok || value == "" {
return defaultValue, true, nil

return m
}

type extractedValue struct {
name string
value string
}

func extractVariable(value interface{}) (extractedValue, bool) {
sValue, ok := value.(string)
if !ok {
return extractedValue{}, false
}
return value, true, nil
matches := pattern.FindStringSubmatch(sValue)
if len(matches) == 0 {
return extractedValue{}, false
}
groups := matchGroups(matches)
if escaped := groups["escaped"]; escaped != "" {
return extractedValue{}, false
}
val := groups["named"]
if val == "" {
val = groups["braced"]
}
name := val
var defaultValue string
switch {
case strings.Contains(val, ":?"):
name, _ = partition(val, ":?")
case strings.Contains(val, "?"):
name, _ = partition(val, "?")
case strings.Contains(val, ":-"):
name, defaultValue = partition(val, ":-")
case strings.Contains(val, "-"):
name, defaultValue = partition(val, "-")
}
return extractedValue{name: name, value: defaultValue}, true
}

// Soft default (fall back if unset or empty)
func softDefault(substitution string, mapping Mapping) (string, bool, error) {
return withDefault(substitution, mapping, "-:")
}

// Hard default (fall back if-and-only-if empty)
func hardDefault(substitution string, mapping Mapping) (string, bool, error) {
if !strings.Contains(substitution, "-") {
return withDefault(substitution, mapping, "-")
}

func withDefault(substitution string, mapping Mapping, sep string) (string, bool, error) {
if !strings.Contains(substitution, sep) {
return "", false, nil
}
name, defaultValue := partition(substitution, "-")
name, defaultValue := partition(substitution, sep)
value, ok := mapping(name)
if !ok {
return defaultValue, true, nil
Expand All @@ -120,26 +186,20 @@ func hardDefault(substitution string, mapping Mapping) (string, bool, error) {
}

func requiredNonEmpty(substitution string, mapping Mapping) (string, bool, error) {
if !strings.Contains(substitution, ":?") {
return "", false, nil
}
name, errorMessage := partition(substitution, ":?")
value, ok := mapping(name)
if !ok || value == "" {
return "", true, &InvalidTemplateError{
Template: fmt.Sprintf("required variable %s is missing a value: %s", name, errorMessage),
}
}
return value, true, nil
return withRequired(substitution, mapping, ":?", func(v string) bool { return v != "" })
}

func required(substitution string, mapping Mapping) (string, bool, error) {
if !strings.Contains(substitution, "?") {
return withRequired(substitution, mapping, "?", func(_ string) bool { return true })
}

func withRequired(substitution string, mapping Mapping, sep string, valid func(string) bool) (string, bool, error) {
if !strings.Contains(substitution, sep) {
return "", false, nil
}
name, errorMessage := partition(substitution, "?")
name, errorMessage := partition(substitution, sep)
value, ok := mapping(name)
if !ok {
if !ok || !valid(value) {
return "", true, &InvalidTemplateError{
Template: fmt.Sprintf("required variable %s is missing a value: %s", name, errorMessage),
}
Expand Down
88 changes: 88 additions & 0 deletions cli/compose/template/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,91 @@ func TestSubstituteWithCustomFunc(t *testing.T) {
_, err = SubstituteWith("ok ${NOTHERE}", defaultMapping, pattern, errIsMissing)
assert.Check(t, is.ErrorContains(err, "required variable"))
}

func TestExtractVariables(t *testing.T) {
testCases := []struct {
dict map[string]interface{}
expected map[string]string
}{
{
dict: map[string]interface{}{},
expected: map[string]string{},
},
{
dict: map[string]interface{}{
"foo": "bar",
},
expected: map[string]string{},
},
{
dict: map[string]interface{}{
"foo": "$bar",
},
expected: map[string]string{
"bar": "",
},
},
{
dict: map[string]interface{}{
"foo": "${bar}",
},
expected: map[string]string{
"bar": "",
},
},
{
dict: map[string]interface{}{
"foo": "${bar?:foo}",
},
expected: map[string]string{
"bar": "",
},
},
{
dict: map[string]interface{}{
"foo": "${bar?foo}",
},
expected: map[string]string{
"bar": "",
},
},
{
dict: map[string]interface{}{
"foo": "${bar:-foo}",
},
expected: map[string]string{
"bar": "foo",
},
},
{
dict: map[string]interface{}{
"foo": "${bar-foo}",
},
expected: map[string]string{
"bar": "foo",
},
},
{
dict: map[string]interface{}{
"foo": "${bar:-foo}",
"bar": map[string]interface{}{
"foo": "${fruit:-banana}",
"bar": "vegetable",
},
"baz": []interface{}{
"foo",
"$toto",
},
},
expected: map[string]string{
"bar": "foo",
"fruit": "banana",
"toto": "",
},
},
}
for _, tc := range testCases {
actual := ExtractVariables(tc.dict)
assert.Check(t, is.DeepEqual(actual, tc.expected))
}
}

0 comments on commit afb87e4

Please sign in to comment.