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