github.com/hashicorp/hcl/v2@v2.20.0/json/structure_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package json 5 6 import ( 7 "fmt" 8 "reflect" 9 "strings" 10 "testing" 11 12 "github.com/davecgh/go-spew/spew" 13 "github.com/go-test/deep" 14 "github.com/hashicorp/hcl/v2" 15 "github.com/zclconf/go-cty/cty" 16 ) 17 18 func TestBodyPartialContent(t *testing.T) { 19 tests := []struct { 20 src string 21 schema *hcl.BodySchema 22 want *hcl.BodyContent 23 diagCount int 24 }{ 25 { 26 `{}`, 27 &hcl.BodySchema{}, 28 &hcl.BodyContent{ 29 Attributes: map[string]*hcl.Attribute{}, 30 MissingItemRange: hcl.Range{ 31 Filename: "test.json", 32 Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, 33 End: hcl.Pos{Line: 1, Column: 3, Byte: 2}, 34 }, 35 }, 36 0, 37 }, 38 { 39 `[]`, 40 &hcl.BodySchema{}, 41 &hcl.BodyContent{ 42 Attributes: map[string]*hcl.Attribute{}, 43 MissingItemRange: hcl.Range{ 44 Filename: "test.json", 45 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 46 End: hcl.Pos{Line: 1, Column: 2, Byte: 1}, 47 }, 48 }, 49 0, 50 }, 51 { 52 `[{}]`, 53 &hcl.BodySchema{}, 54 &hcl.BodyContent{ 55 Attributes: map[string]*hcl.Attribute{}, 56 MissingItemRange: hcl.Range{ 57 Filename: "test.json", 58 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 59 End: hcl.Pos{Line: 1, Column: 2, Byte: 1}, 60 }, 61 }, 62 0, 63 }, 64 { 65 `[[]]`, 66 &hcl.BodySchema{}, 67 &hcl.BodyContent{ 68 Attributes: map[string]*hcl.Attribute{}, 69 MissingItemRange: hcl.Range{ 70 Filename: "test.json", 71 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 72 End: hcl.Pos{Line: 1, Column: 2, Byte: 1}, 73 }, 74 }, 75 1, // elements of root array must be objects 76 }, 77 { 78 `{"//": "comment that should be ignored"}`, 79 &hcl.BodySchema{}, 80 &hcl.BodyContent{ 81 Attributes: map[string]*hcl.Attribute{}, 82 MissingItemRange: hcl.Range{ 83 Filename: "test.json", 84 Start: hcl.Pos{Line: 1, Column: 40, Byte: 39}, 85 End: hcl.Pos{Line: 1, Column: 41, Byte: 40}, 86 }, 87 }, 88 0, 89 }, 90 { 91 `{"//": "comment that should be ignored", "//": "another comment"}`, 92 &hcl.BodySchema{}, 93 &hcl.BodyContent{ 94 Attributes: map[string]*hcl.Attribute{}, 95 MissingItemRange: hcl.Range{ 96 Filename: "test.json", 97 Start: hcl.Pos{Line: 1, Column: 65, Byte: 64}, 98 End: hcl.Pos{Line: 1, Column: 66, Byte: 65}, 99 }, 100 }, 101 0, 102 }, 103 { 104 `{"name":"Ermintrude"}`, 105 &hcl.BodySchema{ 106 Attributes: []hcl.AttributeSchema{ 107 { 108 Name: "name", 109 }, 110 }, 111 }, 112 &hcl.BodyContent{ 113 Attributes: map[string]*hcl.Attribute{ 114 "name": &hcl.Attribute{ 115 Name: "name", 116 Expr: &expression{ 117 src: &stringVal{ 118 Value: "Ermintrude", 119 SrcRange: hcl.Range{ 120 Filename: "test.json", 121 Start: hcl.Pos{ 122 Byte: 8, 123 Line: 1, 124 Column: 9, 125 }, 126 End: hcl.Pos{ 127 Byte: 20, 128 Line: 1, 129 Column: 21, 130 }, 131 }, 132 }, 133 }, 134 Range: hcl.Range{ 135 Filename: "test.json", 136 Start: hcl.Pos{ 137 Byte: 1, 138 Line: 1, 139 Column: 2, 140 }, 141 End: hcl.Pos{ 142 Byte: 20, 143 Line: 1, 144 Column: 21, 145 }, 146 }, 147 NameRange: hcl.Range{ 148 Filename: "test.json", 149 Start: hcl.Pos{ 150 Byte: 1, 151 Line: 1, 152 Column: 2, 153 }, 154 End: hcl.Pos{ 155 Byte: 7, 156 Line: 1, 157 Column: 8, 158 }, 159 }, 160 }, 161 }, 162 MissingItemRange: hcl.Range{ 163 Filename: "test.json", 164 Start: hcl.Pos{Line: 1, Column: 21, Byte: 20}, 165 End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, 166 }, 167 }, 168 0, 169 }, 170 { 171 `[{"name":"Ermintrude"}]`, 172 &hcl.BodySchema{ 173 Attributes: []hcl.AttributeSchema{ 174 { 175 Name: "name", 176 }, 177 }, 178 }, 179 &hcl.BodyContent{ 180 Attributes: map[string]*hcl.Attribute{ 181 "name": &hcl.Attribute{ 182 Name: "name", 183 Expr: &expression{ 184 src: &stringVal{ 185 Value: "Ermintrude", 186 SrcRange: hcl.Range{ 187 Filename: "test.json", 188 Start: hcl.Pos{ 189 Byte: 9, 190 Line: 1, 191 Column: 10, 192 }, 193 End: hcl.Pos{ 194 Byte: 21, 195 Line: 1, 196 Column: 22, 197 }, 198 }, 199 }, 200 }, 201 Range: hcl.Range{ 202 Filename: "test.json", 203 Start: hcl.Pos{ 204 Byte: 2, 205 Line: 1, 206 Column: 3, 207 }, 208 End: hcl.Pos{ 209 Byte: 21, 210 Line: 1, 211 Column: 22, 212 }, 213 }, 214 NameRange: hcl.Range{ 215 Filename: "test.json", 216 Start: hcl.Pos{ 217 Byte: 2, 218 Line: 1, 219 Column: 3, 220 }, 221 End: hcl.Pos{ 222 Byte: 8, 223 Line: 1, 224 Column: 9, 225 }, 226 }, 227 }, 228 }, 229 MissingItemRange: hcl.Range{ 230 Filename: "test.json", 231 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 232 End: hcl.Pos{Line: 1, Column: 2, Byte: 1}, 233 }, 234 }, 235 0, 236 }, 237 { 238 `{"name":"Ermintrude"}`, 239 &hcl.BodySchema{ 240 Attributes: []hcl.AttributeSchema{ 241 { 242 Name: "name", 243 Required: true, 244 }, 245 { 246 Name: "age", 247 Required: true, 248 }, 249 }, 250 }, 251 &hcl.BodyContent{ 252 Attributes: map[string]*hcl.Attribute{ 253 "name": &hcl.Attribute{ 254 Name: "name", 255 Expr: &expression{ 256 src: &stringVal{ 257 Value: "Ermintrude", 258 SrcRange: hcl.Range{ 259 Filename: "test.json", 260 Start: hcl.Pos{ 261 Byte: 8, 262 Line: 1, 263 Column: 9, 264 }, 265 End: hcl.Pos{ 266 Byte: 20, 267 Line: 1, 268 Column: 21, 269 }, 270 }, 271 }, 272 }, 273 Range: hcl.Range{ 274 Filename: "test.json", 275 Start: hcl.Pos{ 276 Byte: 1, 277 Line: 1, 278 Column: 2, 279 }, 280 End: hcl.Pos{ 281 Byte: 20, 282 Line: 1, 283 Column: 21, 284 }, 285 }, 286 NameRange: hcl.Range{ 287 Filename: "test.json", 288 Start: hcl.Pos{ 289 Byte: 1, 290 Line: 1, 291 Column: 2, 292 }, 293 End: hcl.Pos{ 294 Byte: 7, 295 Line: 1, 296 Column: 8, 297 }, 298 }, 299 }, 300 }, 301 MissingItemRange: hcl.Range{ 302 Filename: "test.json", 303 Start: hcl.Pos{Line: 1, Column: 21, Byte: 20}, 304 End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, 305 }, 306 }, 307 1, 308 }, 309 { 310 `{"resource": null}`, 311 &hcl.BodySchema{ 312 Blocks: []hcl.BlockHeaderSchema{ 313 { 314 Type: "resource", 315 }, 316 }, 317 }, 318 &hcl.BodyContent{ 319 Attributes: map[string]*hcl.Attribute{}, 320 // We don't find any blocks if the value is json null. 321 Blocks: nil, 322 MissingItemRange: hcl.Range{ 323 Filename: "test.json", 324 Start: hcl.Pos{Line: 1, Column: 18, Byte: 17}, 325 End: hcl.Pos{Line: 1, Column: 19, Byte: 18}, 326 }, 327 }, 328 0, 329 }, 330 { 331 `{"resource": { "nested": null }}`, 332 &hcl.BodySchema{ 333 Blocks: []hcl.BlockHeaderSchema{ 334 { 335 Type: "resource", 336 LabelNames: []string{"name"}, 337 }, 338 }, 339 }, 340 &hcl.BodyContent{ 341 Attributes: map[string]*hcl.Attribute{}, 342 Blocks: nil, 343 MissingItemRange: hcl.Range{ 344 Filename: "test.json", 345 Start: hcl.Pos{Line: 1, Column: 32, Byte: 31}, 346 End: hcl.Pos{Line: 1, Column: 33, Byte: 32}, 347 }, 348 }, 349 0, 350 }, 351 { 352 `{"resource":{}}`, 353 &hcl.BodySchema{ 354 Blocks: []hcl.BlockHeaderSchema{ 355 { 356 Type: "resource", 357 }, 358 }, 359 }, 360 &hcl.BodyContent{ 361 Attributes: map[string]*hcl.Attribute{}, 362 Blocks: hcl.Blocks{ 363 { 364 Type: "resource", 365 Labels: []string{}, 366 Body: &body{ 367 val: &objectVal{ 368 Attrs: []*objectAttr{}, 369 SrcRange: hcl.Range{ 370 Filename: "test.json", 371 Start: hcl.Pos{ 372 Byte: 12, 373 Line: 1, 374 Column: 13, 375 }, 376 End: hcl.Pos{ 377 Byte: 14, 378 Line: 1, 379 Column: 15, 380 }, 381 }, 382 OpenRange: hcl.Range{ 383 Filename: "test.json", 384 Start: hcl.Pos{ 385 Byte: 12, 386 Line: 1, 387 Column: 13, 388 }, 389 End: hcl.Pos{ 390 Byte: 13, 391 Line: 1, 392 Column: 14, 393 }, 394 }, 395 CloseRange: hcl.Range{ 396 Filename: "test.json", 397 Start: hcl.Pos{ 398 Byte: 13, 399 Line: 1, 400 Column: 14, 401 }, 402 End: hcl.Pos{ 403 Byte: 14, 404 Line: 1, 405 Column: 15, 406 }, 407 }, 408 }, 409 }, 410 411 DefRange: hcl.Range{ 412 Filename: "test.json", 413 Start: hcl.Pos{ 414 Byte: 12, 415 Line: 1, 416 Column: 13, 417 }, 418 End: hcl.Pos{ 419 Byte: 13, 420 Line: 1, 421 Column: 14, 422 }, 423 }, 424 TypeRange: hcl.Range{ 425 Filename: "test.json", 426 Start: hcl.Pos{ 427 Byte: 1, 428 Line: 1, 429 Column: 2, 430 }, 431 End: hcl.Pos{ 432 Byte: 11, 433 Line: 1, 434 Column: 12, 435 }, 436 }, 437 LabelRanges: []hcl.Range{}, 438 }, 439 }, 440 MissingItemRange: hcl.Range{ 441 Filename: "test.json", 442 Start: hcl.Pos{Line: 1, Column: 15, Byte: 14}, 443 End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, 444 }, 445 }, 446 0, 447 }, 448 { 449 `{"resource":[{},{}]}`, 450 &hcl.BodySchema{ 451 Blocks: []hcl.BlockHeaderSchema{ 452 { 453 Type: "resource", 454 }, 455 }, 456 }, 457 &hcl.BodyContent{ 458 Attributes: map[string]*hcl.Attribute{}, 459 Blocks: hcl.Blocks{ 460 { 461 Type: "resource", 462 Labels: []string{}, 463 Body: &body{ 464 val: &objectVal{ 465 Attrs: []*objectAttr{}, 466 SrcRange: hcl.Range{ 467 Filename: "test.json", 468 Start: hcl.Pos{ 469 Byte: 13, 470 Line: 1, 471 Column: 14, 472 }, 473 End: hcl.Pos{ 474 Byte: 15, 475 Line: 1, 476 Column: 16, 477 }, 478 }, 479 OpenRange: hcl.Range{ 480 Filename: "test.json", 481 Start: hcl.Pos{ 482 Byte: 13, 483 Line: 1, 484 Column: 14, 485 }, 486 End: hcl.Pos{ 487 Byte: 14, 488 Line: 1, 489 Column: 15, 490 }, 491 }, 492 CloseRange: hcl.Range{ 493 Filename: "test.json", 494 Start: hcl.Pos{ 495 Byte: 14, 496 Line: 1, 497 Column: 15, 498 }, 499 End: hcl.Pos{ 500 Byte: 15, 501 Line: 1, 502 Column: 16, 503 }, 504 }, 505 }, 506 }, 507 508 DefRange: hcl.Range{ 509 Filename: "test.json", 510 Start: hcl.Pos{ 511 Byte: 12, 512 Line: 1, 513 Column: 13, 514 }, 515 End: hcl.Pos{ 516 Byte: 13, 517 Line: 1, 518 Column: 14, 519 }, 520 }, 521 TypeRange: hcl.Range{ 522 Filename: "test.json", 523 Start: hcl.Pos{ 524 Byte: 1, 525 Line: 1, 526 Column: 2, 527 }, 528 End: hcl.Pos{ 529 Byte: 11, 530 Line: 1, 531 Column: 12, 532 }, 533 }, 534 LabelRanges: []hcl.Range{}, 535 }, 536 { 537 Type: "resource", 538 Labels: []string{}, 539 Body: &body{ 540 val: &objectVal{ 541 Attrs: []*objectAttr{}, 542 SrcRange: hcl.Range{ 543 Filename: "test.json", 544 Start: hcl.Pos{ 545 Byte: 16, 546 Line: 1, 547 Column: 17, 548 }, 549 End: hcl.Pos{ 550 Byte: 18, 551 Line: 1, 552 Column: 19, 553 }, 554 }, 555 OpenRange: hcl.Range{ 556 Filename: "test.json", 557 Start: hcl.Pos{ 558 Byte: 16, 559 Line: 1, 560 Column: 17, 561 }, 562 End: hcl.Pos{ 563 Byte: 17, 564 Line: 1, 565 Column: 18, 566 }, 567 }, 568 CloseRange: hcl.Range{ 569 Filename: "test.json", 570 Start: hcl.Pos{ 571 Byte: 17, 572 Line: 1, 573 Column: 18, 574 }, 575 End: hcl.Pos{ 576 Byte: 18, 577 Line: 1, 578 Column: 19, 579 }, 580 }, 581 }, 582 }, 583 584 DefRange: hcl.Range{ 585 Filename: "test.json", 586 Start: hcl.Pos{ 587 Byte: 12, 588 Line: 1, 589 Column: 13, 590 }, 591 End: hcl.Pos{ 592 Byte: 13, 593 Line: 1, 594 Column: 14, 595 }, 596 }, 597 TypeRange: hcl.Range{ 598 Filename: "test.json", 599 Start: hcl.Pos{ 600 Byte: 1, 601 Line: 1, 602 Column: 2, 603 }, 604 End: hcl.Pos{ 605 Byte: 11, 606 Line: 1, 607 Column: 12, 608 }, 609 }, 610 LabelRanges: []hcl.Range{}, 611 }, 612 }, 613 MissingItemRange: hcl.Range{ 614 Filename: "test.json", 615 Start: hcl.Pos{Line: 1, Column: 20, Byte: 19}, 616 End: hcl.Pos{Line: 1, Column: 21, Byte: 20}, 617 }, 618 }, 619 0, 620 }, 621 { 622 `{"resource":{"foo_instance":{"bar":{}}}}`, 623 &hcl.BodySchema{ 624 Blocks: []hcl.BlockHeaderSchema{ 625 { 626 Type: "resource", 627 LabelNames: []string{"type", "name"}, 628 }, 629 }, 630 }, 631 &hcl.BodyContent{ 632 Attributes: map[string]*hcl.Attribute{}, 633 Blocks: hcl.Blocks{ 634 { 635 Type: "resource", 636 Labels: []string{"foo_instance", "bar"}, 637 Body: &body{ 638 val: &objectVal{ 639 Attrs: []*objectAttr{}, 640 SrcRange: hcl.Range{ 641 Filename: "test.json", 642 Start: hcl.Pos{ 643 Byte: 35, 644 Line: 1, 645 Column: 36, 646 }, 647 End: hcl.Pos{ 648 Byte: 37, 649 Line: 1, 650 Column: 38, 651 }, 652 }, 653 OpenRange: hcl.Range{ 654 Filename: "test.json", 655 Start: hcl.Pos{ 656 Byte: 35, 657 Line: 1, 658 Column: 36, 659 }, 660 End: hcl.Pos{ 661 Byte: 36, 662 Line: 1, 663 Column: 37, 664 }, 665 }, 666 CloseRange: hcl.Range{ 667 Filename: "test.json", 668 Start: hcl.Pos{ 669 Byte: 36, 670 Line: 1, 671 Column: 37, 672 }, 673 End: hcl.Pos{ 674 Byte: 37, 675 Line: 1, 676 Column: 38, 677 }, 678 }, 679 }, 680 }, 681 682 DefRange: hcl.Range{ 683 Filename: "test.json", 684 Start: hcl.Pos{ 685 Byte: 35, 686 Line: 1, 687 Column: 36, 688 }, 689 End: hcl.Pos{ 690 Byte: 36, 691 Line: 1, 692 Column: 37, 693 }, 694 }, 695 TypeRange: hcl.Range{ 696 Filename: "test.json", 697 Start: hcl.Pos{ 698 Byte: 1, 699 Line: 1, 700 Column: 2, 701 }, 702 End: hcl.Pos{ 703 Byte: 11, 704 Line: 1, 705 Column: 12, 706 }, 707 }, 708 LabelRanges: []hcl.Range{ 709 { 710 Filename: "test.json", 711 Start: hcl.Pos{ 712 Byte: 13, 713 Line: 1, 714 Column: 14, 715 }, 716 End: hcl.Pos{ 717 Byte: 27, 718 Line: 1, 719 Column: 28, 720 }, 721 }, 722 { 723 Filename: "test.json", 724 Start: hcl.Pos{ 725 Byte: 29, 726 Line: 1, 727 Column: 30, 728 }, 729 End: hcl.Pos{ 730 Byte: 34, 731 Line: 1, 732 Column: 35, 733 }, 734 }, 735 }, 736 }, 737 }, 738 MissingItemRange: hcl.Range{ 739 Filename: "test.json", 740 Start: hcl.Pos{Line: 1, Column: 40, Byte: 39}, 741 End: hcl.Pos{Line: 1, Column: 41, Byte: 40}, 742 }, 743 }, 744 0, 745 }, 746 { 747 `{"resource":{"foo_instance":[{"bar":{}}, {"bar":{}}]}}`, 748 &hcl.BodySchema{ 749 Blocks: []hcl.BlockHeaderSchema{ 750 { 751 Type: "resource", 752 LabelNames: []string{"type", "name"}, 753 }, 754 }, 755 }, 756 &hcl.BodyContent{ 757 Attributes: map[string]*hcl.Attribute{}, 758 Blocks: hcl.Blocks{ 759 { 760 Type: "resource", 761 Labels: []string{"foo_instance", "bar"}, 762 Body: &body{ 763 val: &objectVal{ 764 Attrs: []*objectAttr{}, 765 SrcRange: hcl.Range{ 766 Filename: "test.json", 767 Start: hcl.Pos{ 768 Byte: 36, 769 Line: 1, 770 Column: 37, 771 }, 772 End: hcl.Pos{ 773 Byte: 38, 774 Line: 1, 775 Column: 39, 776 }, 777 }, 778 OpenRange: hcl.Range{ 779 Filename: "test.json", 780 Start: hcl.Pos{ 781 Byte: 36, 782 Line: 1, 783 Column: 37, 784 }, 785 End: hcl.Pos{ 786 Byte: 37, 787 Line: 1, 788 Column: 38, 789 }, 790 }, 791 CloseRange: hcl.Range{ 792 Filename: "test.json", 793 Start: hcl.Pos{ 794 Byte: 37, 795 Line: 1, 796 Column: 38, 797 }, 798 End: hcl.Pos{ 799 Byte: 38, 800 Line: 1, 801 Column: 39, 802 }, 803 }, 804 }, 805 }, 806 807 DefRange: hcl.Range{ 808 Filename: "test.json", 809 Start: hcl.Pos{ 810 Byte: 36, 811 Line: 1, 812 Column: 37, 813 }, 814 End: hcl.Pos{ 815 Byte: 37, 816 Line: 1, 817 Column: 38, 818 }, 819 }, 820 TypeRange: hcl.Range{ 821 Filename: "test.json", 822 Start: hcl.Pos{ 823 Byte: 1, 824 Line: 1, 825 Column: 2, 826 }, 827 End: hcl.Pos{ 828 Byte: 11, 829 Line: 1, 830 Column: 12, 831 }, 832 }, 833 LabelRanges: []hcl.Range{ 834 { 835 Filename: "test.json", 836 Start: hcl.Pos{ 837 Byte: 13, 838 Line: 1, 839 Column: 14, 840 }, 841 End: hcl.Pos{ 842 Byte: 27, 843 Line: 1, 844 Column: 28, 845 }, 846 }, 847 { 848 Filename: "test.json", 849 Start: hcl.Pos{ 850 Byte: 30, 851 Line: 1, 852 Column: 31, 853 }, 854 End: hcl.Pos{ 855 Byte: 35, 856 Line: 1, 857 Column: 36, 858 }, 859 }, 860 }, 861 }, 862 { 863 Type: "resource", 864 Labels: []string{"foo_instance", "bar"}, 865 Body: &body{ 866 val: &objectVal{ 867 Attrs: []*objectAttr{}, 868 SrcRange: hcl.Range{ 869 Filename: "test.json", 870 Start: hcl.Pos{ 871 Byte: 36, 872 Line: 1, 873 Column: 37, 874 }, 875 End: hcl.Pos{ 876 Byte: 38, 877 Line: 1, 878 Column: 39, 879 }, 880 }, 881 OpenRange: hcl.Range{ 882 Filename: "test.json", 883 Start: hcl.Pos{ 884 Byte: 36, 885 Line: 1, 886 Column: 37, 887 }, 888 End: hcl.Pos{ 889 Byte: 37, 890 Line: 1, 891 Column: 38, 892 }, 893 }, 894 CloseRange: hcl.Range{ 895 Filename: "test.json", 896 Start: hcl.Pos{ 897 Byte: 37, 898 Line: 1, 899 Column: 38, 900 }, 901 End: hcl.Pos{ 902 Byte: 38, 903 Line: 1, 904 Column: 39, 905 }, 906 }, 907 }, 908 }, 909 910 DefRange: hcl.Range{ 911 Filename: "test.json", 912 Start: hcl.Pos{ 913 Byte: 48, 914 Line: 1, 915 Column: 49, 916 }, 917 End: hcl.Pos{ 918 Byte: 49, 919 Line: 1, 920 Column: 50, 921 }, 922 }, 923 TypeRange: hcl.Range{ 924 Filename: "test.json", 925 Start: hcl.Pos{ 926 Byte: 1, 927 Line: 1, 928 Column: 2, 929 }, 930 End: hcl.Pos{ 931 Byte: 11, 932 Line: 1, 933 Column: 12, 934 }, 935 }, 936 LabelRanges: []hcl.Range{ 937 { 938 Filename: "test.json", 939 Start: hcl.Pos{ 940 Byte: 13, 941 Line: 1, 942 Column: 14, 943 }, 944 End: hcl.Pos{ 945 Byte: 27, 946 Line: 1, 947 Column: 28, 948 }, 949 }, 950 { 951 Filename: "test.json", 952 Start: hcl.Pos{ 953 Byte: 42, 954 Line: 1, 955 Column: 43, 956 }, 957 End: hcl.Pos{ 958 Byte: 47, 959 Line: 1, 960 Column: 48, 961 }, 962 }, 963 }, 964 }, 965 }, 966 MissingItemRange: hcl.Range{ 967 Filename: "test.json", 968 Start: hcl.Pos{Line: 1, Column: 54, Byte: 53}, 969 End: hcl.Pos{Line: 1, Column: 55, Byte: 54}, 970 }, 971 }, 972 0, 973 }, 974 { 975 `{"name":"Ermintrude"}`, 976 &hcl.BodySchema{ 977 Blocks: []hcl.BlockHeaderSchema{ 978 { 979 Type: "name", 980 }, 981 }, 982 }, 983 &hcl.BodyContent{ 984 Attributes: map[string]*hcl.Attribute{}, 985 MissingItemRange: hcl.Range{ 986 Filename: "test.json", 987 Start: hcl.Pos{Line: 1, Column: 21, Byte: 20}, 988 End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, 989 }, 990 }, 991 1, // name is supposed to be a block 992 }, 993 { 994 `[{"name":"Ermintrude"},{"name":"Ermintrude"}]`, 995 &hcl.BodySchema{ 996 Attributes: []hcl.AttributeSchema{ 997 { 998 Name: "name", 999 }, 1000 }, 1001 }, 1002 &hcl.BodyContent{ 1003 Attributes: map[string]*hcl.Attribute{ 1004 "name": { 1005 Name: "name", 1006 Expr: &expression{ 1007 src: &stringVal{ 1008 Value: "Ermintrude", 1009 SrcRange: hcl.Range{ 1010 Filename: "test.json", 1011 Start: hcl.Pos{ 1012 Byte: 8, 1013 Line: 1, 1014 Column: 9, 1015 }, 1016 End: hcl.Pos{ 1017 Byte: 20, 1018 Line: 1, 1019 Column: 21, 1020 }, 1021 }, 1022 }, 1023 }, 1024 Range: hcl.Range{ 1025 Filename: "test.json", 1026 Start: hcl.Pos{ 1027 Byte: 2, 1028 Line: 1, 1029 Column: 3, 1030 }, 1031 End: hcl.Pos{ 1032 Byte: 21, 1033 Line: 1, 1034 Column: 22, 1035 }, 1036 }, 1037 NameRange: hcl.Range{ 1038 Filename: "test.json", 1039 Start: hcl.Pos{ 1040 Byte: 2, 1041 Line: 1, 1042 Column: 3, 1043 }, 1044 End: hcl.Pos{ 1045 Byte: 8, 1046 Line: 1, 1047 Column: 9, 1048 }, 1049 }, 1050 }, 1051 }, 1052 MissingItemRange: hcl.Range{ 1053 Filename: "test.json", 1054 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 1055 End: hcl.Pos{Line: 1, Column: 2, Byte: 1}, 1056 }, 1057 }, 1058 1, // "name" attribute is defined twice 1059 }, 1060 } 1061 1062 for i, test := range tests { 1063 t.Run(fmt.Sprintf("%02d-%s", i, test.src), func(t *testing.T) { 1064 file, diags := Parse([]byte(test.src), "test.json") 1065 if len(diags) != 0 { 1066 t.Fatalf("Parse produced diagnostics: %s", diags) 1067 } 1068 got, _, diags := file.Body.PartialContent(test.schema) 1069 if len(diags) != test.diagCount { 1070 t.Errorf("Wrong number of diagnostics %d; want %d", len(diags), test.diagCount) 1071 for _, diag := range diags { 1072 t.Logf(" - %s", diag) 1073 } 1074 } 1075 1076 for _, problem := range deep.Equal(got, test.want) { 1077 t.Error(problem) 1078 } 1079 }) 1080 } 1081 } 1082 1083 func TestBodyContent(t *testing.T) { 1084 // We test most of the functionality already in TestBodyPartialContent, so 1085 // this test focuses on the handling of extraneous attributes. 1086 tests := []struct { 1087 src string 1088 schema *hcl.BodySchema 1089 diagCount int 1090 }{ 1091 { 1092 `{"unknown": true}`, 1093 &hcl.BodySchema{}, 1094 1, 1095 }, 1096 { 1097 `{"//": "comment that should be ignored"}`, 1098 &hcl.BodySchema{}, 1099 0, 1100 }, 1101 { 1102 `{"unknow": true}`, 1103 &hcl.BodySchema{ 1104 Attributes: []hcl.AttributeSchema{ 1105 { 1106 Name: "unknown", 1107 }, 1108 }, 1109 }, 1110 1, 1111 }, 1112 { 1113 `{"unknow": true, "unnown": true}`, 1114 &hcl.BodySchema{ 1115 Attributes: []hcl.AttributeSchema{ 1116 { 1117 Name: "unknown", 1118 }, 1119 }, 1120 }, 1121 2, 1122 }, 1123 } 1124 1125 for i, test := range tests { 1126 t.Run(fmt.Sprintf("%02d-%s", i, test.src), func(t *testing.T) { 1127 file, diags := Parse([]byte(test.src), "test.json") 1128 if len(diags) != 0 { 1129 t.Fatalf("Parse produced diagnostics: %s", diags) 1130 } 1131 _, diags = file.Body.Content(test.schema) 1132 if len(diags) != test.diagCount { 1133 t.Errorf("Wrong number of diagnostics %d; want %d", len(diags), test.diagCount) 1134 for _, diag := range diags { 1135 t.Logf(" - %s", diag) 1136 } 1137 } 1138 }) 1139 } 1140 } 1141 1142 func TestJustAttributes(t *testing.T) { 1143 // We test most of the functionality already in TestBodyPartialContent, so 1144 // this test focuses on the handling of extraneous attributes. 1145 tests := []struct { 1146 src string 1147 want hcl.Attributes 1148 diagCount int 1149 }{ 1150 { 1151 `{}`, 1152 map[string]*hcl.Attribute{}, 1153 0, 1154 }, 1155 { 1156 `{"foo": true}`, 1157 map[string]*hcl.Attribute{ 1158 "foo": { 1159 Name: "foo", 1160 Expr: &expression{ 1161 src: &booleanVal{ 1162 Value: true, 1163 SrcRange: hcl.Range{ 1164 Filename: "test.json", 1165 Start: hcl.Pos{Byte: 8, Line: 1, Column: 9}, 1166 End: hcl.Pos{Byte: 12, Line: 1, Column: 13}, 1167 }, 1168 }, 1169 }, 1170 Range: hcl.Range{ 1171 Filename: "test.json", 1172 Start: hcl.Pos{Byte: 1, Line: 1, Column: 2}, 1173 End: hcl.Pos{Byte: 12, Line: 1, Column: 13}, 1174 }, 1175 NameRange: hcl.Range{ 1176 Filename: "test.json", 1177 Start: hcl.Pos{Byte: 1, Line: 1, Column: 2}, 1178 End: hcl.Pos{Byte: 6, Line: 1, Column: 7}, 1179 }, 1180 }, 1181 }, 1182 0, 1183 }, 1184 { 1185 `{"//": "comment that should be ignored"}`, 1186 map[string]*hcl.Attribute{}, 1187 0, 1188 }, 1189 { 1190 `{"foo": true, "foo": true}`, 1191 map[string]*hcl.Attribute{ 1192 "foo": { 1193 Name: "foo", 1194 Expr: &expression{ 1195 src: &booleanVal{ 1196 Value: true, 1197 SrcRange: hcl.Range{ 1198 Filename: "test.json", 1199 Start: hcl.Pos{Byte: 8, Line: 1, Column: 9}, 1200 End: hcl.Pos{Byte: 12, Line: 1, Column: 13}, 1201 }, 1202 }, 1203 }, 1204 Range: hcl.Range{ 1205 Filename: "test.json", 1206 Start: hcl.Pos{Byte: 1, Line: 1, Column: 2}, 1207 End: hcl.Pos{Byte: 12, Line: 1, Column: 13}, 1208 }, 1209 NameRange: hcl.Range{ 1210 Filename: "test.json", 1211 Start: hcl.Pos{Byte: 1, Line: 1, Column: 2}, 1212 End: hcl.Pos{Byte: 6, Line: 1, Column: 7}, 1213 }, 1214 }, 1215 }, 1216 1, // attribute foo was already defined 1217 }, 1218 } 1219 1220 for i, test := range tests { 1221 t.Run(fmt.Sprintf("%02d-%s", i, test.src), func(t *testing.T) { 1222 file, diags := Parse([]byte(test.src), "test.json") 1223 if len(diags) != 0 { 1224 t.Fatalf("Parse produced diagnostics: %s", diags) 1225 } 1226 got, diags := file.Body.JustAttributes() 1227 if len(diags) != test.diagCount { 1228 t.Errorf("Wrong number of diagnostics %d; want %d", len(diags), test.diagCount) 1229 for _, diag := range diags { 1230 t.Logf(" - %s", diag) 1231 } 1232 } 1233 if !reflect.DeepEqual(got, test.want) { 1234 t.Errorf("wrong result\ngot: %s\nwant: %s", spew.Sdump(got), spew.Sdump(test.want)) 1235 } 1236 }) 1237 } 1238 } 1239 1240 func TestExpressionVariables(t *testing.T) { 1241 tests := []struct { 1242 Src string 1243 Want []hcl.Traversal 1244 }{ 1245 { 1246 `{"a":true}`, 1247 nil, 1248 }, 1249 { 1250 `{"a":"${foo}"}`, 1251 []hcl.Traversal{ 1252 { 1253 hcl.TraverseRoot{ 1254 Name: "foo", 1255 SrcRange: hcl.Range{ 1256 Filename: "test.json", 1257 Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, 1258 End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 1259 }, 1260 }, 1261 }, 1262 }, 1263 }, 1264 { 1265 `{"a":["${foo}"]}`, 1266 []hcl.Traversal{ 1267 { 1268 hcl.TraverseRoot{ 1269 Name: "foo", 1270 SrcRange: hcl.Range{ 1271 Filename: "test.json", 1272 Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, 1273 End: hcl.Pos{Line: 1, Column: 13, Byte: 12}, 1274 }, 1275 }, 1276 }, 1277 }, 1278 }, 1279 { 1280 `{"a":{"b":"${foo}"}}`, 1281 []hcl.Traversal{ 1282 { 1283 hcl.TraverseRoot{ 1284 Name: "foo", 1285 SrcRange: hcl.Range{ 1286 Filename: "test.json", 1287 Start: hcl.Pos{Line: 1, Column: 14, Byte: 13}, 1288 End: hcl.Pos{Line: 1, Column: 17, Byte: 16}, 1289 }, 1290 }, 1291 }, 1292 }, 1293 }, 1294 { 1295 `{"a":{"${foo}":"b"}}`, 1296 []hcl.Traversal{ 1297 { 1298 hcl.TraverseRoot{ 1299 Name: "foo", 1300 SrcRange: hcl.Range{ 1301 Filename: "test.json", 1302 Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, 1303 End: hcl.Pos{Line: 1, Column: 13, Byte: 12}, 1304 }, 1305 }, 1306 }, 1307 }, 1308 }, 1309 } 1310 1311 for _, test := range tests { 1312 t.Run(test.Src, func(t *testing.T) { 1313 file, diags := Parse([]byte(test.Src), "test.json") 1314 if len(diags) != 0 { 1315 t.Fatalf("Parse produced diagnostics: %s", diags) 1316 } 1317 attrs, diags := file.Body.JustAttributes() 1318 if len(diags) != 0 { 1319 t.Fatalf("JustAttributes produced diagnostics: %s", diags) 1320 } 1321 got := attrs["a"].Expr.Variables() 1322 if !reflect.DeepEqual(got, test.Want) { 1323 t.Errorf("wrong result\ngot: %s\nwant: %s", spew.Sdump(got), spew.Sdump(test.Want)) 1324 } 1325 }) 1326 } 1327 } 1328 1329 func TestExpressionAsTraversal(t *testing.T) { 1330 e := &expression{ 1331 src: &stringVal{ 1332 Value: "foo.bar[0]", 1333 }, 1334 } 1335 traversal := e.AsTraversal() 1336 if len(traversal) != 3 { 1337 t.Fatalf("incorrect traversal %#v; want length 3", traversal) 1338 } 1339 } 1340 1341 func TestStaticExpressionList(t *testing.T) { 1342 e := &expression{ 1343 src: &arrayVal{ 1344 Values: []node{ 1345 &stringVal{ 1346 Value: "hello", 1347 }, 1348 }, 1349 }, 1350 } 1351 exprs := e.ExprList() 1352 if len(exprs) != 1 { 1353 t.Fatalf("incorrect exprs %#v; want length 1", exprs) 1354 } 1355 if exprs[0].(*expression).src != e.src.(*arrayVal).Values[0] { 1356 t.Fatalf("wrong first expression node") 1357 } 1358 } 1359 1360 func TestExpression_Value(t *testing.T) { 1361 src := `{ 1362 "string": "string_val", 1363 "number": 5, 1364 "bool_true": true, 1365 "bool_false": false, 1366 "array": ["a"], 1367 "object": {"key": "value"}, 1368 "null": null 1369 }` 1370 expected := map[string]cty.Value{ 1371 "string": cty.StringVal("string_val"), 1372 "number": cty.NumberIntVal(5), 1373 "bool_true": cty.BoolVal(true), 1374 "bool_false": cty.BoolVal(false), 1375 "array": cty.TupleVal([]cty.Value{cty.StringVal("a")}), 1376 "object": cty.ObjectVal(map[string]cty.Value{ 1377 "key": cty.StringVal("value"), 1378 }), 1379 "null": cty.NullVal(cty.DynamicPseudoType), 1380 } 1381 1382 file, diags := Parse([]byte(src), "") 1383 if len(diags) != 0 { 1384 t.Errorf("got %d diagnostics on parse; want 0", len(diags)) 1385 for _, diag := range diags { 1386 t.Logf("- %s", diag.Error()) 1387 } 1388 } 1389 if file == nil { 1390 t.Errorf("got nil File; want actual file") 1391 } 1392 if file.Body == nil { 1393 t.Fatalf("got nil Body; want actual body") 1394 } 1395 attrs, diags := file.Body.JustAttributes() 1396 if len(diags) != 0 { 1397 t.Errorf("got %d diagnostics on decode; want 0", len(diags)) 1398 for _, diag := range diags { 1399 t.Logf("- %s", diag.Error()) 1400 } 1401 } 1402 1403 for ek, ev := range expected { 1404 val, diags := attrs[ek].Expr.Value(&hcl.EvalContext{}) 1405 if len(diags) != 0 { 1406 t.Errorf("got %d diagnostics on eval; want 0", len(diags)) 1407 for _, diag := range diags { 1408 t.Logf("- %s", diag.Error()) 1409 } 1410 } 1411 1412 if !val.RawEquals(ev) { 1413 t.Errorf("wrong result %#v; want %#v", val, ev) 1414 } 1415 } 1416 1417 } 1418 1419 // TestExpressionValue_Diags asserts that Value() returns diagnostics 1420 // from nested evaluations for complex objects (e.g. ObjectVal, ArrayVal) 1421 func TestExpressionValue_Diags(t *testing.T) { 1422 cases := []struct { 1423 name string 1424 src string 1425 expected cty.Value 1426 error string 1427 }{ 1428 { 1429 name: "string: happy", 1430 src: `{"v": "happy ${VAR1}"}`, 1431 expected: cty.StringVal("happy case"), 1432 }, 1433 { 1434 name: "string: unhappy", 1435 src: `{"v": "happy ${UNKNOWN}"}`, 1436 expected: cty.UnknownVal(cty.String).RefineNotNull(), 1437 error: "Unknown variable", 1438 }, 1439 { 1440 name: "object_val: happy", 1441 src: `{"v": {"key": "happy ${VAR1}"}}`, 1442 expected: cty.ObjectVal(map[string]cty.Value{ 1443 "key": cty.StringVal("happy case"), 1444 }), 1445 }, 1446 { 1447 name: "object_val: unhappy", 1448 src: `{"v": {"key": "happy ${UNKNOWN}"}}`, 1449 expected: cty.ObjectVal(map[string]cty.Value{ 1450 "key": cty.UnknownVal(cty.String).RefineNotNull(), 1451 }), 1452 error: "Unknown variable", 1453 }, 1454 { 1455 name: "object_key: happy", 1456 src: `{"v": {"happy ${VAR1}": "val"}}`, 1457 expected: cty.ObjectVal(map[string]cty.Value{ 1458 "happy case": cty.StringVal("val"), 1459 }), 1460 }, 1461 { 1462 name: "object_key: unhappy", 1463 src: `{"v": {"happy ${UNKNOWN}": "val"}}`, 1464 expected: cty.DynamicVal, 1465 error: "Unknown variable", 1466 }, 1467 { 1468 name: "array: happy", 1469 src: `{"v": ["happy ${VAR1}"]}`, 1470 expected: cty.TupleVal([]cty.Value{cty.StringVal("happy case")}), 1471 }, 1472 { 1473 name: "array: unhappy", 1474 src: `{"v": ["happy ${UNKNOWN}"]}`, 1475 expected: cty.TupleVal([]cty.Value{cty.UnknownVal(cty.String).RefineNotNull()}), 1476 error: "Unknown variable", 1477 }, 1478 } 1479 1480 ctx := &hcl.EvalContext{ 1481 Variables: map[string]cty.Value{ 1482 "VAR1": cty.StringVal("case"), 1483 }, 1484 } 1485 1486 for _, c := range cases { 1487 t.Run(c.name, func(t *testing.T) { 1488 file, diags := Parse([]byte(c.src), "") 1489 1490 if len(diags) != 0 { 1491 t.Errorf("got %d diagnostics on parse; want 0", len(diags)) 1492 for _, diag := range diags { 1493 t.Logf("- %s", diag.Error()) 1494 } 1495 t.FailNow() 1496 } 1497 if file == nil { 1498 t.Errorf("got nil File; want actual file") 1499 } 1500 if file.Body == nil { 1501 t.Fatalf("got nil Body; want actual body") 1502 } 1503 1504 attrs, diags := file.Body.JustAttributes() 1505 if len(diags) != 0 { 1506 t.Errorf("got %d diagnostics on decode; want 0", len(diags)) 1507 for _, diag := range diags { 1508 t.Logf("- %s", diag.Error()) 1509 } 1510 t.FailNow() 1511 } 1512 1513 val, diags := attrs["v"].Expr.Value(ctx) 1514 if c.error == "" && len(diags) != 0 { 1515 t.Errorf("got %d diagnostics on eval; want 0", len(diags)) 1516 for _, diag := range diags { 1517 t.Logf("- %s", diag.Error()) 1518 } 1519 t.FailNow() 1520 } else if c.error != "" && len(diags) == 0 { 1521 t.Fatalf("got 0 diagnostics on eval, want 1 with %s", c.error) 1522 } else if c.error != "" && len(diags) != 0 { 1523 if !strings.Contains(diags[0].Error(), c.error) { 1524 t.Fatalf("found error: %s; want %s", diags[0].Error(), c.error) 1525 } 1526 } 1527 1528 if !val.RawEquals(c.expected) { 1529 t.Errorf("wrong result %#v; want %#v", val, c.expected) 1530 } 1531 }) 1532 } 1533 1534 }