6c4344623b
The main HCL package is more visible this way, and so it's easier than having to pick it out from dozens of other package directories.
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 argument",
|
|
Detail: fmt.Sprintf(
|
|
"Argument %q was already set 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, thisLeftovers)
|
|
}
|
|
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 argument",
|
|
Detail: fmt.Sprintf(
|
|
"Argument %q was already set 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 argument",
|
|
Detail: fmt.Sprintf(
|
|
"The argument %q is required, but was not set.",
|
|
attrS.Name,
|
|
),
|
|
})
|
|
}
|
|
}
|
|
|
|
leftoverBody := MergeBodies(mergedLeftovers)
|
|
return content, leftoverBody, diags
|
|
}
|