opentofu/lang/blocktoattr/schema.go
Martin Atkins 39e609d5fd vendor: switch to HCL 2.0 in the HCL repository
Previously we were using the experimental HCL 2 repository, but now we'll
shift over to the v2 import path within the main HCL repository as part of
actually releasing HCL 2.0 as stable.

This is a mechanical search/replace to the new import paths. It also
switches to the v2.0.0 release of HCL, which includes some new code that
Terraform didn't previously have but should not change any behavior that
matters for Terraform's purposes.

For the moment the experimental HCL2 repository is still an indirect
dependency via terraform-config-inspect, so it remains in our go.sum and
vendor directories for the moment. Because terraform-config-inspect uses
a much smaller subset of the HCL2 functionality, this does still manage
to prune the vendor directory a little. A subsequent release of
terraform-config-inspect should allow us to completely remove that old
repository in a future commit.
2019-10-02 15:10:21 -07:00

147 lines
4.4 KiB
Go

package blocktoattr
import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/zclconf/go-cty/cty"
)
func ambiguousNames(schema *configschema.Block) map[string]struct{} {
if schema == nil {
return nil
}
ambiguousNames := make(map[string]struct{})
for name, attrS := range schema.Attributes {
aty := attrS.Type
if (aty.IsListType() || aty.IsSetType()) && aty.ElementType().IsObjectType() {
ambiguousNames[name] = struct{}{}
}
}
return ambiguousNames
}
func effectiveSchema(given *hcl.BodySchema, body hcl.Body, ambiguousNames map[string]struct{}, dynamicExpanded bool) *hcl.BodySchema {
ret := &hcl.BodySchema{}
appearsAsBlock := make(map[string]struct{})
{
// We'll construct some throwaway schemas here just to probe for
// whether each of our ambiguous names seems to be being used as
// an attribute or a block. We need to check both because in JSON
// syntax we rely on the schema to decide between attribute or block
// interpretation and so JSON will always answer yes to both of
// these questions and we want to prefer the attribute interpretation
// in that case.
var probeSchema hcl.BodySchema
for name := range ambiguousNames {
probeSchema = hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: name,
},
},
}
content, _, _ := body.PartialContent(&probeSchema)
if _, exists := content.Attributes[name]; exists {
// Can decode as an attribute, so we'll go with that.
continue
}
probeSchema = hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{
Type: name,
},
},
}
content, _, _ = body.PartialContent(&probeSchema)
if len(content.Blocks) > 0 || dynamicExpanded {
// A dynamic block with an empty iterator returns nothing.
// If there's no attribute and we have either a block or a
// dynamic expansion, we need to rewrite this one as a
// block for a successful result.
appearsAsBlock[name] = struct{}{}
}
}
if !dynamicExpanded {
// If we're deciding for a context where dynamic blocks haven't
// been expanded yet then we need to probe for those too.
probeSchema = hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{
Type: "dynamic",
LabelNames: []string{"type"},
},
},
}
content, _, _ := body.PartialContent(&probeSchema)
for _, block := range content.Blocks {
if _, exists := ambiguousNames[block.Labels[0]]; exists {
appearsAsBlock[block.Labels[0]] = struct{}{}
}
}
}
}
for _, attrS := range given.Attributes {
if _, exists := appearsAsBlock[attrS.Name]; exists {
ret.Blocks = append(ret.Blocks, hcl.BlockHeaderSchema{
Type: attrS.Name,
})
} else {
ret.Attributes = append(ret.Attributes, attrS)
}
}
// Anything that is specified as a block type in the input schema remains
// that way by just passing through verbatim.
ret.Blocks = append(ret.Blocks, given.Blocks...)
return ret
}
// SchemaForCtyElementType converts a cty object type into an
// approximately-equivalent configschema.Block representing the element of
// a list or set. If the given type is not an object type then this
// function will panic.
func SchemaForCtyElementType(ty cty.Type) *configschema.Block {
atys := ty.AttributeTypes()
ret := &configschema.Block{
Attributes: make(map[string]*configschema.Attribute, len(atys)),
}
for name, aty := range atys {
ret.Attributes[name] = &configschema.Attribute{
Type: aty,
Optional: true,
}
}
return ret
}
// SchemaForCtyContainerType converts a cty list-of-object or set-of-object type
// into an approximately-equivalent configschema.NestedBlock. If the given type
// is not of the expected kind then this function will panic.
func SchemaForCtyContainerType(ty cty.Type) *configschema.NestedBlock {
var nesting configschema.NestingMode
switch {
case ty.IsListType():
nesting = configschema.NestingList
case ty.IsSetType():
nesting = configschema.NestingSet
default:
panic("unsuitable type")
}
nested := SchemaForCtyElementType(ty.ElementType())
return &configschema.NestedBlock{
Nesting: nesting,
Block: *nested,
}
}
// TypeCanBeBlocks returns true if the given type is a list-of-object or
// set-of-object type, and would thus be subject to the blocktoattr fixup
// if used as an attribute type.
func TypeCanBeBlocks(ty cty.Type) bool {
return (ty.IsListType() || ty.IsSetType()) && ty.ElementType().IsObjectType()
}