Skip to content

Commit

Permalink
hcldec: Fix DefaultSpec to allow attribute and block specs
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
apparentlymart committed May 22, 2018
1 parent 9db880a commit bbbd0ef
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 1 deletion.
5 changes: 4 additions & 1 deletion hcldec/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}

Expand Down
42 changes: 42 additions & 0 deletions hcldec/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
115 changes: 115 additions & 0 deletions hcldec/spec_test.go
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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)
}
})
}

0 comments on commit bbbd0ef

Please sign in to comment.