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: "", } } // 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 }