hcldec: BlockAttrsSpec spec type

This is the hcldec interface to Body.JustAttributes, producing a map whose
keys are the child attribute names and whose values are the results of
evaluating those expressions.

We can't just expose a JustAttributes-style spec directly here because
it's not really compatible with how hcldec thinks about things, but we
can expose a spec that decodes a specific child block because that can
then compose properly with other specs at the same level without
interfering with their operation.

The primary use for this is to allow the use of the block syntax to define
a map:

    dynamic_stuff {
      foo = "bar"
    }

JustAttributes is normally used in static analysis situations such as
enumerating the contents of a block to decide what to include in the
final EvalContext. That's not really possible with the hcldec model
because both structural decoding and expression evaluation happen
together. Therefore the use of this is pretty limited: it's useful if you
want to be compatible with an existing format based on legacy HCL where a
map was conventionally defined using block syntax, relying on the fact
that HCL did not make a strong distinction between attribute and block
syntax.
This commit is contained in:
Martin Atkins 2018-08-09 16:53:16 -07:00
parent 59bb5c2670
commit bb724af7fd
6 changed files with 412 additions and 0 deletions

View File

@ -258,6 +258,40 @@ of the given type must have.
`block` expects a single nested spec block, which is applied to the body of
each matching block to produce the resulting map items.
## `block_attrs` spec blocks
The `block_attrs` spec type is similar to an `attr` spec block of a map type,
but it produces a map from the attributes of a block rather than from an
attribute's expression.
```hcl
block_attrs {
block_type = "variables"
element_type = string
required = false
}
```
This allows a map with user-defined keys to be produced within block syntax,
but due to the constraints of that syntax it also means that the user will
be unable to dynamically-generate either individual key names using key
expressions or the entire map value using a `for` expression.
`block_attrs` spec blocks accept the following arguments:
* `block_type` (required) - The block type name to expect within the HCL
input file. This may be omitted when a default name selector is created
by a parent `object` spec, if the input block type name should match the
output JSON object property name.
* `element_type` (required) - The value type to require for each of the
attributes within a matched block. The resulting value will be a JSON
object whose property values are of this type.
* `required` (optional) - If `true`, an error will be produced if a block
of the given type is not present. If `false` -- the default -- an absent
block will be indicated by producing `null`.
## `literal` spec blocks
The `literal` spec type returns a given literal value, and creates no

View File

