hcldec: Fix DefaultSpec to allow attribute and block specs
Previously it was not implementing the two optional interfaces required for this, and so decoding would fail for any AttrSpec or block spec nested inside. Now it passes through attribute requirements from both the primary and default, and passes block requirements only from the primary, thus allowing either fallback between two attributes, fallback from an attribute to a constant, or fallback from a block to a constant. Other permutations are also possible, but not very important.
This commit is contained in:
parent
9db880accf
commit
bbbd0ef30d
@ -65,7 +65,10 @@ func ChildBlockTypes(spec Spec) map[string]Spec {
|
||||
visit = func(s Spec) {
|
||||
if bs, ok := s.(blockSpec); ok {
|
||||
for _, blockS := range bs.blockHeaderSchemata() {
|
||||
ret[blockS.Type] = bs.nestedSpec()
|
||||
nested := bs.nestedSpec()
|
||||
if nested != nil { // nil can be returned to dynamically opt out of this interface
|
||||
ret[blockS.Type] = nested
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -848,6 +848,16 @@ func findLabelSpecs(spec Spec) []string {
|
||||
//
|
||||
// The two specifications must have the same implied result type for correct
|
||||
// operation. If not, the result is undefined.
|
||||
//
|
||||
// Any requirements imposed by the "Default" spec apply even if "Primary" does
|
||||
// not return null. For example, if the "Default" spec is for a required
|
||||
// attribute then that attribute is always required, regardless of the result
|
||||
// of the "Primary" spec.
|
||||
//
|
||||
// The "Default" spec must not describe a nested block, since otherwise the
|
||||
// result of ChildBlockTypes would not be decidable without evaluation. If
|
||||
// the default spec _does_ describe a nested block then the result is
|
||||
// undefined.
|
||||
type DefaultSpec struct {
|
||||
Primary Spec
|
||||
Default Spec
|
||||
@ -872,6 +882,38 @@ func (s *DefaultSpec) impliedType() cty.Type {
|
||||
return s.Primary.impliedType()
|
||||
}
|
||||
|
||||
// attrSpec implementation
|
||||
func (s *DefaultSpec) attrSchemata() []hcl.AttributeSchema {
|
||||
// We must pass through the union of both of our nested specs so that
|
||||
// we'll have both values available in the result.
|
||||
var ret []hcl.AttributeSchema
|
||||
if as, ok := s.Primary.(attrSpec); ok {
|
||||
ret = append(ret, as.attrSchemata()...)
|
||||
}
|
||||
if as, ok := s.Default.(attrSpec); ok {
|
||||
ret = append(ret, as.attrSchemata()...)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// blockSpec implementation
|
||||
func (s *DefaultSpec) blockHeaderSchemata() []hcl.BlockHeaderSchema {
|
||||
// Only the primary spec may describe a block, since otherwise
|
||||
// our nestedSpec method below can't know which to return.
|
||||
if bs, ok := s.Primary.(blockSpec); ok {
|
||||
return bs.blockHeaderSchemata()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// blockSpec implementation
|
||||
func (s *DefaultSpec) nestedSpec() Spec {
|
||||
if bs, ok := s.Primary.(blockSpec); ok {
|
||||
return bs.nestedSpec()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DefaultSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
|
||||
// We can't tell from here which of the two specs will ultimately be used
|
||||
// in our result, so we'll just assume the first. This is usually the right
|
||||
|
@ -1,5 +1,16 @@
|
||||
package hcldec
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/apparentlymart/go-dump/dump"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
||||
)
|
||||
|
||||
// Verify that all of our spec types implement the necessary interfaces
|
||||
var _ Spec = ObjectSpec(nil)
|
||||
var _ Spec = TupleSpec(nil)
|
||||
@ -16,8 +27,112 @@ var _ Spec = (*TransformExprSpec)(nil)
|
||||
var _ Spec = (*TransformFuncSpec)(nil)
|
||||
|
||||
var _ attrSpec = (*AttrSpec)(nil)
|
||||
var _ attrSpec = (*DefaultSpec)(nil)
|
||||
|
||||
var _ blockSpec = (*BlockSpec)(nil)
|
||||
var _ blockSpec = (*BlockListSpec)(nil)
|
||||
var _ blockSpec = (*BlockSetSpec)(nil)
|
||||
var _ blockSpec = (*BlockMapSpec)(nil)
|
||||
var _ blockSpec = (*DefaultSpec)(nil)
|
||||
|
||||
var _ specNeedingVariables = (*AttrSpec)(nil)
|
||||
var _ specNeedingVariables = (*BlockSpec)(nil)
|
||||
var _ specNeedingVariables = (*BlockListSpec)(nil)
|
||||
var _ specNeedingVariables = (*BlockSetSpec)(nil)
|
||||
var _ specNeedingVariables = (*BlockMapSpec)(nil)
|
||||
|
||||
func TestDefaultSpec(t *testing.T) {
|
||||
config := `
|
||||
foo = fooval
|
||||
bar = barval
|
||||
`
|
||||
f, diags := hclsyntax.ParseConfig([]byte(config), "", hcl.Pos{Line: 1, Column: 1})
|
||||
if diags.HasErrors() {
|
||||
t.Fatal(diags.Error())
|
||||
}
|
||||
|
||||
t.Run("primary set", func(t *testing.T) {
|
||||
spec := &DefaultSpec{
|
||||
Primary: &AttrSpec{
|
||||
Name: "foo",
|
||||
Type: cty.String,
|
||||
},
|
||||
Default: &AttrSpec{
|
||||
Name: "bar",
|
||||
Type: cty.String,
|
||||
},
|
||||
}
|
||||
|
||||
gotVars := Variables(f.Body, spec)
|
||||
wantVars := []hcl.Traversal{
|
||||
{
|
||||
hcl.TraverseRoot{
|
||||
Name: "fooval",
|
||||
SrcRange: hcl.Range{
|
||||
Filename: "",
|
||||
Start: hcl.Pos{Line: 2, Column: 7, Byte: 7},
|
||||
End: hcl.Pos{Line: 2, Column: 13, Byte: 13},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
hcl.TraverseRoot{
|
||||
Name: "barval",
|
||||
SrcRange: hcl.Range{
|
||||
Filename: "",
|
||||
Start: hcl.Pos{Line: 3, Column: 7, Byte: 20},
|
||||
End: hcl.Pos{Line: 3, Column: 13, Byte: 26},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(gotVars, wantVars) {
|
||||
t.Errorf("wrong Variables result\ngot: %s\nwant: %s", dump.Value(gotVars), dump.Value(wantVars))
|
||||
}
|
||||
|
||||
ctx := &hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"fooval": cty.StringVal("foo value"),
|
||||
"barval": cty.StringVal("bar value"),
|
||||
},
|
||||
}
|
||||
|
||||
got, err := Decode(f.Body, spec, ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := cty.StringVal("foo value")
|
||||
if !got.RawEquals(want) {
|
||||
t.Errorf("wrong Decode result\ngot: %#v\nwant: %#v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("primary not set", func(t *testing.T) {
|
||||
spec := &DefaultSpec{
|
||||
Primary: &AttrSpec{
|
||||
Name: "foo",
|
||||
Type: cty.String,
|
||||
},
|
||||
Default: &AttrSpec{
|
||||
Name: "bar",
|
||||
Type: cty.String,
|
||||
},
|
||||
}
|
||||
|
||||
ctx := &hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"fooval": cty.NullVal(cty.String),
|
||||
"barval": cty.StringVal("bar value"),
|
||||
},
|
||||
}
|
||||
|
||||
got, err := Decode(f.Body, spec, ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := cty.StringVal("bar value")
|
||||
if !got.RawEquals(want) {
|
||||
t.Errorf("wrong Decode result\ngot: %#v\nwant: %#v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user