mirror of
https://github.com/letic/terraform-provider-google.git
synced 2024-09-07 09:36:44 +00:00
227 lines
6.3 KiB
Go
227 lines
6.3 KiB
Go
package hcl
|
|
|
|
import (
|
|
"fmt"
|
|
)
|
|
|
|
// MergeFiles combines the given files to produce a single body that contains
|
|
// configuration from all of the given files.
|
|
//
|
|
// The ordering of the given files decides the order in which contained
|
|
// elements will be returned. If any top-level attributes are defined with
|
|
// the same name across multiple files, a diagnostic will be produced from
|
|
// the Content and PartialContent methods describing this error in a
|
|
// user-friendly way.
|
|
func MergeFiles(files []*File) Body {
|
|
var bodies []Body
|
|
for _, file := range files {
|
|
bodies = append(bodies, file.Body)
|
|
}
|
|
return MergeBodies(bodies)
|
|
}
|
|
|
|
// MergeBodies is like MergeFiles except it deals directly with bodies, rather
|
|
// than with entire files.
|
|
func MergeBodies(bodies []Body) Body {
|
|
if len(bodies) == 0 {
|
|
// Swap out for our singleton empty body, to reduce the number of
|
|
// empty slices we have hanging around.
|
|
return emptyBody
|
|
}
|
|
|
|
// If any of the given bodies are already merged bodies, we'll unpack
|
|
// to flatten to a single mergedBodies, since that's conceptually simpler.
|
|
// This also, as a side-effect, eliminates any empty bodies, since
|
|
// empties are merged bodies with no inner bodies.
|
|
var newLen int
|
|
var flatten bool
|
|
for _, body := range bodies {
|
|
if children, merged := body.(mergedBodies); merged {
|
|
newLen += len(children)
|
|
flatten = true
|
|
} else {
|
|
newLen++
|
|
}
|
|
}
|
|
|
|
if !flatten { // not just newLen == len, because we might have mergedBodies with single bodies inside
|
|
return mergedBodies(bodies)
|
|
}
|
|
|
|
if newLen == 0 {
|
|
// Don't allocate a new empty when we already have one
|
|
return emptyBody
|
|
}
|
|
|
|
new := make([]Body, 0, newLen)
|
|
for _, body := range bodies {
|
|
if children, merged := body.(mergedBodies); merged {
|
|
new = append(new, children...)
|
|
} else {
|
|
new = append(new, body)
|
|
}
|
|
}
|
|
return mergedBodies(new)
|
|
}
|
|
|
|
var emptyBody = mergedBodies([]Body{})
|
|
|
|
// EmptyBody returns a body with no content. This body can be used as a
|
|
// placeholder when a body is required but no body content is available.
|
|
func EmptyBody() Body {
|
|
return emptyBody
|
|
}
|
|
|
|
type mergedBodies []Body
|
|
|
|
// Content returns the content produced by applying the given schema to all
|
|
// of the merged bodies and merging the result.
|
|
//
|
|
// Although required attributes _are_ supported, they should be used sparingly
|
|
// with merged bodies since in this case there is no contextual information
|
|
// with which to return good diagnostics. Applications working with merged
|
|
// bodies may wish to mark all attributes as optional and then check for
|
|
// required attributes afterwards, to produce better diagnostics.
|
|
func (mb mergedBodies) Content(schema *BodySchema) (*BodyContent, Diagnostics) {
|
|
// the returned body will always be empty in this case, because mergedContent
|
|
// will only ever call Content on the child bodies.
|
|
content, _, diags := mb.mergedContent(schema, false)
|
|
return content, diags
|
|
}
|
|
|
|
func (mb mergedBodies) PartialContent(schema *BodySchema) (*BodyContent, Body, Diagnostics) {
|
|
return mb.mergedContent(schema, true)
|
|
}
|
|
|
|
func (mb mergedBodies) JustAttributes() (Attributes, Diagnostics) {
|
|
attrs := make(map[string]*Attribute)
|
|
var diags Diagnostics
|
|
|
|
for _, body := range mb {
|
|
thisAttrs, thisDiags := body.JustAttributes()
|
|
|
|
if len(thisDiags) != 0 {
|
|
diags = append(diags, thisDiags...)
|
|
}
|
|
|
|
if thisAttrs != nil {
|
|
for name, attr := range thisAttrs {
|
|
if existing := attrs[name]; existing != nil {
|
|
diags = diags.Append(&Diagnostic{
|
|
Severity: DiagError,
|
|
Summary: "Duplicate attribute",
|
|
Detail: fmt.Sprintf(
|
|
"Attribute %q was already assigned at %s",
|
|
name, existing.NameRange.String(),
|
|
),
|
|
Subject: &attr.NameRange,
|
|
})
|
|
continue
|
|
}
|
|
|
|
attrs[name] = attr
|
|
}
|
|
}
|
|
}
|
|
|
|
return attrs, diags
|
|
}
|
|
|
|
func (mb mergedBodies) MissingItemRange() Range {
|
|
if len(mb) == 0 {
|
|
// Nothing useful to return here, so we'll return some garbage.
|
|
return Range{
|
|
Filename: "<empty>",
|
|
}
|
|
}
|
|
|
|
// arbitrarily use the first body's missing item range
|
|
return mb[0].MissingItemRange()
|
|
}
|
|
|
|
func (mb mergedBodies) mergedContent(schema *BodySchema, partial bool) (*BodyContent, Body, Diagnostics) {
|
|
// We need to produce a new schema with none of the attributes marked as
|
|
// required, since _any one_ of our bodies can contribute an attribute value.
|
|
// We'll separately check that all required attributes are present at
|
|
// the end.
|
|
mergedSchema := &BodySchema{
|
|
Blocks: schema.Blocks,
|
|
}
|
|
for _, attrS := range schema.Attributes {
|
|
mergedAttrS := attrS
|
|
mergedAttrS.Required = false
|
|
mergedSchema.Attributes = append(mergedSchema.Attributes, mergedAttrS)
|
|
}
|
|
|
|
var mergedLeftovers []Body
|
|
content := &BodyContent{
|
|
Attributes: map[string]*Attribute{},
|
|
}
|
|
|
|
var diags Diagnostics
|
|
for _, body := range mb {
|
|
var thisContent *BodyContent
|
|
var thisLeftovers Body
|
|
var thisDiags Diagnostics
|
|
|
|
if partial {
|
|
thisContent, thisLeftovers, thisDiags = body.PartialContent(mergedSchema)
|
|
} else {
|
|
thisContent, thisDiags = body.Content(mergedSchema)
|
|
}
|
|
|
|
if thisLeftovers != nil {
|
|
mergedLeftovers = append(mergedLeftovers)
|
|
}
|
|
if len(thisDiags) != 0 {
|
|
diags = append(diags, thisDiags...)
|
|
}
|
|
|
|
if thisContent.Attributes != nil {
|
|
for name, attr := range thisContent.Attributes {
|
|
if existing := content.Attributes[name]; existing != nil {
|
|
diags = diags.Append(&Diagnostic{
|
|
Severity: DiagError,
|
|
Summary: "Duplicate attribute",
|
|
Detail: fmt.Sprintf(
|
|
"Attribute %q was already assigned at %s",
|
|
name, existing.NameRange.String(),
|
|
),
|
|
Subject: &attr.NameRange,
|
|
})
|
|
continue
|
|
}
|
|
content.Attributes[name] = attr
|
|
}
|
|
}
|
|
|
|
if len(thisContent.Blocks) != 0 {
|
|
content.Blocks = append(content.Blocks, thisContent.Blocks...)
|
|
}
|
|
}
|
|
|
|
// Finally, we check for required attributes.
|
|
for _, attrS := range schema.Attributes {
|
|
if !attrS.Required {
|
|
continue
|
|
}
|
|
|
|
if content.Attributes[attrS.Name] == nil {
|
|
// We don't have any context here to produce a good diagnostic,
|
|
// which is why we warn in the Content docstring to minimize the
|
|
// use of required attributes on merged bodies.
|
|
diags = diags.Append(&Diagnostic{
|
|
Severity: DiagError,
|
|
Summary: "Missing required attribute",
|
|
Detail: fmt.Sprintf(
|
|
"The attribute %q is required, but was not assigned.",
|
|
attrS.Name,
|
|
),
|
|
})
|
|
}
|
|
}
|
|
|
|
leftoverBody := MergeBodies(mergedLeftovers)
|
|
return content, leftoverBody, diags
|
|
}
|