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  }