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