1package hcl
2
3import (
4	"fmt"
5)
6
7// MergeFiles combines the given files to produce a single body that contains
8// configuration from all of the given files.
9//
10// The ordering of the given files decides the order in which contained
11// elements will be returned. If any top-level attributes are defined with
12// the same name across multiple files, a diagnostic will be produced from
13// the Content and PartialContent methods describing this error in a
14// user-friendly way.
15func MergeFiles(files []*File) Body {
16	var bodies []Body
17	for _, file := range files {
18		bodies = append(bodies, file.Body)
19	}
20	return MergeBodies(bodies)
21}
22
23// MergeBodies is like MergeFiles except it deals directly with bodies, rather
24// than with entire files.
25func MergeBodies(bodies []Body) Body {
26	if len(bodies) == 0 {
27		// Swap out for our singleton empty body, to reduce the number of
28		// empty slices we have hanging around.
29		return emptyBody
30	}
31
32	// If any of the given bodies are already merged bodies, we'll unpack
33	// to flatten to a single mergedBodies, since that's conceptually simpler.
34	// This also, as a side-effect, eliminates any empty bodies, since
35	// empties are merged bodies with no inner bodies.
36	var newLen int
37	var flatten bool
38	for _, body := range bodies {
39		if children, merged := body.(mergedBodies); merged {
40			newLen += len(children)
41			flatten = true
42		} else {
43			newLen++
44		}
45	}
46
47	if !flatten { // not just newLen == len, because we might have mergedBodies with single bodies inside
48		return mergedBodies(bodies)
49	}
50
51	if newLen == 0 {
52		// Don't allocate a new empty when we already have one
53		return emptyBody
54	}
55
56	new := make([]Body, 0, newLen)
57	for _, body := range bodies {
58		if children, merged := body.(mergedBodies); merged {
59			new = append(new, children...)
60		} else {
61			new = append(new, body)
62		}
63	}
64	return mergedBodies(new)
65}
66
67var emptyBody = mergedBodies([]Body{})
68
69// EmptyBody returns a body with no content. This body can be used as a
70// placeholder when a body is required but no body content is available.
71func EmptyBody() Body {
72	return emptyBody
73}
74
75type mergedBodies []Body
76
77// Content returns the content produced by applying the given schema to all
78// of the merged bodies and merging the result.
79//
80// Although required attributes _are_ supported, they should be used sparingly
81// with merged bodies since in this case there is no contextual information
82// with which to return good diagnostics. Applications working with merged
83// bodies may wish to mark all attributes as optional and then check for
84// required attributes afterwards, to produce better diagnostics.
85func (mb mergedBodies) Content(schema *BodySchema) (*BodyContent, Diagnostics) {
86	// the returned body will always be empty in this case, because mergedContent
87	// will only ever call Content on the child bodies.
88	content, _, diags := mb.mergedContent(schema, false)
89	return content, diags
90}
91
92func (mb mergedBodies) PartialContent(schema *BodySchema) (*BodyContent, Body, Diagnostics) {
93	return mb.mergedContent(schema, true)
94}
95
96func (mb mergedBodies) JustAttributes() (Attributes, Diagnostics) {
97	attrs := make(map[string]*Attribute)
98	var diags Diagnostics
99
100	for _, body := range mb {
101		thisAttrs, thisDiags := body.JustAttributes()
102
103		if len(thisDiags) != 0 {
104			diags = append(diags, thisDiags...)
105		}
106
107		if thisAttrs != nil {
108			for name, attr := range thisAttrs {
109				if existing := attrs[name]; existing != nil {
110					diags = diags.Append(&Diagnostic{
111						Severity: DiagError,
112						Summary:  "Duplicate argument",
113						Detail: fmt.Sprintf(
114							"Argument %q was already set at %s",
115							name, existing.NameRange.String(),
116						),
117						Subject: &attr.NameRange,
118					})
119					continue
120				}
121
122				attrs[name] = attr
123			}
124		}
125	}
126
127	return attrs, diags
128}
129
130func (mb mergedBodies) MissingItemRange() Range {
131	if len(mb) == 0 {
132		// Nothing useful to return here, so we'll return some garbage.
133		return Range{
134			Filename: "<empty>",
135		}
136	}
137
138	// arbitrarily use the first body's missing item range
139	return mb[0].MissingItemRange()
140}
141
142func (mb mergedBodies) mergedContent(schema *BodySchema, partial bool) (*BodyContent, Body, Diagnostics) {
143	// We need to produce a new schema with none of the attributes marked as
144	// required, since _any one_ of our bodies can contribute an attribute value.
145	// We'll separately check that all required attributes are present at
146	// the end.
147	mergedSchema := &BodySchema{
148		Blocks: schema.Blocks,
149	}
150	for _, attrS := range schema.Attributes {
151		mergedAttrS := attrS
152		mergedAttrS.Required = false
153		mergedSchema.Attributes = append(mergedSchema.Attributes, mergedAttrS)
154	}
155
156	var mergedLeftovers []Body
157	content := &BodyContent{
158		Attributes: map[string]*Attribute{},
159	}
160
161	var diags Diagnostics
162	for _, body := range mb {
163		var thisContent *BodyContent
164		var thisLeftovers Body
165		var thisDiags Diagnostics
166
167		if partial {
168			thisContent, thisLeftovers, thisDiags = body.PartialContent(mergedSchema)
169		} else {
170			thisContent, thisDiags = body.Content(mergedSchema)
171		}
172
173		if thisLeftovers != nil {
174			mergedLeftovers = append(mergedLeftovers, thisLeftovers)
175		}
176		if len(thisDiags) != 0 {
177			diags = append(diags, thisDiags...)
178		}
179
180		if thisContent.Attributes != nil {
181			for name, attr := range thisContent.Attributes {
182				if existing := content.Attributes[name]; existing != nil {
183					diags = diags.Append(&Diagnostic{
184						Severity: DiagError,
185						Summary:  "Duplicate argument",
186						Detail: fmt.Sprintf(
187							"Argument %q was already set at %s",
188							name, existing.NameRange.String(),
189						),
190						Subject: &attr.NameRange,
191					})
192					continue
193				}
194				content.Attributes[name] = attr
195			}
196		}
197
198		if len(thisContent.Blocks) != 0 {
199			content.Blocks = append(content.Blocks, thisContent.Blocks...)
200		}
201	}
202
203	// Finally, we check for required attributes.
204	for _, attrS := range schema.Attributes {
205		if !attrS.Required {
206			continue
207		}
208
209		if content.Attributes[attrS.Name] == nil {
210			// We don't have any context here to produce a good diagnostic,
211			// which is why we warn in the Content docstring to minimize the
212			// use of required attributes on merged bodies.
213			diags = diags.Append(&Diagnostic{
214				Severity: DiagError,
215				Summary:  "Missing required argument",
216				Detail: fmt.Sprintf(
217					"The argument %q is required, but was not set.",
218					attrS.Name,
219				),
220			})
221		}
222	}
223
224	leftoverBody := MergeBodies(mergedLeftovers)
225	return content, leftoverBody, diags
226}
227