Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for using nested lists and maps #39

Merged
merged 1 commit into from
Aug 13, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Today's example of Go being a shitty language: without generics, in order to support map[x]y for arbitrary x and y, I had to remove all types from the code and use reflection. That makes the code harder to read and more brittle. We've known how to do generics for several decades; there is no excuse for this.

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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example 2 of Go being a shitty language. Here, type casting is required, because the incoming type genuinely could be anything, but even once I know the type is a map, I still have no way to express map[x]y for arbitrary x and y, so I'm used to use a ton more reflection just to loop over the keys and values. Brittle code that's harder to read = no fun.

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