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