github.com/opentofu/opentofu@v1.7.1/internal/gohcl/decode.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package gohcl 5 6 import ( 7 "fmt" 8 "reflect" 9 10 "github.com/zclconf/go-cty/cty" 11 12 "github.com/hashicorp/hcl/v2" 13 "github.com/zclconf/go-cty/cty/convert" 14 "github.com/zclconf/go-cty/cty/gocty" 15 ) 16 17 // DecodeBody extracts the configuration within the given body into the given 18 // value. This value must be a non-nil pointer to either a struct or 19 // a map, where in the former case the configuration will be decoded using 20 // struct tags and in the latter case only attributes are allowed and their 21 // values are decoded into the map. 22 // 23 // The given EvalContext is used to resolve any variables or functions in 24 // expressions encountered while decoding. This may be nil to require only 25 // constant values, for simple applications that do not support variables or 26 // functions. 27 // 28 // The returned diagnostics should be inspected with its HasErrors method to 29 // determine if the populated value is valid and complete. If error diagnostics 30 // are returned then the given value may have been partially-populated but 31 // may still be accessed by a careful caller for static analysis and editor 32 // integration use-cases. 33 func DecodeBody(body hcl.Body, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics { 34 rv := reflect.ValueOf(val) 35 if rv.Kind() != reflect.Ptr { 36 panic(fmt.Sprintf("target value must be a pointer, not %s", rv.Type().String())) 37 } 38 39 return decodeBodyToValue(body, ctx, rv.Elem()) 40 } 41 42 func decodeBodyToValue(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics { 43 et := val.Type() 44 switch et.Kind() { 45 case reflect.Struct: 46 return decodeBodyToStruct(body, ctx, val) 47 case reflect.Map: 48 return decodeBodyToMap(body, ctx, val) 49 default: 50 panic(fmt.Sprintf("target value must be pointer to struct or map, not %s", et.String())) 51 } 52 } 53 54 func decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics { 55 schema, partial := ImpliedBodySchema(val.Interface()) 56 57 var content *hcl.BodyContent 58 var leftovers hcl.Body 59 var diags hcl.Diagnostics 60 if partial { 61 content, leftovers, diags = body.PartialContent(schema) 62 } else { 63 content, diags = body.Content(schema) 64 } 65 if content == nil { 66 return diags 67 } 68 69 tags := getFieldTags(val.Type()) 70 71 if tags.Body != nil { 72 fieldIdx := *tags.Body 73 field := val.Type().Field(fieldIdx) 74 fieldV := val.Field(fieldIdx) 75 switch { 76 case bodyType.AssignableTo(field.Type): 77 fieldV.Set(reflect.ValueOf(body)) 78 79 default: 80 diags = append(diags, decodeBodyToValue(body, ctx, fieldV)...) 81 } 82 } 83 84 if tags.Remain != nil { 85 fieldIdx := *tags.Remain 86 field := val.Type().Field(fieldIdx) 87 fieldV := val.Field(fieldIdx) 88 switch { 89 case bodyType.AssignableTo(field.Type): 90 fieldV.Set(reflect.ValueOf(leftovers)) 91 case attrsType.AssignableTo(field.Type): 92 attrs, attrsDiags := leftovers.JustAttributes() 93 if len(attrsDiags) > 0 { 94 diags = append(diags, attrsDiags...) 95 } 96 fieldV.Set(reflect.ValueOf(attrs)) 97 default: 98 diags = append(diags, decodeBodyToValue(leftovers, ctx, fieldV)...) 99 } 100 } 101 102 for name, fieldIdx := range tags.Attributes { 103 attr := content.Attributes[name] 104 field := val.Type().Field(fieldIdx) 105 fieldV := val.Field(fieldIdx) 106 107 if attr == nil { 108 if !exprType.AssignableTo(field.Type) { 109 continue 110 } 111 112 // As a special case, if the target is of type hcl.Expression then 113 // we'll assign an actual expression that evalues to a cty null, 114 // so the caller can deal with it within the cty realm rather 115 // than within the Go realm. 116 synthExpr := hcl.StaticExpr(cty.NullVal(cty.DynamicPseudoType), body.MissingItemRange()) 117 fieldV.Set(reflect.ValueOf(synthExpr)) 118 continue 119 } 120 121 switch { 122 case attrType.AssignableTo(field.Type): 123 fieldV.Set(reflect.ValueOf(attr)) 124 case exprType.AssignableTo(field.Type): 125 fieldV.Set(reflect.ValueOf(attr.Expr)) 126 case field.Type.Kind() == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct: 127 // TODO might want to check for nil here 128 rn := reflect.New(field.Type.Elem()) 129 fieldV.Set(rn) 130 diags = append(diags, DecodeExpression( 131 attr.Expr, ctx, fieldV.Interface(), 132 )...) 133 default: 134 diags = append(diags, DecodeExpression( 135 attr.Expr, ctx, fieldV.Addr().Interface(), 136 )...) 137 } 138 } 139 140 blocksByType := content.Blocks.ByType() 141 142 for typeName, fieldIdx := range tags.Blocks { 143 blocks := blocksByType[typeName] 144 field := val.Type().Field(fieldIdx) 145 146 ty := field.Type 147 isSlice := false 148 isPtr := false 149 if ty.Kind() == reflect.Slice { 150 isSlice = true 151 ty = ty.Elem() 152 } 153 if ty.Kind() == reflect.Ptr { 154 isPtr = true 155 ty = ty.Elem() 156 } 157 158 if len(blocks) > 1 && !isSlice { 159 diags = append(diags, &hcl.Diagnostic{ 160 Severity: hcl.DiagError, 161 Summary: fmt.Sprintf("Duplicate %s block", typeName), 162 Detail: fmt.Sprintf( 163 "Only one %s block is allowed. Another was defined at %s.", 164 typeName, blocks[0].DefRange.String(), 165 ), 166 Subject: &blocks[1].DefRange, 167 }) 168 continue 169 } 170 171 if len(blocks) == 0 { 172 if isSlice || isPtr { 173 if val.Field(fieldIdx).IsNil() { 174 val.Field(fieldIdx).Set(reflect.Zero(field.Type)) 175 } 176 } else { 177 diags = append(diags, &hcl.Diagnostic{ 178 Severity: hcl.DiagError, 179 Summary: fmt.Sprintf("Missing %s block", typeName), 180 Detail: fmt.Sprintf("A %s block is required.", typeName), 181 Subject: body.MissingItemRange().Ptr(), 182 }) 183 } 184 continue 185 } 186 187 switch { 188 189 case isSlice: 190 elemType := ty 191 if isPtr { 192 elemType = reflect.PtrTo(ty) 193 } 194 sli := val.Field(fieldIdx) 195 if sli.IsNil() { 196 sli = reflect.MakeSlice(reflect.SliceOf(elemType), len(blocks), len(blocks)) 197 } 198 199 for i, block := range blocks { 200 if isPtr { 201 if i >= sli.Len() { 202 sli = reflect.Append(sli, reflect.New(ty)) 203 } 204 v := sli.Index(i) 205 if v.IsNil() { 206 v = reflect.New(ty) 207 } 208 diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...) 209 sli.Index(i).Set(v) 210 } else { 211 if i >= sli.Len() { 212 sli = reflect.Append(sli, reflect.Indirect(reflect.New(ty))) 213 } 214 diags = append(diags, decodeBlockToValue(block, ctx, sli.Index(i))...) 215 } 216 } 217 218 if sli.Len() > len(blocks) { 219 sli.SetLen(len(blocks)) 220 } 221 222 val.Field(fieldIdx).Set(sli) 223 224 default: 225 block := blocks[0] 226 if isPtr { 227 v := val.Field(fieldIdx) 228 if v.IsNil() { 229 v = reflect.New(ty) 230 } 231 diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...) 232 val.Field(fieldIdx).Set(v) 233 } else { 234 diags = append(diags, decodeBlockToValue(block, ctx, val.Field(fieldIdx))...) 235 } 236 237 } 238 239 } 240 241 return diags 242 } 243 244 func decodeBodyToMap(body hcl.Body, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics { 245 attrs, diags := body.JustAttributes() 246 if attrs == nil { 247 return diags 248 } 249 250 mv := reflect.MakeMap(v.Type()) 251 252 for k, attr := range attrs { 253 switch { 254 case attrType.AssignableTo(v.Type().Elem()): 255 mv.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(attr)) 256 case exprType.AssignableTo(v.Type().Elem()): 257 mv.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(attr.Expr)) 258 default: 259 ev := reflect.New(v.Type().Elem()) 260 diags = append(diags, DecodeExpression(attr.Expr, ctx, ev.Interface())...) 261 mv.SetMapIndex(reflect.ValueOf(k), ev.Elem()) 262 } 263 } 264 265 v.Set(mv) 266 267 return diags 268 } 269 270 func decodeBlockToValue(block *hcl.Block, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics { 271 diags := decodeBodyToValue(block.Body, ctx, v) 272 273 if len(block.Labels) > 0 { 274 blockTags := getFieldTags(v.Type()) 275 for li, lv := range block.Labels { 276 lfieldIdx := blockTags.Labels[li].FieldIndex 277 v.Field(lfieldIdx).Set(reflect.ValueOf(lv)) 278 } 279 } 280 281 return diags 282 } 283 284 // DecodeExpression extracts the value of the given expression into the given 285 // value. This value must be something that gocty is able to decode into, 286 // since the final decoding is delegated to that package. If a reference to 287 // a struct is provided which contains gohcl tags, it will be decoded using 288 // the attr and optional tags. 289 // 290 // The given EvalContext is used to resolve any variables or functions in 291 // expressions encountered while decoding. This may be nil to require only 292 // constant values, for simple applications that do not support variables or 293 // functions. 294 // 295 // The returned diagnostics should be inspected with its HasErrors method to 296 // determine if the populated value is valid and complete. If error diagnostics 297 // are returned then the given value may have been partially-populated but 298 // may still be accessed by a careful caller for static analysis and editor 299 // integration use-cases. 300 func DecodeExpression(expr hcl.Expression, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics { 301 srcVal, diags := expr.Value(ctx) 302 if diags.HasErrors() { 303 return diags 304 } 305 306 return append(diags, DecodeValue(srcVal, expr.StartRange(), expr.Range(), val)...) 307 } 308 309 // DecodeValue extracts the given value into the provided target. 310 // This value must be something that gocty is able to decode into, 311 // since the final decoding is delegated to that package. If a reference to 312 // a struct is provided which contains gohcl tags, it will be decoded using 313 // the attr and optional tags. 314 // 315 // The returned diagnostics should be inspected with its HasErrors method to 316 // determine if the populated value is valid and complete. If error diagnostics 317 // are returned then the given value may have been partially-populated but 318 // may still be accessed by a careful caller for static analysis and editor 319 // integration use-cases. 320 func DecodeValue(srcVal cty.Value, subject hcl.Range, context hcl.Range, val interface{}) hcl.Diagnostics { 321 rv := reflect.ValueOf(val) 322 if rv.Type().Kind() == reflect.Ptr && rv.Type().Elem().Kind() == reflect.Struct && hasFieldTags(rv.Elem().Type()) { 323 attrs := make(hcl.Attributes) 324 for k, v := range srcVal.AsValueMap() { 325 attrs[k] = &hcl.Attribute{ 326 Name: k, 327 Expr: hcl.StaticExpr(v, context), 328 Range: subject, 329 } 330 331 } 332 return decodeBodyToStruct(synthBody{ 333 attrs: attrs, 334 subject: subject, 335 context: context, 336 }, nil, rv.Elem()) 337 338 } 339 340 convTy, err := gocty.ImpliedType(val) 341 if err != nil { 342 panic(fmt.Sprintf("unsuitable DecodeExpression target: %s", err)) 343 } 344 345 var diags hcl.Diagnostics 346 347 srcVal, err = convert.Convert(srcVal, convTy) 348 if err != nil { 349 diags = append(diags, &hcl.Diagnostic{ 350 Severity: hcl.DiagError, 351 Summary: "Unsuitable value type", 352 Detail: fmt.Sprintf("Unsuitable value: %s", err.Error()), 353 Subject: subject.Ptr(), 354 Context: context.Ptr(), 355 }) 356 return diags 357 } 358 359 err = gocty.FromCtyValue(srcVal, val) 360 if err != nil { 361 diags = append(diags, &hcl.Diagnostic{ 362 Severity: hcl.DiagError, 363 Summary: "Unsuitable value type", 364 Detail: fmt.Sprintf("Unsuitable value: %s", err.Error()), 365 Subject: subject.Ptr(), 366 Context: context.Ptr(), 367 }) 368 } 369 370 return diags 371 } 372 373 type synthBody struct { 374 attrs hcl.Attributes 375 subject hcl.Range 376 context hcl.Range 377 } 378 379 func (s synthBody) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) { 380 body, partial, diags := s.PartialContent(schema) 381 382 attrs, _ := partial.JustAttributes() 383 for name := range attrs { 384 diags = append(diags, &hcl.Diagnostic{ 385 Severity: hcl.DiagError, 386 Summary: "Unsupported argument", 387 Detail: fmt.Sprintf("An argument named %q is not expected here.", name), 388 Subject: s.subject.Ptr(), 389 Context: s.context.Ptr(), 390 }) 391 } 392 393 return body, diags 394 } 395 396 func (s synthBody) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) { 397 var diags hcl.Diagnostics 398 399 for _, block := range schema.Blocks { 400 panic("hcl block tags are not allowed in attribute structs: " + block.Type) 401 } 402 403 attrs := make(hcl.Attributes) 404 remainder := make(hcl.Attributes) 405 406 for _, attr := range schema.Attributes { 407 v, ok := s.attrs[attr.Name] 408 if !ok { 409 if attr.Required { 410 diags = append(diags, &hcl.Diagnostic{ 411 Severity: hcl.DiagError, 412 Summary: "Missing required argument", 413 Detail: fmt.Sprintf("The argument %q is required, but no definition was found.", attr.Name), 414 Subject: s.subject.Ptr(), 415 Context: s.context.Ptr(), 416 }) 417 } 418 continue 419 } 420 421 attrs[attr.Name] = v 422 } 423 424 for k, v := range s.attrs { 425 if _, ok := attrs[k]; !ok { 426 remainder[k] = v 427 } 428 } 429 430 return &hcl.BodyContent{ 431 Attributes: attrs, 432 MissingItemRange: s.context, 433 }, synthBody{attrs: remainder}, diags 434 } 435 436 func (s synthBody) JustAttributes() (hcl.Attributes, hcl.Diagnostics) { 437 return s.attrs, nil 438 } 439 func (s synthBody) MissingItemRange() hcl.Range { 440 return s.context 441 }