Skip to content

Commit

Permalink
Merge pull request #35 from gruntwork-io/variable-types
Browse files Browse the repository at this point in the history
Variable references
  • Loading branch information
brikis98 committed Jul 13, 2017
2 parents 68281ad + f7495e6 commit 1bc00c9
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 31 deletions.
24 changes: 22 additions & 2 deletions _docs/README.md
Expand Up @@ -185,6 +185,7 @@ variables:
- <CHOICE>
- <CHOICE>
default: <DEFAULT>
reference: <NAME>

dependencies:
- name: <DEPENDENCY_NAME>
Expand Down Expand Up @@ -230,6 +231,7 @@ keys:
* `default` (Optional): A default value for this variable. The user can just hit ENTER at the command line to use the
default value, if one is provided. If running Boilerplate with the `--non-interactive` flag, the default is
used for this value if no value is provided via the `--var` or `--var-file` options.
* `reference` (Optional): The name of another variable whose value should be used for this one.

See the [Variables](#variables) section for more info.

Expand Down Expand Up @@ -291,7 +293,7 @@ four ways to provide a value for a variable:
1. Defaults defined in `boilerplate.yml`. The final fallback is the optional `default` that you can include as part of
the variable definition in `boilerplate.yml`.

Note that variables can reference other variables using interpolation syntax:
Note that variables can reference other variables using Go templating syntax:

```yaml
variables:
Expand All @@ -302,7 +304,25 @@ variables:
default: "{{"{{"}} .Foo {{"}}"}}-bar"
```

If you rendered `{{"{{"}} .Bar {{"}}"}}` with the variables above, you would get `foo-bar`.
If you rendered `{{"{{"}} .Bar {{"}}"}}` with the variables above, you would get `foo-bar`. Note that this will always
return a string. If you want to reference another variable of a non-string type (e.g. a list), use the `reference`
keyword:

```yaml
variables:
- name: Foo
type: list
default:
- 1
- 2
- 3

- name: Bar
type: list
reference: Foo
```

In the example above, the `Bar` variable will be set to the same (list) value as `Foo`.

#### Dependencies

Expand Down
87 changes: 61 additions & 26 deletions config/get_variables.go
Expand Up @@ -8,6 +8,8 @@ import (
"strings"
)

const MaxReferenceDepth = 20

// Get a value for each of the variables specified in boilerplateConfig, other than those already in existingVariables.
// The value for a variable can come from the user (if the non-interactive option isn't set), the default value in the
// config, or a command line option.
Expand All @@ -17,28 +19,6 @@ func GetVariables(options *BoilerplateOptions, boilerplateConfig, rootBoilerplat
vars[key] = value
}

variablesInConfig := getAllVariablesInConfig(boilerplateConfig)

for _, variable := range variablesInConfig {
var value interface{}
var err error

value, alreadyExists := vars[variable.Name()]
if !alreadyExists {
value, err = getVariable(variable, options)
if err != nil {
return vars, err
}
}

unmarshalled, err := variables.UnmarshalValueForVariable(value, variable)
if err != nil {
return vars, err
}

vars[variable.Name()] = unmarshalled
}

// Add a variable for all variables contained in the root config file. This will allow Golang template users
// to directly access these with an expression like "{{ .BoilerplateConfigVars.foo.Default }}"
rootConfigVars := map[string]variables.Variable{}
Expand All @@ -62,17 +42,56 @@ func GetVariables(options *BoilerplateOptions, boilerplateConfig, rootBoilerplat
thisTemplateProps["CurrentDep"] = thisDep
vars["This"] = thisTemplateProps

variablesInConfig := getAllVariablesInConfig(boilerplateConfig)

for _, variable := range variablesInConfig {
unmarshalled, err := getUnmarshalledValueForVariable(variable, variablesInConfig, vars, options, 0)
if err != nil {
return nil, err
}
vars[variable.Name()] = unmarshalled
}

return vars, nil
}

func getUnmarshalledValueForVariable(variable variables.Variable, variablesInConfig map[string]variables.Variable, alreadyUnmarshalledVariables map[string]interface{}, options *BoilerplateOptions, referenceDepth int) (interface{}, error) {
if referenceDepth > MaxReferenceDepth {
return nil, errors.WithStackTrace(CyclicalReference{VariableName: variable.Name(), ReferenceName: variable.Reference()})
}

if variable.Reference() != "" {
reference, containsReference := variablesInConfig[variable.Reference()]
if !containsReference {
return nil, errors.WithStackTrace(MissingReference{VariableName: variable.Name(), ReferenceName: variable.Reference()})
}
return getUnmarshalledValueForVariable(reference, variablesInConfig, alreadyUnmarshalledVariables, options, referenceDepth + 1)
}

value, alreadyExists := alreadyUnmarshalledVariables[variable.Name()]
if !alreadyExists {
variableValue, err := getVariable(variable, options)
if err != nil {
return nil, err
}
value = variableValue
}

return variables.UnmarshalValueForVariable(value, variable)
}

// Get all the variables defined in the given config and its dependencies
func getAllVariablesInConfig(boilerplateConfig *BoilerplateConfig) []variables.Variable {
allVariables := []variables.Variable{}
func getAllVariablesInConfig(boilerplateConfig *BoilerplateConfig) map[string]variables.Variable {
allVariables := map[string]variables.Variable{}

allVariables = append(allVariables, boilerplateConfig.Variables...)
for _, variable := range boilerplateConfig.Variables {
allVariables[variable.Name()] = variable
}

for _, dependency := range boilerplateConfig.Dependencies {
allVariables = append(allVariables, dependency.GetNamespacedVariables()...)
for _, variable := range dependency.GetNamespacedVariables() {
allVariables[variable.Name()] = variable
}
}

return allVariables
Expand Down Expand Up @@ -145,4 +164,20 @@ func getVariableFromUser(variable variables.Variable, options *BoilerplateOption
type MissingVariableWithNonInteractiveMode string
func (variableName MissingVariableWithNonInteractiveMode) Error() string {
return fmt.Sprintf("Variable '%s' does not have a default, no value was specified at the command line using the --%s option, and the --%s flag is set, so cannot prompt user for a value.", string(variableName), OPT_VAR, OPT_NON_INTERACTIVE)
}

type MissingReference struct {
VariableName string
ReferenceName string
}
func (err MissingReference) Error() string {
return fmt.Sprintf("Variable %s references unknown variable %s", err.VariableName, err.ReferenceName)
}

type CyclicalReference struct {
VariableName string
ReferenceName string
}
func (err CyclicalReference) Error() string {
return fmt.Sprintf("Variable %s seems to have an cyclical reference with variable %s", err.VariableName, err.ReferenceName)
}
6 changes: 5 additions & 1 deletion examples/variables-recursive/README.md
Expand Up @@ -4,4 +4,8 @@ This shows an example of variables that reference other variables.

Foo = {{ .Foo }}
Bar = {{ .Bar }}
Baz = {{ .Baz }}
Baz = {{ .Baz }}
FooList = {{ range $index, $element := .FooList }}{{ if gt $index 0 }}, {{ end }}{{ $element }}{{ end }}
BarList = {{ range $index, $element := .BarList }}{{ if gt $index 0 }}, {{ end }}{{ $element }}{{ 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 }}
24 changes: 23 additions & 1 deletion examples/variables-recursive/boilerplate.yml
Expand Up @@ -5,4 +5,26 @@ variables:
default: "{{ .Foo }}-bar"

- name: Baz
default: "{{ .Bar }}-baz"
default: "{{ .Bar }}-baz"

- name: FooList
type: list
default:
- foo
- bar
- baz

- name: BarList
type: list
reference: FooList

- name: FooMap
type: map
default:
foo: 1
bar: 2
baz: 3

- name: BarMap
type: map
reference: FooMap
Expand Up @@ -4,4 +4,8 @@ This shows an example of variables that reference other variables.

Foo = foo
Bar = foo-bar
Baz = foo-bar-baz
Baz = foo-bar-baz
FooList = foo, bar, baz
BarList = foo, bar, baz
FooMap = bar: 2, baz: 3, foo: 1
BarMap = bar: 2, baz: 3, foo: 1
16 changes: 16 additions & 0 deletions variables/variables.go
Expand Up @@ -23,6 +23,9 @@ type Variable interface {
// The default value for teh variable, if any
Default() interface{}

// The name of another variable from which this variable should take its value
Reference() string

// The values this variable can take. Applies only if Type() is Enum.
Options() []string

Expand All @@ -47,6 +50,7 @@ type defaultVariable struct {
name string
description string
defaultValue interface{}
reference string
variableType BoilerplateType
options []string
}
Expand Down Expand Up @@ -133,6 +137,10 @@ func (variable defaultVariable) Default() interface{} {
return variable.defaultValue
}

func (variable defaultVariable) Reference() string {
return variable.reference
}

func (variable defaultVariable) Options() []string {
return variable.options
}
Expand Down Expand Up @@ -282,6 +290,14 @@ func UnmarshalVariableFromBoilerplateConfigYaml(fields map[string]interface{}) (
variable.description = *description
}

reference, err := unmarshalStringField(fields, "reference", false, *name)
if err != nil {
return nil, err
}
if reference != nil {
variable.reference = *reference
}

options, err := unmarshalOptionsField(fields, *name, variableType)
if err != nil {
return nil, err
Expand Down

0 comments on commit 1bc00c9

Please sign in to comment.