From 7dc23373498d642bdfdee7170125556681d01bb5 Mon Sep 17 00:00:00 2001 From: Ross Strickland Date: Thu, 31 Jul 2025 13:54:29 -0600 Subject: [PATCH] Allow json var files Co-authored-by: rosstr-clickup --- README.md | 28 ++++++- cli/boilerplate_cli.go | 3 +- integration-tests/json_varfile_test.go | 82 +++++++++++++++++++ .../json/example1.json | 2 +- .../json/example2.json | 2 +- .../examples-var-files/json/vars.json | 3 + variables/yaml_helpers.go | 39 ++++++++- variables/yaml_helpers_test.go | 35 ++++++++ 8 files changed, 183 insertions(+), 11 deletions(-) create mode 100644 integration-tests/json_varfile_test.go create mode 100644 test-fixtures/examples-var-files/json/vars.json diff --git a/README.md b/README.md index b061f6a1..04417aa6 100644 --- a/README.md +++ b/README.md @@ -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 ""), `zero` (render the zero value for the variable), or `error` (return an error and exit immediately). Default: `error`. @@ -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: @@ -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 `.` 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 `.` 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 @@ -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 diff --git a/cli/boilerplate_cli.go b/cli/boilerplate_cli.go index 69ca50b7..7c10e975 100644 --- a/cli/boilerplate_cli.go +++ b/cli/boilerplate_cli.go @@ -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: @@ -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, diff --git a/integration-tests/json_varfile_test.go b/integration-tests/json_varfile_test.go new file mode 100644 index 00000000..929fafd8 --- /dev/null +++ b/integration-tests/json_varfile_test.go @@ -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)) +} \ No newline at end of file diff --git a/test-fixtures/examples-expected-output/json/example1.json b/test-fixtures/examples-expected-output/json/example1.json index 3108d66b..24865f7d 100644 --- a/test-fixtures/examples-expected-output/json/example1.json +++ b/test-fixtures/examples-expected-output/json/example1.json @@ -1,3 +1,3 @@ { - "foo": "Hello" + "foo": "Hello from JSON" } diff --git a/test-fixtures/examples-expected-output/json/example2.json b/test-fixtures/examples-expected-output/json/example2.json index 6bd805cf..55b7de88 100644 --- a/test-fixtures/examples-expected-output/json/example2.json +++ b/test-fixtures/examples-expected-output/json/example2.json @@ -1,3 +1,3 @@ { - "foo: "Hello" + "foo: "Hello from JSON" } diff --git a/test-fixtures/examples-var-files/json/vars.json b/test-fixtures/examples-var-files/json/vars.json new file mode 100644 index 00000000..1b1a2833 --- /dev/null +++ b/test-fixtures/examples-var-files/json/vars.json @@ -0,0 +1,3 @@ +{ + "Foo": "Hello from JSON" +} \ No newline at end of file diff --git a/variables/yaml_helpers.go b/variables/yaml_helpers.go index aea58340..994b1689 100644 --- a/variables/yaml_helpers.go +++ b/variables/yaml_helpers.go @@ -1,8 +1,10 @@ package variables import ( + "encoding/json" "fmt" "os" + "path/filepath" "reflect" "strconv" "strings" @@ -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 @@ -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{} diff --git a/variables/yaml_helpers_test.go b/variables/yaml_helpers_test.go index d49b4cf2..1a0ed6b7 100644 --- a/variables/yaml_helpers_test.go +++ b/variables/yaml_helpers_test.go @@ -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() @@ -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()