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

Do type checking after rendering variables #40

Merged
merged 3 commits into from Aug 20, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 13 additions & 13 deletions cli/boilerplate_cli.go
Expand Up @@ -3,9 +3,9 @@ package cli
import (
"github.com/urfave/cli"
"fmt"
"github.com/gruntwork-io/boilerplate/config"
"github.com/gruntwork-io/boilerplate/templates"
"github.com/gruntwork-io/boilerplate/variables"
"github.com/gruntwork-io/boilerplate/options"
)

// Customize the --help text for the app so we don't show extraneous info
Expand Down Expand Up @@ -56,32 +56,32 @@ func CreateBoilerplateCli(version string) *cli.App {

app.Flags = []cli.Flag {
cli.StringFlag{
Name: config.OPT_TEMPLATE_FOLDER,
Name: options.OPT_TEMPLATE_FOLDER,
Usage: "Generate the project from the templates in `FOLDER`.",
},
cli.StringFlag{
Name: config.OPT_OUTPUT_FOLDER,
Name: options.OPT_OUTPUT_FOLDER,
Usage: "Create the output files and folders in `FOLDER`.",
},
cli.BoolFlag{
Name: config.OPT_NON_INTERACTIVE,
Usage: fmt.Sprintf("Do not prompt for input variables. All variables must be set via --%s and --%s options instead.", config.OPT_VAR, config.OPT_VAR_FILE),
Name: options.OPT_NON_INTERACTIVE,
Usage: fmt.Sprintf("Do not prompt for input variables. All variables must be set via --%s and --%s options instead.", options.OPT_VAR, options.OPT_VAR_FILE),
},
cli.StringSliceFlag{
Name: config.OPT_VAR,
Name: options.OPT_VAR,
Usage: "Use `NAME=VALUE` to set variable NAME to VALUE. May be specified more than once.",
},
cli.StringSliceFlag{
Name: config.OPT_VAR_FILE,
Name: options.OPT_VAR_FILE,
Usage: "Load variable values from the YAML file `FILE`. May be specified more than once.",
},
cli.StringFlag{
Name: config.OPT_MISSING_KEY_ACTION,
Usage: fmt.Sprintf("What `ACTION` to take if a template looks up a variable that is not defined. Must be one of: %s. Default: %s.", config.ALL_MISSING_KEY_ACTIONS, config.DEFAULT_MISSING_KEY_ACTION),
Name: options.OPT_MISSING_KEY_ACTION,
Usage: fmt.Sprintf("What `ACTION` to take if a template looks up a variable that is not defined. Must be one of: %s. Default: %s.", options.ALL_MISSING_KEY_ACTIONS, options.DEFAULT_MISSING_KEY_ACTION),
},
cli.StringFlag{
Name: config.OPT_MISSING_CONFIG_ACTION,
Usage: fmt.Sprintf("What `ACTION` to take if a the template folder does not contain a boilerplate.yml file. Must be one of: %s. Default: %s.", config.ALL_MISSING_CONFIG_ACTIONS, config.DEFAULT_MISSING_CONFIG_ACTION),
Name: options.OPT_MISSING_CONFIG_ACTION,
Usage: fmt.Sprintf("What `ACTION` to take if a the template folder does not contain a boilerplate.yml file. Must be one of: %s. Default: %s.", options.ALL_MISSING_CONFIG_ACTIONS, options.DEFAULT_MISSING_CONFIG_ACTION),
},
}

Expand All @@ -96,13 +96,13 @@ func runApp(cliContext *cli.Context) error {
return nil
}

options, err := config.ParseOptions(cliContext)
opts, err := options.ParseOptions(cliContext)
if err != nil {
return err
}

// The root boilerplate.yml is not itself a dependency, so we pass an empty Dependency.
emptyDep := variables.Dependency{}

return templates.ProcessTemplate(options, options, emptyDep)
return templates.ProcessTemplate(opts, opts, emptyDep)
}
19 changes: 6 additions & 13 deletions config/config.go
Expand Up @@ -8,18 +8,11 @@ import (
"fmt"
"github.com/gruntwork-io/boilerplate/errors"
"github.com/gruntwork-io/boilerplate/variables"
"github.com/gruntwork-io/boilerplate/options"
)

