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:
parent
59bb5c2670
commit
bb724af7fd
@ -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
|
||||
|
@ -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"`
|
||||
|
@ -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 {}
|
||||
`,
|
||||
|
183
hcldec/spec.go
183
hcldec/spec.go
@ -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",
|
||||
}
|
||||
}
|
||||
|
@ -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 := `
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user