1package hcl 2 3import ( 4 "github.com/hashicorp/hcl/hcl/token" 5 "io/ioutil" 6 "path/filepath" 7 "reflect" 8 "testing" 9 "time" 10 11 "github.com/davecgh/go-spew/spew" 12 "github.com/hashicorp/hcl/hcl/ast" 13) 14 15func TestDecode_interface(t *testing.T) { 16 cases := []struct { 17 File string 18 Err bool 19 Out interface{} 20 }{ 21 { 22 "basic.hcl", 23 false, 24 map[string]interface{}{ 25 "foo": "bar", 26 "bar": "${file(\"bing/bong.txt\")}", 27 }, 28 }, 29 { 30 "basic_squish.hcl", 31 false, 32 map[string]interface{}{ 33 "foo": "bar", 34 "bar": "${file(\"bing/bong.txt\")}", 35 "foo-bar": "baz", 36 }, 37 }, 38 { 39 "empty.hcl", 40 false, 41 map[string]interface{}{ 42 "resource": []map[string]interface{}{ 43 map[string]interface{}{ 44 "foo": []map[string]interface{}{ 45 map[string]interface{}{}, 46 }, 47 }, 48 }, 49 }, 50 }, 51 { 52 "tfvars.hcl", 53 false, 54 map[string]interface{}{ 55 "regularvar": "Should work", 56 "map.key1": "Value", 57 "map.key2": "Other value", 58 }, 59 }, 60 { 61 "escape.hcl", 62 false, 63 map[string]interface{}{ 64 "foo": "bar\"baz\\n", 65 "qux": "back\\slash", 66 "bar": "new\nline", 67 "qax": `slash\:colon`, 68 "nested": `${HH\\:mm\\:ss}`, 69 "nestedquotes": `${"\"stringwrappedinquotes\""}`, 70 }, 71 }, 72 { 73 "float.hcl", 74 false, 75 map[string]interface{}{ 76 "a": 1.02, 77 "b": 2, 78 }, 79 }, 80 { 81 "multiline_bad.hcl", 82 true, 83 nil, 84 }, 85 { 86 "multiline_literal.hcl", 87 true, 88 nil, 89 }, 90 { 91 "multiline_literal_with_hil.hcl", 92 false, 93 map[string]interface{}{"multiline_literal_with_hil": "${hello\n world}"}, 94 }, 95 { 96 "multiline_no_marker.hcl", 97 true, 98 nil, 99 }, 100 { 101 "multiline.hcl", 102 false, 103 map[string]interface{}{"foo": "bar\nbaz\n"}, 104 }, 105 { 106 "multiline_indented.hcl", 107 false, 108 map[string]interface{}{"foo": " bar\n baz\n"}, 109 }, 110 { 111 "multiline_no_hanging_indent.hcl", 112 false, 113 map[string]interface{}{"foo": " baz\n bar\n foo\n"}, 114 }, 115 { 116 "multiline_no_eof.hcl", 117 false, 118 map[string]interface{}{"foo": "bar\nbaz\n", "key": "value"}, 119 }, 120 { 121 "multiline.json", 122 false, 123 map[string]interface{}{"foo": "bar\nbaz"}, 124 }, 125 { 126 "null_strings.json", 127 false, 128 map[string]interface{}{ 129 "module": []map[string]interface{}{ 130 map[string]interface{}{ 131 "app": []map[string]interface{}{ 132 map[string]interface{}{"foo": ""}, 133 }, 134 }, 135 }, 136 }, 137 }, 138 { 139 "scientific.json", 140 false, 141 map[string]interface{}{ 142 "a": 1e-10, 143 "b": 1e+10, 144 "c": 1e10, 145 "d": 1.2e-10, 146 "e": 1.2e+10, 147 "f": 1.2e10, 148 }, 149 }, 150 { 151 "scientific.hcl", 152 false, 153 map[string]interface{}{ 154 "a": 1e-10, 155 "b": 1e+10, 156 "c": 1e10, 157 "d": 1.2e-10, 158 "e": 1.2e+10, 159 "f": 1.2e10, 160 }, 161 }, 162 { 163 "terraform_heroku.hcl", 164 false, 165 map[string]interface{}{ 166 "name": "terraform-test-app", 167 "config_vars": []map[string]interface{}{ 168 map[string]interface{}{ 169 "FOO": "bar", 170 }, 171 }, 172 }, 173 }, 174 { 175 "structure_multi.hcl", 176 false, 177 map[string]interface{}{ 178 "foo": []map[string]interface{}{ 179 map[string]interface{}{ 180 "baz": []map[string]interface{}{ 181 map[string]interface{}{"key": 7}, 182 }, 183 }, 184 map[string]interface{}{ 185 "bar": []map[string]interface{}{ 186 map[string]interface{}{"key": 12}, 187 }, 188 }, 189 }, 190 }, 191 }, 192 { 193 "structure_multi.json", 194 false, 195 map[string]interface{}{ 196 "foo": []map[string]interface{}{ 197 map[string]interface{}{ 198 "baz": []map[string]interface{}{ 199 map[string]interface{}{"key": 7}, 200 }, 201 }, 202 map[string]interface{}{ 203 "bar": []map[string]interface{}{ 204 map[string]interface{}{"key": 12}, 205 }, 206 }, 207 }, 208 }, 209 }, 210 { 211 "list_of_lists.hcl", 212 false, 213 map[string]interface{}{ 214 "foo": []interface{}{ 215 []interface{}{"foo"}, 216 []interface{}{"bar"}, 217 }, 218 }, 219 }, 220 { 221 "list_of_maps.hcl", 222 false, 223 map[string]interface{}{ 224 "foo": []interface{}{ 225 map[string]interface{}{"somekey1": "someval1"}, 226 map[string]interface{}{"somekey2": "someval2", "someextrakey": "someextraval"}, 227 }, 228 }, 229 }, 230 { 231 "assign_deep.hcl", 232 false, 233 map[string]interface{}{ 234 "resource": []interface{}{ 235 map[string]interface{}{ 236 "foo": []interface{}{ 237 map[string]interface{}{ 238 "bar": []map[string]interface{}{ 239 map[string]interface{}{}}}}}}}, 240 }, 241 { 242 "structure_list.hcl", 243 false, 244 map[string]interface{}{ 245 "foo": []map[string]interface{}{ 246 map[string]interface{}{ 247 "key": 7, 248 }, 249 map[string]interface{}{ 250 "key": 12, 251 }, 252 }, 253 }, 254 }, 255 { 256 "structure_list.json", 257 false, 258 map[string]interface{}{ 259 "foo": []map[string]interface{}{ 260 map[string]interface{}{ 261 "key": 7, 262 }, 263 map[string]interface{}{ 264 "key": 12, 265 }, 266 }, 267 }, 268 }, 269 { 270 "structure_list_deep.json", 271 false, 272 map[string]interface{}{ 273 "bar": []map[string]interface{}{ 274 map[string]interface{}{ 275 "foo": []map[string]interface{}{ 276 map[string]interface{}{ 277 "name": "terraform_example", 278 "ingress": []map[string]interface{}{ 279 map[string]interface{}{ 280 "from_port": 22, 281 }, 282 map[string]interface{}{ 283 "from_port": 80, 284 }, 285 }, 286 }, 287 }, 288 }, 289 }, 290 }, 291 }, 292 293 { 294 "structure_list_empty.json", 295 false, 296 map[string]interface{}{ 297 "foo": []interface{}{}, 298 }, 299 }, 300 301 { 302 "nested_block_comment.hcl", 303 false, 304 map[string]interface{}{ 305 "bar": "value", 306 }, 307 }, 308 309 { 310 "unterminated_block_comment.hcl", 311 true, 312 nil, 313 }, 314 315 { 316 "unterminated_brace.hcl", 317 true, 318 nil, 319 }, 320 321 { 322 "nested_provider_bad.hcl", 323 true, 324 nil, 325 }, 326 327 { 328 "object_list.json", 329 false, 330 map[string]interface{}{ 331 "resource": []map[string]interface{}{ 332 map[string]interface{}{ 333 "aws_instance": []map[string]interface{}{ 334 map[string]interface{}{ 335 "db": []map[string]interface{}{ 336 map[string]interface{}{ 337 "vpc": "foo", 338 "provisioner": []map[string]interface{}{ 339 map[string]interface{}{ 340 "file": []map[string]interface{}{ 341 map[string]interface{}{ 342 "source": "foo", 343 "destination": "bar", 344 }, 345 }, 346 }, 347 }, 348 }, 349 }, 350 }, 351 }, 352 }, 353 }, 354 }, 355 }, 356 357 // Terraform GH-8295 sanity test that basic decoding into 358 // interface{} works. 359 { 360 "terraform_variable_invalid.json", 361 false, 362 map[string]interface{}{ 363 "variable": []map[string]interface{}{ 364 map[string]interface{}{ 365 "whatever": "abc123", 366 }, 367 }, 368 }, 369 }, 370 371 { 372 "interpolate.json", 373 false, 374 map[string]interface{}{ 375 "default": `${replace("europe-west", "-", " ")}`, 376 }, 377 }, 378 379 { 380 "block_assign.hcl", 381 true, 382 nil, 383 }, 384 385 { 386 "escape_backslash.hcl", 387 false, 388 map[string]interface{}{ 389 "output": []map[string]interface{}{ 390 map[string]interface{}{ 391 "one": `${replace(var.sub_domain, ".", "\\.")}`, 392 "two": `${replace(var.sub_domain, ".", "\\\\.")}`, 393 "many": `${replace(var.sub_domain, ".", "\\\\\\\\.")}`, 394 }, 395 }, 396 }, 397 }, 398 399 { 400 "git_crypt.hcl", 401 true, 402 nil, 403 }, 404 405 { 406 "object_with_bool.hcl", 407 false, 408 map[string]interface{}{ 409 "path": []map[string]interface{}{ 410 map[string]interface{}{ 411 "policy": "write", 412 "permissions": []map[string]interface{}{ 413 map[string]interface{}{ 414 "bool": []interface{}{false}, 415 }, 416 }, 417 }, 418 }, 419 }, 420 }, 421 } 422 423 for _, tc := range cases { 424 t.Run(tc.File, func(t *testing.T) { 425 d, err := ioutil.ReadFile(filepath.Join(fixtureDir, tc.File)) 426 if err != nil { 427 t.Fatalf("err: %s", err) 428 } 429 430 var out interface{} 431 err = Decode(&out, string(d)) 432 if (err != nil) != tc.Err { 433 t.Fatalf("Input: %s\n\nError: %s", tc.File, err) 434 } 435 436 if !reflect.DeepEqual(out, tc.Out) { 437 t.Fatalf("Input: %s. Actual, Expected.\n\n%#v\n\n%#v", tc.File, out, tc.Out) 438 } 439 440 var v interface{} 441 err = Unmarshal(d, &v) 442 if (err != nil) != tc.Err { 443 t.Fatalf("Input: %s\n\nError: %s", tc.File, err) 444 } 445 446 if !reflect.DeepEqual(v, tc.Out) { 447 t.Fatalf("Input: %s. Actual, Expected.\n\n%#v\n\n%#v", tc.File, out, tc.Out) 448 } 449 }) 450 } 451} 452 453func TestDecode_interfaceInline(t *testing.T) { 454 cases := []struct { 455 Value string 456 Err bool 457 Out interface{} 458 }{ 459 {"t t e{{}}", true, nil}, 460 {"t=0t d {}", true, map[string]interface{}{"t": 0}}, 461 {"v=0E0v d{}", true, map[string]interface{}{"v": float64(0)}}, 462 } 463 464 for _, tc := range cases { 465 t.Logf("Testing: %q", tc.Value) 466 467 var out interface{} 468 err := Decode(&out, tc.Value) 469 if (err != nil) != tc.Err { 470 t.Fatalf("Input: %q\n\nError: %s", tc.Value, err) 471 } 472 473 if !reflect.DeepEqual(out, tc.Out) { 474 t.Fatalf("Input: %q. Actual, Expected.\n\n%#v\n\n%#v", tc.Value, out, tc.Out) 475 } 476 477 var v interface{} 478 err = Unmarshal([]byte(tc.Value), &v) 479 if (err != nil) != tc.Err { 480 t.Fatalf("Input: %q\n\nError: %s", tc.Value, err) 481 } 482 483 if !reflect.DeepEqual(v, tc.Out) { 484 t.Fatalf("Input: %q. Actual, Expected.\n\n%#v\n\n%#v", tc.Value, out, tc.Out) 485 } 486 } 487} 488 489func TestDecode_equal(t *testing.T) { 490 cases := []struct { 491 One, Two string 492 }{ 493 { 494 "basic.hcl", 495 "basic.json", 496 }, 497 { 498 "float.hcl", 499 "float.json", 500 }, 501 /* 502 { 503 "structure.hcl", 504 "structure.json", 505 }, 506 */ 507 { 508 "structure.hcl", 509 "structure_flat.json", 510 }, 511 { 512 "terraform_heroku.hcl", 513 "terraform_heroku.json", 514 }, 515 } 516 517 for _, tc := range cases { 518 p1 := filepath.Join(fixtureDir, tc.One) 519 p2 := filepath.Join(fixtureDir, tc.Two) 520 521 d1, err := ioutil.ReadFile(p1) 522 if err != nil { 523 t.Fatalf("err: %s", err) 524 } 525 526 d2, err := ioutil.ReadFile(p2) 527 if err != nil { 528 t.Fatalf("err: %s", err) 529 } 530 531 var i1, i2 interface{} 532 err = Decode(&i1, string(d1)) 533 if err != nil { 534 t.Fatalf("err: %s", err) 535 } 536 537 err = Decode(&i2, string(d2)) 538 if err != nil { 539 t.Fatalf("err: %s", err) 540 } 541 542 if !reflect.DeepEqual(i1, i2) { 543 t.Fatalf( 544 "%s != %s\n\n%#v\n\n%#v", 545 tc.One, tc.Two, 546 i1, i2) 547 } 548 } 549} 550 551func TestDecode_flatMap(t *testing.T) { 552 var val map[string]map[string]string 553 554 err := Decode(&val, testReadFile(t, "structure_flatmap.hcl")) 555 if err != nil { 556 t.Fatalf("err: %s", err) 557 } 558 559 expected := map[string]map[string]string{ 560 "foo": map[string]string{ 561 "foo": "bar", 562 "key": "7", 563 }, 564 } 565 566 if !reflect.DeepEqual(val, expected) { 567 t.Fatalf("Actual: %#v\n\nExpected: %#v", val, expected) 568 } 569} 570 571func TestDecode_structure(t *testing.T) { 572 type Embedded interface{} 573 574 type V struct { 575 Embedded `hcl:"-"` 576 Key int 577 Foo string 578 } 579 580 var actual V 581 582 err := Decode(&actual, testReadFile(t, "flat.hcl")) 583 if err != nil { 584 t.Fatalf("err: %s", err) 585 } 586 587 expected := V{ 588 Key: 7, 589 Foo: "bar", 590 } 591 592 if !reflect.DeepEqual(actual, expected) { 593 t.Fatalf("Actual: %#v\n\nExpected: %#v", actual, expected) 594 } 595} 596 597func TestDecode_structurePtr(t *testing.T) { 598 type V struct { 599 Key int 600 Foo string 601 } 602 603 var actual *V 604 605 err := Decode(&actual, testReadFile(t, "flat.hcl")) 606 if err != nil { 607 t.Fatalf("err: %s", err) 608 } 609 610 expected := &V{ 611 Key: 7, 612 Foo: "bar", 613 } 614 615 if !reflect.DeepEqual(actual, expected) { 616 t.Fatalf("Actual: %#v\n\nExpected: %#v", actual, expected) 617 } 618} 619 620func TestDecode_structureArray(t *testing.T) { 621 // This test is extracted from a failure in Consul (consul.io), 622 // hence the interesting structure naming. 623 624 type KeyPolicyType string 625 626 type KeyPolicy struct { 627 Prefix string `hcl:",key"` 628 Policy KeyPolicyType 629 } 630 631 type Policy struct { 632 Keys []KeyPolicy `hcl:"key,expand"` 633 } 634 635 expected := Policy{ 636 Keys: []KeyPolicy{ 637 KeyPolicy{ 638 Prefix: "", 639 Policy: "read", 640 }, 641 KeyPolicy{ 642 Prefix: "foo/", 643 Policy: "write", 644 }, 645 KeyPolicy{ 646 Prefix: "foo/bar/", 647 Policy: "read", 648 }, 649 KeyPolicy{ 650 Prefix: "foo/bar/baz", 651 Policy: "deny", 652 }, 653 }, 654 } 655 656 files := []string{ 657 "decode_policy.hcl", 658 "decode_policy.json", 659 } 660 661 for _, f := range files { 662 var actual Policy 663 664 err := Decode(&actual, testReadFile(t, f)) 665 if err != nil { 666 t.Fatalf("Input: %s\n\nerr: %s", f, err) 667 } 668 669 if !reflect.DeepEqual(actual, expected) { 670 t.Fatalf("Input: %s\n\nActual: %#v\n\nExpected: %#v", f, actual, expected) 671 } 672 } 673} 674 675func TestDecode_sliceExpand(t *testing.T) { 676 type testInner struct { 677 Name string `hcl:",key"` 678 Key string 679 } 680 681 type testStruct struct { 682 Services []testInner `hcl:"service,expand"` 683 } 684 685 expected := testStruct{ 686 Services: []testInner{ 687 testInner{ 688 Name: "my-service-0", 689 Key: "value", 690 }, 691 testInner{ 692 Name: "my-service-1", 693 Key: "value", 694 }, 695 }, 696 } 697 698 files := []string{ 699 "slice_expand.hcl", 700 } 701 702 for _, f := range files { 703 t.Logf("Testing: %s", f) 704 705 var actual testStruct 706 err := Decode(&actual, testReadFile(t, f)) 707 if err != nil { 708 t.Fatalf("Input: %s\n\nerr: %s", f, err) 709 } 710 711 if !reflect.DeepEqual(actual, expected) { 712 t.Fatalf("Input: %s\n\nActual: %#v\n\nExpected: %#v", f, actual, expected) 713 } 714 } 715} 716 717func TestDecode_structureMap(t *testing.T) { 718 // This test is extracted from a failure in Terraform (terraform.io), 719 // hence the interesting structure naming. 720 721 type hclVariable struct { 722 Default interface{} 723 Description string 724 Fields []string `hcl:",decodedFields"` 725 } 726 727 type rawConfig struct { 728 Variable map[string]hclVariable 729 } 730 731 expected := rawConfig{ 732 Variable: map[string]hclVariable{ 733 "foo": hclVariable{ 734 Default: "bar", 735 Description: "bar", 736 Fields: []string{"Default", "Description"}, 737 }, 738 739 "amis": hclVariable{ 740 Default: []map[string]interface{}{ 741 map[string]interface{}{ 742 "east": "foo", 743 }, 744 }, 745 Fields: []string{"Default"}, 746 }, 747 }, 748 } 749 750 files := []string{ 751 "decode_tf_variable.hcl", 752 "decode_tf_variable.json", 753 } 754 755 for _, f := range files { 756 t.Logf("Testing: %s", f) 757 758 var actual rawConfig 759 err := Decode(&actual, testReadFile(t, f)) 760 if err != nil { 761 t.Fatalf("Input: %s\n\nerr: %s", f, err) 762 } 763 764 if !reflect.DeepEqual(actual, expected) { 765 t.Fatalf("Input: %s\n\nActual: %#v\n\nExpected: %#v", f, actual, expected) 766 } 767 } 768} 769 770func TestDecode_structureMapInvalid(t *testing.T) { 771 // Terraform GH-8295 772 773 type hclVariable struct { 774 Default interface{} 775 Description string 776 Fields []string `hcl:",decodedFields"` 777 } 778 779 type rawConfig struct { 780 Variable map[string]*hclVariable 781 } 782 783 var actual rawConfig 784 err := Decode(&actual, testReadFile(t, "terraform_variable_invalid.json")) 785 if err == nil { 786 t.Fatal("expected error") 787 } 788} 789 790func TestDecode_structureMapExtraKeys(t *testing.T) { 791 type hclVariable struct { 792 A int 793 B int 794 Found []string `hcl:",decodedFields"` 795 Extra map[string][]token.Pos `hcl:",unusedKeyPositions"` 796 } 797 798 q := hclVariable{ 799 A: 1, 800 B: 2, 801 Found: []string{"A", "B"}, 802 Extra: map[string][]token.Pos { 803 "extra1": {{ 804 Line: 3, 805 Column: 1, 806 Offset: 12, 807 }}, 808 "extra2": {{ 809 Line: 4, 810 Column: 1, 811 Offset: 23, 812 }}, 813 }, 814 } 815 816 var p hclVariable 817 ast, _ := Parse(testReadFile(t, "structure_map_extra_keys.hcl")) 818 DecodeObject(&p, ast) 819 if !reflect.DeepEqual(p, q) { 820 t.Fatal("not equal") 821 } 822 823 p.Extra = map[string][]token.Pos{ 824 "extra1": {{}}, 825 "extra2": {{}}, 826 } 827 828 var j hclVariable 829 ast, _ = Parse(testReadFile(t, "structure_map_extra_keys.json")) 830 DecodeObject(&j, ast) 831 if !reflect.DeepEqual(p, j) { 832 t.Fatal("not equal") 833 } 834} 835 836func TestDecode_interfaceNonPointer(t *testing.T) { 837 var value interface{} 838 err := Decode(value, testReadFile(t, "basic_int_string.hcl")) 839 if err == nil { 840 t.Fatal("should error") 841 } 842} 843 844func TestDecode_intString(t *testing.T) { 845 var value struct { 846 Count int 847 } 848 849 err := Decode(&value, testReadFile(t, "basic_int_string.hcl")) 850 if err != nil { 851 t.Fatalf("err: %s", err) 852 } 853 854 if value.Count != 3 { 855 t.Fatalf("bad: %#v", value.Count) 856 } 857} 858 859func TestDecode_float32(t *testing.T) { 860 var value struct { 861 A float32 `hcl:"a"` 862 B float32 `hcl:"b"` 863 } 864 865 err := Decode(&value, testReadFile(t, "float.hcl")) 866 if err != nil { 867 t.Fatalf("err: %s", err) 868 } 869 870 if got, want := value.A, float32(1.02); got != want { 871 t.Fatalf("wrong result %#v; want %#v", got, want) 872 } 873 if got, want := value.B, float32(2); got != want { 874 t.Fatalf("wrong result %#v; want %#v", got, want) 875 } 876} 877 878func TestDecode_float64(t *testing.T) { 879 var value struct { 880 A float64 `hcl:"a"` 881 B float64 `hcl:"b"` 882 } 883 884 err := Decode(&value, testReadFile(t, "float.hcl")) 885 if err != nil { 886 t.Fatalf("err: %s", err) 887 } 888 889 if got, want := value.A, float64(1.02); got != want { 890 t.Fatalf("wrong result %#v; want %#v", got, want) 891 } 892 if got, want := value.B, float64(2); got != want { 893 t.Fatalf("wrong result %#v; want %#v", got, want) 894 } 895} 896 897func TestDecode_intStringAliased(t *testing.T) { 898 var value struct { 899 Count time.Duration 900 } 901 902 err := Decode(&value, testReadFile(t, "basic_int_string.hcl")) 903 if err != nil { 904 t.Fatalf("err: %s", err) 905 } 906 907 if value.Count != time.Duration(3) { 908 t.Fatalf("bad: %#v", value.Count) 909 } 910} 911 912func TestDecode_Node(t *testing.T) { 913 // given 914 var value struct { 915 Content ast.Node 916 Nested struct { 917 Content ast.Node 918 } 919 } 920 921 content := ` 922content { 923 hello = "world" 924} 925` 926 927 // when 928 err := Decode(&value, content) 929 930 // then 931 if err != nil { 932 t.Errorf("unable to decode content, %v", err) 933 return 934 } 935 936 // verify ast.Node can be decoded later 937 var v map[string]interface{} 938 err = DecodeObject(&v, value.Content) 939 if err != nil { 940 t.Errorf("unable to decode content, %v", err) 941 return 942 } 943 944 if v["hello"] != "world" { 945 t.Errorf("expected mapping to be returned") 946 } 947} 948 949func TestDecode_NestedNode(t *testing.T) { 950 // given 951 var value struct { 952 Nested struct { 953 Content ast.Node 954 } 955 } 956 957 content := ` 958nested "content" { 959 hello = "world" 960} 961` 962 963 // when 964 err := Decode(&value, content) 965 966 // then 967 if err != nil { 968 t.Errorf("unable to decode content, %v", err) 969 return 970 } 971 972 // verify ast.Node can be decoded later 973 var v map[string]interface{} 974 err = DecodeObject(&v, value.Nested.Content) 975 if err != nil { 976 t.Errorf("unable to decode content, %v", err) 977 return 978 } 979 980 if v["hello"] != "world" { 981 t.Errorf("expected mapping to be returned") 982 } 983} 984 985// https://github.com/hashicorp/hcl/issues/60 986func TestDecode_topLevelKeys(t *testing.T) { 987 type Template struct { 988 Source string 989 } 990 991 templates := struct { 992 Templates []*Template `hcl:"template"` 993 }{} 994 995 err := Decode(&templates, ` 996 template { 997 source = "blah" 998 } 999 1000 template { 1001 source = "blahblah" 1002 }`) 1003 1004 if err != nil { 1005 t.Fatal(err) 1006 } 1007 1008 if templates.Templates[0].Source != "blah" { 1009 t.Errorf("bad source: %s", templates.Templates[0].Source) 1010 } 1011 1012 if templates.Templates[1].Source != "blahblah" { 1013 t.Errorf("bad source: %s", templates.Templates[1].Source) 1014 } 1015} 1016 1017func TestDecode_flattenedJSON(t *testing.T) { 1018 // make sure we can also correctly extract a Name key too 1019 type V struct { 1020 Name string `hcl:",key"` 1021 Description string 1022 Default map[string]string 1023 } 1024 type Vars struct { 1025 Variable []*V 1026 } 1027 1028 cases := []struct { 1029 JSON string 1030 Out interface{} 1031 Expected interface{} 1032 }{ 1033 { // Nested object, no sibling keys 1034 JSON: ` 1035{ 1036 "var_name": { 1037 "default": { 1038 "key1": "a", 1039 "key2": "b" 1040 } 1041 } 1042} 1043 `, 1044 Out: &[]*V{}, 1045 Expected: &[]*V{ 1046 &V{ 1047 Name: "var_name", 1048 Default: map[string]string{"key1": "a", "key2": "b"}, 1049 }, 1050 }, 1051 }, 1052 1053 { // Nested object with a sibling key (this worked previously) 1054 JSON: ` 1055{ 1056 "var_name": { 1057 "description": "Described", 1058 "default": { 1059 "key1": "a", 1060 "key2": "b" 1061 } 1062 } 1063} 1064 `, 1065 Out: &[]*V{}, 1066 Expected: &[]*V{ 1067 &V{ 1068 Name: "var_name", 1069 Description: "Described", 1070 Default: map[string]string{"key1": "a", "key2": "b"}, 1071 }, 1072 }, 1073 }, 1074 1075 { // Multiple nested objects, one with a sibling key 1076 JSON: ` 1077{ 1078 "variable": { 1079 "var_1": { 1080 "default": { 1081 "key1": "a", 1082 "key2": "b" 1083 } 1084 }, 1085 "var_2": { 1086 "description": "Described", 1087 "default": { 1088 "key1": "a", 1089 "key2": "b" 1090 } 1091 } 1092 } 1093} 1094 `, 1095 Out: &Vars{}, 1096 Expected: &Vars{ 1097 Variable: []*V{ 1098 &V{ 1099 Name: "var_1", 1100 Default: map[string]string{"key1": "a", "key2": "b"}, 1101 }, 1102 &V{ 1103 Name: "var_2", 1104 Description: "Described", 1105 Default: map[string]string{"key1": "a", "key2": "b"}, 1106 }, 1107 }, 1108 }, 1109 }, 1110 1111 { // Nested object to maps 1112 JSON: ` 1113{ 1114 "variable": { 1115 "var_name": { 1116 "description": "Described", 1117 "default": { 1118 "key1": "a", 1119 "key2": "b" 1120 } 1121 } 1122 } 1123} 1124 `, 1125 Out: &[]map[string]interface{}{}, 1126 Expected: &[]map[string]interface{}{ 1127 { 1128 "variable": []map[string]interface{}{ 1129 { 1130 "var_name": []map[string]interface{}{ 1131 { 1132 "description": "Described", 1133 "default": []map[string]interface{}{ 1134 { 1135 "key1": "a", 1136 "key2": "b", 1137 }, 1138 }, 1139 }, 1140 }, 1141 }, 1142 }, 1143 }, 1144 }, 1145 }, 1146 1147 { // Nested object to maps without a sibling key should decode the same as above 1148 JSON: ` 1149{ 1150 "variable": { 1151 "var_name": { 1152 "default": { 1153 "key1": "a", 1154 "key2": "b" 1155 } 1156 } 1157 } 1158} 1159 `, 1160 Out: &[]map[string]interface{}{}, 1161 Expected: &[]map[string]interface{}{ 1162 { 1163 "variable": []map[string]interface{}{ 1164 { 1165 "var_name": []map[string]interface{}{ 1166 { 1167 "default": []map[string]interface{}{ 1168 { 1169 "key1": "a", 1170 "key2": "b", 1171 }, 1172 }, 1173 }, 1174 }, 1175 }, 1176 }, 1177 }, 1178 }, 1179 }, 1180 1181 { // Nested objects, one with a sibling key, and one without 1182 JSON: ` 1183{ 1184 "variable": { 1185 "var_1": { 1186 "default": { 1187 "key1": "a", 1188 "key2": "b" 1189 } 1190 }, 1191 "var_2": { 1192 "description": "Described", 1193 "default": { 1194 "key1": "a", 1195 "key2": "b" 1196 } 1197 } 1198 } 1199} 1200 `, 1201 Out: &[]map[string]interface{}{}, 1202 Expected: &[]map[string]interface{}{ 1203 { 1204 "variable": []map[string]interface{}{ 1205 { 1206 "var_1": []map[string]interface{}{ 1207 { 1208 "default": []map[string]interface{}{ 1209 { 1210 "key1": "a", 1211 "key2": "b", 1212 }, 1213 }, 1214 }, 1215 }, 1216 }, 1217 }, 1218 }, 1219 { 1220 "variable": []map[string]interface{}{ 1221 { 1222 "var_2": []map[string]interface{}{ 1223 { 1224 "description": "Described", 1225 "default": []map[string]interface{}{ 1226 { 1227 "key1": "a", 1228 "key2": "b", 1229 }, 1230 }, 1231 }, 1232 }, 1233 }, 1234 }, 1235 }, 1236 }, 1237 }, 1238 } 1239 1240 for i, tc := range cases { 1241 err := Decode(tc.Out, tc.JSON) 1242 if err != nil { 1243 t.Fatalf("[%d] err: %s", i, err) 1244 } 1245 1246 if !reflect.DeepEqual(tc.Out, tc.Expected) { 1247 t.Fatalf("[%d]\ngot: %s\nexpected: %s\n", i, spew.Sdump(tc.Out), spew.Sdump(tc.Expected)) 1248 } 1249 } 1250} 1251