@ -135,6 +135,9 @@ func decodeSpecBlock(block *hcl.Block) (hcldec.Spec, hcl.Diagnostics) {
case "block_map":
return decodeBlockMapSpec(block.Body, impliedName)
case "block_attrs":
return decodeBlockAttrsSpec(block.Body, impliedName)
case "default":
return decodeDefaultSpec(block.Body)
@ -429,6 +432,47 @@ func decodeBlockNestedSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
return spec, diags
}
func decodeBlockAttrsSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) {
type content struct {
TypeName *string `hcl:"block_type"`
ElementType hcl.Expression `hcl:"element_type"`
Required *bool `hcl:"required"`
}
var args content
diags := gohcl.DecodeBody(body, nil, &args)
if diags.HasErrors() {
return errSpec, diags
}
spec := &hcldec.BlockAttrsSpec{
TypeName: impliedName,
}
if args.Required != nil {
spec.Required = *args.Required
}
if args.TypeName != nil {
spec.TypeName = *args.TypeName
}
var typeDiags hcl.Diagnostics
spec.ElementType, typeDiags = evalTypeExpr(args.ElementType)
diags = append(diags, typeDiags...)
if spec.TypeName == "" {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing block_type in block_attrs spec",
Detail: "The block_type attribute is required, to specify the block type name that is expected in an input HCL file.",
Subject: body.MissingItemRange().Ptr(),
})
return errSpec, diags
}
return spec, diags
}
func decodeLiteralSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
type content struct {
Value cty.Value `hcl:"value"`

View File

@ -243,6 +243,121 @@ b {}
},
{
`
b {
}
`,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
},
nil,
cty.MapValEmpty(cty.String),
0,
},
{
`
b {
hello = "world"
}
`,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
},
nil,
cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
}),
0,
},
{
`
b {
hello = true
}
`,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
},
nil,
cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("true"),
}),
0,
},
{
`
b {
hello = true
goodbye = 5
}
`,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
},
nil,
cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("true"),
"goodbye": cty.StringVal("5"),
}),
0,
},
{
``,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
},
nil,
cty.NullVal(cty.Map(cty.String)),
0,
},
{
``,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
Required: true,
},
nil,
cty.NullVal(cty.Map(cty.String)),
1, // missing b block
},
{
`
b {
}
b {
}
`,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
},
nil,
cty.MapValEmpty(cty.String),
1, // duplicate b block
},
{
`
b {
}
b {
}
`,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
Required: true,
},
nil,
cty.MapValEmpty(cty.String),
1, // duplicate b block
},
{
`
b {}
b {}
`,

View File

@ -3,6 +3,7 @@ package hcldec
import (
"bytes"
"fmt"
"sort"
"github.com/hashicorp/hcl2/hcl"
"github.com/zclconf/go-cty/cty"
@ -765,6 +766,163 @@ func (s *BlockMapSpec) sourceRange(content *hcl.BodyContent, blockLabels []block
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
// element value, after conversion to the given element type. The resulting
// value is a cty.Map of the given element type.
//
// This spec imposes a validation constraint that there be exactly one block
// of the given type name and that this block may contain only attributes. The
// block does not accept any labels.
//
// This is an alternative to an AttrSpec of a map type for situations where
// block syntax is desired. Note that block syntax does not permit dynamic
// keys, construction of the result via a "for" expression, etc. In most cases
// an AttrSpec is preferred if the desired result is a map whose keys are
// chosen by the user rather than by schema.
type BlockAttrsSpec struct {
TypeName string
ElementType cty.Type
Required bool
}
func (s *BlockAttrsSpec) visitSameBodyChildren(cb visitFunc) {
// leaf node
}
// blockSpec implementation
func (s *BlockAttrsSpec) blockHeaderSchemata() []hcl.BlockHeaderSchema {
return []hcl.BlockHeaderSchema{
{
Type: s.TypeName,
LabelNames: nil,
},
}
}
// blockSpec implementation
func (s *BlockAttrsSpec) nestedSpec() Spec {
// This is an odd case: we aren't actually going to apply a nested spec
// in this case, since we're going to interpret the body directly as
// attributes, but we need to return something non-nil so that the
// decoder will recognize this as a block spec. We won't actually be
// using this for anything at decode time.
return noopSpec{}
}
// specNeedingVariables implementation
func (s *BlockAttrsSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal {
block, _ := s.findBlock(content)
if block == nil {
return nil
}
var vars []hcl.Traversal
attrs, diags := block.Body.JustAttributes()
if diags.HasErrors() {
return nil
}
for _, attr := range attrs {
vars = append(vars, attr.Expr.Variables()...)
}
// We'll return the variables references in source order so that any
// error messages that result are also in source order.
sort.Slice(vars, func(i, j int) bool {
return vars[i].SourceRange().Start.Byte < vars[j].SourceRange().Start.Byte
})
return vars
}
func (s *BlockAttrsSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
var diags hcl.Diagnostics
block, other := s.findBlock(content)
if block == nil {
if s.Required {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Missing %s block", s.TypeName),
Detail: fmt.Sprintf(
"A block of type %q is required here.", s.TypeName,
),
Subject: &content.MissingItemRange,
})
}
return cty.NullVal(cty.Map(s.ElementType)), diags
}
if other != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Duplicate %s block", s.TypeName),
Detail: fmt.Sprintf(
"Only one block of type %q is allowed. Previous definition was at %s.",
s.TypeName, block.DefRange.String(),
),
Subject: &other.DefRange,
})
}
attrs, attrDiags := block.Body.JustAttributes()
diags = append(diags, attrDiags...)
if len(attrs) == 0 {
return cty.MapValEmpty(s.ElementType), diags
}
vals := make(map[string]cty.Value, len(attrs))
for name, attr := range attrs {
attrVal, attrDiags := attr.Expr.Value(ctx)
diags = append(diags, attrDiags...)
attrVal, err := convert.Convert(attrVal, s.ElementType)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid attribute value",
Detail: fmt.Sprintf("Invalid value for attribute of %q block: %s.", s.TypeName, err),
Subject: attr.Expr.Range().Ptr(),
})
attrVal = cty.UnknownVal(s.ElementType)
}
vals[name] = attrVal
}
return cty.MapVal(vals), diags
}
func (s *BlockAttrsSpec) impliedType() cty.Type {
return cty.Map(s.ElementType)
}
func (s *BlockAttrsSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
block, _ := s.findBlock(content)
if block == nil {
return content.MissingItemRange
}
return block.DefRange
}
func (s *BlockAttrsSpec) findBlock(content *hcl.BodyContent) (block *hcl.Block, other *hcl.Block) {
for _, candidate := range content.Blocks {
if candidate.Type != s.TypeName {
continue
}
if block != nil {
return block, candidate
}
block = candidate
}
return block, nil
}
// A BlockLabelSpec is a Spec that returns a cty.String representing the
// label of the block its given body belongs to, if indeed its given body
// belongs to a block. It is a programming error to use this in a non-block
@ -1038,3 +1196,28 @@ func (s *TransformFuncSpec) sourceRange(content *hcl.BodyContent, blockLabels []
// not super-accurate, because there's nothing better to return.
return s.Wrapped.sourceRange(content, blockLabels)
}
// noopSpec is a placeholder spec that does nothing, used in situations where
// a non-nil placeholder spec is required. It is not exported because there is
// no reason to use it directly; it is always an implementation detail only.
type noopSpec struct {
}
func (s noopSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
return cty.NullVal(cty.DynamicPseudoType), nil
}
func (s noopSpec) impliedType() cty.Type {
return cty.DynamicPseudoType
}
func (s noopSpec) visitSameBodyChildren(cb visitFunc) {
// nothing to do
}
func (s noopSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
// No useful range for a noopSpec, and nobody should be calling this anyway.
return hcl.Range{
Filename: "noopSpec",
}
}

