github.com/opentofu/opentofu@v1.7.1/internal/lang/blocktoattr/schema.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 blocktoattr
     7  
     8  import (
     9  	"github.com/hashicorp/hcl/v2"
    10  	"github.com/opentofu/opentofu/internal/configs/configschema"
    11  	"github.com/zclconf/go-cty/cty"
    12  )
    13  
    14  func ambiguousNames(schema *configschema.Block) map[string]struct{} {
    15  	if schema == nil {
    16  		return nil
    17  	}
    18  	ambiguousNames := make(map[string]struct{})
    19  	for name, attrS := range schema.Attributes {
    20  		aty := attrS.Type
    21  		if (aty.IsListType() || aty.IsSetType()) && aty.ElementType().IsObjectType() {
    22  			ambiguousNames[name] = struct{}{}
    23  		}
    24  	}
    25  	return ambiguousNames
    26  }
    27  
    28  func effectiveSchema(given *hcl.BodySchema, body hcl.Body, ambiguousNames map[string]struct{}, dynamicExpanded bool) *hcl.BodySchema {
    29  	ret := &hcl.BodySchema{}
    30  
    31  	appearsAsBlock := make(map[string]struct{})
    32  	{
    33  		// We'll construct some throwaway schemas here just to probe for
    34  		// whether each of our ambiguous names seems to be being used as
    35  		// an attribute or a block. We need to check both because in JSON
    36  		// syntax we rely on the schema to decide between attribute or block
    37  		// interpretation and so JSON will always answer yes to both of
    38  		// these questions and we want to prefer the attribute interpretation
    39  		// in that case.
    40  		var probeSchema hcl.BodySchema
    41  
    42  		for name := range ambiguousNames {
    43  			probeSchema = hcl.BodySchema{
    44  				Attributes: []hcl.AttributeSchema{
    45  					{
    46  						Name: name,
    47  					},
    48  				},
    49  			}
    50  			content, _, _ := body.PartialContent(&probeSchema)
    51  			if _, exists := content.Attributes[name]; exists {
    52  				// Can decode as an attribute, so we'll go with that.
    53  				continue
    54  			}
    55  			probeSchema = hcl.BodySchema{
    56  				Blocks: []hcl.BlockHeaderSchema{
    57  					{
    58  						Type: name,
    59  					},
    60  				},
    61  			}
    62  			content, _, _ = body.PartialContent(&probeSchema)
    63  			if len(content.Blocks) > 0 || dynamicExpanded {
    64  				// A dynamic block with an empty iterator returns nothing.
    65  				// If there's no attribute and we have either a block or a
    66  				// dynamic expansion, we need to rewrite this one as a
    67  				// block for a successful result.
    68  				appearsAsBlock[name] = struct{}{}
    69  			}
    70  		}
    71  		if !dynamicExpanded {
    72  			// If we're deciding for a context where dynamic blocks haven't
    73  			// been expanded yet then we need to probe for those too.
    74  			probeSchema = hcl.BodySchema{
    75  				Blocks: []hcl.BlockHeaderSchema{
    76  					{
    77  						Type:       "dynamic",
    78  						LabelNames: []string{"type"},
    79  					},
    80  				},
    81  			}
    82  			content, _, _ := body.PartialContent(&probeSchema)
    83  			for _, block := range content.Blocks {
    84  				if _, exists := ambiguousNames[block.Labels[0]]; exists {
    85  					appearsAsBlock[block.Labels[0]] = struct{}{}
    86  				}
    87  			}
    88  		}
    89  	}
    90  
    91  	for _, attrS := range given.Attributes {
    92  		if _, exists := appearsAsBlock[attrS.Name]; exists {
    93  			ret.Blocks = append(ret.Blocks, hcl.BlockHeaderSchema{
    94  				Type: attrS.Name,
    95  			})
    96  		} else {
    97  			ret.Attributes = append(ret.Attributes, attrS)
    98  		}
    99  	}
   100  
   101  	// Anything that is specified as a block type in the input schema remains
   102  	// that way by just passing through verbatim.
   103  	ret.Blocks = append(ret.Blocks, given.Blocks...)
   104  
   105  	return ret
   106  }
   107  
   108  // SchemaForCtyElementType converts a cty object type into an
   109  // approximately-equivalent configschema.Block representing the element of
   110  // a list or set. If the given type is not an object type then this
   111  // function will panic.
   112  func SchemaForCtyElementType(ty cty.Type) *configschema.Block {
   113  	atys := ty.AttributeTypes()
   114  	ret := &configschema.Block{
   115  		Attributes: make(map[string]*configschema.Attribute, len(atys)),
   116  	}
   117  	for name, aty := range atys {
   118  		ret.Attributes[name] = &configschema.Attribute{
   119  			Type:     aty,
   120  			Optional: true,
   121  		}
   122  	}
   123  	return ret
   124  }
   125  
   126  // SchemaForCtyContainerType converts a cty list-of-object or set-of-object type
   127  // into an approximately-equivalent configschema.NestedBlock. If the given type
   128  // is not of the expected kind then this function will panic.
   129  func SchemaForCtyContainerType(ty cty.Type) *configschema.NestedBlock {
   130  	var nesting configschema.NestingMode
   131  	switch {
   132  	case ty.IsListType():
   133  		nesting = configschema.NestingList
   134  	case ty.IsSetType():
   135  		nesting = configschema.NestingSet
   136  	default:
   137  		panic("unsuitable type")
   138  	}
   139  	nested := SchemaForCtyElementType(ty.ElementType())
   140  	return &configschema.NestedBlock{
   141  		Nesting: nesting,
   142  		Block:   *nested,
   143  	}
   144  }
   145  
   146  // TypeCanBeBlocks returns true if the given type is a list-of-object or
   147  // set-of-object type, and would thus be subject to the blocktoattr fixup
   148  // if used as an attribute type.
   149  func TypeCanBeBlocks(ty cty.Type) bool {
   150  	return (ty.IsListType() || ty.IsSetType()) && ty.ElementType().IsObjectType()
   151  }