github.com/opentofu/opentofu@v1.7.1/internal/lang/blocktoattr/schema.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package blocktoattr 7 8 import ( 9 "github.com/hashicorp/hcl/v2" 10 "github.com/opentofu/opentofu/internal/configs/configschema" 11 "github.com/zclconf/go-cty/cty" 12 ) 13 14 func ambiguousNames(schema *configschema.Block) map[string]struct{} { 15 if schema == nil { 16 return nil 17 } 18 ambiguousNames := make(map[string]struct{}) 19 for name, attrS := range schema.Attributes { 20 aty := attrS.Type 21 if (aty.IsListType() || aty.IsSetType()) && aty.ElementType().IsObjectType() { 22 ambiguousNames[name] = struct{}{} 23 } 24 } 25 return ambiguousNames 26 } 27 28 func effectiveSchema(given *hcl.BodySchema, body hcl.Body, ambiguousNames map[string]struct{}, dynamicExpanded bool) *hcl.BodySchema { 29 ret := &hcl.BodySchema{} 30 31 appearsAsBlock := make(map[string]struct{}) 32 { 33 // We'll construct some throwaway schemas here just to probe for 34 // whether each of our ambiguous names seems to be being used as 35 // an attribute or a block. We need to check both because in JSON 36 // syntax we rely on the schema to decide between attribute or block 37 // interpretation and so JSON will always answer yes to both of 38 // these questions and we want to prefer the attribute interpretation 39 // in that case. 40 var probeSchema hcl.BodySchema 41 42 for name := range ambiguousNames { 43 probeSchema = hcl.BodySchema{ 44 Attributes: []hcl.AttributeSchema{ 45 { 46 Name: name, 47 }, 48 }, 49 } 50 content, _, _ := body.PartialContent(&probeSchema) 51 if _, exists := content.Attributes[name]; exists { 52 // Can decode as an attribute, so we'll go with that. 53 continue 54 } 55 probeSchema = hcl.BodySchema{ 56 Blocks: []hcl.BlockHeaderSchema{ 57 { 58 Type: name, 59 }, 60 }, 61 } 62 content, _, _ = body.PartialContent(&probeSchema) 63 if len(content.Blocks) > 0 || dynamicExpanded { 64 // A dynamic block with an empty iterator returns nothing. 65 // If there's no attribute and we have either a block or a 66 // dynamic expansion, we need to rewrite this one as a 67 // block for a successful result. 68 appearsAsBlock[name] = struct{}{} 69 } 70 } 71 if !dynamicExpanded { 72 // If we're deciding for a context where dynamic blocks haven't 73 // been expanded yet then we need to probe for those too. 74 probeSchema = hcl.BodySchema{ 75 Blocks: []hcl.BlockHeaderSchema{ 76 { 77 Type: "dynamic", 78 LabelNames: []string{"type"}, 79 }, 80 }, 81 } 82 content, _, _ := body.PartialContent(&probeSchema) 83 for _, block := range content.Blocks { 84 if _, exists := ambiguousNames[block.Labels[0]]; exists { 85 appearsAsBlock[block.Labels[0]] = struct{}{} 86 } 87 } 88 } 89 } 90 91 for _, attrS := range given.Attributes { 92 if _, exists := appearsAsBlock[attrS.Name]; exists { 93 ret.Blocks = append(ret.Blocks, hcl.BlockHeaderSchema{ 94 Type: attrS.Name, 95 }) 96 } else { 97 ret.Attributes = append(ret.Attributes, attrS) 98 } 99 } 100 101 // Anything that is specified as a block type in the input schema remains 102 // that way by just passing through verbatim. 103 ret.Blocks = append(ret.Blocks, given.Blocks...) 104 105 return ret 106 } 107 108 // SchemaForCtyElementType converts a cty object type into an 109 // approximately-equivalent configschema.Block representing the element of 110 // a list or set. If the given type is not an object type then this 111 // function will panic. 112 func SchemaForCtyElementType(ty cty.Type) *configschema.Block { 113 atys := ty.AttributeTypes() 114 ret := &configschema.Block{ 115 Attributes: make(map[string]*configschema.Attribute, len(atys)), 116 } 117 for name, aty := range atys { 118 ret.Attributes[name] = &configschema.Attribute{ 119 Type: aty, 120 Optional: true, 121 } 122 } 123 return ret 124 } 125 126 // SchemaForCtyContainerType converts a cty list-of-object or set-of-object type 127 // into an approximately-equivalent configschema.NestedBlock. If the given type 128 // is not of the expected kind then this function will panic. 129 func SchemaForCtyContainerType(ty cty.Type) *configschema.NestedBlock { 130 var nesting configschema.NestingMode 131 switch { 132 case ty.IsListType(): 133 nesting = configschema.NestingList 134 case ty.IsSetType(): 135 nesting = configschema.NestingSet 136 default: 137 panic("unsuitable type") 138 } 139 nested := SchemaForCtyElementType(ty.ElementType()) 140 return &configschema.NestedBlock{ 141 Nesting: nesting, 142 Block: *nested, 143 } 144 } 145 146 // TypeCanBeBlocks returns true if the given type is a list-of-object or 147 // set-of-object type, and would thus be subject to the blocktoattr fixup 148 // if used as an attribute type. 149 func TypeCanBeBlocks(ty cty.Type) bool { 150 return (ty.IsListType() || ty.IsSetType()) && ty.ElementType().IsObjectType() 151 }