github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/lang/blocktoattr/schema.go (about)

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