configs: Parser.LoadValuesFile

This method loads a "values file" -- also known as a "tfvars file" -- and
returns the values found inside.

A values file is an HCL file (in either native or JSON syntax) whose
top-level body is treated as a set of arbitrary key/value pairs whose
values may not depend on any variables or functions.

We will load values files through a configs.Parser -- even though values
files are not strictly-speaking part of configuration -- because this
causes them to be registered in our source code cache so that we can
generate source code snippets if we need to report any diagnostics.
This commit is contained in:
Martin Atkins 2018-02-01 19:12:56 -08:00
parent a0f4a313ef
commit 05e3b47ba1
2 changed files with 156 additions and 0 deletions

43
configs/parser_values.go Normal file
View File

@ -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
}

View File

@ -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
}
}
})
}
}