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