hcldec: BlockTupleSpec and BlockObjectSpec

When nested attributes are of type cty.DynamicPseudoType, a block spec
that is backed by a cty collection is annoying to use because it requires
all of the blocks to have homogenous types for such attributes.

These new specs are similar to BlockListSpec and BlockMapSpec
respectively, but permit each nested block result to have its own distinct
type.

In return for this flexibility, we lose the ability to predict the exact
type of the result: these specs must just indicate their type as being
cty.DynamicPseudoType themselves, since we need to know how many blocks
there are and what types are inside them before we can know our final
result type.
This commit is contained in:
Martin Atkins 2018-08-22 12:31:30 -07:00
parent b82170e941
commit ed8144cda1
2 changed files with 569 additions and 1 deletions

View File

@ -714,6 +714,309 @@ b "foo" {}
cty.MapValEmpty(cty.String),
1, // missing name
},
{
`
b {}
b {}
`,
&BlockTupleSpec{
TypeName: "b",
Nested: ObjectSpec{},
},
nil,
cty.TupleVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal}),
0,
},
{
``,
&BlockTupleSpec{
TypeName: "b",
Nested: ObjectSpec{},
},
nil,
cty.EmptyTupleVal,
0,
},
{
`
b "foo" {}
b "bar" {}
`,
&BlockTupleSpec{
TypeName: "b",
Nested: &BlockLabelSpec{
Name: "name",
Index: 0,
},
},
nil,
cty.TupleVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}),
0,
},
{
`
b {}
b {}
b {}
`,
&BlockTupleSpec{
TypeName: "b",
Nested: ObjectSpec{},
MaxItems: 2,
},
nil,
cty.TupleVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal, cty.EmptyObjectVal}),
1, // too many b blocks
},
{
`
b {}
b {}
`,
&BlockTupleSpec{
TypeName: "b",
Nested: ObjectSpec{},
MinItems: 10,
},
nil,
cty.TupleVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal}),
1, // insufficient b blocks
},
{
`
b {
a = true
}
b {
a = 1
}
`,
&BlockTupleSpec{
TypeName: "b",
Nested: &AttrSpec{
Name: "a",
Type: cty.DynamicPseudoType,
},
},
nil,
cty.TupleVal([]cty.Value{
cty.True,
cty.NumberIntVal(1),
}),
0,
},
{
`
b {
a = true
}
b {
a = "not a bool"
}
`,
&BlockTupleSpec{
TypeName: "b",
Nested: &AttrSpec{
Name: "a",
Type: cty.DynamicPseudoType,
},
},
nil,
cty.TupleVal([]cty.Value{
cty.True,
cty.StringVal("not a bool"),
}),
0,
},
{
`
b "foo" {}
b "bar" {}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"key"},
Nested: ObjectSpec{},
},
nil,
cty.ObjectVal(map[string]cty.Value{"foo": cty.EmptyObjectVal, "bar": cty.EmptyObjectVal}),
0,
},
{
`
b "foo" "bar" {}
b "bar" "baz" {}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"key1", "key2"},
Nested: ObjectSpec{},
},
nil,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"bar": cty.EmptyObjectVal,
}),
"bar": cty.ObjectVal(map[string]cty.Value{
"baz": cty.EmptyObjectVal,
}),
}),
0,
},
{
`
b "foo" "bar" {}
b "bar" "bar" {}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"key1", "key2"},
Nested: ObjectSpec{},
},
nil,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"bar": cty.EmptyObjectVal,
}),
"bar": cty.ObjectVal(map[string]cty.Value{
"bar": cty.EmptyObjectVal,
}),
}),
0,
},
{
`
b "foo" "bar" {}
b "foo" "baz" {}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"key1", "key2"},
Nested: ObjectSpec{},
},
nil,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"bar": cty.EmptyObjectVal,
"baz": cty.EmptyObjectVal,
}),
}),
0,
},
{
`
b "foo" "bar" {}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"key"},
Nested: ObjectSpec{},
},
nil,
cty.EmptyObjectVal,
1, // too many labels
},
{
`
b "bar" {}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"key1", "key2"},
Nested: ObjectSpec{},
},
nil,
cty.EmptyObjectVal,
1, // not enough labels
},
{
`
b "foo" {}
b "foo" {}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"key"},
Nested: ObjectSpec{},
},
nil,
cty.ObjectVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}),
1, // duplicate b block
},
{
`
b "foo" "bar" {}
b "foo" "bar" {}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"key1", "key2"},
Nested: ObjectSpec{},
},
nil,
cty.ObjectVal(map[string]cty.Value{"foo": cty.ObjectVal(map[string]cty.Value{"bar": cty.EmptyObjectVal})}),
1, // duplicate b block
},
{
`
b "foo" "bar" {}
b "bar" "baz" {}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"type"},
Nested: &BlockLabelSpec{
Name: "name",
Index: 0,
},
},
nil,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
"bar": cty.StringVal("baz"),
}),
0,
},
{
`
b "foo" {}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"type"},
Nested: &BlockLabelSpec{
Name: "name",
Index: 0,
},
},
nil,
cty.EmptyObjectVal,
1, // missing name
},
{
`
b "foo" {
arg = true
}
b "bar" {
arg = 1
}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"type"},
Nested: &AttrSpec{
Name: "arg",
Type: cty.DynamicPseudoType,
},
},
nil,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.True,
"bar": cty.NumberIntVal(1),
}),
0,
},
}
for i, test := range tests {

View File

@ -547,6 +547,127 @@ func (s *BlockListSpec) sourceRange(content *hcl.BodyContent, blockLabels []bloc
return sourceRange(childBlock.Body, labelsForBlock(childBlock), s.Nested)
}
// A BlockTupleSpec is a Spec that produces a cty tuple of the results of
// decoding all of the nested blocks of a given type, using a nested spec.
//
// This is similar to BlockListSpec, but it permits the nested blocks to have
// different result types in situations where cty.DynamicPseudoType attributes
// are present.
type BlockTupleSpec struct {
TypeName string
Nested Spec
MinItems int
MaxItems int
}
func (s *BlockTupleSpec) visitSameBodyChildren(cb visitFunc) {
// leaf node ("Nested" does not use the same body)
}
// blockSpec implementation
func (s *BlockTupleSpec) blockHeaderSchemata() []hcl.BlockHeaderSchema {
return []hcl.BlockHeaderSchema{
{
Type: s.TypeName,
LabelNames: findLabelSpecs(s.Nested),
},
}
}
// blockSpec implementation
func (s *BlockTupleSpec) nestedSpec() Spec {
return s.Nested
}
// specNeedingVariables implementation
func (s *BlockTupleSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal {
var ret []hcl.Traversal
for _, childBlock := range content.Blocks {
if childBlock.Type != s.TypeName {
continue
}
ret = append(ret, Variables(childBlock.Body, s.Nested)...)
}
return ret
}
func (s *BlockTupleSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
var diags hcl.Diagnostics
if s.Nested == nil {
panic("BlockListSpec with no Nested Spec")
}
var elems []cty.Value
var sourceRanges []hcl.Range
for _, childBlock := range content.Blocks {
if childBlock.Type != s.TypeName {
continue
}
val, _, childDiags := decode(childBlock.Body, labelsForBlock(childBlock), ctx, s.Nested, false)
diags = append(diags, childDiags...)
elems = append(elems, val)
sourceRanges = append(sourceRanges, sourceRange(childBlock.Body, labelsForBlock(childBlock), s.Nested))
}
if len(elems) < s.MinItems {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Insufficient %s blocks", s.TypeName),
Detail: fmt.Sprintf("At least %d %q blocks are required.", s.MinItems, s.TypeName),
Subject: &content.MissingItemRange,
})
} else if s.MaxItems > 0 && len(elems) > s.MaxItems {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Too many %s blocks", s.TypeName),
Detail: fmt.Sprintf("No more than %d %q blocks are allowed", s.MaxItems, s.TypeName),
Subject: &sourceRanges[s.MaxItems],
})
}
var ret cty.Value
if len(elems) == 0 {
ret = cty.EmptyTupleVal
} else {
ret = cty.TupleVal(elems)
}
return ret, diags
}
func (s *BlockTupleSpec) impliedType() cty.Type {
// We can't predict our type, because we don't know how many blocks
// there will be until we decode.
return cty.DynamicPseudoType
}
func (s *BlockTupleSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
// We return the source range of the _first_ block of the given type,
// since they are not guaranteed to form a contiguous range.
var childBlock *hcl.Block
for _, candidate := range content.Blocks {
if candidate.Type != s.TypeName {
continue
}
childBlock = candidate
break
}
if childBlock == nil {
return content.MissingItemRange
}
return sourceRange(childBlock.Body, labelsForBlock(childBlock), s.Nested)
}
// A BlockSetSpec is a Spec that produces a cty set of the results of
// decoding all of the nested blocks of a given type, using a nested spec.
type BlockSetSpec struct {
@ -749,7 +870,7 @@ func (s *BlockMapSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel
var diags hcl.Diagnostics
if s.Nested == nil {
panic("BlockSetSpec with no Nested Spec")
panic("BlockMapSpec with no Nested Spec")
}
if ImpliedType(s).HasDynamicTypes() {
panic("cty.DynamicPseudoType attributes may not be used inside a BlockMapSpec")
@ -845,6 +966,150 @@ func (s *BlockMapSpec) sourceRange(content *hcl.BodyContent, blockLabels []block
return sourceRange(childBlock.Body, labelsForBlock(childBlock), s.Nested)
}
// A BlockObjectSpec is a Spec that produces a cty object of the results of
// decoding all of the nested blocks of a given type, using a nested spec.
//
// One level of object structure is created for each of the given label names.
// There must be at least one given label name.
//
// This is similar to BlockMapSpec, but it permits the nested blocks to have
// different result types in situations where cty.DynamicPseudoType attributes
// are present.
type BlockObjectSpec struct {
TypeName string
LabelNames []string
Nested Spec
}
func (s *BlockObjectSpec) visitSameBodyChildren(cb visitFunc) {
// leaf node ("Nested" does not use the same body)
}
// blockSpec implementation
func (s *BlockObjectSpec) blockHeaderSchemata() []hcl.BlockHeaderSchema {
return []hcl.BlockHeaderSchema{
{
Type: s.TypeName,
LabelNames: append(s.LabelNames, findLabelSpecs(s.Nested)...),
},
}
}
// blockSpec implementation
func (s *BlockObjectSpec) nestedSpec() Spec {
return s.Nested
}
// specNeedingVariables implementation
func (s *BlockObjectSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal {
var ret []hcl.Traversal
for _, childBlock := range content.Blocks {
if childBlock.Type != s.TypeName {
continue
}
ret = append(ret, Variables(childBlock.Body, s.Nested)...)
}
return ret
}
func (s *BlockObjectSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
var diags hcl.Diagnostics
if s.Nested == nil {
panic("BlockObjectSpec with no Nested Spec")
}
elems := map[string]interface{}{}
for _, childBlock := range content.Blocks {
if childBlock.Type != s.TypeName {
continue
}
childLabels := labelsForBlock(childBlock)
val, _, childDiags := decode(childBlock.Body, childLabels[len(s.LabelNames):], ctx, s.Nested, false)
targetMap := elems
for _, key := range childBlock.Labels[:len(s.LabelNames)-1] {
if _, exists := targetMap[key]; !exists {
targetMap[key] = make(map[string]interface{})
}
targetMap = targetMap[key].(map[string]interface{})
}
diags = append(diags, childDiags...)
key := childBlock.Labels[len(s.LabelNames)-1]
if _, exists := targetMap[key]; exists {
labelsBuf := bytes.Buffer{}
for _, label := range childBlock.Labels {
fmt.Fprintf(&labelsBuf, " %q", label)
}
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Duplicate %s block", s.TypeName),
Detail: fmt.Sprintf(
"A block for %s%s was already defined. The %s labels must be unique.",
s.TypeName, labelsBuf.String(), s.TypeName,
),
Subject: &childBlock.DefRange,
})
continue
}
targetMap[key] = val
}
if len(elems) == 0 {
return cty.EmptyObjectVal, diags
}
var ctyObj func(map[string]interface{}, int) cty.Value
ctyObj = func(raw map[string]interface{}, depth int) cty.Value {
vals := make(map[string]cty.Value, len(raw))
if depth == 1 {
for k, v := range raw {
vals[k] = v.(cty.Value)
}
} else {
for k, v := range raw {
vals[k] = ctyObj(v.(map[string]interface{}), depth-1)
}
}
return cty.ObjectVal(vals)
}
return ctyObj(elems, len(s.LabelNames)), diags
}
func (s *BlockObjectSpec) impliedType() cty.Type {
// We can't predict our type, since we don't know how many blocks are
// present and what labels they have until we decode.
return cty.DynamicPseudoType
}
func (s *BlockObjectSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
// We return the source range of the _first_ block of the given type,
// since they are not guaranteed to form a contiguous range.
var childBlock *hcl.Block
for _, candidate := range content.Blocks {
if candidate.Type != s.TypeName {
continue
}
childBlock = candidate
break
}
if childBlock == nil {
return content.MissingItemRange
}
return sourceRange(childBlock.Body, labelsForBlock(childBlock), s.Nested)
}
// A BlockAttrsSpec is a Spec that interprets a single block as if it were
// a map of some element type. That is, each attribute within the block
// becomes a key in the resulting map and the attribute's value becomes the