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  }