View File

@ -21,6 +21,7 @@ var _ Spec = (*BlockSpec)(nil)
var _ Spec = (*BlockListSpec)(nil)
var _ Spec = (*BlockSetSpec)(nil)
var _ Spec = (*BlockMapSpec)(nil)
var _ Spec = (*BlockAttrsSpec)(nil)
var _ Spec = (*BlockLabelSpec)(nil)
var _ Spec = (*DefaultSpec)(nil)
var _ Spec = (*TransformExprSpec)(nil)
@ -33,6 +34,7 @@ var _ blockSpec = (*BlockSpec)(nil)
var _ blockSpec = (*BlockListSpec)(nil)
var _ blockSpec = (*BlockSetSpec)(nil)
var _ blockSpec = (*BlockMapSpec)(nil)
var _ blockSpec = (*BlockAttrsSpec)(nil)
var _ blockSpec = (*DefaultSpec)(nil)
var _ specNeedingVariables = (*AttrSpec)(nil)
@ -40,6 +42,7 @@ var _ specNeedingVariables = (*BlockSpec)(nil)
var _ specNeedingVariables = (*BlockListSpec)(nil)
var _ specNeedingVariables = (*BlockSetSpec)(nil)
var _ specNeedingVariables = (*BlockMapSpec)(nil)
var _ specNeedingVariables = (*BlockAttrsSpec)(nil)
func TestDefaultSpec(t *testing.T) {
config := `

View File

@ -7,6 +7,7 @@ import (
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
func TestVariables(t *testing.T) {
@ -118,6 +119,38 @@ b {
},
{
`
b {
a = foo
b = bar
}
`,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
},
[]hcl.Traversal{
{
hcl.TraverseRoot{
Name: "foo",
SrcRange: hcl.Range{
Start: hcl.Pos{Line: 3, Column: 7, Byte: 11},
End: hcl.Pos{Line: 3, Column: 10, Byte: 14},
},
},
},
{
hcl.TraverseRoot{
Name: "bar",
SrcRange: hcl.Range{
Start: hcl.Pos{Line: 4, Column: 7, Byte: 21},
End: hcl.Pos{Line: 4, Column: 10, Byte: 24},
},
},
},
},
},
{
`
b {
a = foo
}