| package hcldec |
| |
| import ( |
| "bytes" |
| "fmt" |
| "sort" |
| |
| "github.com/hashicorp/hcl/v2" |
| "github.com/hashicorp/hcl/v2/ext/customdecode" |
| "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 |
| } |
| |
| // UnknownBody can be optionally implemented by an hcl.Body instance which may |
| // be entirely unknown. |
| type UnknownBody interface { |
| Unknown() bool |
| } |
| |
| 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 |
| } |
| |
| if decodeFn := customdecode.CustomExpressionDecoderForType(s.Type); decodeFn != nil { |
| v, diags := decodeFn(attr.Expr, ctx) |
| if v == cty.NilVal { |
| v = cty.UnknownVal(s.Type) |
| } |
| return v, diags |
| } |
| |
| 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.Range().Ptr(), |
| Context: hcl.RangeBetween(attr.NameRange, attr.Expr.Range()).Ptr(), |
| Expression: attr.Expr, |
| EvalContext: ctx, |
| }) |
| // 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...) |
| |
| if u, ok := childBlock.Body.(UnknownBody); ok { |
| if u.Unknown() { |
| // If any block Body is unknown, then the entire block value |
| // must be unknown |
| return cty.UnknownVal(s.impliedType()), diags |
| } |
| } |
| |
| 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], |
| }) |
| } |
| |
| if len(elems) == 0 { |
| return cty.ListValEmpty(s.Nested.impliedType()), diags |
| } |
| |
| // 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 |
| } |
| } |
| |
| return cty.ListVal(elems), 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...) |
| |
| if u, ok := childBlock.Body.(UnknownBody); ok { |
| if u.Unknown() { |
| // If any block Body is unknown, then the entire block value |
| // must be unknown |
| return cty.UnknownVal(s.impliedType()), diags |
| } |
| } |
| |
| 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], |
| }) |
| } |
| |
| if len(elems) == 0 { |
| return cty.EmptyTupleVal, diags |
| } |
| |
| return cty.TupleVal(elems), 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...) |
| |
| if u, ok := childBlock.Body.(UnknownBody); ok { |
| if u.Unknown() { |
| // If any block Body is unknown, then the entire block value |
| // must be unknown |
| return cty.UnknownVal(s.impliedType()), diags |
| } |
| } |
| |
| 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], |
| }) |
| } |
| |
| if len(elems) == 0 { |
| return cty.SetValEmpty(s.Nested.impliedType()), diags |
| } |
| |
| // 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 |
| } |
| } |
| |
| return cty.SetVal(elems), 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 |
| } |
| |
| if u, ok := childBlock.Body.(UnknownBody); ok { |
| if u.Unknown() { |
| // If any block Body is unknown, then the entire block value |
| // must be unknown |
| return cty.UnknownVal(s.impliedType()), diags |
| } |
| } |
| |
| 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 |
| } |
| |
| if u, ok := childBlock.Body.(UnknownBody); ok { |
| if u.Unknown() { |
| // If any block Body is unknown, then the entire block value |
| // must be unknown |
| return cty.UnknownVal(s.impliedType()), diags |
| } |
| } |
| |
| 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 { |
| if decodeFn := customdecode.CustomExpressionDecoderForType(s.ElementType); decodeFn != nil { |
| attrVal, attrDiags := decodeFn(attr.Expr, ctx) |
| diags = append(diags, attrDiags...) |
| if attrVal == cty.NilVal { |
| attrVal = cty.UnknownVal(s.ElementType) |
| } |
| vals[name] = attrVal |
| continue |
| } |
| |
| 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(), |
| Context: hcl.RangeBetween(attr.NameRange, attr.Expr.Range()).Ptr(), |
| Expression: attr.Expr, |
| EvalContext: ctx, |
| }) |
| 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) |
| } |
| |
| // ValidateFuncSpec is a spec that allows for extended |
| // developer-defined validation. The validation function receives the |
| // result of the wrapped spec. |
| // |
| // The Subject field of the returned Diagnostic is optional. If not |
| // specified, it is automatically populated with the range covered by |
| // the wrapped spec. |
| // |
| type ValidateSpec struct { |
| Wrapped Spec |
| Func func(value cty.Value) hcl.Diagnostics |
| } |
| |
| func (s *ValidateSpec) visitSameBodyChildren(cb visitFunc) { |
| cb(s.Wrapped) |
| } |
| |
| func (s *ValidateSpec) 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 |
| } |
| |
| validateDiags := s.Func(wrappedVal) |
| // Auto-populate the Subject fields if they weren't set. |
| for i := range validateDiags { |
| if validateDiags[i].Subject == nil { |
| validateDiags[i].Subject = s.sourceRange(content, blockLabels).Ptr() |
| } |
| } |
| |
| diags = append(diags, validateDiags...) |
| return wrappedVal, diags |
| } |
| |
| func (s *ValidateSpec) impliedType() cty.Type { |
| return s.Wrapped.impliedType() |
| } |
| |
| func (s *ValidateSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { |
| 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", |
| } |
| } |