| package blocktoattr |
| |
| import ( |
| "github.com/hashicorp/hcl/v2" |
| "github.com/hashicorp/terraform/internal/configs/configschema" |
| "github.com/zclconf/go-cty/cty" |
| ) |
| |
| func ambiguousNames(schema *configschema.Block) map[string]struct{} { |
| if schema == nil { |
| return nil |
| } |
| ambiguousNames := make(map[string]struct{}) |
| for name, attrS := range schema.Attributes { |
| aty := attrS.Type |
| if (aty.IsListType() || aty.IsSetType()) && aty.ElementType().IsObjectType() { |
| ambiguousNames[name] = struct{}{} |
| } |
| } |
| return ambiguousNames |
| } |
| |
| func effectiveSchema(given *hcl.BodySchema, body hcl.Body, ambiguousNames map[string]struct{}, dynamicExpanded bool) *hcl.BodySchema { |
| ret := &hcl.BodySchema{} |
| |
| appearsAsBlock := make(map[string]struct{}) |
| { |
| // We'll construct some throwaway schemas here just to probe for |
| // whether each of our ambiguous names seems to be being used as |
| // an attribute or a block. We need to check both because in JSON |
| // syntax we rely on the schema to decide between attribute or block |
| // interpretation and so JSON will always answer yes to both of |
| // these questions and we want to prefer the attribute interpretation |
| // in that case. |
| var probeSchema hcl.BodySchema |
| |
| for name := range ambiguousNames { |
| probeSchema = hcl.BodySchema{ |
| Attributes: []hcl.AttributeSchema{ |
| { |
| Name: name, |
| }, |
| }, |
| } |
| content, _, _ := body.PartialContent(&probeSchema) |
| if _, exists := content.Attributes[name]; exists { |
| // Can decode as an attribute, so we'll go with that. |
| continue |
| } |
| probeSchema = hcl.BodySchema{ |
| Blocks: []hcl.BlockHeaderSchema{ |
| { |
| Type: name, |
| }, |
| }, |
| } |
| content, _, _ = body.PartialContent(&probeSchema) |
| if len(content.Blocks) > 0 || dynamicExpanded { |
| // A dynamic block with an empty iterator returns nothing. |
| // If there's no attribute and we have either a block or a |
| // dynamic expansion, we need to rewrite this one as a |
| // block for a successful result. |
| appearsAsBlock[name] = struct{}{} |
| } |
| } |
| if !dynamicExpanded { |
| // If we're deciding for a context where dynamic blocks haven't |
| // been expanded yet then we need to probe for those too. |
| probeSchema = hcl.BodySchema{ |
| Blocks: []hcl.BlockHeaderSchema{ |
| { |
| Type: "dynamic", |
| LabelNames: []string{"type"}, |
| }, |
| }, |
| } |
| content, _, _ := body.PartialContent(&probeSchema) |
| for _, block := range content.Blocks { |
| if _, exists := ambiguousNames[block.Labels[0]]; exists { |
| appearsAsBlock[block.Labels[0]] = struct{}{} |
| } |
| } |
| } |
| } |
| |
| for _, attrS := range given.Attributes { |
| if _, exists := appearsAsBlock[attrS.Name]; exists { |
| ret.Blocks = append(ret.Blocks, hcl.BlockHeaderSchema{ |
| Type: attrS.Name, |
| }) |
| } else { |
| ret.Attributes = append(ret.Attributes, attrS) |
| } |
| } |
| |
| // Anything that is specified as a block type in the input schema remains |
| // that way by just passing through verbatim. |
| ret.Blocks = append(ret.Blocks, given.Blocks...) |
| |
| return ret |
| } |
| |
| // SchemaForCtyElementType converts a cty object type into an |
| // approximately-equivalent configschema.Block representing the element of |
| // a list or set. If the given type is not an object type then this |
| // function will panic. |
| func SchemaForCtyElementType(ty cty.Type) *configschema.Block { |
| atys := ty.AttributeTypes() |
| ret := &configschema.Block{ |
| Attributes: make(map[string]*configschema.Attribute, len(atys)), |
| } |
| for name, aty := range atys { |
| ret.Attributes[name] = &configschema.Attribute{ |
| Type: aty, |
| Optional: true, |
| } |
| } |
| return ret |
| } |
| |
| // SchemaForCtyContainerType converts a cty list-of-object or set-of-object type |
| // into an approximately-equivalent configschema.NestedBlock. If the given type |
| // is not of the expected kind then this function will panic. |
| func SchemaForCtyContainerType(ty cty.Type) *configschema.NestedBlock { |
| var nesting configschema.NestingMode |
| switch { |
| case ty.IsListType(): |
| nesting = configschema.NestingList |
| case ty.IsSetType(): |
| nesting = configschema.NestingSet |
| default: |
| panic("unsuitable type") |
| } |
| nested := SchemaForCtyElementType(ty.ElementType()) |
| return &configschema.NestedBlock{ |
| Nesting: nesting, |
| Block: *nested, |
| } |
| } |
| |
| // TypeCanBeBlocks returns true if the given type is a list-of-object or |
| // set-of-object type, and would thus be subject to the blocktoattr fixup |
| // if used as an attribute type. |
| func TypeCanBeBlocks(ty cty.Type) bool { |
| return (ty.IsListType() || ty.IsSetType()) && ty.ElementType().IsObjectType() |
| } |