Skip to content

Commit

Permalink
Merge pull request #39 from gruntwork-io/nested-objects
Browse files Browse the repository at this point in the history
Add support for using nested lists and maps
  • Loading branch information
brikis98 committed Aug 13, 2017
2 parents 7c388a7 + 2c09892 commit d00a55a
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 34 deletions.
4 changes: 2 additions & 2 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,8 +315,8 @@ func TestParseBoilerplateConfigAllTypes(t *testing.T) {
variables.NewIntVariable("var3").WithDefault(5),
variables.NewFloatVariable("var4").WithDefault(5.5),
variables.NewBoolVariable("var5").WithDefault(true),
variables.NewListVariable("var6").WithDefault([]string{"foo", "bar", "baz"}),
variables.NewMapVariable("var7").WithDefault(map[string]string{"key1": "value1", "key2": "value2", "key3": "value3"}),
variables.NewListVariable("var6").WithDefault([]interface{}{"foo", "bar", "baz"}),
variables.NewMapVariable("var7").WithDefault(map[interface{}]interface{}{"key1": "value1", "key2": "value2", "key3": "value3"}),
variables.NewEnumVariable("var8", []string{"foo", "bar", "baz"}).WithDefault("bar"),
},
Dependencies: []variables.Dependency{},
Expand Down
4 changes: 3 additions & 1 deletion examples/variables-recursive/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ BarList = {{ range $index, $element := .BarList }}{{ if gt $index 0 }}, {{ end }
FooMap = {{ range $index, $key := (.FooMap | keys) }}{{ if gt $index 0 }}, {{ end }}{{ $key }}: {{ index $.FooMap $key }}{{ end }}
BarMap = {{ range $index, $key := (.BarMap | keys) }}{{ if gt $index 0 }}, {{ end }}{{ $key }}: {{ index $.BarMap $key }}{{ end }}
ListWithTemplates = {{ range $index, $element := .ListWithTemplates }}{{ if gt $index 0 }}, {{ end }}{{ $element }}{{ end }}
MapWithTemplates = {{ range $index, $key := (.MapWithTemplates | keys) }}{{ if gt $index 0 }}, {{ end }}{{ $key }}: {{ index $.MapWithTemplates $key }}{{ end }}
MapWithTemplates = {{ range $index, $key := (.MapWithTemplates | keys) }}{{ if gt $index 0 }}, {{ end }}{{ $key }}: {{ index $.MapWithTemplates $key }}{{ end }}
ListWithNestedMap = {{ range $index, $item := .ListWithNestedMap }}{{ if gt $index 0 }}, {{ end }}(name: {{ $item.name }}, value: {{ $item.value }}){{ end }}
MapWithNestedList = {{ range $index, $key := (.MapWithNestedList | keys) }}{{ if gt $index 0 }}, {{ end }}(key: {{ $key }}, value: {{ range $index2, $value := (index $.MapWithNestedList $key) }}{{ if gt $index2 0 }}, {{ end }}{{ $value }}{{ end }}){{ end }}
15 changes: 15 additions & 0 deletions examples/variables-recursive/boilerplate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ variables:
"{{ .Bar }}": "{{ .Bar }}"
"{{ .Baz }}": "{{ .Baz }}"

- name: ListWithNestedMap
type: list
default:
- name: foo
value: foo

- name: bar
value: bar

- name: MapWithNestedList
type: map
default:
foo: [1, 2, 3]
bar: [4, 5, 6]

dependencies:
- name: variables
template-folder: ../variables
Expand Down
24 changes: 19 additions & 5 deletions templates/template_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -418,16 +418,21 @@ func slice(start interface{}, end interface{}, increment interface{}) ([]int, er

// Return the keys in the given map. This method always returns the keys in sorted order to provide a stable iteration
// order.
func keys(m map[string]string) []string {
func keys(value interface{}) ([]string, error) {
valueType := reflect.ValueOf(value)
if valueType.Kind() != reflect.Map {
return nil, errors.WithStackTrace(InvalidTypeForMethodArgument{"keys", "Map", valueType.Kind().String()})
}

out := []string{}

for key, _ := range m {
out = append(out, key)
for _, key := range valueType.MapKeys() {
out = append(out, fmt.Sprintf("%v", key.Interface()))
}

sort.Strings(out)

return out
return out, nil
}

// Run the given shell command specified in args in the working dir specified by templatePath and return stdout as a
Expand Down Expand Up @@ -540,4 +545,13 @@ func (args InvalidSnippetArguments) Error() string {
return fmt.Sprintf("The snippet helper expects the following args: snippet <TEMPLATE_PATH> <PATH> [SNIPPET_NAME]. Instead, got args: %s", []string(args))
}

var NoArgsPassedToShellHelper = fmt.Errorf("The shell helper requires at least one argument")
var NoArgsPassedToShellHelper = fmt.Errorf("The shell helper requires at least one argument")

type InvalidTypeForMethodArgument struct {
MethodName string
ExpectedType string
ActualType string
}
func (err InvalidTypeForMethodArgument) Error() string {
return fmt.Sprintf("Method %s expects type %s, but got %s", err.MethodName, err.ExpectedType, err.ActualType)
}
30 changes: 16 additions & 14 deletions templates/template_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"path"
"fmt"
"github.com/gruntwork-io/boilerplate/variables"
"reflect"
)

const MaxRenderAttempts = 15
Expand Down Expand Up @@ -88,28 +89,29 @@ func renderVariables(variables map[string]interface{}, options *config.Boilerpla
// Variable values are allowed to use Go templating syntax (e.g. to reference other variables), so here, we render
// those templates and return a new map of variables that are fully resolved.
func renderVariable(variable interface{}, variables map[string]interface{}, options *config.BoilerplateOptions) (interface{}, error) {
switch variableType := variable.(type) {
case string:
return renderTemplateRecursively(options.TemplateFolder, variableType, variables, options)
case []string:
values := []string{}
for _, value := range variableType {
rendered, err := renderTemplateRecursively(options.TemplateFolder, value, variables, options)
valueType := reflect.ValueOf(variable)

switch valueType.Kind() {
case reflect.String:
return renderTemplateRecursively(options.TemplateFolder, variable.(string), variables, options)
case reflect.Slice:
values := []interface{}{}
for i := 0; i < valueType.Len(); i++ {
rendered, err := renderVariable(valueType.Index(i).Interface(), variables, options)
if err != nil {
return nil, err
return nil, err
}
values = append(values, rendered)
}
return values, nil
case map[string]string:
values := map[string]string{}
for key, value := range variableType {
renderedKey, err := renderTemplateRecursively(options.TemplateFolder, key, variables, options)
case reflect.Map:
values := map[interface{}]interface{}{}
for _, key := range valueType.MapKeys() {
renderedKey, err := renderVariable(key.Interface(), variables, options)
if err != nil {
return nil, err
}

renderedValue, err := renderTemplateRecursively(options.TemplateFolder, value, variables, options)
renderedValue, err := renderVariable(valueType.MapIndex(key).Interface(), variables, options)
if err != nil {
return nil, err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ BarList = foo, bar, baz
FooMap = bar: 2, baz: 3, foo: 1
BarMap = bar: 2, baz: 3, foo: 1
ListWithTemplates = foo, foo-bar, foo-bar-baz
MapWithTemplates = foo: foo, foo-bar: foo-bar, foo-bar-baz: foo-bar-baz
MapWithTemplates = foo: foo, foo-bar: foo-bar, foo-bar-baz: foo-bar-baz
ListWithNestedMap = (name: foo, value: foo), (name: bar, value: bar)
MapWithNestedList = (key: bar, value: 4, 5, 6), (key: foo, value: 1, 2, 3)
16 changes: 5 additions & 11 deletions variables/variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package variables

import (
"fmt"
"github.com/gruntwork-io/boilerplate/util"
"reflect"
)

// An interface for a variable defined in a boilerplate.yml config file
Expand Down Expand Up @@ -202,18 +202,12 @@ func UnmarshalValueForVariable(value interface{}, variable Variable) (interface{
return asBool, nil
}
case List:
if asList, isList := value.([]interface{}); isList {
return util.ToStringList(asList), nil
}
if asList, isList := value.([]string); isList {
return asList, nil
if reflect.TypeOf(value).Kind() == reflect.Slice {
return value, nil
}
case Map:
if asMap, isMap := value.(map[interface{}]interface{}); isMap {
return util.ToStringMap(asMap), nil
}
if asMap, isMap := value.(map[string]string); isMap {
return asMap, nil
if reflect.TypeOf(value).Kind() == reflect.Map {
return value, nil
}
case Enum:
if asString, isString := value.(string); isString {
Expand Down

0 comments on commit d00a55a

Please sign in to comment.