const BOILERPLATE_CONFIG_FILE = "boilerplate.yml"

const OPT_TEMPLATE_FOLDER = "template-folder"
const OPT_OUTPUT_FOLDER = "output-folder"
const OPT_NON_INTERACTIVE = "non-interactive"
const OPT_VAR = "var"
const OPT_VAR_FILE = "var-file"
const OPT_MISSING_KEY_ACTION = "missing-key-action"
const OPT_MISSING_CONFIG_ACTION = "missing-config-action"

// The contents of a boilerplate.yml config file
type BoilerplateConfig struct {
Variables []variables.Variable
Expand Down Expand Up @@ -63,8 +56,8 @@ func (config *BoilerplateConfig) UnmarshalYAML(unmarshal func(interface{}) error
}

// Load the boilerplate.yml config contents for the folder specified in the given options
func LoadBoilerplateConfig(options *BoilerplateOptions) (*BoilerplateConfig, error) {
configPath := BoilerplateConfigPath(options.TemplateFolder)
func LoadBoilerplateConfig(opts *options.BoilerplateOptions) (*BoilerplateConfig, error) {
configPath := BoilerplateConfigPath(opts.TemplateFolder)

if util.PathExists(configPath) {
util.Logger.Printf("Loading boilerplate config from %s", configPath)
Expand All @@ -74,8 +67,8 @@ func LoadBoilerplateConfig(options *BoilerplateOptions) (*BoilerplateConfig, err
}

return ParseBoilerplateConfig(bytes)
} else if options.OnMissingConfig == Ignore {
util.Logger.Printf("Warning: boilerplate config file not found at %s. The %s flag is set, so ignoring. Note that no variables will be available while generating.", configPath, OPT_MISSING_CONFIG_ACTION)
} else if opts.OnMissingConfig == options.Ignore {
util.Logger.Printf("Warning: boilerplate config file not found at %s. The %s flag is set, so ignoring. Note that no variables will be available while generating.", configPath, options.OPT_MISSING_CONFIG_ACTION)
return &BoilerplateConfig{}, nil
} else {
return nil, errors.WithStackTrace(BoilerplateConfigNotFound(configPath))
Expand All @@ -102,5 +95,5 @@ func BoilerplateConfigPath(templateFolder string) string {

type BoilerplateConfigNotFound string
func (err BoilerplateConfigNotFound) Error() string {
return fmt.Sprintf("Could not find %s in %s and the %s flag is set to %s", BOILERPLATE_CONFIG_FILE, string(err), OPT_MISSING_CONFIG_ACTION, Exit)
return fmt.Sprintf("Could not find %s in %s and the %s flag is set to %s", BOILERPLATE_CONFIG_FILE, string(err), options.OPT_MISSING_CONFIG_ACTION, options.Exit)
}
31 changes: 5 additions & 26 deletions config/config_test.go
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/gruntwork-io/boilerplate/errors"
"path"
"github.com/gruntwork-io/boilerplate/variables"
"github.com/gruntwork-io/boilerplate/options"
)

func TestParseBoilerplateConfigEmpty(t *testing.T) {
Expand Down Expand Up @@ -181,28 +182,6 @@ func TestParseBoilerplateConfigOneVariableEnumWrongType(t *testing.T) {
assert.True(t, errors.IsError(err, variables.InvalidTypeForField{FieldName: "options", ExpectedType: "List", ActualType: reflect.TypeOf("string"), Context: "foo"}), "Expected a InvalidTypeForField error but got %s", reflect.TypeOf(errors.Unwrap(err)))
}

// YAML is whitespace sensitive, so we need to be careful that we don't introduce unnecessary indentation
const CONFIG_ONE_VARIABLE_ENUM_INVALID_DEFAULT =
`variables:
- name: foo
type: enum
options:
- foo
- bar
- baz
default: invalid
`

func TestParseBoilerplateConfigOneVariableEnumInvalidDefault(t *testing.T) {
t.Parallel()

_, err := ParseBoilerplateConfig([]byte(CONFIG_ONE_VARIABLE_ENUM_INVALID_DEFAULT))

assert.NotNil(t, err)
_, isInvalidVariableValueErr := errors.Unwrap(err).(variables.InvalidVariableValue)
assert.True(t, isInvalidVariableValueErr, "Expected a InvalidVariableValue error but got %s", reflect.TypeOf(errors.Unwrap(err)))
}

// YAML is whitespace sensitive, so we need to be careful that we don't introduce unnecessary indentation
const CONFIG_ONE_VARIABLE_OPTIONS_FOR_NON_ENUM =
`variables:
Expand Down Expand Up @@ -651,7 +630,7 @@ func TestParseBoilerplateConfigMultipleHooks(t *testing.T) {
func TestLoadBoilerplateConfigFullConfig(t *testing.T) {
t.Parallel()

actual, err := LoadBoilerplateConfig(&BoilerplateOptions{TemplateFolder: "../test-fixtures/config-test/full-config"})
actual, err := LoadBoilerplateConfig(&options.BoilerplateOptions{TemplateFolder: "../test-fixtures/config-test/full-config"})
expected := &BoilerplateConfig{
Variables: []variables.Variable{
variables.NewStringVariable("foo"),
Expand Down Expand Up @@ -684,7 +663,7 @@ func TestLoadBoilerplateConfigNoConfig(t *testing.T) {
t.Parallel()

templateFolder := "../test-fixtures/config-test/no-config"
_, err := LoadBoilerplateConfig(&BoilerplateOptions{TemplateFolder: templateFolder})
_, err := LoadBoilerplateConfig(&options.BoilerplateOptions{TemplateFolder: templateFolder})
expectedErr := BoilerplateConfigNotFound(path.Join(templateFolder, "boilerplate.yml"))

assert.True(t, errors.IsError(err, expectedErr), "Expected error %v but got %v", expectedErr, err)
Expand All @@ -694,7 +673,7 @@ func TestLoadBoilerplateConfigNoConfigIgnore(t *testing.T) {
t.Parallel()

templateFolder := "../test-fixtures/config-test/no-config"
actual, err := LoadBoilerplateConfig(&BoilerplateOptions{TemplateFolder: templateFolder, OnMissingConfig: Ignore})
actual, err := LoadBoilerplateConfig(&options.BoilerplateOptions{TemplateFolder: templateFolder, OnMissingConfig: options.Ignore})
expected := &BoilerplateConfig{}

assert.Nil(t, err, "Unexpected error: %v", err)
Expand All @@ -704,7 +683,7 @@ func TestLoadBoilerplateConfigNoConfigIgnore(t *testing.T) {
func TestLoadBoilerplateConfigInvalidConfig(t *testing.T) {
t.Parallel()

_, err := LoadBoilerplateConfig(&BoilerplateOptions{TemplateFolder: "../test-fixtures/config-test/invalid-config"})
_, err := LoadBoilerplateConfig(&options.BoilerplateOptions{TemplateFolder: "../test-fixtures/config-test/invalid-config"})

assert.NotNil(t, err)

Expand Down
64 changes: 40 additions & 24 deletions config/get_variables.go
Expand Up @@ -6,16 +6,18 @@ import (
"github.com/gruntwork-io/boilerplate/errors"
"github.com/gruntwork-io/boilerplate/variables"
"strings"
"github.com/gruntwork-io/boilerplate/options"
"github.com/gruntwork-io/boilerplate/render"
)

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.
func GetVariables(options *BoilerplateOptions, boilerplateConfig, rootBoilerplateConfig *BoilerplateConfig, thisDep variables.Dependency) (map[string]interface{}, error) {
func GetVariables(opts *options.BoilerplateOptions, boilerplateConfig, rootBoilerplateConfig *BoilerplateConfig, thisDep variables.Dependency) (map[string]interface{}, error) {
vars := map[string]interface{}{}
for key, value := range options.Vars {
for key, value := range opts.Vars {
vars[key] = value
}

Expand All @@ -38,51 +40,65 @@ func GetVariables(options *BoilerplateOptions, boilerplateConfig, rootBoilerplat
// Add a variable for "the boilerplate template currently being processed.
thisTemplateProps := map[string]interface{}{}
thisTemplateProps["Config"] = boilerplateConfig
thisTemplateProps["Options"] = options
thisTemplateProps["Options"] = opts
thisTemplateProps["CurrentDep"] = thisDep
vars["This"] = thisTemplateProps

variablesInConfig := getAllVariablesInConfig(boilerplateConfig)

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

// The reason we loop over variablesInConfig a second time is we want to load them all into our map so if they
// are referenced by another variable, we can find them, regardless of the order in which they were defined
for _, variable := range variablesInConfig {
rawValue := vars[variable.Name()]

renderedValue, err := render.RenderVariable(rawValue, vars, opts)
if err != nil {
return nil, err
}

renderedValueWithType, err := variables.ConvertType(renderedValue, variable)
if err != nil {
return nil, err
}

vars[variable.Name()] = renderedValueWithType
}

return vars, nil
}

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

value, alreadyExists := alreadyUnmarshalledVariables[variable.Name()]
value, alreadyExists := valuesForPreviousVariables[variable.Name()]
if alreadyExists {
return variables.UnmarshalValueForVariable(value, variable)
return value, nil
}

if variable.Reference() != "" {
value, alreadyExists := alreadyUnmarshalledVariables[variable.Reference()]
value, alreadyExists := valuesForPreviousVariables[variable.Reference()]
if alreadyExists {
return variables.UnmarshalValueForVariable(value, variable)
return value, nil
}

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)
return getValueForVariable(reference, variablesInConfig, valuesForPreviousVariables, opts, referenceDepth + 1)
}

value, err := getVariable(variable, options)
if err != nil {
return nil, err
}
return variables.UnmarshalValueForVariable(value, variable)
return getVariable(variable, opts)
}

// Get all the variables defined in the given config and its dependencies
Expand All @@ -104,25 +120,25 @@ func getAllVariablesInConfig(boilerplateConfig *BoilerplateConfig) map[string]va

// Get a value for the given variable. The value can come from the user (if the non-interactive option isn't set), the
// default value in the config, or a command line option.
func getVariable(variable variables.Variable, options *BoilerplateOptions) (interface{}, error) {
valueFromVars, valueSpecifiedInVars := getVariableFromVars(variable, options)
func getVariable(variable variables.Variable, opts *options.BoilerplateOptions) (interface{}, error) {
valueFromVars, valueSpecifiedInVars := getVariableFromVars(variable, opts)

if valueSpecifiedInVars {
util.Logger.Printf("Using value specified via command line options for variable '%s': %s", variable.FullName(), valueFromVars)
return valueFromVars, nil
} else if options.NonInteractive && variable.Default() != nil {
} else if opts.NonInteractive && variable.Default() != nil {
util.Logger.Printf("Using default value for variable '%s': %v", variable.FullName(), variable.Default())
return variable.Default(), nil
} else if options.NonInteractive {
} else if opts.NonInteractive {
return nil, errors.WithStackTrace(MissingVariableWithNonInteractiveMode(variable.FullName()))
} else {
return getVariableFromUser(variable, options)
return getVariableFromUser(variable, opts)
}
}

// Return the value of the given variable from vars passed in as command line options
func getVariableFromVars(variable variables.Variable, options *BoilerplateOptions) (interface{}, bool) {
for name, value := range options.Vars {
func getVariableFromVars(variable variables.Variable, opts *options.BoilerplateOptions) (interface{}, bool) {
for name, value := range opts.Vars {
if name == variable.Name() {
return value, true
}
Expand All @@ -132,7 +148,7 @@ func getVariableFromVars(variable variables.Variable, options *BoilerplateOption
}

// Get the value for the given variable by prompting the user
func getVariableFromUser(variable variables.Variable, options *BoilerplateOptions) (interface{}, error) {
func getVariableFromUser(variable variables.Variable, opts *options.BoilerplateOptions) (interface{}, error) {
util.BRIGHT_GREEN.Printf("\n%s\n", variable.FullName())
if variable.Description() != "" {
fmt.Printf(" %s\n", variable.Description())
Expand Down Expand Up @@ -168,7 +184,7 @@ 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)
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), options.OPT_VAR, options.OPT_NON_INTERACTIVE)
}

type MissingReference struct {
Expand Down