Skip to content
Open
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
28 changes: 24 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ The `boilerplate` binary supports the following options:
* `--non-interactive` (optional): Do not prompt for input variables. All variables must be set via `--var` and
`--var-file` options instead.
* `--var NAME=VALUE` (optional): Use `NAME=VALUE` to set variable `NAME` to `VALUE`. May be specified more than once.
* `--var-file FILE` (optional): Load variable values from the YAML file `FILE`. May be specified more than once.
* `--var-file FILE` (optional): Load variable values from the YAML or JSON file `FILE`. May be specified more than once.
* `--missing-key-action ACTION` (optional): What to do if a template looks up a variable that is not defined. Must
be one of: `invalid` (render the text "<no value>"), `zero` (render the zero value for the variable), or `error`
(return an error and exit immediately). Default: `error`.
Expand Down Expand Up @@ -206,6 +206,7 @@ Generate a project in ~/output from the templates in ~/templates, using variable

```
boilerplate --template-url ~/templates --output-folder ~/output --var-file vars.yml
boilerplate --template-url ~/templates --output-folder ~/output --var-file vars.json
```

Generate a project in ~/output from the templates in this repo's `include` example dir, using variables read from a file:
Expand Down Expand Up @@ -416,11 +417,12 @@ five ways to provide a value for a variable:
`--var foo='{key: "value"}' --var bar='["a", "b", "c"]'`. If you want to specify the value of a
variable for a specific dependency, use the `<DEPENDENCY_NAME>.<VARIABLE_NAME>` syntax. For example:
`boilerplate --var Description='Welcome to my home page!' --var about.Description='About Us' --var ShowLogo=false`.
1. `--var-file` option(s) you pass in when calling boilerplate. Example: `boilerplate --var-file vars.yml`. The vars
file must be a simple YAML file that defines key, value pairs, where the key is the name of a variable (or
1. `--var-file` option(s) you pass in when calling boilerplate. Example: `boilerplate --var-file vars.yml` or `boilerplate --var-file vars.json`. The vars
file can be either YAML or JSON format that defines key, value pairs, where the key is the name of a variable (or
`<DEPENDENCY_NAME>.<VARIABLE_NAME>` for a variable in a dependency) and the value is the value to set for that
variable. Example:
variable. Examples:

YAML format:
```yaml
Title: Boilerplate
ShowLogo: false
Expand All @@ -433,6 +435,24 @@ five ways to provide a value for a variable:
- value1
- value2
```

JSON format:
```json
{
"Title": "Boilerplate",
"ShowLogo": false,
"Description": "Welcome to my home page!",
"about.Description": "Welcome to my home page!",
"ExampleOfAMap": {
"key1": "value1",
"key2": "value2"
},
"ExampleOfAList": [
"value1",
"value2"
]
}
```
1. Manual input. If no value is specified via the `--var` or `--var-file` flags, Boilerplate will interactively prompt
the user to provide a value. Note that the `--non-interactive` flag disables this functionality.
1. Defaults defined in `boilerplate.yml`. The final fallback is the optional `default` that you can include as part of
Expand Down
3 changes: 2 additions & 1 deletion cli/boilerplate_cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Generate a project in ~/output from the templates in ~/templates, using variable
Generate a project in ~/output from the templates in ~/templates, using variables read from a file:

boilerplate --template-url ~/templates --output-folder ~/output --var-file vars.yml
boilerplate --template-url ~/templates --output-folder ~/output --var-file vars.json

Generate a project in ~/output from the templates in this repo's include example dir, using variables read from a file:

Expand Down Expand Up @@ -74,7 +75,7 @@ func CreateBoilerplateCli() *cli.App {
},
&cli.StringSliceFlag{
Name: options.OptVarFile,
Usage: "Load variable values from the YAML file `FILE`. May be specified more than once.",
Usage: "Load variable values from the YAML or JSON file `FILE`. May be specified more than once.",
},
&cli.StringFlag{
Name: options.OptMissingKeyAction,
Expand Down
82 changes: 82 additions & 0 deletions integration-tests/json_varfile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package integration_tests

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/gruntwork-io/boilerplate/cli"
"github.com/gruntwork-io/boilerplate/errors"
)

// Test JSON variable file support specifically
func TestJsonVarFileSupport(t *testing.T) {
t.Parallel()

// Create temporary directories for template and output
templateFolder, err := os.MkdirTemp("", "boilerplate-json-template")
require.NoError(t, err)
defer os.RemoveAll(templateFolder)

outputFolder, err := os.MkdirTemp("", "boilerplate-json-output")
require.NoError(t, err)
defer os.RemoveAll(outputFolder)

// Create a simple boilerplate.yml
boilerplateConfig := `variables:
- name: Name
type: string
- name: Active
type: bool`

err = os.WriteFile(filepath.Join(templateFolder, "boilerplate.yml"), []byte(boilerplateConfig), 0644)
require.NoError(t, err)

// Create a simple template file
templateContent := `Hello {{ .Name }}!
Your active status is: {{ .Active }}.`

err = os.WriteFile(filepath.Join(templateFolder, "greeting.txt"), []byte(templateContent), 0644)
require.NoError(t, err)

// Create JSON variable file
jsonVars := `{
"Name": "John Doe",
"Active": true
}`

varFile := filepath.Join(templateFolder, "vars.json")
err = os.WriteFile(varFile, []byte(jsonVars), 0644)
require.NoError(t, err)

// Run boilerplate with JSON variable file
app := cli.CreateBoilerplateCli()
args := []string{
"boilerplate",
"--template-url",
templateFolder,
"--output-folder",
outputFolder,
"--var-file",
varFile,
"--non-interactive",
"--missing-key-action",
"error",
}

err = app.Run(args)
require.NoError(t, err, errors.PrintErrorWithStackTrace(err))

// Verify the output
outputFile := filepath.Join(outputFolder, "greeting.txt")
content, err := os.ReadFile(outputFile)
require.NoError(t, err)

expectedContent := `Hello John Doe!
Your active status is: true.`

assert.Equal(t, expectedContent, string(content))
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"foo": "Hello"
"foo": "Hello from JSON"
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"foo: "Hello"
"foo: "Hello from JSON"
}
3 changes: 3 additions & 0 deletions test-fixtures/examples-var-files/json/vars.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"Foo": "Hello from JSON"
}
39 changes: 35 additions & 4 deletions variables/yaml_helpers.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package variables

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
Expand Down Expand Up @@ -490,14 +492,22 @@ func parseVariablesFromVarFiles(varFileList []string) (map[string]any, error) {
return vars, nil
}

