github.com/hashicorp/hcl/v2@v2.20.0/cmd/hcldec/spec.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package main 5 6 import ( 7 "fmt" 8 9 "github.com/hashicorp/hcl/v2" 10 "github.com/hashicorp/hcl/v2/ext/userfunc" 11 "github.com/hashicorp/hcl/v2/gohcl" 12 "github.com/hashicorp/hcl/v2/hcldec" 13 "github.com/zclconf/go-cty/cty" 14 "github.com/zclconf/go-cty/cty/function" 15 ) 16 17 type specFileContent struct { 18 Variables map[string]cty.Value 19 Functions map[string]function.Function 20 RootSpec hcldec.Spec 21 } 22 23 var specCtx = &hcl.EvalContext{ 24 Functions: specFuncs, 25 } 26 27 func loadSpecFile(filename string) (specFileContent, hcl.Diagnostics) { 28 file, diags := parser.ParseHCLFile(filename) 29 if diags.HasErrors() { 30 return specFileContent{RootSpec: errSpec}, diags 31 } 32 33 vars, funcs, specBody, declDiags := decodeSpecDecls(file.Body) 34 diags = append(diags, declDiags...) 35 36 spec, specDiags := decodeSpecRoot(specBody) 37 diags = append(diags, specDiags...) 38 39 return specFileContent{ 40 Variables: vars, 41 Functions: funcs, 42 RootSpec: spec, 43 }, diags 44 } 45 46 func decodeSpecDecls(body hcl.Body) (map[string]cty.Value, map[string]function.Function, hcl.Body, hcl.Diagnostics) { 47 funcs, body, diags := userfunc.DecodeUserFunctions(body, "function", func() *hcl.EvalContext { 48 return specCtx 49 }) 50 51 content, body, moreDiags := body.PartialContent(&hcl.BodySchema{ 52 Blocks: []hcl.BlockHeaderSchema{ 53 { 54 Type: "variables", 55 }, 56 }, 57 }) 58 diags = append(diags, moreDiags...) 59 60 vars := make(map[string]cty.Value) 61 for _, block := range content.Blocks { 62 // We only have one block type in our schema, so we can assume all 63 // blocks are of that type. 64 attrs, moreDiags := block.Body.JustAttributes() 65 diags = append(diags, moreDiags...) 66 67 for name, attr := range attrs { 68 val, moreDiags := attr.Expr.Value(specCtx) 69 diags = append(diags, moreDiags...) 70 vars[name] = val 71 } 72 } 73 74 return vars, funcs, body, diags 75 } 76 77 func decodeSpecRoot(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) { 78 content, diags := body.Content(specSchemaUnlabelled) 79 80 if len(content.Blocks) == 0 { 81 if diags.HasErrors() { 82 // If we already have errors then they probably explain 83 // why we have no blocks, so we'll skip our additional 84 // error message added below. 85 return errSpec, diags 86 } 87 88 diags = append(diags, &hcl.Diagnostic{ 89 Severity: hcl.DiagError, 90 Summary: "Missing spec block", 91 Detail: "A spec file must have exactly one root block specifying how to map to a JSON value.", 92 Subject: body.MissingItemRange().Ptr(), 93 }) 94 return errSpec, diags 95 } 96 97 if len(content.Blocks) > 1 { 98 diags = append(diags, &hcl.Diagnostic{ 99 Severity: hcl.DiagError, 100 Summary: "Extraneous spec block", 101 Detail: "A spec file must have exactly one root block specifying how to map to a JSON value.", 102 Subject: &content.Blocks[1].DefRange, 103 }) 104 return errSpec, diags 105 } 106 107 spec, specDiags := decodeSpecBlock(content.Blocks[0]) 108 diags = append(diags, specDiags...) 109 return spec, diags 110 } 111 112 func decodeSpecBlock(block *hcl.Block) (hcldec.Spec, hcl.Diagnostics) { 113 var impliedName string 114 if len(block.Labels) > 0 { 115 impliedName = block.Labels[0] 116 } 117 118 switch block.Type { 119 120 case "object": 121 return decodeObjectSpec(block.Body) 122 123 case "array": 124 return decodeArraySpec(block.Body) 125 126 case "attr": 127 return decodeAttrSpec(block.Body, impliedName) 128 129 case "block": 130 return decodeBlockSpec(block.Body, impliedName) 131 132 case "block_list": 133 return decodeBlockListSpec(block.Body, impliedName) 134 135 case "block_set": 136 return decodeBlockSetSpec(block.Body, impliedName) 137 138 case "block_map": 139 return decodeBlockMapSpec(block.Body, impliedName) 140 141 case "block_attrs": 142 return decodeBlockAttrsSpec(block.Body, impliedName) 143 144 case "default": 145 return decodeDefaultSpec(block.Body) 146 147 case "transform": 148 return decodeTransformSpec(block.Body) 149 150 case "literal": 151 return decodeLiteralSpec(block.Body) 152 153 default: 154 // Should never happen, because the above cases should be exhaustive 155 // for our schema. 156 var diags hcl.Diagnostics 157 diags = append(diags, &hcl.Diagnostic{ 158 Severity: hcl.DiagError, 159 Summary: "Invalid spec block", 160 Detail: fmt.Sprintf("Blocks of type %q are not expected here.", block.Type), 161 Subject: &block.TypeRange, 162 }) 163 return errSpec, diags 164 } 165 } 166 167 func decodeObjectSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) { 168 content, diags := body.Content(specSchemaLabelled) 169 170 spec := make(hcldec.ObjectSpec) 171 for _, block := range content.Blocks { 172 propSpec, propDiags := decodeSpecBlock(block) 173 diags = append(diags, propDiags...) 174 spec[block.Labels[0]] = propSpec 175 } 176 177 return spec, diags 178 } 179 180 func decodeArraySpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) { 181 content, diags := body.Content(specSchemaUnlabelled) 182 183 spec := make(hcldec.TupleSpec, 0, len(content.Blocks)) 184 for _, block := range content.Blocks { 185 elemSpec, elemDiags := decodeSpecBlock(block) 186 diags = append(diags, elemDiags...) 187 spec = append(spec, elemSpec) 188 } 189 190 return spec, diags 191 } 192 193 func decodeAttrSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) { 194 type content struct { 195 Name *string `hcl:"name"` 196 Type hcl.Expression `hcl:"type"` 197 Required *bool `hcl:"required"` 198 } 199 200 var args content 201 diags := gohcl.DecodeBody(body, nil, &args) 202 if diags.HasErrors() { 203 return errSpec, diags 204 } 205 206 spec := &hcldec.AttrSpec{ 207 Name: impliedName, 208 } 209 210 if args.Required != nil { 211 spec.Required = *args.Required 212 } 213 if args.Name != nil { 214 spec.Name = *args.Name 215 } 216 217 var typeDiags hcl.Diagnostics 218 spec.Type, typeDiags = evalTypeExpr(args.Type) 219 diags = append(diags, typeDiags...) 220 221 if spec.Name == "" { 222 diags = append(diags, &hcl.Diagnostic{ 223 Severity: hcl.DiagError, 224 Summary: "Missing name in attribute spec", 225 Detail: "The name attribute is required, to specify the attribute name that is expected in an input HCL file.", 226 Subject: body.MissingItemRange().Ptr(), 227 }) 228 return errSpec, diags 229 } 230 231 return spec, diags 232 } 233 234 func decodeBlockSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) { 235 type content struct { 236 TypeName *string `hcl:"block_type"` 237 Required *bool `hcl:"required"` 238 Nested hcl.Body `hcl:",remain"` 239 } 240 241 var args content 242 diags := gohcl.DecodeBody(body, nil, &args) 243 if diags.HasErrors() { 244 return errSpec, diags 245 } 246 247 spec := &hcldec.BlockSpec{ 248 TypeName: impliedName, 249 } 250 251 if args.Required != nil { 252 spec.Required = *args.Required 253 } 254 if args.TypeName != nil { 255 spec.TypeName = *args.TypeName 256 } 257 258 nested, nestedDiags := decodeBlockNestedSpec(args.Nested) 259 diags = append(diags, nestedDiags...) 260 spec.Nested = nested 261 262 return spec, diags 263 } 264 265 func decodeBlockListSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) { 266 type content struct { 267 TypeName *string `hcl:"block_type"` 268 MinItems *int `hcl:"min_items"` 269 MaxItems *int `hcl:"max_items"` 270 Nested hcl.Body `hcl:",remain"` 271 } 272 273 var args content 274 diags := gohcl.DecodeBody(body, nil, &args) 275 if diags.HasErrors() { 276 return errSpec, diags 277 } 278 279 spec := &hcldec.BlockListSpec{ 280 TypeName: impliedName, 281 } 282 283 if args.MinItems != nil { 284 spec.MinItems = *args.MinItems 285 } 286 if args.MaxItems != nil { 287 spec.MaxItems = *args.MaxItems 288 } 289 if args.TypeName != nil { 290 spec.TypeName = *args.TypeName 291 } 292 293 nested, nestedDiags := decodeBlockNestedSpec(args.Nested) 294 diags = append(diags, nestedDiags...) 295 spec.Nested = nested 296 297 if spec.TypeName == "" { 298 diags = append(diags, &hcl.Diagnostic{ 299 Severity: hcl.DiagError, 300 Summary: "Missing block_type in block_list spec", 301 Detail: "The block_type attribute is required, to specify the block type name that is expected in an input HCL file.", 302 Subject: body.MissingItemRange().Ptr(), 303 }) 304 return errSpec, diags 305 } 306 307 return spec, diags 308 } 309 310 func decodeBlockSetSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) { 311 type content struct { 312 TypeName *string `hcl:"block_type"` 313 MinItems *int `hcl:"min_items"` 314 MaxItems *int `hcl:"max_items"` 315 Nested hcl.Body `hcl:",remain"` 316 } 317 318 var args content 319 diags := gohcl.DecodeBody(body, nil, &args) 320 if diags.HasErrors() { 321 return errSpec, diags 322 } 323 324 spec := &hcldec.BlockSetSpec{ 325 TypeName: impliedName, 326 } 327 328 if args.MinItems != nil { 329 spec.MinItems = *args.MinItems 330 } 331 if args.MaxItems != nil { 332 spec.MaxItems = *args.MaxItems 333 } 334 if args.TypeName != nil { 335 spec.TypeName = *args.TypeName 336 } 337 338 nested, nestedDiags := decodeBlockNestedSpec(args.Nested) 339 diags = append(diags, nestedDiags...) 340 spec.Nested = nested 341 342 if spec.TypeName == "" { 343 diags = append(diags, &hcl.Diagnostic{ 344 Severity: hcl.DiagError, 345 Summary: "Missing block_type in block_set spec", 346 Detail: "The block_type attribute is required, to specify the block type name that is expected in an input HCL file.", 347 Subject: body.MissingItemRange().Ptr(), 348 }) 349 return errSpec, diags 350 } 351 352 return spec, diags 353 } 354 355 func decodeBlockMapSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) { 356 type content struct { 357 TypeName *string `hcl:"block_type"` 358 Labels []string `hcl:"labels"` 359 Nested hcl.Body `hcl:",remain"` 360 } 361 362 var args content 363 diags := gohcl.DecodeBody(body, nil, &args) 364 if diags.HasErrors() { 365 return errSpec, diags 366 } 367 368 spec := &hcldec.BlockMapSpec{ 369 TypeName: impliedName, 370 } 371 372 if args.TypeName != nil { 373 spec.TypeName = *args.TypeName 374 } 375 spec.LabelNames = args.Labels 376 377 nested, nestedDiags := decodeBlockNestedSpec(args.Nested) 378 diags = append(diags, nestedDiags...) 379 spec.Nested = nested 380 381 if spec.TypeName == "" { 382 diags = append(diags, &hcl.Diagnostic{ 383 Severity: hcl.DiagError, 384 Summary: "Missing block_type in block_map spec", 385 Detail: "The block_type attribute is required, to specify the block type name that is expected in an input HCL file.", 386 Subject: body.MissingItemRange().Ptr(), 387 }) 388 return errSpec, diags 389 } 390 if len(spec.LabelNames) < 1 { 391 diags = append(diags, &hcl.Diagnostic{ 392 Severity: hcl.DiagError, 393 Summary: "Invalid block label name list", 394 Detail: "A block_map must have at least one label specified.", 395 Subject: body.MissingItemRange().Ptr(), 396 }) 397 return errSpec, diags 398 } 399 400 if hcldec.ImpliedType(spec).HasDynamicTypes() { 401 diags = append(diags, &hcl.Diagnostic{ 402 Severity: hcl.DiagError, 403 Summary: "Invalid block_map spec", 404 Detail: "A block_map spec may not contain attributes with type 'any'.", 405 Subject: body.MissingItemRange().Ptr(), 406 }) 407 } 408 409 return spec, diags 410 } 411 412 func decodeBlockNestedSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) { 413 content, diags := body.Content(specSchemaUnlabelled) 414 415 if len(content.Blocks) == 0 { 416 if diags.HasErrors() { 417 // If we already have errors then they probably explain 418 // why we have no blocks, so we'll skip our additional 419 // error message added below. 420 return errSpec, diags 421 } 422 423 diags = append(diags, &hcl.Diagnostic{ 424 Severity: hcl.DiagError, 425 Summary: "Missing spec block", 426 Detail: "A block spec must have exactly one child spec specifying how to decode block contents.", 427 Subject: body.MissingItemRange().Ptr(), 428 }) 429 return errSpec, diags 430 } 431 432 if len(content.Blocks) > 1 { 433 diags = append(diags, &hcl.Diagnostic{ 434 Severity: hcl.DiagError, 435 Summary: "Extraneous spec block", 436 Detail: "A block spec must have exactly one child spec specifying how to decode block contents.", 437 Subject: &content.Blocks[1].DefRange, 438 }) 439 return errSpec, diags 440 } 441 442 spec, specDiags := decodeSpecBlock(content.Blocks[0]) 443 diags = append(diags, specDiags...) 444 return spec, diags 445 } 446 447 func decodeBlockAttrsSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Diagnostics) { 448 type content struct { 449 TypeName *string `hcl:"block_type"` 450 ElementType hcl.Expression `hcl:"element_type"` 451 Required *bool `hcl:"required"` 452 } 453 454 var args content 455 diags := gohcl.DecodeBody(body, nil, &args) 456 if diags.HasErrors() { 457 return errSpec, diags 458 } 459 460 spec := &hcldec.BlockAttrsSpec{ 461 TypeName: impliedName, 462 } 463 464 if args.Required != nil { 465 spec.Required = *args.Required 466 } 467 if args.TypeName != nil { 468 spec.TypeName = *args.TypeName 469 } 470 471 var typeDiags hcl.Diagnostics 472 spec.ElementType, typeDiags = evalTypeExpr(args.ElementType) 473 diags = append(diags, typeDiags...) 474 475 if spec.TypeName == "" { 476 diags = append(diags, &hcl.Diagnostic{ 477 Severity: hcl.DiagError, 478 Summary: "Missing block_type in block_attrs spec", 479 Detail: "The block_type attribute is required, to specify the block type name that is expected in an input HCL file.", 480 Subject: body.MissingItemRange().Ptr(), 481 }) 482 return errSpec, diags 483 } 484 485 return spec, diags 486 } 487 488 func decodeLiteralSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) { 489 type content struct { 490 Value cty.Value `hcl:"value"` 491 } 492 493 var args content 494 diags := gohcl.DecodeBody(body, specCtx, &args) 495 if diags.HasErrors() { 496 return errSpec, diags 497 } 498 499 return &hcldec.LiteralSpec{ 500 Value: args.Value, 501 }, diags 502 } 503 504 func decodeDefaultSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) { 505 content, diags := body.Content(specSchemaUnlabelled) 506 507 if len(content.Blocks) == 0 { 508 if diags.HasErrors() { 509 // If we already have errors then they probably explain 510 // why we have no blocks, so we'll skip our additional 511 // error message added below. 512 return errSpec, diags 513 } 514 515 diags = append(diags, &hcl.Diagnostic{ 516 Severity: hcl.DiagError, 517 Summary: "Missing spec block", 518 Detail: "A default block must have at least one nested spec, each specifying a possible outcome.", 519 Subject: body.MissingItemRange().Ptr(), 520 }) 521 return errSpec, diags 522 } 523 524 if len(content.Blocks) == 1 && !diags.HasErrors() { 525 diags = append(diags, &hcl.Diagnostic{ 526 Severity: hcl.DiagWarning, 527 Summary: "Useless default block", 528 Detail: "A default block with only one spec is equivalent to using that spec alone.", 529 Subject: &content.Blocks[1].DefRange, 530 }) 531 } 532 533 var spec hcldec.Spec 534 for _, block := range content.Blocks { 535 candidateSpec, candidateDiags := decodeSpecBlock(block) 536 diags = append(diags, candidateDiags...) 537 if candidateDiags.HasErrors() { 538 continue 539 } 540 541 if spec == nil { 542 spec = candidateSpec 543 } else { 544 spec = &hcldec.DefaultSpec{ 545 Primary: spec, 546 Default: candidateSpec, 547 } 548 } 549 } 550 551 return spec, diags 552 } 553 554 func decodeTransformSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) { 555 type content struct { 556 Result hcl.Expression `hcl:"result"` 557 Nested hcl.Body `hcl:",remain"` 558 } 559 560 var args content 561 diags := gohcl.DecodeBody(body, nil, &args) 562 if diags.HasErrors() { 563 return errSpec, diags 564 } 565 566 spec := &hcldec.TransformExprSpec{ 567 Expr: args.Result, 568 VarName: "nested", 569 TransformCtx: specCtx, 570 } 571 572 nestedContent, nestedDiags := args.Nested.Content(specSchemaUnlabelled) 573 diags = append(diags, nestedDiags...) 574 575 if len(nestedContent.Blocks) != 1 { 576 if nestedDiags.HasErrors() { 577 // If we already have errors then they probably explain 578 // why we have the wrong number of blocks, so we'll skip our 579 // additional error message added below. 580 return errSpec, diags 581 } 582 583 diags = append(diags, &hcl.Diagnostic{ 584 Severity: hcl.DiagError, 585 Summary: "Invalid transform spec", 586 Detail: "A transform spec block must have exactly one nested spec block.", 587 Subject: body.MissingItemRange().Ptr(), 588 }) 589 return errSpec, diags 590 } 591 592 nestedSpec, nestedDiags := decodeSpecBlock(nestedContent.Blocks[0]) 593 diags = append(diags, nestedDiags...) 594 spec.Wrapped = nestedSpec 595 596 return spec, diags 597 } 598 599 var errSpec = &hcldec.LiteralSpec{ 600 Value: cty.NullVal(cty.DynamicPseudoType), 601 } 602 603 var specBlockTypes = []string{ 604 "object", 605 "array", 606 607 "literal", 608 609 "attr", 610 611 "block", 612 "block_list", 613 "block_map", 614 "block_set", 615 616 "default", 617 "transform", 618 } 619 620 var specSchemaUnlabelled *hcl.BodySchema 621 var specSchemaLabelled *hcl.BodySchema 622 623 var specSchemaLabelledLabels = []string{"key"} 624 625 func init() { 626 specSchemaLabelled = &hcl.BodySchema{ 627 Blocks: make([]hcl.BlockHeaderSchema, 0, len(specBlockTypes)), 628 } 629 specSchemaUnlabelled = &hcl.BodySchema{ 630 Blocks: make([]hcl.BlockHeaderSchema, 0, len(specBlockTypes)), 631 } 632 633 for _, name := range specBlockTypes { 634 specSchemaLabelled.Blocks = append( 635 specSchemaLabelled.Blocks, 636 hcl.BlockHeaderSchema{ 637 Type: name, 638 LabelNames: specSchemaLabelledLabels, 639 }, 640 ) 641 specSchemaUnlabelled.Blocks = append( 642 specSchemaUnlabelled.Blocks, 643 hcl.BlockHeaderSchema{ 644 Type: name, 645 }, 646 ) 647 } 648 }