summaryrefslogtreecommitdiff
path: root/hcldec
diff options
context:
space:
mode:
authorMartin Atkins <mart@degeneration.co.uk>2018-08-09 16:53:16 -0700
committerMartin Atkins <mart@degeneration.co.uk>2018-08-09 16:53:16 -0700
commitbb724af7fd64ce766b39cad5f147d09d93221546 (patch)
tree6b95224280481773ff921c1214fafdac1c7548c8 /hcldec
parent59bb5c26709127dde2b5dceeb12ce2d656fe58c0 (diff)
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.
Diffstat (limited to 'hcldec')
-rw-r--r--hcldec/public_test.go115
-rw-r--r--hcldec/spec.go183
-rw-r--r--hcldec/spec_test.go3
-rw-r--r--hcldec/variables_test.go33
4 files changed, 334 insertions, 0 deletions
diff --git a/hcldec/public_test.go b/hcldec/public_test.go
index 23406dc..05ed17a 100644
--- a/hcldec/public_test.go
+++ b/hcldec/public_test.go
@@ -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 {}
`,
diff --git a/hcldec/spec.go b/hcldec/spec.go
index 0d1288c..0bb3f3e 100644
--- a/hcldec/spec.go
+++ b/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",
+ }
+}
diff --git a/hcldec/spec_test.go b/hcldec/spec_test.go
index 90aa213..6a80a9d 100644
--- a/hcldec/spec_test.go
+++ b/hcldec/spec_test.go
@@ -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 := `
diff --git a/hcldec/variables_test.go b/hcldec/variables_test.go
index 5869e8d..5258ea4 100644
--- a/hcldec/variables_test.go
+++ b/hcldec/variables_test.go
@@ -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) {
@@ -120,6 +121,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
}
b {
a = bar