diff --git a/configs/parser_values.go b/configs/parser_values.go new file mode 100644 index 000000000..b7f1c1c5d --- /dev/null +++ b/configs/parser_values.go @@ -0,0 +1,43 @@ +package configs + +import ( + "github.com/hashicorp/hcl2/hcl" + "github.com/zclconf/go-cty/cty" +) + +// LoadValuesFile reads the file at the given path and parses it as a "values +// file", which is an HCL config file whose top-level attributes are treated +// as arbitrary key.value pairs. +// +// If the file cannot be read -- for example, if it does not exist -- then +// a nil map will be returned along with error diagnostics. Callers may wish +// to disregard the returned diagnostics in this case and instead generate +// their own error message(s) with additional context. +// +// If the returned diagnostics has errors when a non-nil map is returned +// then the map may be incomplete but should be valid enough for careful +// static analysis. +// +// This method wraps LoadHCLFile, and so it inherits the syntax selection +// behaviors documented for that method. +func (p *Parser) LoadValuesFile(path string) (map[string]cty.Value, hcl.Diagnostics) { + body, diags := p.LoadHCLFile(path) + if body == nil { + return nil, diags + } + + vals := make(map[string]cty.Value) + attrs, attrDiags := body.JustAttributes() + diags = append(diags, attrDiags...) + if attrs == nil { + return vals, diags + } + + for name, attr := range attrs { + val, valDiags := attr.Expr.Value(nil) + diags = append(diags, valDiags...) + vals[name] = val + } + + return vals, diags +} diff --git a/configs/parser_values_test.go b/configs/parser_values_test.go new file mode 100644 index 000000000..9c95b65f2 --- /dev/null +++ b/configs/parser_values_test.go @@ -0,0 +1,113 @@ +package configs + +import ( + "testing" + + "github.com/zclconf/go-cty/cty" +) + +func TestParserLoadValuesFile(t *testing.T) { + tests := map[string]struct { + Source string + Want map[string]cty.Value + DiagCount int + }{ + "empty.tfvars": { + "", + map[string]cty.Value{}, + 0, + }, + "empty.json": { + "{}", + map[string]cty.Value{}, + 0, + }, + "zerolen.json": { + "", + map[string]cty.Value{}, + 2, // syntax error and missing root object + }, + "one-number.tfvars": { + "foo = 1\n", + map[string]cty.Value{ + "foo": cty.NumberIntVal(1), + }, + 0, + }, + "one-number.tfvars.json": { + `{"foo": 1}`, + map[string]cty.Value{ + "foo": cty.NumberIntVal(1), + }, + 0, + }, + "two-bools.tfvars": { + "foo = true\nbar = false\n", + map[string]cty.Value{ + "foo": cty.True, + "bar": cty.False, + }, + 0, + }, + "two-bools.tfvars.json": { + `{"foo": true, "bar": false}`, + map[string]cty.Value{ + "foo": cty.True, + "bar": cty.False, + }, + 0, + }, + "invalid-syntax.tfvars": { + "foo bar baz\n", + map[string]cty.Value{}, + 1, // attribute or block definition required + }, + "block.tfvars": { + "foo = true\ninvalid {\n}\n", + map[string]cty.Value{ + "foo": cty.True, + }, + 1, // blocks are not allowed + }, + "variables.tfvars": { + "baz = true\nfoo = var.baz\n", + map[string]cty.Value{ + "baz": cty.True, + "foo": cty.DynamicVal, + }, + 1, // variables cannot be referenced here + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + p := testParser(map[string]string{ + name: test.Source, + }) + got, diags := p.LoadValuesFile(name) + if len(diags) != test.DiagCount { + t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount) + for _, diag := range diags { + t.Logf("- %s", diag) + } + } + + if len(got) != len(test.Want) { + t.Errorf("wrong number of result keys %d; want %d", len(got), len(test.Want)) + } + + for name, gotVal := range got { + wantVal := test.Want[name] + if wantVal == cty.NilVal { + t.Errorf("unexpected result key %q", name) + continue + } + + if !gotVal.RawEquals(wantVal) { + t.Errorf("wrong value for %q\ngot: %#v\nwant: %#v", name, gotVal, wantVal) + continue + } + } + }) + } +}