github.com/hashicorp/hcl/v2@v2.20.0/gohcl/decode_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package gohcl 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "reflect" 10 "testing" 11 12 "github.com/davecgh/go-spew/spew" 13 "github.com/hashicorp/hcl/v2" 14 hclJSON "github.com/hashicorp/hcl/v2/json" 15 "github.com/zclconf/go-cty/cty" 16 ) 17 18 func TestDecodeBody(t *testing.T) { 19 deepEquals := func(other interface{}) func(v interface{}) bool { 20 return func(v interface{}) bool { 21 return reflect.DeepEqual(v, other) 22 } 23 } 24 25 type withNameExpression struct { 26 Name hcl.Expression `hcl:"name"` 27 } 28 29 type withTwoAttributes struct { 30 A string `hcl:"a,optional"` 31 B string `hcl:"b,optional"` 32 } 33 34 type withNestedBlock struct { 35 Plain string `hcl:"plain,optional"` 36 Nested *withTwoAttributes `hcl:"nested,block"` 37 } 38 39 type withListofNestedBlocks struct { 40 Nested []*withTwoAttributes `hcl:"nested,block"` 41 } 42 43 type withListofNestedBlocksNoPointers struct { 44 Nested []withTwoAttributes `hcl:"nested,block"` 45 } 46 47 tests := []struct { 48 Body map[string]interface{} 49 Target func() interface{} 50 Check func(v interface{}) bool 51 DiagCount int 52 }{ 53 { 54 map[string]interface{}{}, 55 makeInstantiateType(struct{}{}), 56 deepEquals(struct{}{}), 57 0, 58 }, 59 { 60 map[string]interface{}{}, 61 makeInstantiateType(struct { 62 Name string `hcl:"name"` 63 }{}), 64 deepEquals(struct { 65 Name string `hcl:"name"` 66 }{}), 67 1, // name is required 68 }, 69 { 70 map[string]interface{}{}, 71 makeInstantiateType(struct { 72 Name *string `hcl:"name"` 73 }{}), 74 deepEquals(struct { 75 Name *string `hcl:"name"` 76 }{}), 77 0, 78 }, // name nil 79 { 80 map[string]interface{}{}, 81 makeInstantiateType(struct { 82 Name string `hcl:"name,optional"` 83 }{}), 84 deepEquals(struct { 85 Name string `hcl:"name,optional"` 86 }{}), 87 0, 88 }, // name optional 89 { 90 map[string]interface{}{}, 91 makeInstantiateType(withNameExpression{}), 92 func(v interface{}) bool { 93 if v == nil { 94 return false 95 } 96 97 wne, valid := v.(withNameExpression) 98 if !valid { 99 return false 100 } 101 102 if wne.Name == nil { 103 return false 104 } 105 106 nameVal, _ := wne.Name.Value(nil) 107 if !nameVal.IsNull() { 108 return false 109 } 110 111 return true 112 }, 113 0, 114 }, 115 { 116 map[string]interface{}{ 117 "name": "Ermintrude", 118 }, 119 makeInstantiateType(withNameExpression{}), 120 func(v interface{}) bool { 121 if v == nil { 122 return false 123 } 124 125 wne, valid := v.(withNameExpression) 126 if !valid { 127 return false 128 } 129 130 if wne.Name == nil { 131 return false 132 } 133 134 nameVal, _ := wne.Name.Value(nil) 135 if !nameVal.Equals(cty.StringVal("Ermintrude")).True() { 136 return false 137 } 138 139 return true 140 }, 141 0, 142 }, 143 { 144 map[string]interface{}{ 145 "name": "Ermintrude", 146 }, 147 makeInstantiateType(struct { 148 Name string `hcl:"name"` 149 }{}), 150 deepEquals(struct { 151 Name string `hcl:"name"` 152 }{"Ermintrude"}), 153 0, 154 }, 155 { 156 map[string]interface{}{ 157 "name": "Ermintrude", 158 "age": 23, 159 }, 160 makeInstantiateType(struct { 161 Name string `hcl:"name"` 162 }{}), 163 deepEquals(struct { 164 Name string `hcl:"name"` 165 }{"Ermintrude"}), 166 1, // Extraneous "age" property 167 }, 168 { 169 map[string]interface{}{ 170 "name": "Ermintrude", 171 "age": 50, 172 }, 173 makeInstantiateType(struct { 174 Name string `hcl:"name"` 175 Attrs hcl.Attributes `hcl:",remain"` 176 }{}), 177 func(gotI interface{}) bool { 178 got := gotI.(struct { 179 Name string `hcl:"name"` 180 Attrs hcl.Attributes `hcl:",remain"` 181 }) 182 return got.Name == "Ermintrude" && len(got.Attrs) == 1 && got.Attrs["age"] != nil 183 }, 184 0, 185 }, 186 { 187 map[string]interface{}{ 188 "name": "Ermintrude", 189 "age": 50, 190 }, 191 makeInstantiateType(struct { 192 Name string `hcl:"name"` 193 Remain hcl.Body `hcl:",remain"` 194 }{}), 195 func(gotI interface{}) bool { 196 got := gotI.(struct { 197 Name string `hcl:"name"` 198 Remain hcl.Body `hcl:",remain"` 199 }) 200 201 attrs, _ := got.Remain.JustAttributes() 202 203 return got.Name == "Ermintrude" && len(attrs) == 1 && attrs["age"] != nil 204 }, 205 0, 206 }, 207 { 208 map[string]interface{}{ 209 "name": "Ermintrude", 210 "living": true, 211 }, 212 makeInstantiateType(struct { 213 Name string `hcl:"name"` 214 Remain map[string]cty.Value `hcl:",remain"` 215 }{}), 216 deepEquals(struct { 217 Name string `hcl:"name"` 218 Remain map[string]cty.Value `hcl:",remain"` 219 }{ 220 Name: "Ermintrude", 221 Remain: map[string]cty.Value{ 222 "living": cty.True, 223 }, 224 }), 225 0, 226 }, 227 { 228 map[string]interface{}{ 229 "name": "Ermintrude", 230 "age": 50, 231 }, 232 makeInstantiateType(struct { 233 Name string `hcl:"name"` 234 Body hcl.Body `hcl:",body"` 235 Remain hcl.Body `hcl:",remain"` 236 }{}), 237 func(gotI interface{}) bool { 238 got := gotI.(struct { 239 Name string `hcl:"name"` 240 Body hcl.Body `hcl:",body"` 241 Remain hcl.Body `hcl:",remain"` 242 }) 243 244 attrs, _ := got.Body.JustAttributes() 245 246 return got.Name == "Ermintrude" && len(attrs) == 2 && 247 attrs["name"] != nil && attrs["age"] != nil 248 }, 249 0, 250 }, 251 { 252 map[string]interface{}{ 253 "noodle": map[string]interface{}{}, 254 }, 255 makeInstantiateType(struct { 256 Noodle struct{} `hcl:"noodle,block"` 257 }{}), 258 func(gotI interface{}) bool { 259 // Generating no diagnostics is good enough for this one. 260 return true 261 }, 262 0, 263 }, 264 { 265 map[string]interface{}{ 266 "noodle": []map[string]interface{}{{}}, 267 }, 268 makeInstantiateType(struct { 269 Noodle struct{} `hcl:"noodle,block"` 270 }{}), 271 func(gotI interface{}) bool { 272 // Generating no diagnostics is good enough for this one. 273 return true 274 }, 275 0, 276 }, 277 { 278 map[string]interface{}{ 279 "noodle": []map[string]interface{}{{}, {}}, 280 }, 281 makeInstantiateType(struct { 282 Noodle struct{} `hcl:"noodle,block"` 283 }{}), 284 func(gotI interface{}) bool { 285 // Generating one diagnostic is good enough for this one. 286 return true 287 }, 288 1, 289 }, 290 { 291 map[string]interface{}{}, 292 makeInstantiateType(struct { 293 Noodle struct{} `hcl:"noodle,block"` 294 }{}), 295 func(gotI interface{}) bool { 296 // Generating one diagnostic is good enough for this one. 297 return true 298 }, 299 1, 300 }, 301 { 302 map[string]interface{}{ 303 "noodle": []map[string]interface{}{}, 304 }, 305 makeInstantiateType(struct { 306 Noodle struct{} `hcl:"noodle,block"` 307 }{}), 308 func(gotI interface{}) bool { 309 // Generating one diagnostic is good enough for this one. 310 return true 311 }, 312 1, 313 }, 314 { 315 map[string]interface{}{ 316 "noodle": map[string]interface{}{}, 317 }, 318 makeInstantiateType(struct { 319 Noodle *struct{} `hcl:"noodle,block"` 320 }{}), 321 func(gotI interface{}) bool { 322 return gotI.(struct { 323 Noodle *struct{} `hcl:"noodle,block"` 324 }).Noodle != nil 325 }, 326 0, 327 }, 328 { 329 map[string]interface{}{ 330 "noodle": []map[string]interface{}{{}}, 331 }, 332 makeInstantiateType(struct { 333 Noodle *struct{} `hcl:"noodle,block"` 334 }{}), 335 func(gotI interface{}) bool { 336 return gotI.(struct { 337 Noodle *struct{} `hcl:"noodle,block"` 338 }).Noodle != nil 339 }, 340 0, 341 }, 342 { 343 map[string]interface{}{ 344 "noodle": []map[string]interface{}{}, 345 }, 346 makeInstantiateType(struct { 347 Noodle *struct{} `hcl:"noodle,block"` 348 }{}), 349 func(gotI interface{}) bool { 350 return gotI.(struct { 351 Noodle *struct{} `hcl:"noodle,block"` 352 }).Noodle == nil 353 }, 354 0, 355 }, 356 { 357 map[string]interface{}{ 358 "noodle": []map[string]interface{}{{}, {}}, 359 }, 360 makeInstantiateType(struct { 361 Noodle *struct{} `hcl:"noodle,block"` 362 }{}), 363 func(gotI interface{}) bool { 364 // Generating one diagnostic is good enough for this one. 365 return true 366 }, 367 1, 368 }, 369 { 370 map[string]interface{}{ 371 "noodle": []map[string]interface{}{}, 372 }, 373 makeInstantiateType(struct { 374 Noodle []struct{} `hcl:"noodle,block"` 375 }{}), 376 func(gotI interface{}) bool { 377 noodle := gotI.(struct { 378 Noodle []struct{} `hcl:"noodle,block"` 379 }).Noodle 380 return len(noodle) == 0 381 }, 382 0, 383 }, 384 { 385 map[string]interface{}{ 386 "noodle": []map[string]interface{}{{}}, 387 }, 388 makeInstantiateType(struct { 389 Noodle []struct{} `hcl:"noodle,block"` 390 }{}), 391 func(gotI interface{}) bool { 392 noodle := gotI.(struct { 393 Noodle []struct{} `hcl:"noodle,block"` 394 }).Noodle 395 return len(noodle) == 1 396 }, 397 0, 398 }, 399 { 400 map[string]interface{}{ 401 "noodle": []map[string]interface{}{{}, {}}, 402 }, 403 makeInstantiateType(struct { 404 Noodle []struct{} `hcl:"noodle,block"` 405 }{}), 406 func(gotI interface{}) bool { 407 noodle := gotI.(struct { 408 Noodle []struct{} `hcl:"noodle,block"` 409 }).Noodle 410 return len(noodle) == 2 411 }, 412 0, 413 }, 414 { 415 map[string]interface{}{ 416 "noodle": map[string]interface{}{}, 417 }, 418 makeInstantiateType(struct { 419 Noodle struct { 420 Name string `hcl:"name,label"` 421 } `hcl:"noodle,block"` 422 }{}), 423 func(gotI interface{}) bool { 424 // Generating two diagnostics is good enough for this one. 425 // (one for the missing noodle block and the other for 426 // the JSON serialization detecting the missing level of 427 // heirarchy for the label.) 428 return true 429 }, 430 2, 431 }, 432 { 433 map[string]interface{}{ 434 "noodle": map[string]interface{}{ 435 "foo_foo": map[string]interface{}{}, 436 }, 437 }, 438 makeInstantiateType(struct { 439 Noodle struct { 440 Name string `hcl:"name,label"` 441 } `hcl:"noodle,block"` 442 }{}), 443 func(gotI interface{}) bool { 444 noodle := gotI.(struct { 445 Noodle struct { 446 Name string `hcl:"name,label"` 447 } `hcl:"noodle,block"` 448 }).Noodle 449 return noodle.Name == "foo_foo" 450 }, 451 0, 452 }, 453 { 454 map[string]interface{}{ 455 "noodle": map[string]interface{}{ 456 "foo_foo": map[string]interface{}{}, 457 "bar_baz": map[string]interface{}{}, 458 }, 459 }, 460 makeInstantiateType(struct { 461 Noodle struct { 462 Name string `hcl:"name,label"` 463 } `hcl:"noodle,block"` 464 }{}), 465 func(gotI interface{}) bool { 466 // One diagnostic is enough for this one. 467 return true 468 }, 469 1, 470 }, 471 { 472 map[string]interface{}{ 473 "noodle": map[string]interface{}{ 474 "foo_foo": map[string]interface{}{}, 475 "bar_baz": map[string]interface{}{}, 476 }, 477 }, 478 makeInstantiateType(struct { 479 Noodles []struct { 480 Name string `hcl:"name,label"` 481 } `hcl:"noodle,block"` 482 }{}), 483 func(gotI interface{}) bool { 484 noodles := gotI.(struct { 485 Noodles []struct { 486 Name string `hcl:"name,label"` 487 } `hcl:"noodle,block"` 488 }).Noodles 489 return len(noodles) == 2 && (noodles[0].Name == "foo_foo" || noodles[0].Name == "bar_baz") && (noodles[1].Name == "foo_foo" || noodles[1].Name == "bar_baz") && noodles[0].Name != noodles[1].Name 490 }, 491 0, 492 }, 493 { 494 map[string]interface{}{ 495 "noodle": map[string]interface{}{ 496 "foo_foo": map[string]interface{}{ 497 "type": "rice", 498 }, 499 }, 500 }, 501 makeInstantiateType(struct { 502 Noodle struct { 503 Name string `hcl:"name,label"` 504 Type string `hcl:"type"` 505 } `hcl:"noodle,block"` 506 }{}), 507 func(gotI interface{}) bool { 508 noodle := gotI.(struct { 509 Noodle struct { 510 Name string `hcl:"name,label"` 511 Type string `hcl:"type"` 512 } `hcl:"noodle,block"` 513 }).Noodle 514 return noodle.Name == "foo_foo" && noodle.Type == "rice" 515 }, 516 0, 517 }, 518 519 { 520 map[string]interface{}{ 521 "name": "Ermintrude", 522 "age": 34, 523 }, 524 makeInstantiateType(map[string]string(nil)), 525 deepEquals(map[string]string{ 526 "name": "Ermintrude", 527 "age": "34", 528 }), 529 0, 530 }, 531 { 532 map[string]interface{}{ 533 "name": "Ermintrude", 534 "age": 89, 535 }, 536 makeInstantiateType(map[string]*hcl.Attribute(nil)), 537 func(gotI interface{}) bool { 538 got := gotI.(map[string]*hcl.Attribute) 539 return len(got) == 2 && got["name"] != nil && got["age"] != nil 540 }, 541 0, 542 }, 543 { 544 map[string]interface{}{ 545 "name": "Ermintrude", 546 "age": 13, 547 }, 548 makeInstantiateType(map[string]hcl.Expression(nil)), 549 func(gotI interface{}) bool { 550 got := gotI.(map[string]hcl.Expression) 551 return len(got) == 2 && got["name"] != nil && got["age"] != nil 552 }, 553 0, 554 }, 555 { 556 map[string]interface{}{ 557 "name": "Ermintrude", 558 "living": true, 559 }, 560 makeInstantiateType(map[string]cty.Value(nil)), 561 deepEquals(map[string]cty.Value{ 562 "name": cty.StringVal("Ermintrude"), 563 "living": cty.True, 564 }), 565 0, 566 }, 567 { 568 // Retain "nested" block while decoding 569 map[string]interface{}{ 570 "plain": "foo", 571 }, 572 func() interface{} { 573 return &withNestedBlock{ 574 Plain: "bar", 575 Nested: &withTwoAttributes{ 576 A: "bar", 577 }, 578 } 579 }, 580 func(gotI interface{}) bool { 581 foo := gotI.(withNestedBlock) 582 return foo.Plain == "foo" && foo.Nested != nil && foo.Nested.A == "bar" 583 }, 584 0, 585 }, 586 { 587 // Retain values in "nested" block while decoding 588 map[string]interface{}{ 589 "nested": map[string]interface{}{ 590 "a": "foo", 591 }, 592 }, 593 func() interface{} { 594 return &withNestedBlock{ 595 Nested: &withTwoAttributes{ 596 B: "bar", 597 }, 598 } 599 }, 600 func(gotI interface{}) bool { 601 foo := gotI.(withNestedBlock) 602 return foo.Nested.A == "foo" && foo.Nested.B == "bar" 603 }, 604 0, 605 }, 606 { 607 // Retain values in "nested" block list while decoding 608 map[string]interface{}{ 609 "nested": []map[string]interface{}{ 610 { 611 "a": "foo", 612 }, 613 }, 614 }, 615 func() interface{} { 616 return &withListofNestedBlocks{ 617 Nested: []*withTwoAttributes{ 618 &withTwoAttributes{ 619 B: "bar", 620 }, 621 }, 622 } 623 }, 624 func(gotI interface{}) bool { 625 n := gotI.(withListofNestedBlocks) 626 return n.Nested[0].A == "foo" && n.Nested[0].B == "bar" 627 }, 628 0, 629 }, 630 { 631 // Remove additional elements from the list while decoding nested blocks 632 map[string]interface{}{ 633 "nested": []map[string]interface{}{ 634 { 635 "a": "foo", 636 }, 637 }, 638 }, 639 func() interface{} { 640 return &withListofNestedBlocks{ 641 Nested: []*withTwoAttributes{ 642 &withTwoAttributes{ 643 B: "bar", 644 }, 645 &withTwoAttributes{ 646 B: "bar", 647 }, 648 }, 649 } 650 }, 651 func(gotI interface{}) bool { 652 n := gotI.(withListofNestedBlocks) 653 return len(n.Nested) == 1 654 }, 655 0, 656 }, 657 { 658 // Make sure decoding value slices works the same as pointer slices. 659 map[string]interface{}{ 660 "nested": []map[string]interface{}{ 661 { 662 "b": "bar", 663 }, 664 { 665 "b": "baz", 666 }, 667 }, 668 }, 669 func() interface{} { 670 return &withListofNestedBlocksNoPointers{ 671 Nested: []withTwoAttributes{ 672 { 673 B: "foo", 674 }, 675 }, 676 } 677 }, 678 func(gotI interface{}) bool { 679 n := gotI.(withListofNestedBlocksNoPointers) 680 return n.Nested[0].B == "bar" && len(n.Nested) == 2 681 }, 682 0, 683 }, 684 } 685 686 for i, test := range tests { 687 // For convenience here we're going to use the JSON parser 688 // to process the given body. 689 buf, err := json.Marshal(test.Body) 690 if err != nil { 691 t.Fatalf("error JSON-encoding body for test %d: %s", i, err) 692 } 693 694 t.Run(string(buf), func(t *testing.T) { 695 file, diags := hclJSON.Parse(buf, "test.json") 696 if len(diags) != 0 { 697 t.Fatalf("diagnostics while parsing: %s", diags.Error()) 698 } 699 700 targetVal := reflect.ValueOf(test.Target()) 701 702 diags = DecodeBody(file.Body, nil, targetVal.Interface()) 703 if len(diags) != test.DiagCount { 704 t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount) 705 for _, diag := range diags { 706 t.Logf(" - %s", diag.Error()) 707 } 708 } 709 got := targetVal.Elem().Interface() 710 if !test.Check(got) { 711 t.Errorf("wrong result\ngot: %s", spew.Sdump(got)) 712 } 713 }) 714 } 715 716 } 717 718 func TestDecodeExpression(t *testing.T) { 719 tests := []struct { 720 Value cty.Value 721 Target interface{} 722 Want interface{} 723 DiagCount int 724 }{ 725 { 726 cty.StringVal("hello"), 727 "", 728 "hello", 729 0, 730 }, 731 { 732 cty.StringVal("hello"), 733 cty.NilVal, 734 cty.StringVal("hello"), 735 0, 736 }, 737 { 738 cty.NumberIntVal(2), 739 "", 740 "2", 741 0, 742 }, 743 { 744 cty.StringVal("true"), 745 false, 746 true, 747 0, 748 }, 749 { 750 cty.NullVal(cty.String), 751 "", 752 "", 753 1, // null value is not allowed 754 }, 755 { 756 cty.UnknownVal(cty.String), 757 "", 758 "", 759 1, // value must be known 760 }, 761 { 762 cty.ListVal([]cty.Value{cty.True}), 763 false, 764 false, 765 1, // bool required 766 }, 767 } 768 769 for i, test := range tests { 770 t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) { 771 expr := &fixedExpression{test.Value} 772 773 targetVal := reflect.New(reflect.TypeOf(test.Target)) 774 775 diags := DecodeExpression(expr, nil, targetVal.Interface()) 776 if len(diags) != test.DiagCount { 777 t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount) 778 for _, diag := range diags { 779 t.Logf(" - %s", diag.Error()) 780 } 781 } 782 got := targetVal.Elem().Interface() 783 if !reflect.DeepEqual(got, test.Want) { 784 t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 785 } 786 }) 787 } 788 } 789 790 type fixedExpression struct { 791 val cty.Value 792 } 793 794 func (e *fixedExpression) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { 795 return e.val, nil 796 } 797 798 func (e *fixedExpression) Range() (r hcl.Range) { 799 return 800 } 801 func (e *fixedExpression) StartRange() (r hcl.Range) { 802 return 803 } 804 805 func (e *fixedExpression) Variables() []hcl.Traversal { 806 return nil 807 } 808 809 func makeInstantiateType(target interface{}) func() interface{} { 810 return func() interface{} { 811 return reflect.New(reflect.TypeOf(target)).Interface() 812 } 813 }