cuelang.org/go@v0.10.1/encoding/toml/decode_test.go (about) 1 // Copyright 2024 The CUE Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package toml_test 16 17 import ( 18 "encoding/json" 19 "io" 20 "strings" 21 "testing" 22 23 "github.com/go-quicktest/qt" 24 gotoml "github.com/pelletier/go-toml/v2" 25 26 "cuelang.org/go/cue/ast/astutil" 27 "cuelang.org/go/cue/cuecontext" 28 "cuelang.org/go/cue/errors" 29 "cuelang.org/go/cue/format" 30 "cuelang.org/go/encoding/toml" 31 ) 32 33 func TestDecoder(t *testing.T) { 34 t.Parallel() 35 // Note that we use backquoted Go string literals with indentation for readability. 36 // The whitespace doesn't affect the input TOML, and we cue/format on the "want" CUE source, 37 // so the added newlines and tabs don't change the test behavior. 38 tests := []struct { 39 name string 40 input string 41 wantCUE string 42 wantErr string 43 }{{ 44 name: "Empty", 45 input: "", 46 wantCUE: "", 47 }, { 48 name: "LoneComment", 49 input: ` 50 # Just a comment 51 `, 52 wantCUE: "", 53 }, { 54 name: "RootKeyMissing", 55 input: ` 56 # A comment to verify that parser positions work. 57 = "no key name" 58 `, 59 wantErr: ` 60 invalid character at start of key: =: 61 test.toml:2:1 62 `, 63 }, { 64 name: "RootKeysOne", 65 input: ` 66 key = "value" 67 `, 68 wantCUE: ` 69 key: "value" 70 `, 71 }, { 72 name: "RootMultiple", 73 input: ` 74 key1 = "value1" 75 key2 = "value2" 76 key3 = "value3" 77 `, 78 wantCUE: ` 79 key1: "value1" 80 key2: "value2" 81 key3: "value3" 82 `, 83 }, { 84 name: "RootKeysDots", 85 input: ` 86 a1 = "A" 87 b1.b2 = "B" 88 c1.c2.c3 = "C" 89 `, 90 wantCUE: ` 91 a1: "A" 92 b1: b2: "B" 93 c1: c2: c3: "C" 94 `, 95 }, { 96 name: "RootKeysCharacters", 97 input: ` 98 a-b = "dashes" 99 a_b = "underscore unquoted" 100 _ = "underscore quoted" 101 _ab = "underscore prefix quoted" 102 123 = "numbers" 103 x._.y._ = "underscores quoted" 104 `, 105 wantCUE: ` 106 "a-b": "dashes" 107 a_b: "underscore unquoted" 108 "_": "underscore quoted" 109 "_ab": "underscore prefix quoted" 110 "123": "numbers" 111 x: "_": y: "_": "underscores quoted" 112 `, 113 }, { 114 name: "RootKeysQuoted", 115 input: ` 116 "1.2.3" = "quoted dots" 117 "foo bar" = "quoted space" 118 'foo "bar"' = "nested quotes" 119 `, 120 wantCUE: ` 121 "1.2.3": "quoted dots" 122 "foo bar": "quoted space" 123 "foo \"bar\"": "nested quotes" 124 `, 125 }, { 126 name: "RootKeysMixed", 127 input: ` 128 site."foo.com".title = "foo bar" 129 `, 130 wantCUE: ` 131 site: "foo.com": title: "foo bar" 132 `, 133 }, { 134 name: "KeysDuplicateSimple", 135 input: ` 136 foo = "same key" 137 foo = "same key" 138 `, 139 wantErr: ` 140 duplicate key: foo: 141 test.toml:2:1 142 `, 143 }, { 144 name: "KeysDuplicateQuoted", 145 input: ` 146 "foo" = "same key" 147 foo = "same key" 148 `, 149 wantErr: ` 150 duplicate key: foo: 151 test.toml:2:1 152 `, 153 }, { 154 name: "KeysDuplicateWhitespace", 155 input: ` 156 foo . bar = "same key" 157 foo.bar = "same key" 158 `, 159 wantErr: ` 160 duplicate key: foo.bar: 161 test.toml:2:1 162 `, 163 }, { 164 name: "KeysDuplicateDots", 165 input: ` 166 foo."bar.baz".zzz = "same key" 167 foo."bar.baz".zzz = "same key" 168 `, 169 wantErr: ` 170 duplicate key: foo."bar.baz".zzz: 171 test.toml:2:1 172 `, 173 }, { 174 name: "KeysNotDuplicateDots", 175 input: ` 176 foo."bar.baz" = "different key" 177 "foo.bar".baz = "different key" 178 `, 179 wantCUE: ` 180 foo: "bar.baz": "different key" 181 "foo.bar": baz: "different key" 182 `, 183 }, { 184 name: "BasicStrings", 185 input: ` 186 escapes = "foo \"bar\" \n\t\\ baz" 187 unicode = "foo \u00E9" 188 `, 189 wantCUE: ` 190 escapes: "foo \"bar\" \n\t\\ baz" 191 unicode: "foo é" 192 `, 193 }, { 194 // Leading tabs do matter in this test. 195 // TODO: use our own multiline strings where it gives better results. 196 name: "MultilineBasicStrings", 197 input: ` 198 nested = """ can contain "" quotes """ 199 four = """"four"""" 200 double = """ 201 line one 202 line two""" 203 double_indented = """ 204 line one 205 line two 206 """ 207 escaped = """\ 208 line one \ 209 line two.\ 210 """ 211 `, 212 wantCUE: ` 213 nested: " can contain \"\" quotes " 214 four: "\"four\"" 215 double: "line one\nline two" 216 double_indented: "\tline one\n\tline two\n\t" 217 escaped: "line one line two." 218 `, 219 }, { 220 // TODO: we can probably do better in many cases, e.g. #"" 221 name: "LiteralStrings", 222 input: ` 223 winpath = 'C:\Users\nodejs\templates' 224 winpath2 = '\\ServerX\admin$\system32\' 225 quoted = 'Tom "Dubs" Preston-Werner' 226 regex = '<\i\c*\s*>' 227 `, 228 wantCUE: ` 229 winpath: "C:\\Users\\nodejs\\templates" 230 winpath2: "\\\\ServerX\\admin$\\system32\\" 231 quoted: "Tom \"Dubs\" Preston-Werner" 232 regex: "<\\i\\c*\\s*>" 233 `, 234 }, { 235 // Leading tabs do matter in this test. 236 // TODO: use our own multiline strings where it gives better results. 237 name: "MultilineLiteralStrings", 238 input: ` 239 nested = ''' can contain '' quotes ''' 240 four = ''''four'''' 241 double = ''' 242 line one 243 line two''' 244 double_indented = ''' 245 line one 246 line two 247 ''' 248 escaped = '''\ 249 line one \ 250 line two.\ 251 ''' 252 `, 253 wantCUE: ` 254 nested: " can contain '' quotes " 255 four: "'four'" 256 double: "line one\nline two" 257 double_indented: "\tline one\n\tline two\n\t" 258 escaped: "\\\nline one \\\nline two.\\\n" 259 `, 260 }, { 261 name: "Integers", 262 input: ` 263 zero = 0 264 positive = 123 265 plus = +40 266 minus = -40 267 underscores = 1_002_003 268 hexadecimal = 0xdeadBEEF 269 octal = 0o755 270 binary = 0b11010110 271 `, 272 wantCUE: ` 273 zero: 0 274 positive: 123 275 plus: +40 276 minus: -40 277 underscores: 1_002_003 278 hexadecimal: 0xdeadBEEF 279 octal: 0o755 280 binary: 0b11010110 281 `, 282 }, { 283 name: "Floats", 284 input: ` 285 pi = 3.1415 286 plus = +1.23 287 minus = -4.56 288 exponent = 1e067 289 exponent_plus = 5e+20 290 exponent_minus = -2E-4 291 exponent_dot = 6.789e-30 292 `, 293 wantCUE: ` 294 pi: 3.1415 295 plus: +1.23 296 minus: -4.56 297 exponent: 1e067 298 exponent_plus: 5e+20 299 exponent_minus: -2E-4 300 exponent_dot: 6.789e-30 301 `, 302 }, { 303 name: "Bools", 304 input: ` 305 positive = true 306 negative = false 307 `, 308 wantCUE: ` 309 positive: true 310 negative: false 311 `, 312 }, { 313 name: "Arrays", 314 input: ` 315 integers = [1, 2, 3] 316 colors = ["red", "yellow", "green"] 317 nested_ints = [[1, 2], [3, 4, 5]] 318 nested_mixed = [[1, 2], ["a", "b", "c"], {extra = "keys"}] 319 strings = ["all", 'strings', """are the same""", '''type'''] 320 mixed_numbers = [0.1, 0.2, 0.5, 1, 2, 5] 321 `, 322 wantCUE: ` 323 integers: [1, 2, 3] 324 colors: ["red", "yellow", "green"] 325 nested_ints: [[1, 2], [3, 4, 5]] 326 nested_mixed: [[1, 2], ["a", "b", "c"], {extra: "keys"}] 327 strings: ["all", "strings", "are the same", "type"] 328 mixed_numbers: [0.1, 0.2, 0.5, 1, 2, 5] 329 `, 330 }, { 331 name: "InlineTables", 332 input: ` 333 empty = {} 334 point = {x = 1, y = 2} 335 animal = {type.name = "pug"} 336 deep = {l1 = {l2 = {l3 = "leaf"}}} 337 `, 338 wantCUE: ` 339 empty: {} 340 point: {x: 1, y: 2} 341 animal: {type: name: "pug"} 342 deep: {l1: {l2: {l3: "leaf"}}} 343 `, 344 }, { 345 name: "InlineTablesDuplicate", 346 input: ` 347 point = {x = "same key", x = "same key"} 348 `, 349 wantErr: ` 350 duplicate key: point.x: 351 test.toml:1:26 352 `, 353 }, { 354 name: "ArrayInlineTablesDuplicate", 355 input: ` 356 point = [{}, {}, {x = "same key", x = "same key"}] 357 `, 358 wantErr: ` 359 duplicate key: point.2.x: 360 test.toml:1:35 361 `, 362 }, { 363 name: "InlineTablesNotDuplicateScoping", 364 input: ` 365 repeat = {repeat = {repeat = "leaf"}} 366 struct1 = {sibling = "leaf"} 367 struct2 = {sibling = "leaf"} 368 arrays = [{sibling = "leaf"}, {sibling = "leaf"}] 369 `, 370 wantCUE: ` 371 repeat: {repeat: {repeat: "leaf"}} 372 struct1: {sibling: "leaf"} 373 struct2: {sibling: "leaf"} 374 arrays: [{sibling: "leaf"}, {sibling: "leaf"}] 375 `, 376 }, { 377 name: "TablesEmpty", 378 input: ` 379 [foo] 380 [bar] 381 `, 382 wantCUE: ` 383 foo: {} 384 bar: {} 385 `, 386 }, { 387 name: "TablesOne", 388 input: ` 389 [foo] 390 single = "single" 391 `, 392 wantCUE: ` 393 foo: { 394 single: "single" 395 } 396 `, 397 }, { 398 name: "TablesMultiple", 399 input: ` 400 root1 = "root1 value" 401 root2 = "root2 value" 402 [foo] 403 foo1 = "foo1 value" 404 foo2 = "foo2 value" 405 [bar] 406 bar1 = "bar1 value" 407 bar2 = "bar2 value" 408 `, 409 wantCUE: ` 410 root1: "root1 value" 411 root2: "root2 value" 412 foo: { 413 foo1: "foo1 value" 414 foo2: "foo2 value" 415 } 416 bar: { 417 bar1: "bar1 value" 418 bar2: "bar2 value" 419 } 420 `, 421 }, { 422 // A lot of these edge cases are covered by RootKeys tests already. 423 name: "TablesKeysComplex", 424 input: ` 425 [foo.bar . "baz.zzz zzz"] 426 one = "1" 427 [123-456] 428 two = "2" 429 `, 430 wantCUE: ` 431 foo: bar: "baz.zzz zzz": { 432 one: "1" 433 } 434 "123-456": { 435 two: "2" 436 } 437 `, 438 }, { 439 name: "TableKeysDuplicateSimple", 440 input: ` 441 [foo] 442 [foo] 443 `, 444 wantErr: ` 445 duplicate key: foo: 446 test.toml:2:2 447 `, 448 }, { 449 name: "TableKeysDuplicateOverlap", 450 input: ` 451 [foo] 452 bar = "leaf" 453 [foo.bar] 454 baz = "second leaf" 455 `, 456 wantErr: ` 457 duplicate key: foo.bar: 458 test.toml:3:2 459 `, 460 }, { 461 name: "TableInnerKeysDuplicateSimple", 462 input: ` 463 [foo] 464 bar = "same key" 465 bar = "same key" 466 `, 467 wantErr: ` 468 duplicate key: foo.bar: 469 test.toml:3:1 470 `, 471 }, { 472 name: "TablesNotDuplicateScoping", 473 input: ` 474 [repeat] 475 repeat.repeat = "leaf" 476 [struct1] 477 sibling = "leaf" 478 [struct2] 479 sibling = "leaf" 480 `, 481 wantCUE: ` 482 repeat: { 483 repeat: repeat: "leaf" 484 } 485 struct1: { 486 sibling: "leaf" 487 } 488 struct2: { 489 sibling: "leaf" 490 } 491 `, 492 }, { 493 name: "ArrayTablesEmpty", 494 input: ` 495 [[foo]] 496 `, 497 wantCUE: ` 498 foo: [ 499 {}, 500 ] 501 `, 502 }, { 503 name: "ArrayTablesOne", 504 input: ` 505 [[foo]] 506 single = "single" 507 `, 508 wantCUE: ` 509 foo: [ 510 { 511 single: "single" 512 }, 513 ] 514 `, 515 }, { 516 name: "ArrayTablesMultiple", 517 input: ` 518 root = "root value" 519 [[foo]] 520 foo1 = "foo1 value" 521 foo2 = "foo2 value" 522 [[foo]] 523 foo3 = "foo3 value" 524 foo4 = "foo4 value" 525 [[foo]] 526 [[foo]] 527 single = "single" 528 `, 529 wantCUE: ` 530 root: "root value" 531 foo: [ 532 { 533 foo1: "foo1 value" 534 foo2: "foo2 value" 535 }, 536 { 537 foo3: "foo3 value" 538 foo4: "foo4 value" 539 }, 540 {}, 541 { 542 single: "single" 543 }, 544 ] 545 `, 546 }, { 547 name: "ArrayTablesSeparate", 548 input: ` 549 root = "root value" 550 [[foo]] 551 foo1 = "foo1 value" 552 [[bar]] 553 bar1 = "bar1 value" 554 [[baz]] 555 `, 556 wantCUE: ` 557 root: "root value" 558 foo: [ 559 { 560 foo1: "foo1 value" 561 }, 562 ] 563 bar: [ 564 { 565 bar1: "bar1 value" 566 }, 567 ] 568 baz: [ 569 {}, 570 ] 571 `, 572 }, { 573 name: "ArrayTablesSubtable", 574 input: ` 575 [[foo]] 576 foo1 = "foo1 value" 577 [foo.subtable1] 578 sub1 = "sub1 value" 579 [foo.subtable2] 580 sub2 = "sub2 value" 581 [foo.subtable2.deeper] 582 sub2d = "sub2d value" 583 [[foo]] 584 foo2 = "foo2 value" 585 `, 586 wantCUE: ` 587 foo: [ 588 { 589 foo1: "foo1 value" 590 subtable1: { 591 sub1: "sub1 value" 592 } 593 subtable2: { 594 sub2: "sub2 value" 595 } 596 subtable2: deeper: { 597 sub2d: "sub2d value" 598 } 599 }, 600 { 601 foo2: "foo2 value" 602 }, 603 ] 604 `, 605 }, { 606 name: "ArrayTablesNested", 607 input: ` 608 [[foo]] 609 foo1 = "foo1 value" 610 [[foo.nested1]] 611 nest1a = "nest1a value" 612 [[foo.nested1]] 613 nest1b = "nest1b value" 614 [[foo.nested2]] 615 nest2 = "nest2 value" 616 [[foo.nested2.deeper]] 617 nest2d = "nest2d value" 618 [[foo.nested3.directly.deeper]] 619 nest3d = "nest3d value" 620 [[foo]] 621 foo2 = "foo2 value" 622 `, 623 wantCUE: ` 624 foo: [ 625 { 626 foo1: "foo1 value" 627 nested1: [ 628 { 629 nest1a: "nest1a value" 630 }, 631 { 632 nest1b: "nest1b value" 633 }, 634 ] 635 nested2: [ 636 { 637 nest2: "nest2 value" 638 deeper: [ 639 { 640 nest2d: "nest2d value" 641 } 642 ] 643 }, 644 ] 645 nested3: directly: deeper: [ 646 { 647 nest3d: "nest3d value" 648 }, 649 ] 650 }, 651 { 652 foo2: "foo2 value" 653 }, 654 ] 655 `, 656 }, { 657 name: "RedeclareKeyAsTableArray", 658 input: ` 659 foo = "foo value" 660 [middle] 661 middle = "to ensure we don't rely on the last key" 662 [[foo]] 663 baz = "baz value" 664 `, 665 wantErr: ` 666 cannot redeclare key "foo" as a table array: 667 test.toml:4:3 668 `, 669 }, { 670 name: "RedeclareTableAsTableArray", 671 input: ` 672 [foo] 673 bar = "bar value" 674 [middle] 675 middle = "to ensure we don't rely on the last key" 676 [[foo]] 677 baz = "baz value" 678 `, 679 wantErr: ` 680 cannot redeclare key "foo" as a table array: 681 test.toml:5:3 682 `, 683 }, { 684 name: "RedeclareArrayAsTableArray", 685 input: ` 686 foo = ["inline array"] 687 [middle] 688 middle = "to ensure we don't rely on the last key" 689 [[foo]] 690 baz = "baz value" 691 `, 692 wantErr: ` 693 cannot redeclare key "foo" as a table array: 694 test.toml:4:3 695 `, 696 }, { 697 name: "RedeclareTableArrayAsKey", 698 input: ` 699 [[foo.foo2]] 700 bar = "bar value" 701 [middle] 702 middle = "to ensure we don't rely on the last key" 703 [foo] 704 foo2 = "redeclaring" 705 `, 706 wantErr: ` 707 cannot redeclare table array "foo.foo2" as a table: 708 test.toml:6:1 709 `, 710 }, { 711 name: "RedeclareTableArrayAsTable", 712 input: ` 713 [[foo]] 714 bar = "bar value" 715 [middle] 716 middle = "to ensure we don't rely on the last key" 717 [foo] 718 baz = "baz value" 719 `, 720 wantErr: ` 721 cannot redeclare table array "foo" as a table: 722 test.toml:5:2 723 `, 724 }, { 725 name: "KeysNotDuplicateTableArrays", 726 input: ` 727 [[foo]] 728 bar = "foo.0.bar" 729 [[foo]] 730 bar = "foo.1.bar" 731 [[foo]] 732 bar = "foo.2.bar" 733 [[foo.nested]] 734 bar = "foo.2.nested.0.bar" 735 [[foo.nested]] 736 bar = "foo.2.nested.1.bar" 737 [[foo.nested]] 738 bar = "foo.2.nested.2.bar" 739 `, 740 wantCUE: ` 741 foo: [ 742 { 743 bar: "foo.0.bar" 744 }, 745 { 746 bar: "foo.1.bar" 747 }, 748 { 749 bar: "foo.2.bar" 750 nested: [ 751 { 752 bar: "foo.2.nested.0.bar" 753 }, 754 { 755 bar: "foo.2.nested.1.bar" 756 }, 757 { 758 bar: "foo.2.nested.2.bar" 759 }, 760 ] 761 }, 762 ] 763 `, 764 }} 765 for _, test := range tests { 766 t.Run(test.name, func(t *testing.T) { 767 t.Parallel() 768 769 input := unindentMultiline(test.input) 770 dec := toml.NewDecoder("test.toml", strings.NewReader(input)) 771 772 node, err := dec.Decode() 773 if test.wantErr != "" { 774 gotErr := strings.TrimSuffix(errors.Details(err, nil), "\n") 775 wantErr := unindentMultiline(test.wantErr) 776 777 qt.Assert(t, qt.Equals(gotErr, wantErr)) 778 qt.Assert(t, qt.IsNil(node)) 779 // We don't continue, so we can't expect any decoded CUE. 780 qt.Assert(t, qt.Equals(test.wantCUE, "")) 781 782 // Validate that go-toml's Unmarshal also rejects this input. 783 err = gotoml.Unmarshal([]byte(input), new(any)) 784 qt.Assert(t, qt.IsNotNil(err)) 785 return 786 } 787 qt.Assert(t, qt.IsNil(err)) 788 789 file, err := astutil.ToFile(node) 790 qt.Assert(t, qt.IsNil(err)) 791 792 node2, err := dec.Decode() 793 qt.Assert(t, qt.IsNil(node2)) 794 qt.Assert(t, qt.Equals(err, io.EOF)) 795 796 wantFormatted, err := format.Source([]byte(test.wantCUE)) 797 qt.Assert(t, qt.IsNil(err), qt.Commentf("wantCUE:\n%s", test.wantCUE)) 798 799 formatted, err := format.Node(file) 800 qt.Assert(t, qt.IsNil(err)) 801 t.Logf("CUE:\n%s", formatted) 802 qt.Assert(t, qt.Equals(string(formatted), string(wantFormatted))) 803 804 // Ensure that the CUE node can be compiled into a cue.Value and validated. 805 ctx := cuecontext.New() 806 // TODO(mvdan): cue.Context can only build ast.Expr or ast.File, not ast.Node; 807 // it's then likely not the right choice for the interface to return ast.Node. 808 val := ctx.BuildFile(file) 809 qt.Assert(t, qt.IsNil(val.Err())) 810 qt.Assert(t, qt.IsNil(val.Validate())) 811 812 // Validate that the decoded CUE value is equivalent 813 // to the Go value that go-toml's Unmarshal produces. 814 // We use JSON equality as some details such as which integer types are used 815 // are not actually relevant to an "equal data" check. 816 var unmarshalTOML any 817 err = gotoml.Unmarshal([]byte(input), &unmarshalTOML) 818 qt.Assert(t, qt.IsNil(err)) 819 jsonTOML, err := json.Marshal(unmarshalTOML) 820 qt.Assert(t, qt.IsNil(err)) 821 t.Logf("json.Marshal via go-toml:\t%s\n", jsonTOML) 822 823 jsonCUE, err := json.Marshal(val) 824 qt.Assert(t, qt.IsNil(err)) 825 t.Logf("json.Marshal via CUE:\t%s\n", jsonCUE) 826 qt.Assert(t, qt.JSONEquals(jsonCUE, unmarshalTOML)) 827 828 // Ensure that the decoded CUE can be re-encoded as TOML, 829 // and the resulting TOML is still JSON-equivalent. 830 t.Run("reencode", func(t *testing.T) { 831 sb := new(strings.Builder) 832 enc := toml.NewEncoder(sb) 833 834 err := enc.Encode(val) 835 qt.Assert(t, qt.IsNil(err)) 836 cueTOML := sb.String() 837 t.Logf("reencoded TOML:\n%s", cueTOML) 838 839 var unmarshalCueTOML any 840 err = gotoml.Unmarshal([]byte(cueTOML), &unmarshalCueTOML) 841 qt.Assert(t, qt.IsNil(err)) 842 qt.Assert(t, qt.JSONEquals(jsonCUE, unmarshalCueTOML)) 843 }) 844 }) 845 } 846 } 847 848 // unindentMultiline mimics CUE's behavior with `"""` multi-line strings, 849 // where a leading newline is omitted, and any whitespace preceding the trailing newline 850 // is removed from the start of all lines. 851 func unindentMultiline(s string) string { 852 i := strings.LastIndexByte(s, '\n') 853 if i < 0 { 854 // Not a multi-line string. 855 return s 856 } 857 trim := s[i:] 858 s = strings.ReplaceAll(s, trim, "\n") 859 s = strings.TrimPrefix(s, "\n") 860 s = strings.TrimSuffix(s, "\n") 861 return s 862 }