github.com/opentofu/opentofu@v1.7.1/internal/configs/configschema/decoder_spec.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 configschema 7 8 import ( 9 "runtime" 10 "sync" 11 "unsafe" 12 13 "github.com/hashicorp/hcl/v2/hcldec" 14 "github.com/zclconf/go-cty/cty" 15 ) 16 17 var mapLabelNames = []string{"key"} 18 19 // specCache is a global cache of all the generated hcldec.Spec values for 20 // Blocks. This cache is used by the Block.DecoderSpec method to memoize calls 21 // and prevent unnecessary regeneration of the spec, especially when they are 22 // large and deeply nested. 23 // Caching these externally rather than within the struct is required because 24 // Blocks are used by value and copied when working with NestedBlocks, and the 25 // copying of the value prevents any safe synchronisation of the struct itself. 26 // 27 // While we are using the *Block pointer as the cache key, and the Block 28 // contents are mutable, once a Block is created it is treated as immutable for 29 // the duration of its life. Because a Block is a representation of a logical 30 // schema, which cannot change while it's being used, any modifications to the 31 // schema during execution would be an error. 32 type specCache struct { 33 sync.Mutex 34 specs map[uintptr]hcldec.Spec 35 } 36 37 var decoderSpecCache = specCache{ 38 specs: map[uintptr]hcldec.Spec{}, 39 } 40 41 // get returns the Spec associated with eth given Block, or nil if non is 42 // found. 43 func (s *specCache) get(b *Block) hcldec.Spec { 44 s.Lock() 45 defer s.Unlock() 46 k := uintptr(unsafe.Pointer(b)) 47 return s.specs[k] 48 } 49 50 // set stores the given Spec as being the result of b.DecoderSpec(). 51 func (s *specCache) set(b *Block, spec hcldec.Spec) { 52 s.Lock() 53 defer s.Unlock() 54 55 // the uintptr value gets us a unique identifier for each block, without 56 // tying this to the block value itself. 57 k := uintptr(unsafe.Pointer(b)) 58 if _, ok := s.specs[k]; ok { 59 return 60 } 61 62 s.specs[k] = spec 63 64 // This must use a finalizer tied to the Block, otherwise we'll continue to 65 // build up Spec values as the Blocks are recycled. 66 runtime.SetFinalizer(b, s.delete) 67 } 68 69 // delete removes the spec associated with the given Block. 70 func (s *specCache) delete(b *Block) { 71 s.Lock() 72 defer s.Unlock() 73 74 k := uintptr(unsafe.Pointer(b)) 75 delete(s.specs, k) 76 } 77 78 // DecoderSpec returns a hcldec.Spec that can be used to decode a HCL Body 79 // using the facilities in the hcldec package. 80 // 81 // The returned specification is guaranteed to return a value of the same type 82 // returned by method ImpliedType, but it may contain null values if any of the 83 // block attributes are defined as optional and/or computed respectively. 84 func (b *Block) DecoderSpec() hcldec.Spec { 85 ret := hcldec.ObjectSpec{} 86 if b == nil { 87 return ret 88 } 89 90 if spec := decoderSpecCache.get(b); spec != nil { 91 return spec 92 } 93 94 for name, attrS := range b.Attributes { 95 ret[name] = attrS.decoderSpec(name) 96 } 97 98 for name, blockS := range b.BlockTypes { 99 if _, exists := ret[name]; exists { 100 // This indicates an invalid schema, since it's not valid to define 101 // both an attribute and a block type of the same name. We assume 102 // that the provider has already used something like 103 // InternalValidate to validate their schema. 104 continue 105 } 106 107 childSpec := blockS.Block.DecoderSpec() 108 109 switch blockS.Nesting { 110 case NestingSingle, NestingGroup: 111 ret[name] = &hcldec.BlockSpec{ 112 TypeName: name, 113 Nested: childSpec, 114 Required: blockS.MinItems == 1, 115 } 116 if blockS.Nesting == NestingGroup { 117 ret[name] = &hcldec.DefaultSpec{ 118 Primary: ret[name], 119 Default: &hcldec.LiteralSpec{ 120 Value: blockS.EmptyValue(), 121 }, 122 } 123 } 124 case NestingList: 125 // We prefer to use a list where possible, since it makes our 126 // implied type more complete, but if there are any 127 // dynamically-typed attributes inside we must use a tuple 128 // instead, at the expense of our type then not being predictable. 129 if blockS.Block.specType().HasDynamicTypes() { 130 ret[name] = &hcldec.BlockTupleSpec{ 131 TypeName: name, 132 Nested: childSpec, 133 MinItems: blockS.MinItems, 134 MaxItems: blockS.MaxItems, 135 } 136 } else { 137 ret[name] = &hcldec.BlockListSpec{ 138 TypeName: name, 139 Nested: childSpec, 140 MinItems: blockS.MinItems, 141 MaxItems: blockS.MaxItems, 142 } 143 } 144 case NestingSet: 145 // We forbid dynamically-typed attributes inside NestingSet in 146 // InternalValidate, so we don't do anything special to handle that 147 // here. (There is no set analog to tuple and object types, because 148 // cty's set implementation depends on knowing the static type in 149 // order to properly compute its internal hashes.) We assume that 150 // the provider has already used something like InternalValidate to 151 // validate their schema. 152 ret[name] = &hcldec.BlockSetSpec{ 153 TypeName: name, 154 Nested: childSpec, 155 MinItems: blockS.MinItems, 156 MaxItems: blockS.MaxItems, 157 } 158 case NestingMap: 159 // We prefer to use a list where possible, since it makes our 160 // implied type more complete, but if there are any 161 // dynamically-typed attributes inside we must use a tuple 162 // instead, at the expense of our type then not being predictable. 163 if blockS.Block.specType().HasDynamicTypes() { 164 ret[name] = &hcldec.BlockObjectSpec{ 165 TypeName: name, 166 Nested: childSpec, 167 LabelNames: mapLabelNames, 168 } 169 } else { 170 ret[name] = &hcldec.BlockMapSpec{ 171 TypeName: name, 172 Nested: childSpec, 173 LabelNames: mapLabelNames, 174 } 175 } 176 default: 177 // Invalid nesting type is just ignored. It's checked by 178 // InternalValidate. We assume that the provider has already used 179 // something like InternalValidate to validate their schema. 180 continue 181 } 182 } 183 184 decoderSpecCache.set(b, ret) 185 return ret 186 } 187 188 func (a *Attribute) decoderSpec(name string) hcldec.Spec { 189 ret := &hcldec.AttrSpec{Name: name} 190 if a == nil { 191 return ret 192 } 193 194 if a.NestedType != nil { 195 if a.Type != cty.NilType { 196 panic("Invalid attribute schema: NestedType and Type cannot both be set. This is a bug in the provider.") 197 } 198 199 ty := a.NestedType.specType() 200 ret.Type = ty 201 ret.Required = a.Required 202 return ret 203 } 204 205 ret.Type = a.Type 206 ret.Required = a.Required 207 return ret 208 } 209 210 // listOptionalAttrsFromObject is a helper function which does *not* recurse 211 // into NestedType Attributes, because the optional types for each of those will 212 // belong to their own cty.Object definitions. It is used in other functions 213 // which themselves handle that recursion. 214 func listOptionalAttrsFromObject(o *Object) []string { 215 ret := make([]string, 0) 216 217 // This is unlikely to happen outside of tests. 218 if o == nil { 219 return ret 220 } 221 222 for name, attr := range o.Attributes { 223 if attr.Optional || attr.Computed { 224 ret = append(ret, name) 225 } 226 } 227 return ret 228 }