1568 lines
45 KiB
Go
1568 lines
45 KiB
Go
package hcldec
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/hashicorp/hcl2/hcl"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/convert"
|
|
"github.com/zclconf/go-cty/cty/function"
|
|
)
|
|
|
|
// A Spec is a description of how to decode a hcl.Body to a cty.Value.
|
|
//
|
|
// The various other types in this package whose names end in "Spec" are
|
|
// the spec implementations. The most common top-level spec is ObjectSpec,
|
|
// which decodes body content into a cty.Value of an object type.
|
|
type Spec interface {
|
|
// Perform the decode operation on the given body, in the context of
|
|
// the given block (which might be null), using the given eval context.
|
|
//
|
|
// "block" is provided only by the nested calls performed by the spec
|
|
// types that work on block bodies.
|
|
decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics)
|
|
|
|
// Return the cty.Type that should be returned when decoding a body with
|
|
// this spec.
|
|
impliedType() cty.Type
|
|
|
|
// Call the given callback once for each of the nested specs that would
|
|
// get decoded with the same body and block as the receiver. This should
|
|
// not descend into the nested specs used when decoding blocks.
|
|
visitSameBodyChildren(cb visitFunc)
|
|
|
|
// Determine the source range of the value that would be returned for the
|
|
// spec in the given content, in the context of the given block
|
|
// (which might be null). If the corresponding item is missing, return
|
|
// a place where it might be inserted.
|
|
sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range
|
|
}
|
|
|
|
type visitFunc func(spec Spec)
|
|
|
|
// An ObjectSpec is a Spec that produces a cty.Value of an object type whose
|
|
// attributes correspond to the keys of the spec map.
|
|
type ObjectSpec map[string]Spec
|
|
|
|
// attrSpec is implemented by specs that require attributes from the body.
|
|
type attrSpec interface {
|
|
attrSchemata() []hcl.AttributeSchema
|
|
}
|
|
|
|
// blockSpec is implemented by specs that require blocks from the body.
|
|
type blockSpec interface {
|
|
blockHeaderSchemata() []hcl.BlockHeaderSchema
|
|
nestedSpec() Spec
|
|
}
|
|
|
|
// specNeedingVariables is implemented by specs that can use variables
|
|
// from the EvalContext, to declare which variables they need.
|
|
type specNeedingVariables interface {
|
|
variablesNeeded(content *hcl.BodyContent) []hcl.Traversal
|
|
}
|
|
|
|
func (s ObjectSpec) visitSameBodyChildren(cb visitFunc) {
|
|
for _, c := range s {
|
|
cb(c)
|
|
}
|
|
}
|
|
|
|
func (s ObjectSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
vals := make(map[string]cty.Value, len(s))
|
|
var diags hcl.Diagnostics
|
|
|
|
for k, spec := range s {
|
|
var kd hcl.Diagnostics
|
|
vals[k], kd = spec.decode(content, blockLabels, ctx)
|
|
diags = append(diags, kd...)
|
|
}
|
|
|
|
return cty.ObjectVal(vals), diags
|
|
}
|
|
|
|
func (s ObjectSpec) impliedType() cty.Type {
|
|
if len(s) == 0 {
|
|
return cty.EmptyObject
|
|
}
|
|
|
|
attrTypes := make(map[string]cty.Type)
|
|
for k, childSpec := range s {
|
|
attrTypes[k] = childSpec.impliedType()
|
|
}
|
|
return cty.Object(attrTypes)
|
|
}
|
|
|
|
func (s ObjectSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
|
|
// This is not great, but the best we can do. In practice, it's rather
|
|
// strange to ask for the source range of an entire top-level body, since
|
|
// that's already readily available to the caller.
|
|
return content.MissingItemRange
|
|
}
|
|
|
|
// A TupleSpec is a Spec that produces a cty.Value of a tuple type whose
|
|
// elements correspond to the elements of the spec slice.
|
|
type TupleSpec []Spec
|
|
|
|
func (s TupleSpec) visitSameBodyChildren(cb visitFunc) {
|
|
for _, c := range s {
|
|
cb(c)
|
|
}
|
|
}
|
|
|
|
func (s TupleSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
vals := make([]cty.Value, len(s))
|
|
var diags hcl.Diagnostics
|
|
|
|
for i, spec := range s {
|
|
var ed hcl.Diagnostics
|
|
vals[i], ed = spec.decode(content, blockLabels, ctx)
|
|
diags = append(diags, ed...)
|
|
}
|
|
|
|
return cty.TupleVal(vals), diags
|
|
}
|
|
|
|
func (s TupleSpec) impliedType() cty.Type {
|
|
if len(s) == 0 {
|
|
return cty.EmptyTuple
|
|
}
|
|
|
|
attrTypes := make([]cty.Type, len(s))
|
|
for i, childSpec := range s {
|
|
attrTypes[i] = childSpec.impliedType()
|
|
}
|
|
return cty.Tuple(attrTypes)
|
|
}
|
|
|
|
func (s TupleSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
|
|
// This is not great, but the best we can do. In practice, it's rather
|
|
// strange to ask for the source range of an entire top-level body, since
|
|
// that's already readily available to the caller.
|
|
return content.MissingItemRange
|
|
}
|
|
|
|
// An AttrSpec is a Spec that evaluates a particular attribute expression in
|
|
// the body and returns its resulting value converted to the requested type,
|
|
// or produces a diagnostic if the type is incorrect.
|
|
type AttrSpec struct {
|
|
Name string
|
|
Type cty.Type
|
|
Required bool
|
|
}
|
|
|
|
func (s *AttrSpec) visitSameBodyChildren(cb visitFunc) {
|
|
// leaf node
|
|
}
|
|
|
|
// specNeedingVariables implementation
|
|
func (s *AttrSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal {
|
|
attr, exists := content.Attributes[s.Name]
|
|
if !exists {
|
|
return nil
|
|
}
|
|
|
|
return attr.Expr.Variables()
|
|
}
|
|
|
|
// attrSpec implementation
|
|
func (s *AttrSpec) attrSchemata() []hcl.AttributeSchema {
|
|
return []hcl.AttributeSchema{
|
|
{
|
|
Name: s.Name,
|
|
Required: s.Required,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (s *AttrSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
|
|
attr, exists := content.Attributes[s.Name]
|
|
if !exists {
|
|
return content.MissingItemRange
|
|
}
|
|
|
|
return attr.Expr.Range()
|
|
}
|
|
|
|
func (s *AttrSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
attr, exists := content.Attributes[s.Name]
|
|
if !exists {
|
|
// We don't need to check required and emit a diagnostic here, because
|
|
// that would already have happened when building "content".
|
|
return cty.NullVal(s.Type), nil
|
|
}
|
|
|
|
val, diags := attr.Expr.Value(ctx)
|
|
|
|
convVal, err := convert.Convert(val, s.Type)
|
|
if err != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Incorrect attribute value type",
|
|
Detail: fmt.Sprintf(
|
|
"Inappropriate value for attribute %q: %s.",
|
|
s.Name, err.Error(),
|
|
),
|
|
Subject: attr.Expr.StartRange().Ptr(),
|
|
Context: hcl.RangeBetween(attr.NameRange, attr.Expr.StartRange()).Ptr(),
|
|
})
|
|
// We'll return an unknown value of the _correct_ type so that the
|
|
// incomplete result can still be used for some analysis use-cases.
|
|
val = cty.UnknownVal(s.Type)
|
|
} else {
|
|
val = convVal
|
|
}
|
|
|
|
return val, diags
|
|
}
|
|
|
|
func (s *AttrSpec) impliedType() cty.Type {
|
|
return s.Type
|
|
}
|
|
|
|
// A LiteralSpec is a Spec that produces the given literal value, ignoring
|
|
// the given body.
|
|
type LiteralSpec struct {
|
|
Value cty.Value
|
|
}
|
|
|
|
func (s *LiteralSpec) visitSameBodyChildren(cb visitFunc) {
|
|
// leaf node
|
|
}
|
|
|
|
func (s *LiteralSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
return s.Value, nil
|
|
}
|
|
|
|
func (s *LiteralSpec) impliedType() cty.Type {
|
|
return s.Value.Type()
|
|
}
|
|
|
|
func (s *LiteralSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
|
|
// No sensible range to return for a literal, so the caller had better
|
|
// ensure it doesn't cause any diagnostics.
|
|
return hcl.Range{
|
|
Filename: "<unknown>",
|
|
}
|
|
}
|
|
|
|
// An ExprSpec is a Spec that evaluates the given expression, ignoring the
|
|
// given body.
|
|
type ExprSpec struct {
|
|
Expr hcl.Expression
|
|
}
|
|
|
|
func (s *ExprSpec) visitSameBodyChildren(cb visitFunc) {
|
|
// leaf node
|
|
}
|
|
|
|
// specNeedingVariables implementation
|
|
func (s *ExprSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal {
|
|
return s.Expr.Variables()
|
|
}
|
|
|
|
func (s *ExprSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
return s.Expr.Value(ctx)
|
|
}
|
|
|
|
func (s *ExprSpec) impliedType() cty.Type {
|
|
// We can't know the type of our expression until we evaluate it
|
|
return cty.DynamicPseudoType
|
|
}
|
|
|
|
func (s *ExprSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
|
|
return s.Expr.Range()
|
|
}
|
|
|
|
// A BlockSpec is a Spec that produces a cty.Value by decoding the contents
|
|
// of a single nested block of a given type, using a nested spec.
|
|
//
|
|
// If the Required flag is not set, the nested block may be omitted, in which
|
|
// case a null value is produced. If it _is_ set, an error diagnostic is
|
|
// produced if there are no nested blocks of the given type.
|
|
type BlockSpec struct {
|
|
TypeName string
|
|
Nested Spec
|
|
Required bool
|
|
}
|
|
|
|
func (s *BlockSpec) visitSameBodyChildren(cb visitFunc) {
|
|
// leaf node ("Nested" does not use the same body)
|
|
}
|
|
|
|
// blockSpec implementation
|
|
func (s *BlockSpec) blockHeaderSchemata() []hcl.BlockHeaderSchema {
|
|
return []hcl.BlockHeaderSchema{
|
|
{
|
|
Type: s.TypeName,
|
|
LabelNames: findLabelSpecs(s.Nested),
|
|
},
|
|
}
|
|
}
|
|
|
|
// blockSpec implementation
|
|
func (s *BlockSpec) nestedSpec() Spec {
|
|
return s.Nested
|
|
}
|
|
|
|
// specNeedingVariables implementation
|
|
func (s *BlockSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal {
|
|
var childBlock *hcl.Block
|
|
for _, candidate := range content.Blocks {
|
|
if candidate.Type != s.TypeName {
|
|
continue
|
|
}
|
|
|
|
childBlock = candidate
|
|
break
|
|
}
|
|
|
|
if childBlock == nil {
|
|
return nil
|
|
}
|
|
|
|
return Variables(childBlock.Body, s.Nested)
|
|
}
|
|
|
|
func (s *BlockSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
|
|
var childBlock *hcl.Block
|
|
for _, candidate := range content.Blocks {
|
|
if candidate.Type != s.TypeName {
|
|
continue
|
|
}
|
|
|
|
if childBlock != 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, childBlock.DefRange.String(),
|
|
),
|
|
Subject: &candidate.DefRange,
|
|
})
|
|
break
|
|
}
|
|
|
|
childBlock = candidate
|
|
}
|
|
|
|
if childBlock == 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(s.Nested.impliedType()), diags
|
|
}
|
|
|
|
if s.Nested == nil {
|
|
panic("BlockSpec with no Nested Spec")
|
|
}
|
|
val, _, childDiags := decode(childBlock.Body, labelsForBlock(childBlock), ctx, s.Nested, false)
|
|
diags = append(diags, childDiags...)
|
|
return val, diags
|
|
}
|
|
|
|
func (s *BlockSpec) impliedType() cty.Type {
|
|
return s.Nested.impliedType()
|
|
}
|
|
|
|
func (s *BlockSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.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 BlockListSpec is a Spec that produces a cty list of the results of
|
|
// decoding all of the nested blocks of a given type, using a nested spec.
|
|
type BlockListSpec struct {
|
|
TypeName string
|
|
Nested Spec
|
|
MinItems int
|
|
MaxItems int
|
|
}
|
|
|
|
func (s *BlockListSpec) visitSameBodyChildren(cb visitFunc) {
|
|
// leaf node ("Nested" does not use the same body)
|
|
}
|
|
|
|
// blockSpec implementation
|
|
func (s *BlockListSpec) blockHeaderSchemata() []hcl.BlockHeaderSchema {
|
|
return []hcl.BlockHeaderSchema{
|
|
{
|
|
Type: s.TypeName,
|
|
LabelNames: findLabelSpecs(s.Nested),
|
|
},
|
|
}
|
|
}
|
|
|
|
// blockSpec implementation
|
|
func (s *BlockListSpec) nestedSpec() Spec {
|
|
return s.Nested
|
|
}
|
|
|
|
// specNeedingVariables implementation
|
|
func (s *BlockListSpec) 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 *BlockListSpec) 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.ListValEmpty(s.Nested.impliedType())
|
|
} else {
|
|
// Since our target is a list, all of the decoded elements must have the
|
|
// same type or cty.ListVal will panic below. Different types can arise
|
|
// if there is an attribute spec of type cty.DynamicPseudoType in the
|
|
// nested spec; all given values must be convertable to a single type
|
|
// in order for the result to be considered valid.
|
|
etys := make([]cty.Type, len(elems))
|
|
for i, v := range elems {
|
|
etys[i] = v.Type()
|
|
}
|
|
ety, convs := convert.UnifyUnsafe(etys)
|
|
if ety == cty.NilType {
|
|
// FIXME: This is a pretty terrible error message.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("Unconsistent argument types in %s blocks", s.TypeName),
|
|
Detail: "Corresponding attributes in all blocks of this type must be the same.",
|
|
Subject: &sourceRanges[0],
|
|
})
|
|
return cty.DynamicVal, diags
|
|
}
|
|
for i, v := range elems {
|
|
if convs[i] != nil {
|
|
newV, err := convs[i](v)
|
|
if err != nil {
|
|
// FIXME: This is a pretty terrible error message.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("Unconsistent argument types in %s blocks", s.TypeName),
|
|
Detail: fmt.Sprintf("Block with index %d has inconsistent argument types: %s.", i, err),
|
|
Subject: &sourceRanges[i],
|
|
})
|
|
// Bail early here so we won't panic below in cty.ListVal
|
|
return cty.DynamicVal, diags
|
|
}
|
|
elems[i] = newV
|
|
}
|
|
}
|
|
|
|
ret = cty.ListVal(elems)
|
|
}
|
|
|
|
return ret, diags
|
|
}
|
|
|
|
func (s *BlockListSpec) impliedType() cty.Type {
|
|
return cty.List(s.Nested.impliedType())
|
|
}
|
|
|
|
func (s *BlockListSpec) 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 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 {
|
|
TypeName string
|
|
Nested Spec
|
|
MinItems int
|
|
MaxItems int
|
|
}
|
|
|
|
func (s *BlockSetSpec) visitSameBodyChildren(cb visitFunc) {
|
|
// leaf node ("Nested" does not use the same body)
|
|
}
|
|
|
|
// blockSpec implementation
|
|
func (s *BlockSetSpec) blockHeaderSchemata() []hcl.BlockHeaderSchema {
|
|
return []hcl.BlockHeaderSchema{
|
|
{
|
|
Type: s.TypeName,
|
|
LabelNames: findLabelSpecs(s.Nested),
|
|
},
|
|
}
|
|
}
|
|
|
|
// blockSpec implementation
|
|
func (s *BlockSetSpec) nestedSpec() Spec {
|
|
return s.Nested
|
|
}
|
|
|
|
// specNeedingVariables implementation
|
|
func (s *BlockSetSpec) 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 *BlockSetSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
|
|
if s.Nested == nil {
|
|
panic("BlockSetSpec 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.SetValEmpty(s.Nested.impliedType())
|
|
} else {
|
|
// Since our target is a set, all of the decoded elements must have the
|
|
// same type or cty.SetVal will panic below. Different types can arise
|
|
// if there is an attribute spec of type cty.DynamicPseudoType in the
|
|
// nested spec; all given values must be convertable to a single type
|
|
// in order for the result to be considered valid.
|
|
etys := make([]cty.Type, len(elems))
|
|
for i, v := range elems {
|
|
etys[i] = v.Type()
|
|
}
|
|
ety, convs := convert.UnifyUnsafe(etys)
|
|
if ety == cty.NilType {
|
|
// FIXME: This is a pretty terrible error message.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("Unconsistent argument types in %s blocks", s.TypeName),
|
|
Detail: "Corresponding attributes in all blocks of this type must be the same.",
|
|
Subject: &sourceRanges[0],
|
|
})
|
|
return cty.DynamicVal, diags
|
|
}
|
|
for i, v := range elems {
|
|
if convs[i] != nil {
|
|
newV, err := convs[i](v)
|
|
if err != nil {
|
|
// FIXME: This is a pretty terrible error message.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("Unconsistent argument types in %s blocks", s.TypeName),
|
|
Detail: fmt.Sprintf("Block with index %d has inconsistent argument types: %s.", i, err),
|
|
Subject: &sourceRanges[i],
|
|
})
|
|
// Bail early here so we won't panic below in cty.ListVal
|
|
return cty.DynamicVal, diags
|
|
}
|
|
elems[i] = newV
|
|
}
|
|
}
|
|
|
|
ret = cty.SetVal(elems)
|
|
}
|
|
|
|
return ret, diags
|
|
}
|
|
|
|
func (s *BlockSetSpec) impliedType() cty.Type {
|
|
return cty.Set(s.Nested.impliedType())
|
|
}
|
|
|
|
func (s *BlockSetSpec) 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 BlockMapSpec is a Spec that produces a cty map of the results of
|
|
// decoding all of the nested blocks of a given type, using a nested spec.
|
|
//
|
|
// One level of map structure is created for each of the given label names.
|
|
// There must be at least one given label name.
|
|
type BlockMapSpec struct {
|
|
TypeName string
|
|
LabelNames []string
|
|
Nested Spec
|
|
}
|
|
|
|
func (s *BlockMapSpec) visitSameBodyChildren(cb visitFunc) {
|
|
// leaf node ("Nested" does not use the same body)
|
|
}
|
|
|
|
// blockSpec implementation
|
|
func (s *BlockMapSpec) blockHeaderSchemata() []hcl.BlockHeaderSchema {
|
|
return []hcl.BlockHeaderSchema{
|
|
{
|
|
Type: s.TypeName,
|
|
LabelNames: append(s.LabelNames, findLabelSpecs(s.Nested)...),
|
|
},
|
|
}
|
|
}
|
|
|
|
// blockSpec implementation
|
|
func (s *BlockMapSpec) nestedSpec() Spec {
|
|
return s.Nested
|
|
}
|
|
|
|
// specNeedingVariables implementation
|
|
func (s *BlockMapSpec) 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 *BlockMapSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
|
|
if s.Nested == nil {
|
|
panic("BlockMapSpec with no Nested Spec")
|
|
}
|
|
if ImpliedType(s).HasDynamicTypes() {
|
|
panic("cty.DynamicPseudoType attributes may not be used inside a BlockMapSpec")
|
|
}
|
|
|
|
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.MapValEmpty(s.Nested.impliedType()), diags
|
|
}
|
|
|
|
var ctyMap func(map[string]interface{}, int) cty.Value
|
|
ctyMap = 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] = ctyMap(v.(map[string]interface{}), depth-1)
|
|
}
|
|
}
|
|
return cty.MapVal(vals)
|
|
}
|
|
|
|
return ctyMap(elems, len(s.LabelNames)), diags
|
|
}
|
|
|
|
func (s *BlockMapSpec) impliedType() cty.Type {
|
|
ret := s.Nested.impliedType()
|
|
for _ = range s.LabelNames {
|
|
ret = cty.Map(ret)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (s *BlockMapSpec) 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 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
|
|
// 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
|
|
// context, so this spec will panic in that case.
|
|
//
|
|
// This spec only works in the nested spec within a BlockSpec, BlockListSpec,
|
|
// BlockSetSpec or BlockMapSpec.
|
|
//
|
|
// The full set of label specs used against a particular block must have a
|
|
// consecutive set of indices starting at zero. The maximum index found
|
|
// defines how many labels the corresponding blocks must have in cty source.
|
|
type BlockLabelSpec struct {
|
|
Index int
|
|
Name string
|
|
}
|
|
|
|
func (s *BlockLabelSpec) visitSameBodyChildren(cb visitFunc) {
|
|
// leaf node
|
|
}
|
|
|
|
func (s *BlockLabelSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
if s.Index >= len(blockLabels) {
|
|
panic("BlockListSpec used in non-block context")
|
|
}
|
|
|
|
return cty.StringVal(blockLabels[s.Index].Value), nil
|
|
}
|
|
|
|
func (s *BlockLabelSpec) impliedType() cty.Type {
|
|
return cty.String // labels are always strings
|
|
}
|
|
|
|
func (s *BlockLabelSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
|
|
if s.Index >= len(blockLabels) {
|
|
panic("BlockListSpec used in non-block context")
|
|
}
|
|
|
|
return blockLabels[s.Index].Range
|
|
}
|
|
|
|
func findLabelSpecs(spec Spec) []string {
|
|
maxIdx := -1
|
|
var names map[int]string
|
|
|
|
var visit visitFunc
|
|
visit = func(s Spec) {
|
|
if ls, ok := s.(*BlockLabelSpec); ok {
|
|
if maxIdx < ls.Index {
|
|
maxIdx = ls.Index
|
|
}
|
|
if names == nil {
|
|
names = make(map[int]string)
|
|
}
|
|
names[ls.Index] = ls.Name
|
|
}
|
|
s.visitSameBodyChildren(visit)
|
|
}
|
|
|
|
visit(spec)
|
|
|
|
if maxIdx < 0 {
|
|
return nil // no labels at all
|
|
}
|
|
|
|
ret := make([]string, maxIdx+1)
|
|
for i := range ret {
|
|
name := names[i]
|
|
if name == "" {
|
|
// Should never happen if the spec is conformant, since we require
|
|
// consecutive indices starting at zero.
|
|
name = fmt.Sprintf("missing%02d", i)
|
|
}
|
|
ret[i] = name
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
// DefaultSpec is a spec that wraps two specs, evaluating the primary first
|
|
// and then evaluating the default if the primary returns a null value.
|
|
//
|
|
// 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
|
|
}
|
|
|
|
func (s *DefaultSpec) visitSameBodyChildren(cb visitFunc) {
|
|
cb(s.Primary)
|
|
cb(s.Default)
|
|
}
|
|
|
|
func (s *DefaultSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
val, diags := s.Primary.decode(content, blockLabels, ctx)
|
|
if val.IsNull() {
|
|
var moreDiags hcl.Diagnostics
|
|
val, moreDiags = s.Default.decode(content, blockLabels, ctx)
|
|
diags = append(diags, moreDiags...)
|
|
}
|
|
return val, diags
|
|
}
|
|
|
|
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
|
|
// choice because the default is often a literal spec that doesn't have a
|
|
// reasonable source range to return anyway.
|
|
return s.Primary.sourceRange(content, blockLabels)
|
|
}
|
|
|
|
// TransformExprSpec is a spec that wraps another and then evaluates a given
|
|
// hcl.Expression on the result.
|
|
//
|
|
// The implied type of this spec is determined by evaluating the expression
|
|
// with an unknown value of the nested spec's implied type, which may cause
|
|
// the result to be imprecise. This spec should not be used in situations where
|
|
// precise result type information is needed.
|
|
type TransformExprSpec struct {
|
|
Wrapped Spec
|
|
Expr hcl.Expression
|
|
TransformCtx *hcl.EvalContext
|
|
VarName string
|
|
}
|
|
|
|
func (s *TransformExprSpec) visitSameBodyChildren(cb visitFunc) {
|
|
cb(s.Wrapped)
|
|
}
|
|
|
|
func (s *TransformExprSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
wrappedVal, diags := s.Wrapped.decode(content, blockLabels, ctx)
|
|
if diags.HasErrors() {
|
|
// We won't try to run our function in this case, because it'll probably
|
|
// generate confusing additional errors that will distract from the
|
|
// root cause.
|
|
return cty.UnknownVal(s.impliedType()), diags
|
|
}
|
|
|
|
chiCtx := s.TransformCtx.NewChild()
|
|
chiCtx.Variables = map[string]cty.Value{
|
|
s.VarName: wrappedVal,
|
|
}
|
|
resultVal, resultDiags := s.Expr.Value(chiCtx)
|
|
diags = append(diags, resultDiags...)
|
|
return resultVal, diags
|
|
}
|
|
|
|
func (s *TransformExprSpec) impliedType() cty.Type {
|
|
wrappedTy := s.Wrapped.impliedType()
|
|
chiCtx := s.TransformCtx.NewChild()
|
|
chiCtx.Variables = map[string]cty.Value{
|
|
s.VarName: cty.UnknownVal(wrappedTy),
|
|
}
|
|
resultVal, _ := s.Expr.Value(chiCtx)
|
|
return resultVal.Type()
|
|
}
|
|
|
|
func (s *TransformExprSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
|
|
// We'll just pass through our wrapped range here, even though that's
|
|
// not super-accurate, because there's nothing better to return.
|
|
return s.Wrapped.sourceRange(content, blockLabels)
|
|
}
|
|
|
|
// TransformFuncSpec is a spec that wraps another and then evaluates a given
|
|
// cty function with the result. The given function must expect exactly one
|
|
// argument, where the result of the wrapped spec will be passed.
|
|
//
|
|
// The implied type of this spec is determined by type-checking the function
|
|
// with an unknown value of the nested spec's implied type, which may cause
|
|
// the result to be imprecise. This spec should not be used in situations where
|
|
// precise result type information is needed.
|
|
//
|
|
// If the given function produces an error when run, this spec will produce
|
|
// a non-user-actionable diagnostic message. It's the caller's responsibility
|
|
// to ensure that the given function cannot fail for any non-error result
|
|
// of the wrapped spec.
|
|
type TransformFuncSpec struct {
|
|
Wrapped Spec
|
|
Func function.Function
|
|
}
|
|
|
|
func (s *TransformFuncSpec) visitSameBodyChildren(cb visitFunc) {
|
|
cb(s.Wrapped)
|
|
}
|
|
|
|
func (s *TransformFuncSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
wrappedVal, diags := s.Wrapped.decode(content, blockLabels, ctx)
|
|
if diags.HasErrors() {
|
|
// We won't try to run our function in this case, because it'll probably
|
|
// generate confusing additional errors that will distract from the
|
|
// root cause.
|
|
return cty.UnknownVal(s.impliedType()), diags
|
|
}
|
|
|
|
resultVal, err := s.Func.Call([]cty.Value{wrappedVal})
|
|
if err != nil {
|
|
// This is not a good example of a diagnostic because it is reporting
|
|
// a programming error in the calling application, rather than something
|
|
// an end-user could act on.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Transform function failed",
|
|
Detail: fmt.Sprintf("Decoder transform returned an error: %s", err),
|
|
Subject: s.sourceRange(content, blockLabels).Ptr(),
|
|
})
|
|
return cty.UnknownVal(s.impliedType()), diags
|
|
}
|
|
|
|
return resultVal, diags
|
|
}
|
|
|
|
func (s *TransformFuncSpec) impliedType() cty.Type {
|
|
wrappedTy := s.Wrapped.impliedType()
|
|
resultTy, err := s.Func.ReturnType([]cty.Type{wrappedTy})
|
|
if err != nil {
|
|
// Should never happen with a correctly-configured spec
|
|
return cty.DynamicPseudoType
|
|
}
|
|
|
|
return resultTy
|
|
}
|
|
|
|
func (s *TransformFuncSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
|
|
// We'll just pass through our wrapped range here, even though that's
|
|
// 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",
|
|
}
|
|
}
|