// Parse the variables in the given YAML file into a map of variable name to variable value. Along the way, each value
// is parsed as YAML.
// Parse the variables in the given YAML or JSON file into a map of variable name to variable value.
// The parsing format is determined by the file extension (.json for JSON, everything else for YAML).
func ParseVariablesFromVarFile(varFilePath string) (map[string]any, error) {
bytes, err := os.ReadFile(varFilePath)
if err != nil {
return map[string]any{}, errors.WithStackTrace(err)
}
return parseVariablesFromVarFileContents(bytes)

// Determine format based on file extension
ext := strings.ToLower(filepath.Ext(varFilePath))
if ext == ".json" {
return parseVariablesFromJsonFileContents(bytes)
} else {
// Default to YAML for .yml, .yaml, or any other extension
return parseVariablesFromVarFileContents(bytes)
}
}

// Parse the variables in the given YAML contents into a map of variable name to variable value. Along the way, each
Expand All @@ -522,8 +532,29 @@ func parseVariablesFromVarFileContents(varFileContents []byte) (map[string]any,
return vars, nil
}

// Parse the variables in the given JSON contents into a map of variable name to variable value.
func parseVariablesFromJsonFileContents(jsonFileContents []byte) (map[string]any, error) {
vars := make(map[string]any)

if err := json.Unmarshal(jsonFileContents, &vars); err != nil {
return vars, errors.WithStackTrace(err)
}

converted, err := ConvertYAMLToStringMap(vars)
if err != nil {
return nil, errors.WithStackTrace(err)
}

vars, ok := converted.(map[string]any)
if !ok {
return nil, YAMLConversionErr{converted}
}

return vars, nil
}

// Parse variables passed in via command line options, either as a list of NAME=VALUE variable pairs in varsList, or a
// list of paths to YAML files that define NAME: VALUE pairs. Return a map of the NAME: VALUE pairs. Along the way,
// list of paths to YAML or JSON files that define NAME: VALUE pairs. Return a map of the NAME: VALUE pairs. Along the way,
// each VALUE is parsed as YAML.
func ParseVars(varsList []string, varFileList []string) (map[string]any, error) {
variables := map[string]any{}
Expand Down
35 changes: 35 additions & 0 deletions variables/yaml_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ key2: value2
key3: value3
`

const JSON_FILE_ONE_VAR = `{"key": "value"}`

const JSON_FILE_MULTIPLE_VARS = `{
"key1": "value1",
"key2": "value2",
"key3": "value3"
}`

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

Expand Down Expand Up @@ -49,6 +57,33 @@ func TestParseVariablesFromVarFileContents(t *testing.T) {
}
}

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

testCases := []struct {
fileContents string
expectJsonError bool
expectedVars map[string]interface{}
}{
{"{}", false, map[string]interface{}{}},
{JSON_FILE_ONE_VAR, false, map[string]interface{}{"key": "value"}},
{JSON_FILE_MULTIPLE_VARS, false, map[string]interface{}{"key1": "value1", "key2": "value2", "key3": "value3"}},
{"invalid json", true, map[string]interface{}{}},
}

for _, testCase := range testCases {
actualVars, err := parseVariablesFromJsonFileContents([]byte(testCase.fileContents))
if testCase.expectJsonError {
assert.NotNil(t, err)
} else {
assert.Nil(t, err, "Got unexpected error: %v", err)
assert.Equal(t, testCase.expectedVars, actualVars)
}
}
}



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

Expand Down