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