github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/lang/blocktoattr/schema.go (about)

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