92e407e672
The Content and PartialContent methods deal with the case where the caller knows what structure is expected within the body, but sometimes the structure of a body is just a free-form set of attributes that the caller needs to enumerate. The idea here is that the block in question must contain only attributes, and no child blocks. For JSON this just entails interpreting every property as an attribute. For native syntax later this will mean producing an error diagnostic if any blocks appear within the body.
215 lines
6.0 KiB
Go
215 lines
6.0 KiB
Go
package zcl
|
|
|
|
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() (map[string]*Attribute, 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) 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
|
|
}
|