Compare commits
792 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8df3b65280 | |||
| 6c995ec13e | |||
| 9f1bb6c97d | |||
| d3b9283095 | |||
| 013ffc24b8 | |||
| 2762e24a9c | |||
| 5b6828661c | |||
| 2edc61f171 | |||
| 4a1b05ca08 | |||
| 003aa0993b | |||
| 84d730b6c4 | |||
| 97bd897177 | |||
| 7924b1816f | |||
| 2a07b6d9db | |||
| 692b98560b | |||
| 99cd40b175 | |||
| 3aaf147e3e | |||
| a675c6b3e2 | |||
| 9702fae9b8 | |||
| 3cf1eb2312 | |||
| 2af3554f90 | |||
| 180c6ba2ba | |||
| dafc4173ef | |||
| f1a83be671 | |||
| 5aeb70b3f0 | |||
| 8384a5683c | |||
| 4369957cb4 | |||
| a0e8464967 | |||
| c57d0d559f | |||
| 644602b845 | |||
| 36df8eef6e | |||
| 18a2148713 | |||
| bc9958322f | |||
| 6d56ac8027 | |||
| 098464b61b | |||
| 85e2448ce5 | |||
| ee07c9203b | |||
| 014204cfb7 | |||
| 923b2ab478 | |||
| af236b689f | |||
| b730b2be5d | |||
| a437caafe5 | |||
| be6c57be30 | |||
| d55304782e | |||
| 0977c05dd5 | |||
| bccd6e48f4 | |||
| 9b890cf9c5 | |||
| a3d5a0bb53 | |||
| d00d2cca6e | |||
| 86608d7fca | |||
| 4a1877957a | |||
| 3021d6d033 | |||
| 32788f26f8 | |||
| 8ed6d131eb | |||
| 7dad87762a | |||
| 2b69615b5d | |||
| 06fb30bf2e | |||
| 2e087bdf5f | |||
| caeb9f9631 | |||
| e7223fb40e | |||
| 05bedf36d8 | |||
| f5486d590f | |||
| 2ca21fb7b4 | |||
| 34765b4a9e | |||
| 358c8d2c23 | |||
| fd8d0bf4d9 | |||
| a76e18e8c5 | |||
| dff0c128d0 | |||
| 3573ce3770 | |||
| ae933f2e2a | |||
| 3175efb395 | |||
| 9dd7f1af78 | |||
| 4a5c27c299 | |||
| 76cc96f6d8 | |||
| 4835627845 | |||
| cef80b96a4 | |||
| 4040373cfd | |||
| bb026cae89 | |||
| f7d9b9ba53 | |||
| fac33d6fa8 | |||
| e183db7e69 | |||
| 60e4af8cca | |||
| 8bb1e08dc7 | |||
| 7b980e792b | |||
| 44c1513ccd | |||
| fcf9d37d0c | |||
| 986afffb7c | |||
| 8c2c9cc986 | |||
| 55ca4e35e4 | |||
| d34104d493 | |||
| 2aa08368fa | |||
| 654811fbc3 | |||
| 5c05d4d863 | |||
| 643c251c4b | |||
| 8a416daa69 | |||
| fcd9179b7d | |||
| 9f5726004e | |||
| c4a2eef8a4 | |||
| b58c20aa49 | |||
| 090cccf4ba | |||
| 58a592bbf8 | |||
| 94bd3ddcd6 | |||
| e195b58fd0 | |||
| c83d001c6d | |||
| b9e3b9c370 | |||
| d26887310c | |||
| 942841787a | |||
| 28f1efc7d3 | |||
| 7d69e4a728 | |||
| e46d245c09 | |||
| 7baa23f493 | |||
| 2d8433b69e | |||
| 67bc5422f3 | |||
| fb6d1d6c2b | |||
| d017a6dc89 | |||
| d6d3196163 | |||
| 41718a6db3 | |||
| 216628222f | |||
| 322e0b15d2 | |||
| 85bfc0ed51 | |||
| 295a720dfb | |||
| 0a422e3dbd | |||
| 627dade0c7 | |||
| b2e0231cc9 | |||
| ba95863cd3 | |||
| db679df765 | |||
| c5ca2c682b | |||
| ed80712cb4 | |||
| b24772942d | |||
| 9501a05ed7 | |||
| 171a592663 | |||
| 5aaf5ef13b | |||
| adacebd8c7 | |||
| 8bbb673431 | |||
| 2377ac4bc0 | |||
| f5cc8c49eb | |||
| 89d7b412d8 | |||
| 88a8aecdd4 | |||
| 9804fc57e0 | |||
| 068279f13b | |||
| b9edbeb611 | |||
| a97c9317d4 | |||
| 3229a0abfb | |||
| 3f5d8a6b06 | |||
| 146f70ea8a | |||
| e83cf535f5 | |||
| c3ba3ef97a | |||
| 7ee3c8ff25 | |||
| 1e85aa6d78 | |||
| 46fa3225e2 | |||
| 4d51831dab | |||
| 5a1a96cb2d | |||
| ea9040ae83 | |||
| 2373685f1e | |||
| f1391952d4 | |||
| 4a73a200ed | |||
| 4807229e94 | |||
| d8ddc00c61 | |||
| 82f8dad811 | |||
| 75db1016e8 | |||
| de6d715bd2 | |||
| 3ab2fc2b87 | |||
| 1b1dd3d6d5 | |||
| 128b7a8bfb | |||
| 892df5c28e | |||
| d58eb50ebf | |||
| 535fc65c5f | |||
| f158d7d278 | |||
| 5fd6e9cce0 | |||
| 8ce5c3d78f | |||
| 177b4a5e53 | |||
| 5cbdea6192 | |||
| 696dd25c17 | |||
| facb2b13e8 | |||
| 8bbb519477 | |||
| b37e11d74d | |||
| 6cd86876b8 | |||
| f53bc740c1 | |||
| 9bf9be681e | |||
| c862c344b3 | |||
| 0d20a84523 | |||
| 3990899d7e | |||
| 4c7a337083 | |||
| bbaae540ce | |||
| ede6445608 | |||
| b226db6a29 | |||
| d8997efb5a | |||
| 79e78b234c | |||
| 1b5a25c0ef | |||
| 8eae15b2ee | |||
| 2b3de620e8 | |||
| 8645d6376b | |||
| 64fe47161f | |||
| 4dff8eaa4d | |||
| 2dbd29a565 | |||
| f27a07d31a | |||
| 644515958c | |||
| 8683be35f6 | |||
| dc1740d473 | |||
| 11f789ef11 | |||
| 74d21b367f | |||
| 6617e7e73d | |||
| 3dbca20bc9 | |||
| 85c0658984 | |||
| 772d169b52 | |||
| b4ec220f7e | |||
| 3694ae88f6 | |||
| 19751e8a51 | |||
| 925f214125 | |||
| 39f893ad99 | |||
| c871a61015 | |||
| d0d001625c | |||
| 64941b99e2 | |||
| ed02a1f192 | |||
| 4d7c9ddac7 | |||
| feb1830dcc | |||
| 1c33d6ce20 | |||
| 3000471a12 | |||
| 1f33a6a476 | |||
| 2700aad5d2 | |||
| 7ccaa2744e | |||
| df4bb061f8 | |||
| 9e81ce1c33 | |||
| a23850f29b | |||
| 76f53c857b | |||
| 85f5d567e4 | |||
| bd5cba0b0b | |||
| cd54472d03 | |||
| cc0d1a90ff | |||
| 4984dcb5e9 | |||
| 86632bc190 | |||
| d25eec183f | |||
| e96746311c | |||
| 62acca2b68 | |||
| 476492a85c | |||
| ee9b902222 | |||
| fa56f48daf | |||
| f34c9c332f | |||
| a0d685d482 | |||
| 4a5ae9e81e | |||
| 7e2fa1bc80 | |||
| 40cfb6f458 | |||
| 1230ca485e | |||
| 69ab7e10d1 | |||
| fa07960695 | |||
| 8be357dfa1 | |||
| a93b34d984 | |||
| 9c24fbeaad | |||
| f6b38c33b7 | |||
| 773f10110c | |||
| 618f0181ac | |||
| f3bb20ea79 | |||
| b0d6c62255 | |||
| b202375414 | |||
| 250e073408 | |||
| 11f022ab09 | |||
| 840df4a229 | |||
| c2d1fd86e5 | |||
| 238a6fef7d | |||
| 67852cf007 | |||
| d276c42adc | |||
| 95c701b253 | |||
| 3db329a512 | |||
| 45ea20024b | |||
| ea225df3ed | |||
| 4545a3e94b | |||
| 3f2bb0b363 | |||
| 201d5dd422 | |||
| 1e80267558 | |||
| 931f02a519 | |||
| a533331aee | |||
| 466faaab9f | |||
| e443b4fdb8 | |||
| 2b1c52dddd | |||
| 21445f5170 | |||
| 9ba52996d8 | |||
| 6fe332a869 | |||
| 32c1a8d372 | |||
| ee102a3528 | |||
| 9b67e40640 | |||
| dca2103910 | |||
| a713a96e69 | |||
| a7b50eb8f1 | |||
| 24b62ebe61 | |||
| 9bc4641a49 | |||
| b86b890b8d | |||
| 080baa8574 | |||
| 0537b928df | |||
| 2eff2d082a | |||
| 59cddbc573 | |||
| 9e122af5fc | |||
| ed1f9ed9de | |||
| 466bfe8664 | |||
| e1f035461b | |||
| 84f9e9bceb | |||
| ca41df4a59 | |||
| f2378983d9 | |||
| 37714006b6 | |||
| 275e366c17 | |||
| 18af62d3ea | |||
| af00765ca0 | |||
| 5f877c52fd | |||
| 92b16cad91 | |||
| 4a4c2c2a5f | |||
| 5d905981cf | |||
| 7ccacf158e | |||
| 739ceda96c | |||
| 32da85ab11 | |||
| 18d45c446b | |||
| bcd5333b03 | |||
| e5255a5be2 | |||
| cf288a51c5 | |||
| 72a1afdcb2 | |||
| 2714786b37 | |||
| 51d78a5f0c | |||
| 78389c641a | |||
| c3fc668f27 | |||
| 7f016efe03 | |||
| 269b742eb2 | |||
| 7d8ea80dc3 | |||
| 6165b9454f | |||
| 2ddbf6be6d | |||
| da21b0aecf | |||
| 829c005784 | |||
| b24eb93e8e | |||
| 7dc5550057 | |||
| 9a436c7eeb | |||
| 72c999ecbf | |||
| e5a091a092 | |||
| 317b36b24b | |||
| 636a75f316 | |||
| 390927a0cd | |||
| 3f23ab97e0 | |||
| 47611ff9ea | |||
| f4ac7f7bfa | |||
| e75f23188d | |||
| 6c8adbcb17 | |||
| ffc7d3ba6e | |||
| 4efec6b76a | |||
| 0fcf06e374 | |||
| 1d332cd112 | |||
| 9d3a912da0 | |||
| 1da2fc7e28 | |||
| 17299c937b | |||
| 1bae751a45 | |||
| 8a8d1233bb | |||
| ad538d97c9 | |||
| 43fc2fa552 | |||
| dd5837651d | |||
| a0d031abec | |||
| a25f636a07 | |||
| a3b7e1e353 | |||
| bfeb32c9ce | |||
| 0703eeb262 | |||
| d458ddf4d4 | |||
| 4038ec3dae | |||
| 5b92184e42 | |||
| c6f117c45d | |||
| e78ccff9a4 | |||
| b8da9d1854 | |||
| e5d63aa8fc | |||
| ac2d6e2030 | |||
| fcc91f2618 | |||
| 8b34e54764 | |||
| ebffe6db83 | |||
| 9ec4e86883 | |||
| 93a7b0d77d | |||
| 3e8b8db786 | |||
| 8957a768ef | |||
| fad86a5f24 | |||
| 548b128e67 | |||
| a577df2dbb | |||
| cb678e6221 | |||
| 939f889666 | |||
| f9f9ccb777 | |||
| c6892fcf5a | |||
| 844c9093a2 | |||
| 37d06dabcf | |||
| 1718142ede | |||
| ad64e5d2e2 | |||
| 00b2f776a9 | |||
| b8df31de84 | |||
| 16a336b4f3 | |||
| 590d674153 | |||
| 9a1cfcdd8e | |||
| 590d7faf65 | |||
| de035f0fed | |||
| 04925e4882 | |||
| 3760527218 | |||
| fa7ee6461a | |||
| fbf01f7683 | |||
| a0548e793c | |||
| 1fafb71fd9 | |||
| d8be04d4a8 | |||
| 21d3e85fcc | |||
| 93a74fca35 | |||
| a1c9b661b4 | |||
| 87b9d1cf98 | |||
| 90f3b658c6 | |||
| c35bcc5519 | |||
| f698c102c7 | |||
| 2cee819ce4 | |||
| bf051f1718 | |||
| c77f1d815c | |||
| d24deebee3 | |||
| 4526154571 | |||
| 978143ce99 | |||
| 7f9822db35 | |||
| 052233e858 | |||
| 629a2475a9 | |||
| 46573551f1 | |||
| 1f41c556e8 | |||
| 9ac08febd2 | |||
| 2341b4df00 | |||
| 6e79ce63c2 | |||
| e2a07a3b92 | |||
| 0dad1a950c | |||
| 27f0aeee30 | |||
| 721fa81f2e | |||
| f6a13d6e05 | |||
| 2660bb8426 | |||
| 84282bbfd3 | |||
| 0982fd5f1f | |||
| 7dbf7554c4 | |||
| 2790964270 | |||
| 3488a91eff | |||
| 0e8fd64203 | |||
| 70d41bd750 | |||
| a197513ce7 | |||
| bd8df24646 | |||
| 89052d60b4 | |||
| 9fa2fd413d | |||
| b1e11f82a9 | |||
| 165f65408d | |||
| 540c2a7b59 | |||
| a466f0ca79 | |||
| 736a75748b | |||
| ca12c0670d | |||
| 0ee0fe7f7c | |||
| b123c357c5 | |||
| 94ad175728 | |||
| 1e8b0dc3c9 | |||
| 7300b6a97b | |||
| aae4656c64 | |||
| 44f7a7aead | |||
| bac65cc530 | |||
| 91d7afbc0a | |||
| 7b4d82a939 | |||
| 2ab0f8c733 | |||
| b96c535061 | |||
| fd961100c1 | |||
| 1c7e9fe3af | |||
| 07aa85ea0b | |||
| d54ad15d16 | |||
| abe1005d7a | |||
| b4bb91fc13 | |||
| c9a09d8695 | |||
| 3430b0f086 | |||
| a713a3eccc | |||
| 652b9f8232 | |||
| ba1b12be14 | |||
| 2e01f733df | |||
| 1bd9461acb | |||
| 5b4e7e5dcc | |||
| b4905040a8 | |||
| 5c66c78bc5 | |||
| f9ba08244d | |||
| e6908614ee | |||
| a7448fe8de | |||
| 65ca806488 | |||
| 5c94d86029 | |||
| b76eb62117 | |||
| 196ce3a1f6 | |||
| 9f8f82dfe8 | |||
| 661484ae7e | |||
| 34de94e6a8 | |||
| 88263a05cc | |||
| 1dbe20e76c | |||
| 05bf3807d3 | |||
| 06838de5d2 | |||
| db62263e3e | |||
| 2d866e3fae | |||
| 100799f7b7 | |||
| ecd155a62f | |||
| bcacc71a18 | |||
| 16c9a8bdc0 | |||
| f99d6bbca1 | |||
| 8784f9c73a | |||
| a60e466129 | |||
| 44aed552fd | |||
| 1479e10663 | |||
| 9ba7363552 | |||
| 96ff402934 | |||
| 249d0eaf46 | |||
| 19eb8cf036 | |||
| c5fbd3eba6 | |||
| 9ccd9bbc7a | |||
| e7d1a179ae | |||
| 71a8bd4c61 | |||
| 34782191ba | |||
| 7fbde32684 | |||
| 82a6a1977d | |||
| cc3100c329 | |||
| f1ba6388fb | |||
| d05497900e | |||
| e29a498ed5 | |||
| 2b8e33f503 | |||
| d3c92c5999 | |||
| 71c324cf7b | |||
| 4c840f1b8b | |||
| d1e0fc37ce | |||
| 947ab3f90a | |||
| e9e8265313 | |||
| a30fd2239c | |||
| 323fe5d063 | |||
| 24d4446802 | |||
| 5060c72d94 | |||
| 0a459e938d | |||
| e872682c78 | |||
| 145b18309a | |||
| 8e8d2a6aad | |||
| 3f7178ffd6 | |||
| 9fd5922321 | |||
| 610cf85ed6 | |||
| 99f8a2a010 | |||
| 556d384d4c | |||
| eb7280e4a7 | |||
| 7ee1118b4b | |||
| a12e102214 | |||
| ad60b7e437 | |||
| 3503483c73 | |||
| d2d17bccec | |||
| 76a94674c9 | |||
| 80f8b7660b | |||
| 6f6ca41621 | |||
| c4efb7477c | |||
| 903d9455db | |||
| a89a075e1b | |||
| 5e74bb91ea | |||
| 3a4d7af89e | |||
| 8a362ad712 | |||
| 5edf9acd3e | |||
| e95df67ba3 | |||
| bef0f57967 | |||
| e87c92d4f4 | |||
| 8fe62057ea | |||
| 5f42261979 | |||
| 75654e60b8 | |||
| 091e2dc498 | |||
| 095a905e04 | |||
| ec312409d3 | |||
| 26fd12ff54 | |||
| b40204d36a | |||
| 4d5afd743f | |||
| 3ded2e09ee | |||
| 781fbae71e | |||
| 68063a447e | |||
| 84da2c4a25 | |||
| dba45d427f | |||
| 728039f679 | |||
| 1d8903f1d0 | |||
| 65b27e6823 | |||
| 6ea91ef590 | |||
| 51edd0ca49 | |||
| d95bfe020e | |||
| 63909f0a90 | |||
| f9070d3b40 | |||
| 405d48dc28 | |||
| 690ec00a4b | |||
| bef2d19cb0 | |||
| e1803f96f6 | |||
| d9a27b8052 | |||
| ad2aec1dcc | |||
| 489c49b1b4 | |||
| 27c6b39a13 | |||
| 539dd095b3 | |||
| b56e1b27b4 | |||
| 19cbd226da | |||
| 0a1666a81f | |||
| aa79e12a97 | |||
| 81a861c69d | |||
| 78b76feda6 | |||
| 90d6f96e9e | |||
| e33f654429 | |||
| 4edab6691b | |||
| c2dbbc24a9 | |||
| 14d3ac30da | |||
| 5c5490133d | |||
| 216c9ec838 | |||
| a295f02a64 | |||
| dbe63ccdd0 | |||
| 603baefff9 | |||
| c01d1270ff | |||
| 66540cf1fc | |||
| 05bcc0fb0d | |||
| acdc450948 | |||
| 778c285afa | |||
| a1e8a8d702 | |||
| 03c6bf4172 | |||
| a1b12e18b7 | |||
| 4874e8477b | |||
| 9bf0212445 | |||
| 0131db6d73 | |||
| 861c4734ac | |||
| b8b5e76965 | |||
| 4e9e0ee19b | |||
| 8c31c2ec65 | |||
| 6d858869d3 | |||
| 1916042ba2 | |||
| a410399d2c | |||
| 878c11e70e | |||
| 19ece5dc77 | |||
| d01db88be9 | |||
| 2009e44b6f | |||
| 690dbc9ee7 | |||
| 16398bac15 | |||
| 1d6b12b7cb | |||
| 9c1b4e331f | |||
| 4692b8f9ba | |||
| 69d355db53 | |||
| ef23ce9e92 | |||
| 4a000a21a4 | |||
| fe7536c3de | |||
| e94d595cd4 | |||
| 0d5a6db8dd | |||
| a60c71373e | |||
| 5ccdfb18c7 | |||
| 40ecdac242 | |||
| 26ae43fdee | |||
| 048765b449 | |||
| 5c26a6ff6f | |||
| 685a1f1cb7 | |||
| 23f644976a | |||
| 64bc956d5e | |||
| 53be957dac | |||
| 97253b98df | |||
| 76c552dcd7 | |||
| fe206efb84 | |||
| e32a2e0474 | |||
| f6e7596e8d | |||
| 25e50242f6 | |||
| 62e2d802ed | |||
| fee7787d3f | |||
| 3b00596b2e | |||
| 13d49d4606 | |||
| 7e6e4b1314 | |||
| 3616783228 | |||
| d0ec4317d3 | |||
| 22139eb546 | |||
| c9506ee963 | |||
| 3a6d01f7a0 | |||
| d1fa2118c1 | |||
| a1f048ba24 | |||
| ee2c0b51cf | |||
| 439fbba1f8 | |||
| 017119f7a7 | |||
| ce7be745f0 | |||
| d464759235 | |||
| 7cb988051d | |||
| 3ddb37c944 | |||
| f7f14983c3 | |||
| 45932ad32d | |||
| 67b7b944a8 | |||
| 31055c2ff0 | |||
| 5a62685873 | |||
| d05a14897c | |||
| 0599275eb9 | |||
| 0049ab3dc4 | |||
| bfe4a7e160 | |||
| e6271032cc | |||
| 887411a2a8 | |||
| 31c735e72c | |||
| 06484b677b | |||
| de2e921d55 | |||
| 7f292800de | |||
| 923742e542 | |||
| 65ad89c1a7 | |||
| 64ff1ea4d5 | |||
| b39f6ef1f9 | |||
| c187221f01 | |||
| 8e6ab94eec | |||
| 8d9c606c69 | |||
| 288bc57940 | |||
| e3b2497729 | |||
| 1a8565204c | |||
| e58cfd32d4 | |||
| a2ae216b47 | |||
| 8645be8dc7 | |||
| 99b9371c53 | |||
| 92c565e02b | |||
| 6e26017b00 | |||
| 9d93af61de | |||
| 4d8fb95ffe | |||
| 0e41db2176 | |||
| afca7f3334 | |||
| d6a90e60ed | |||
| fe63e9f76d | |||
| 7f50e4c339 | |||
| a402e618c3 | |||
| 2df083520a | |||
| 8176e30b38 | |||
| 14c964fc02 | |||
| f963bc320f | |||
| 0488b850c6 | |||
| 346e676fa2 | |||
| 6d743bb19f | |||
| fa1c2ab68c | |||
| a6c6ad1f5f | |||
| ab7a652912 | |||
| 3102b98900 | |||
| f0cae62430 | |||
| 56c6106477 | |||
| 7d69e5a5c5 | |||
| 07d0c2e4d3 | |||
| 6b9002d8f9 | |||
| 5753e884d0 | |||
| d467309bdd | |||
| 821a80e635 | |||
| dd4c4ffc2b | |||
| da703daafe | |||
| f58048cec0 | |||
| 440592fa85 | |||
| f4f2456dcd | |||
| a77f30ea80 | |||
| d61c80733b | |||
| 894e775e38 | |||
| 8e75093380 | |||
| cf5ad6a245 | |||
| 8fc7451ffc | |||
| 9defd66d3c | |||
| 6adf8057ed | |||
| 36e1197190 | |||
| 6dd2de38a9 | |||
| 209315c2af | |||
| 41a8959f14 | |||
| 16a681db2a | |||
| 9f36448571 | |||
| 222e90a7d3 | |||
| a8327d781a | |||
| 61449e9d32 | |||
| 48c977fb58 | |||
| 42e7853ef6 | |||
| 1f3d0e03c3 | |||
| 36d65b681a | |||
| a56707c85f | |||
| 4b47f52cb0 | |||
| 2f2f28631b | |||
| 543444f747 | |||
| b814e1a94f | |||
| 1fe62f3000 | |||
| 709382e9c1 | |||
| 71e7762db5 | |||
| 34da10d880 | |||
| db15f8a481 | |||
| 8ef71920bd | |||
| fa055bcbba | |||
| 7337a63f5a | |||
| 21af3aacfe | |||
| 66e7f06e7d | |||
| d9de45b5b5 | |||
| d9e8f54d1c | |||
| 7f678451a8 | |||
| 081f3db916 | |||
| 2811a1a3c9 | |||
| 7f30fba1e6 | |||
| 12e974f892 | |||
| c81f1892c2 | |||
| a98788e0d7 | |||
| b74544d345 | |||
| 27416cc1b9 | |||
| 04b60e4f8d | |||
| 9942463786 | |||
| fb5423fba2 | |||
| a2495b4806 | |||
| e118479061 | |||
| c0c5d65185 | |||
| 7c63fff960 | |||
| bcbaee1079 | |||
| f15dd550e8 | |||
| 5ad4cb7120 | |||
| 65684e6bb0 | |||
| 68d2a60b37 | |||
| bf549a2194 | |||
| aa194b5c41 | |||
| 14e2d94bdd | |||
| 1f8a8cbc06 | |||
| 6db660fed5 | |||
| 7d9a3c25bd | |||
| dd04a2f3cd | |||
| e493544dfd | |||
| e9ae961088 | |||
| c1ad095e9b |
@@ -0,0 +1,2 @@
|
||||
cmd/tomll/tomll
|
||||
cmd/tomljson/tomljson
|
||||
@@ -0,0 +1,4 @@
|
||||
* text=auto
|
||||
|
||||
benchmark/benchmark.toml text eol=lf
|
||||
testdata/** text eol=lf
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior. Including TOML files.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen, if other than "should work".
|
||||
|
||||
**Versions**
|
||||
- go-toml: version (git sha)
|
||||
- go: version
|
||||
- operating system: e.g. macOS, Windows, Linux
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here that you think may help to diagnose.
|
||||
@@ -0,0 +1,19 @@
|
||||
<!--
|
||||
|
||||
Thank you for your pull request!
|
||||
|
||||
Please read the Code changes section of the CONTRIBUTING.md file,
|
||||
and make sure you have followed the instructions.
|
||||
|
||||
https://github.com/pelletier/go-toml/blob/v2/CONTRIBUTING.md#code-changes
|
||||
|
||||
-->
|
||||
|
||||
Explanation of what this pull request does.
|
||||
|
||||
More detailed description of the decisions being made and the reasons why (if
|
||||
the patch is non-trivial).
|
||||
|
||||
---
|
||||
|
||||
Paste `benchstat` results here
|
||||
@@ -0,0 +1,17 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: docker
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
@@ -0,0 +1,21 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- build
|
||||
- testing
|
||||
categories:
|
||||
- title: What's new
|
||||
labels:
|
||||
- feature
|
||||
- title: Performance
|
||||
labels:
|
||||
- performance
|
||||
- title: Fixed bugs
|
||||
labels:
|
||||
- bug
|
||||
- title: Documentation
|
||||
labels:
|
||||
- doc
|
||||
- title: Other changes
|
||||
labels:
|
||||
- "*"
|
||||
@@ -0,0 +1,26 @@
|
||||
name: CIFuzz
|
||||
on: [pull_request]
|
||||
jobs:
|
||||
Fuzzing:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Build Fuzzers
|
||||
id: build
|
||||
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
|
||||
with:
|
||||
oss-fuzz-project-name: 'go-toml'
|
||||
dry-run: false
|
||||
language: go
|
||||
- name: Run Fuzzers
|
||||
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
|
||||
with:
|
||||
oss-fuzz-project-name: 'go-toml'
|
||||
fuzz-seconds: 300
|
||||
dry-run: false
|
||||
language: go
|
||||
- name: Upload Crash
|
||||
uses: actions/upload-artifact@v6
|
||||
if: failure() && steps.build.outcome == 'success'
|
||||
with:
|
||||
name: artifacts
|
||||
path: ./out/artifacts
|
||||
@@ -0,0 +1,67 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, v2 ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '26 19 * * 0'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
@@ -0,0 +1,20 @@
|
||||
name: coverage
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- v2
|
||||
|
||||
jobs:
|
||||
report:
|
||||
runs-on: "ubuntu-latest"
|
||||
name: report
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25"
|
||||
- name: Run tests with coverage
|
||||
run: ./ci.sh coverage -d "${GITHUB_BASE_REF-HEAD}"
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Go Versions Compatibility Test
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
go_versions:
|
||||
description: 'Go versions to test (space-separated, e.g., "1.21 1.22 1.23")'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Run Go versions compatibility test
|
||||
run: |
|
||||
VERSIONS="${{ github.event.inputs.go_versions }}"
|
||||
./test-go-versions.sh --output ./test-results $VERSIONS
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: go-versions-test-results
|
||||
path: |
|
||||
test-results/
|
||||
retention-days: 30
|
||||
@@ -0,0 +1,22 @@
|
||||
name: lint
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- v2
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.24"
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.8.0
|
||||
@@ -0,0 +1,39 @@
|
||||
name: release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v2.*"
|
||||
workflow_call:
|
||||
inputs:
|
||||
args:
|
||||
description: "Extra arguments to pass goreleaser"
|
||||
default: ""
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25"
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: '~> v2'
|
||||
args: release ${{ inputs.args }} --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,32 @@
|
||||
name: test
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v2
|
||||
pull_request:
|
||||
branches:
|
||||
- v2
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ 'ubuntu-latest', 'windows-latest', 'macos-latest', 'macos-14' ]
|
||||
go: [ '1.24', '1.25' ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: ${{ matrix.go }}/${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup go ${{ matrix.go }}
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Run unit tests
|
||||
run: go test -race ./...
|
||||
release-check:
|
||||
if: ${{ github.ref != 'refs/heads/v2' }}
|
||||
uses: ./.github/workflows/release.yml
|
||||
with:
|
||||
args: --snapshot
|
||||
@@ -1 +1,8 @@
|
||||
test_program/test_program_bin
|
||||
fuzz/
|
||||
cmd/tomll/tomll
|
||||
cmd/tomljson/tomljson
|
||||
cmd/tomltestgen/tomltestgen
|
||||
dist
|
||||
tests/
|
||||
test-results
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
version = "2"
|
||||
|
||||
[linters]
|
||||
default = "none"
|
||||
enable = [
|
||||
"asciicheck",
|
||||
"bodyclose",
|
||||
"dogsled",
|
||||
"dupl",
|
||||
"durationcheck",
|
||||
"errcheck",
|
||||
"errorlint",
|
||||
"exhaustive",
|
||||
"forbidigo",
|
||||
"gochecknoinits",
|
||||
"goconst",
|
||||
"gocritic",
|
||||
"godoclint",
|
||||
"goheader",
|
||||
"gomodguard",
|
||||
"goprintffuncname",
|
||||
"gosec",
|
||||
"govet",
|
||||
"importas",
|
||||
"ineffassign",
|
||||
"lll",
|
||||
"makezero",
|
||||
"mirror",
|
||||
"misspell",
|
||||
"nakedret",
|
||||
"nilerr",
|
||||
"noctx",
|
||||
"nolintlint",
|
||||
"perfsprint",
|
||||
"prealloc",
|
||||
"predeclared",
|
||||
"revive",
|
||||
"rowserrcheck",
|
||||
"sqlclosecheck",
|
||||
"staticcheck",
|
||||
"thelper",
|
||||
"tparallel",
|
||||
"unconvert",
|
||||
"unparam",
|
||||
"unused",
|
||||
"usetesting",
|
||||
"wastedassign",
|
||||
"whitespace",
|
||||
]
|
||||
|
||||
[linters.settings.exhaustive]
|
||||
default-signifies-exhaustive = true
|
||||
|
||||
[linters.settings.lll]
|
||||
line-length = 150
|
||||
|
||||
[[linters.exclusions.rules]]
|
||||
path = ".test.go"
|
||||
linters = ["goconst", "gosec"]
|
||||
|
||||
[[linters.exclusions.rules]]
|
||||
path = "main.go"
|
||||
linters = ["forbidigo"]
|
||||
|
||||
[[linters.exclusions.rules]]
|
||||
path = "internal"
|
||||
linters = ["revive"]
|
||||
text = "(exported|indent-error-flow): "
|
||||
|
||||
[formatters]
|
||||
enable = [
|
||||
"gci",
|
||||
"gofmt",
|
||||
"gofumpt",
|
||||
"goimports",
|
||||
]
|
||||
@@ -0,0 +1,127 @@
|
||||
version: 2
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
- go fmt ./...
|
||||
- go test ./...
|
||||
builds:
|
||||
- id: tomll
|
||||
main: ./cmd/tomll
|
||||
binary: tomll
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}}
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
targets:
|
||||
- linux_amd64
|
||||
- linux_arm64
|
||||
- linux_arm
|
||||
- linux_riscv64
|
||||
- windows_amd64
|
||||
- windows_arm64
|
||||
- windows_arm
|
||||
- darwin_amd64
|
||||
- darwin_arm64
|
||||
- id: tomljson
|
||||
main: ./cmd/tomljson
|
||||
binary: tomljson
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}}
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
targets:
|
||||
- linux_amd64
|
||||
- linux_arm64
|
||||
- linux_arm
|
||||
- linux_riscv64
|
||||
- windows_amd64
|
||||
- windows_arm64
|
||||
- windows_arm
|
||||
- darwin_amd64
|
||||
- darwin_arm64
|
||||
- id: jsontoml
|
||||
main: ./cmd/jsontoml
|
||||
binary: jsontoml
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}}
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
targets:
|
||||
- linux_amd64
|
||||
- linux_arm64
|
||||
- linux_riscv64
|
||||
- linux_arm
|
||||
- windows_amd64
|
||||
- windows_arm64
|
||||
- windows_arm
|
||||
- darwin_amd64
|
||||
- darwin_arm64
|
||||
universal_binaries:
|
||||
- id: tomll
|
||||
replace: true
|
||||
name_template: tomll
|
||||
- id: tomljson
|
||||
replace: true
|
||||
name_template: tomljson
|
||||
- id: jsontoml
|
||||
replace: true
|
||||
name_template: jsontoml
|
||||
archives:
|
||||
- id: jsontoml
|
||||
format: tar.xz
|
||||
builds:
|
||||
- jsontoml
|
||||
files:
|
||||
- none*
|
||||
name_template: "{{ .Binary }}_{{.Version}}_{{ .Os }}_{{ .Arch }}"
|
||||
- id: tomljson
|
||||
format: tar.xz
|
||||
builds:
|
||||
- tomljson
|
||||
files:
|
||||
- none*
|
||||
name_template: "{{ .Binary }}_{{.Version}}_{{ .Os }}_{{ .Arch }}"
|
||||
- id: tomll
|
||||
format: tar.xz
|
||||
builds:
|
||||
- tomll
|
||||
files:
|
||||
- none*
|
||||
name_template: "{{ .Binary }}_{{.Version}}_{{ .Os }}_{{ .Arch }}"
|
||||
dockers:
|
||||
- id: tools
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
ids:
|
||||
- jsontoml
|
||||
- tomljson
|
||||
- tomll
|
||||
image_templates:
|
||||
- "ghcr.io/pelletier/go-toml:latest"
|
||||
- "ghcr.io/pelletier/go-toml:{{ .Tag }}"
|
||||
- "ghcr.io/pelletier/go-toml:v{{ .Major }}"
|
||||
skip_push: false
|
||||
checksum:
|
||||
name_template: 'sha256sums.txt'
|
||||
snapshot:
|
||||
version_template: "{{ incpatch .Version }}-next"
|
||||
release:
|
||||
github:
|
||||
owner: pelletier
|
||||
name: go-toml
|
||||
draft: true
|
||||
prerelease: auto
|
||||
mode: replace
|
||||
changelog:
|
||||
use: github-native
|
||||
announce:
|
||||
skip: true
|
||||
@@ -1,6 +0,0 @@
|
||||
language: go
|
||||
script: "./test.sh"
|
||||
go:
|
||||
- 1.1
|
||||
- 1.2
|
||||
- tip
|
||||
@@ -0,0 +1,64 @@
|
||||
# Agent Guidelines for go-toml
|
||||
|
||||
This file provides guidelines for AI agents contributing to go-toml. All agents must follow these rules derived from [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||
|
||||
## Project Overview
|
||||
|
||||
go-toml is a TOML library for Go. The goal is to provide an easy-to-use and efficient TOML implementation that gets the job done without getting in the way.
|
||||
|
||||
## Code Change Rules
|
||||
|
||||
### Backward Compatibility
|
||||
|
||||
- **No backward-incompatible changes** unless explicitly discussed and approved
|
||||
- Avoid breaking people's programs unless absolutely necessary
|
||||
|
||||
### Testing Requirements
|
||||
|
||||
- **All bug fixes must include regression tests**
|
||||
- **All new code must be tested**
|
||||
- Run tests before submitting: `go test -race ./...`
|
||||
- Test coverage must not decrease. Check with:
|
||||
```bash
|
||||
go test -covermode=atomic -coverprofile=coverage.out
|
||||
go tool cover -func=coverage.out
|
||||
```
|
||||
- All lines of code touched by changes should be covered by tests
|
||||
|
||||
### Performance Requirements
|
||||
|
||||
- go-toml aims to stay efficient; avoid performance regressions
|
||||
- Run benchmarks to verify: `go test ./... -bench=. -count=10`
|
||||
- Compare results using [benchstat](https://pkg.go.dev/golang.org/x/perf/cmd/benchstat)
|
||||
|
||||
### Documentation
|
||||
|
||||
- New features or feature extensions must include documentation
|
||||
- Documentation lives in [README.md](./README.md) and throughout source code
|
||||
|
||||
### Code Style
|
||||
|
||||
- Follow existing code format and structure
|
||||
- Code must pass `go fmt`
|
||||
- Code must pass linting with the same golangci-lint version as CI (see version in `.github/workflows/lint.yml`):
|
||||
```bash
|
||||
# Install specific version (check lint.yml for current version)
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin <version>
|
||||
# Run linter
|
||||
golangci-lint run ./...
|
||||
```
|
||||
|
||||
### Commit Messages
|
||||
|
||||
- Commit messages must explain **why** the change is needed
|
||||
- Keep messages clear and informative even if details are in the PR description
|
||||
|
||||
## Pull Request Checklist
|
||||
|
||||
Before submitting:
|
||||
|
||||
1. Tests pass (`go test -race ./...`)
|
||||
2. No backward-incompatible changes (unless discussed)
|
||||
3. Relevant documentation added/updated
|
||||
4. No performance regression (verify with benchmarks)
|
||||
5. Title is clear and understandable for changelog
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
# Contributing
|
||||
|
||||
Thank you for your interest in go-toml! We appreciate you considering
|
||||
contributing to go-toml!
|
||||
|
||||
The main goal is the project is to provide an easy-to-use and efficient TOML
|
||||
implementation for Go that gets the job done and gets out of your way – dealing
|
||||
with TOML is probably not the central piece of your project.
|
||||
|
||||
As the single maintainer of go-toml, time is scarce. All help, big or small, is
|
||||
more than welcomed!
|
||||
|
||||
## Ask questions
|
||||
|
||||
Any question you may have, somebody else might have it too. Always feel free to
|
||||
ask them on the [discussion board][discussions]. We will try to answer them as
|
||||
clearly and quickly as possible, time permitting.
|
||||
|
||||
Asking questions also helps us identify areas where the documentation needs
|
||||
improvement, or new features that weren't envisioned before. Sometimes, a
|
||||
seemingly innocent question leads to the fix of a bug. Don't hesitate and ask
|
||||
away!
|
||||
|
||||
[discussions]: https://github.com/pelletier/go-toml/discussions
|
||||
|
||||
## Improve the documentation
|
||||
|
||||
The best way to share your knowledge and experience with go-toml is to improve
|
||||
the documentation. Fix a typo, clarify an interface, add an example, anything
|
||||
goes!
|
||||
|
||||
The documentation is present in the [README][readme] and thorough the source
|
||||
code. On release, it gets updated on [pkg.go.dev][pkg.go.dev]. To make a change
|
||||
to the documentation, create a pull request with your proposed changes. For
|
||||
simple changes like that, the easiest way to go is probably the "Fork this
|
||||
project and edit the file" button on GitHub, displayed at the top right of the
|
||||
file. Unless it's a trivial change (for example a typo), provide a little bit of
|
||||
context in your pull request description or commit message.
|
||||
|
||||
## Report a bug
|
||||
|
||||
Found a bug! Sorry to hear that :(. Help us and other track them down and fix by
|
||||
reporting it. [File a new bug report][bug-report] on the [issues
|
||||
tracker][issues-tracker]. The template should provide enough guidance on what to
|
||||
include. When in doubt: add more details! By reducing ambiguity and providing
|
||||
more information, it decreases back and forth and saves everyone time.
|
||||
|
||||
## Code changes
|
||||
|
||||
Want to contribute a patch? Very happy to hear that!
|
||||
|
||||
First, some high-level rules:
|
||||
|
||||
- A short proposal with some POC code is better than a lengthy piece of text
|
||||
with no code. Code speaks louder than words. That being said, bigger changes
|
||||
should probably start with a [discussion][discussions].
|
||||
- No backward-incompatible patch will be accepted unless discussed. Sometimes
|
||||
it's hard, but we try not to break people's programs unless we absolutely have
|
||||
to.
|
||||
- If you are writing a new feature or extending an existing one, make sure to
|
||||
write some documentation.
|
||||
- Bug fixes need to be accompanied with regression tests.
|
||||
- New code needs to be tested.
|
||||
- Your commit messages need to explain why the change is needed, even if already
|
||||
included in the PR description.
|
||||
|
||||
It does sound like a lot, but those best practices are here to save time overall
|
||||
and continuously improve the quality of the project, which is something everyone
|
||||
benefits from.
|
||||
|
||||
### Get started
|
||||
|
||||
The fairly standard code contribution process looks like that:
|
||||
|
||||
1. [Fork the project][fork].
|
||||
2. Make your changes, commit on any branch you like.
|
||||
3. [Open up a pull request][pull-request]
|
||||
4. Review, potential ask for changes.
|
||||
5. Merge.
|
||||
|
||||
Feel free to ask for help! You can create draft pull requests to gather
|
||||
some early feedback!
|
||||
|
||||
### Run the tests
|
||||
|
||||
You can run tests for go-toml using Go's test tool: `go test -race ./...`.
|
||||
|
||||
During the pull request process, all tests will be ran on Linux, Windows, and
|
||||
MacOS on the last two versions of Go.
|
||||
|
||||
However, given GitHub's new policy to _not_ run Actions on pull requests until a
|
||||
maintainer clicks on button, it is highly recommended that you run them locally
|
||||
as you make changes.
|
||||
|
||||
### Test across Go versions
|
||||
|
||||
The repository includes tooling to test go-toml across multiple Go versions
|
||||
(1.11 through 1.25) both locally and in GitHub Actions.
|
||||
|
||||
#### Local testing with Docker
|
||||
|
||||
Prerequisites: Docker installed and running, Bash shell, `rsync` command.
|
||||
|
||||
```bash
|
||||
# Test all Go versions in parallel (default)
|
||||
./test-go-versions.sh
|
||||
|
||||
# Test specific versions
|
||||
./test-go-versions.sh 1.21 1.22 1.23
|
||||
|
||||
# Test sequentially (slower but uses less resources)
|
||||
./test-go-versions.sh --sequential
|
||||
|
||||
# Verbose output with custom results directory
|
||||
./test-go-versions.sh --verbose --output ./my-results 1.24 1.25
|
||||
|
||||
# Show all options
|
||||
./test-go-versions.sh --help
|
||||
```
|
||||
|
||||
The script creates Docker containers for each Go version and runs the full test
|
||||
suite. Results are saved to a `test-results/` directory with individual logs and
|
||||
a comprehensive summary report.
|
||||
|
||||
The script only exits with a non-zero status code if either of the two most
|
||||
recent Go versions fail.
|
||||
|
||||
#### GitHub Actions testing (maintainers)
|
||||
|
||||
1. Go to the **Actions** tab in the GitHub repository
|
||||
2. Select **"Go Versions Compatibility Test"** from the workflow list
|
||||
3. Click **"Run workflow"**
|
||||
4. Optionally customize:
|
||||
- **Go versions**: Space-separated list (e.g., `1.21 1.22 1.23`)
|
||||
- **Execution mode**: Parallel (faster) or sequential (more stable)
|
||||
|
||||
### Check coverage
|
||||
|
||||
We use `go tool cover` to compute test coverage. Most code editors have a way to
|
||||
run and display code coverage, but at the end of the day, we do this:
|
||||
|
||||
```
|
||||
go test -covermode=atomic -coverprofile=coverage.out
|
||||
go tool cover -func=coverage.out
|
||||
```
|
||||
|
||||
and verify that the overall percentage of tested code does not go down. This is
|
||||
a requirement. As a rule of thumb, all lines of code touched by your changes
|
||||
should be covered. On Unix you can use `./ci.sh coverage -d v2` to check if your
|
||||
code lowers the coverage.
|
||||
|
||||
### Verify performance
|
||||
|
||||
Go-toml aims to stay efficient. We rely on a set of scenarios executed with Go's
|
||||
builtin benchmark systems. Because of their noisy nature, containers provided by
|
||||
GitHub Actions cannot be reliably used for benchmarking. As a result, you are
|
||||
responsible for checking that your changes do not incur a performance penalty.
|
||||
You can run their following to execute benchmarks:
|
||||
|
||||
```
|
||||
go test ./... -bench=. -count=10
|
||||
```
|
||||
|
||||
Benchmark results should be compared against each other with
|
||||
[benchstat][benchstat]. Typical flow looks like this:
|
||||
|
||||
1. On the `v2` branch, run `go test ./... -bench=. -count 10` and save output to
|
||||
a file (for example `old.txt`).
|
||||
2. Make some code changes.
|
||||
3. Run `go test ....` again, and save the output to an other file (for example
|
||||
`new.txt`).
|
||||
4. Run `benchstat old.txt new.txt` to check that time/op does not go up in any
|
||||
test.
|
||||
|
||||
On Unix you can use `./ci.sh benchmark -d v2` to verify how your code impacts
|
||||
performance.
|
||||
|
||||
It is highly encouraged to add the benchstat results to your pull request
|
||||
description. Pull requests that lower performance will receive more scrutiny.
|
||||
|
||||
[benchstat]: https://pkg.go.dev/golang.org/x/perf/cmd/benchstat
|
||||
|
||||
### Style
|
||||
|
||||
Try to look around and follow the same format and structure as the rest of the
|
||||
code. We enforce using `go fmt` on the whole code base.
|
||||
|
||||
---
|
||||
|
||||
## Maintainers-only
|
||||
|
||||
### Merge pull request
|
||||
|
||||
Checklist:
|
||||
|
||||
- Passing CI.
|
||||
- Does not introduce backward-incompatible changes (unless discussed).
|
||||
- Has relevant doc changes.
|
||||
- Benchstat does not show performance regression.
|
||||
- Pull request is [labeled appropriately][pr-labels].
|
||||
- Title will be understandable in the changelog.
|
||||
|
||||
1. Merge using "squash and merge".
|
||||
2. Make sure to edit the commit message to keep all the useful information
|
||||
nice and clean.
|
||||
3. Make sure the commit title is clear and contains the PR number (#123).
|
||||
|
||||
### New release
|
||||
|
||||
1. Decide on the next version number. Use semver. Review commits since last
|
||||
version to assess.
|
||||
2. Tag release. For example:
|
||||
```
|
||||
git checkout v2
|
||||
git pull
|
||||
git tag v2.2.0
|
||||
git push --tags
|
||||
```
|
||||
3. CI automatically builds a draft GitHub release. Review it and edit as
|
||||
necessary. Look for "Other changes". That would indicate a pull request not
|
||||
labeled properly. Tweak labels and pull request titles until changelog looks
|
||||
good for users.
|
||||
4. Check "create discussion" box, in the "Releases" category.
|
||||
5. If new version is an alpha or beta only, check pre-release box.
|
||||
|
||||
|
||||
[issues-tracker]: https://github.com/pelletier/go-toml/issues
|
||||
[bug-report]: https://github.com/pelletier/go-toml/issues/new?template=bug_report.md
|
||||
[pkg.go.dev]: https://pkg.go.dev/github.com/pelletier/go-toml
|
||||
[readme]: ./README.md
|
||||
[fork]: https://help.github.com/articles/fork-a-repo
|
||||
[pull-request]: https://help.github.com/en/articles/creating-a-pull-request
|
||||
[new-release]: https://github.com/pelletier/go-toml/releases/new
|
||||
[gh]: https://github.com/cli/cli
|
||||
[pr-labels]: https://github.com/pelletier/go-toml/blob/v2/.github/release.yml
|
||||
@@ -0,0 +1,5 @@
|
||||
FROM scratch
|
||||
ENV PATH "$PATH:/bin"
|
||||
COPY tomll /bin/tomll
|
||||
COPY tomljson /bin/tomljson
|
||||
COPY jsontoml /bin/jsontoml
|
||||
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
go-toml v2
|
||||
Copyright (c) 2021 - 2023 Thomas Pelletier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,88 +1,636 @@
|
||||
# go-toml
|
||||
# go-toml v2
|
||||
|
||||
Go library for the [TOML](https://github.com/mojombo/toml) format.
|
||||
Go library for the [TOML](https://toml.io/en/) format.
|
||||
|
||||
This library supports TOML version
|
||||
[v0.1.0](https://github.com/mojombo/toml/blob/master/versions/toml-v0.1.0.md)
|
||||
This library supports [TOML v1.0.0](https://toml.io/en/v1.0.0).
|
||||
|
||||
[](https://travis-ci.org/pelletier/go-toml)
|
||||
[🐞 Bug Reports](https://github.com/pelletier/go-toml/issues)
|
||||
|
||||
## Import
|
||||
|
||||
import "github.com/pelletier/go-toml"
|
||||
|
||||
## Usage
|
||||
|
||||
Say you have a TOML file that looks like this:
|
||||
|
||||
```toml
|
||||
[postgres]
|
||||
user = "pelletier"
|
||||
password = "mypassword"
|
||||
```
|
||||
|
||||
Read the username and password like this:
|
||||
|
||||
```go
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pelletier/go-toml"
|
||||
)
|
||||
|
||||
config, err := toml.LoadFile("config.toml")
|
||||
if err != nil {
|
||||
fmt.Println("Error ", err.Error())
|
||||
} else {
|
||||
// retrieve data directly
|
||||
user := config.Get("postgres.user").(string)
|
||||
password := config.Get("postgres.password").(string)
|
||||
|
||||
// or using an intermediate object
|
||||
configTree := config.Get("postgres").(*toml.TomlTree)
|
||||
user = configTree.Get("user").(string)
|
||||
password = configTree.Get("password").(string)
|
||||
fmt.Println("User is ", user, ". Password is ", password)
|
||||
}
|
||||
```
|
||||
[💬 Anything else](https://github.com/pelletier/go-toml/discussions)
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation is available at
|
||||
[godoc.org](http://godoc.org/github.com/pelletier/go-toml).
|
||||
Full API, examples, and implementation notes are available in the Go
|
||||
documentation.
|
||||
|
||||
## Contribute
|
||||
[](https://pkg.go.dev/github.com/pelletier/go-toml/v2)
|
||||
|
||||
Feel free to report bugs and patches using GitHub's pull requests system on
|
||||
[pelletier/go-toml](https://github.com/pelletier/go-toml). Any feedback would be
|
||||
much appreciated!
|
||||
## Import
|
||||
|
||||
### Run tests
|
||||
```go
|
||||
import "github.com/pelletier/go-toml/v2"
|
||||
```
|
||||
|
||||
You have to make sure two kind of tests run:
|
||||
See [Modules](#Modules).
|
||||
|
||||
1. The Go unit tests: `go test`
|
||||
2. The TOML examples base: `./test_program/go-test.sh`
|
||||
## Features
|
||||
|
||||
You can run both of them using `./test.sh`.
|
||||
### Stdlib behavior
|
||||
|
||||
As much as possible, this library is designed to behave similarly as the
|
||||
standard library's `encoding/json`.
|
||||
|
||||
### Performance
|
||||
|
||||
While go-toml favors usability, it is written with performance in mind. Most
|
||||
operations should not be shockingly slow. See [benchmarks](#benchmarks).
|
||||
|
||||
### Strict mode
|
||||
|
||||
`Decoder` can be set to "strict mode", which makes it error when some parts of
|
||||
the TOML document was not present in the target structure. This is a great way
|
||||
to check for typos. [See example in the documentation][strict].
|
||||
|
||||
[strict]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#example-Decoder.DisallowUnknownFields
|
||||
|
||||
### Contextualized errors
|
||||
|
||||
When most decoding errors occur, go-toml returns [`DecodeError`][decode-err],
|
||||
which contains a human readable contextualized version of the error. For
|
||||
example:
|
||||
|
||||
```
|
||||
1| [server]
|
||||
2| path = 100
|
||||
| ~~~ cannot decode TOML integer into struct field toml_test.Server.Path of type string
|
||||
3| port = 50
|
||||
```
|
||||
|
||||
[decode-err]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#DecodeError
|
||||
|
||||
### Local date and time support
|
||||
|
||||
TOML supports native [local date/times][ldt]. It allows to represent a given
|
||||
date, time, or date-time without relation to a timezone or offset. To support
|
||||
this use-case, go-toml provides [`LocalDate`][tld], [`LocalTime`][tlt], and
|
||||
[`LocalDateTime`][tldt]. Those types can be transformed to and from `time.Time`,
|
||||
making them convenient yet unambiguous structures for their respective TOML
|
||||
representation.
|
||||
|
||||
[ldt]: https://toml.io/en/v1.0.0#local-date-time
|
||||
[tld]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDate
|
||||
[tlt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalTime
|
||||
[tldt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDateTime
|
||||
|
||||
### Commented config
|
||||
|
||||
Since TOML is often used for configuration files, go-toml can emit documents
|
||||
annotated with [comments and commented-out values][comments-example]. For
|
||||
example, it can generate the following file:
|
||||
|
||||
```toml
|
||||
# Host IP to connect to.
|
||||
host = '127.0.0.1'
|
||||
# Port of the remote server.
|
||||
port = 4242
|
||||
|
||||
# Encryption parameters (optional)
|
||||
# [TLS]
|
||||
# cipher = 'AEAD-AES128-GCM-SHA256'
|
||||
# version = 'TLS 1.3'
|
||||
```
|
||||
|
||||
[comments-example]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#example-Marshal-Commented
|
||||
|
||||
## Getting started
|
||||
|
||||
Given the following struct, let's see how to read it and write it as TOML:
|
||||
|
||||
```go
|
||||
type MyConfig struct {
|
||||
Version int
|
||||
Name string
|
||||
Tags []string
|
||||
}
|
||||
```
|
||||
|
||||
### Unmarshaling
|
||||
|
||||
[`Unmarshal`][unmarshal] reads a TOML document and fills a Go structure with its
|
||||
content.
|
||||
|
||||
Note that the struct variable names are _capitalized_, while the variables in the toml document are _lowercase_.
|
||||
|
||||
For example:
|
||||
|
||||
```go
|
||||
doc := `
|
||||
version = 2
|
||||
name = "go-toml"
|
||||
tags = ["go", "toml"]
|
||||
`
|
||||
|
||||
var cfg MyConfig
|
||||
err := toml.Unmarshal([]byte(doc), &cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println("version:", cfg.Version)
|
||||
fmt.Println("name:", cfg.Name)
|
||||
fmt.Println("tags:", cfg.Tags)
|
||||
|
||||
// Output:
|
||||
// version: 2
|
||||
// name: go-toml
|
||||
// tags: [go toml]
|
||||
```
|
||||
|
||||
[unmarshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Unmarshal
|
||||
|
||||
|
||||
Here is an example using tables with some simple nesting:
|
||||
|
||||
```go
|
||||
doc := `
|
||||
age = 45
|
||||
fruits = ["apple", "pear"]
|
||||
|
||||
# these are very important!
|
||||
[my-variables]
|
||||
first = 1
|
||||
second = 0.2
|
||||
third = "abc"
|
||||
|
||||
# this is not so important.
|
||||
[my-variables.b]
|
||||
bfirst = 123
|
||||
`
|
||||
|
||||
var Document struct {
|
||||
Age int
|
||||
Fruits []string
|
||||
|
||||
Myvariables struct {
|
||||
First int
|
||||
Second float64
|
||||
Third string
|
||||
|
||||
B struct {
|
||||
Bfirst int
|
||||
}
|
||||
} `toml:"my-variables"`
|
||||
}
|
||||
|
||||
err := toml.Unmarshal([]byte(doc), &Document)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println("age:", Document.Age)
|
||||
fmt.Println("fruits:", Document.Fruits)
|
||||
fmt.Println("my-variables.first:", Document.Myvariables.First)
|
||||
fmt.Println("my-variables.second:", Document.Myvariables.Second)
|
||||
fmt.Println("my-variables.third:", Document.Myvariables.Third)
|
||||
fmt.Println("my-variables.B.Bfirst:", Document.Myvariables.B.Bfirst)
|
||||
|
||||
// Output:
|
||||
// age: 45
|
||||
// fruits: [apple pear]
|
||||
// my-variables.first: 1
|
||||
// my-variables.second: 0.2
|
||||
// my-variables.third: abc
|
||||
// my-variables.B.Bfirst: 123
|
||||
```
|
||||
|
||||
|
||||
### Marshaling
|
||||
|
||||
[`Marshal`][marshal] is the opposite of Unmarshal: it represents a Go structure
|
||||
as a TOML document:
|
||||
|
||||
```go
|
||||
cfg := MyConfig{
|
||||
Version: 2,
|
||||
Name: "go-toml",
|
||||
Tags: []string{"go", "toml"},
|
||||
}
|
||||
|
||||
b, err := toml.Marshal(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
|
||||
// Output:
|
||||
// Version = 2
|
||||
// Name = 'go-toml'
|
||||
// Tags = ['go', 'toml']
|
||||
```
|
||||
|
||||
[marshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Marshal
|
||||
|
||||
## Unstable API
|
||||
|
||||
This API does not yet follow the backward compatibility guarantees of this
|
||||
library. They provide early access to features that may have rough edges or an
|
||||
API subject to change.
|
||||
|
||||
### Parser
|
||||
|
||||
Parser is the unstable API that allows iterative parsing of a TOML document at
|
||||
the AST level. See https://pkg.go.dev/github.com/pelletier/go-toml/v2/unstable.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Execution time speedup compared to other Go TOML libraries:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Marshal/HugoFrontMatter-2</td><td>1.9x</td><td>2.2x</td></tr>
|
||||
<tr><td>Marshal/ReferenceFile/map-2</td><td>1.7x</td><td>2.1x</td></tr>
|
||||
<tr><td>Marshal/ReferenceFile/struct-2</td><td>2.2x</td><td>3.0x</td></tr>
|
||||
<tr><td>Unmarshal/HugoFrontMatter-2</td><td>2.9x</td><td>2.7x</td></tr>
|
||||
<tr><td>Unmarshal/ReferenceFile/map-2</td><td>2.6x</td><td>2.7x</td></tr>
|
||||
<tr><td>Unmarshal/ReferenceFile/struct-2</td><td>4.6x</td><td>5.1x</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<details><summary>See more</summary>
|
||||
<p>The table above has the results of the most common use-cases. The table below
|
||||
contains the results of all benchmarks, including unrealistic ones. It is
|
||||
provided for completeness.</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Marshal/SimpleDocument/map-2</td><td>1.8x</td><td>2.7x</td></tr>
|
||||
<tr><td>Marshal/SimpleDocument/struct-2</td><td>2.7x</td><td>3.8x</td></tr>
|
||||
<tr><td>Unmarshal/SimpleDocument/map-2</td><td>3.8x</td><td>3.0x</td></tr>
|
||||
<tr><td>Unmarshal/SimpleDocument/struct-2</td><td>5.6x</td><td>4.1x</td></tr>
|
||||
<tr><td>UnmarshalDataset/example-2</td><td>3.0x</td><td>3.2x</td></tr>
|
||||
<tr><td>UnmarshalDataset/code-2</td><td>2.3x</td><td>2.9x</td></tr>
|
||||
<tr><td>UnmarshalDataset/twitter-2</td><td>2.6x</td><td>2.7x</td></tr>
|
||||
<tr><td>UnmarshalDataset/citm_catalog-2</td><td>2.2x</td><td>2.3x</td></tr>
|
||||
<tr><td>UnmarshalDataset/canada-2</td><td>1.8x</td><td>1.5x</td></tr>
|
||||
<tr><td>UnmarshalDataset/config-2</td><td>4.1x</td><td>2.9x</td></tr>
|
||||
<tr><td>geomean</td><td>2.7x</td><td>2.8x</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p>
|
||||
</details>
|
||||
|
||||
## Modules
|
||||
|
||||
go-toml uses Go's standard modules system.
|
||||
|
||||
Installation instructions:
|
||||
|
||||
- Go ≥ 1.16: Nothing to do. Use the import in your code. The `go` command deals
|
||||
with it automatically.
|
||||
- Go ≥ 1.13: `GO111MODULE=on go get github.com/pelletier/go-toml/v2`.
|
||||
|
||||
In case of trouble: [Go Modules FAQ][mod-faq].
|
||||
|
||||
[mod-faq]: https://github.com/golang/go/wiki/Modules#why-does-installing-a-tool-via-go-get-fail-with-error-cannot-find-main-module
|
||||
|
||||
## Tools
|
||||
|
||||
Go-toml provides three handy command line tools:
|
||||
|
||||
* `tomljson`: Reads a TOML file and outputs its JSON representation.
|
||||
|
||||
```
|
||||
$ go install github.com/pelletier/go-toml/v2/cmd/tomljson@latest
|
||||
$ tomljson --help
|
||||
```
|
||||
|
||||
* `jsontoml`: Reads a JSON file and outputs a TOML representation.
|
||||
|
||||
```
|
||||
$ go install github.com/pelletier/go-toml/v2/cmd/jsontoml@latest
|
||||
$ jsontoml --help
|
||||
```
|
||||
|
||||
* `tomll`: Lints and reformats a TOML file.
|
||||
|
||||
```
|
||||
$ go install github.com/pelletier/go-toml/v2/cmd/tomll@latest
|
||||
$ tomll --help
|
||||
```
|
||||
|
||||
### Docker image
|
||||
|
||||
Those tools are also available as a [Docker image][docker]. For example, to use
|
||||
`tomljson`:
|
||||
|
||||
```
|
||||
docker run -i ghcr.io/pelletier/go-toml:v2 tomljson < example.toml
|
||||
```
|
||||
|
||||
Multiple versions are available on [ghcr.io][docker].
|
||||
|
||||
[docker]: https://github.com/pelletier/go-toml/pkgs/container/go-toml
|
||||
|
||||
## Migrating from v1
|
||||
|
||||
This section describes the differences between v1 and v2, with some pointers on
|
||||
how to get the original behavior when possible.
|
||||
|
||||
### Decoding / Unmarshal
|
||||
|
||||
#### Automatic field name guessing
|
||||
|
||||
When unmarshaling to a struct, if a key in the TOML document does not exactly
|
||||
match the name of a struct field or any of the `toml`-tagged field, v1 tries
|
||||
multiple variations of the key ([code][v1-keys]).
|
||||
|
||||
V2 instead does a case-insensitive matching, like `encoding/json`.
|
||||
|
||||
This could impact you if you are relying on casing to differentiate two fields,
|
||||
and one of them is a not using the `toml` struct tag. The recommended solution
|
||||
is to be specific about tag names for those fields using the `toml` struct tag.
|
||||
|
||||
[v1-keys]: https://github.com/pelletier/go-toml/blob/a2e52561804c6cd9392ebf0048ca64fe4af67a43/marshal.go#L775-L781
|
||||
|
||||
#### Ignore preexisting value in interface
|
||||
|
||||
When decoding into a non-nil `interface{}`, go-toml v1 uses the type of the
|
||||
element in the interface to decode the object. For example:
|
||||
|
||||
```go
|
||||
type inner struct {
|
||||
B interface{}
|
||||
}
|
||||
type doc struct {
|
||||
A interface{}
|
||||
}
|
||||
|
||||
d := doc{
|
||||
A: inner{
|
||||
B: "Before",
|
||||
},
|
||||
}
|
||||
|
||||
data := `
|
||||
[A]
|
||||
B = "After"
|
||||
`
|
||||
|
||||
toml.Unmarshal([]byte(data), &d)
|
||||
fmt.Printf("toml v1: %#v\n", d)
|
||||
|
||||
// toml v1: main.doc{A:main.inner{B:"After"}}
|
||||
```
|
||||
|
||||
In this case, field `A` is of type `interface{}`, containing a `inner` struct.
|
||||
V1 sees that type and uses it when decoding the object.
|
||||
|
||||
When decoding an object into an `interface{}`, V2 instead disregards whatever
|
||||
value the `interface{}` may contain and replaces it with a
|
||||
`map[string]interface{}`. With the same data structure as above, here is what
|
||||
the result looks like:
|
||||
|
||||
```go
|
||||
toml.Unmarshal([]byte(data), &d)
|
||||
fmt.Printf("toml v2: %#v\n", d)
|
||||
|
||||
// toml v2: main.doc{A:map[string]interface {}{"B":"After"}}
|
||||
```
|
||||
|
||||
This is to match `encoding/json`'s behavior. There is no way to make the v2
|
||||
decoder behave like v1.
|
||||
|
||||
#### Values out of array bounds ignored
|
||||
|
||||
When decoding into an array, v1 returns an error when the number of elements
|
||||
contained in the doc is superior to the capacity of the array. For example:
|
||||
|
||||
```go
|
||||
type doc struct {
|
||||
A [2]string
|
||||
}
|
||||
d := doc{}
|
||||
err := toml.Unmarshal([]byte(`A = ["one", "two", "many"]`), &d)
|
||||
fmt.Println(err)
|
||||
|
||||
// (1, 1): unmarshal: TOML array length (3) exceeds destination array length (2)
|
||||
```
|
||||
|
||||
In the same situation, v2 ignores the last value:
|
||||
|
||||
```go
|
||||
err := toml.Unmarshal([]byte(`A = ["one", "two", "many"]`), &d)
|
||||
fmt.Println("err:", err, "d:", d)
|
||||
// err: <nil> d: {[one two]}
|
||||
```
|
||||
|
||||
This is to match `encoding/json`'s behavior. There is no way to make the v2
|
||||
decoder behave like v1.
|
||||
|
||||
#### Support for `toml.Unmarshaler` has been dropped
|
||||
|
||||
This method was not widely used, poorly defined, and added a lot of complexity.
|
||||
A similar effect can be achieved by implementing the `encoding.TextUnmarshaler`
|
||||
interface and use strings.
|
||||
|
||||
#### Support for `default` struct tag has been dropped
|
||||
|
||||
This feature adds complexity and a poorly defined API for an effect that can be
|
||||
accomplished outside of the library.
|
||||
|
||||
It does not seem like other format parsers in Go support that feature (the
|
||||
project referenced in the original ticket #202 has not been updated since 2017).
|
||||
Given that go-toml v2 should not touch values not in the document, the same
|
||||
effect can be achieved by pre-filling the struct with defaults (libraries like
|
||||
[go-defaults][go-defaults] can help). Also, string representation is not well
|
||||
defined for all types: it creates issues like #278.
|
||||
|
||||
The recommended replacement is pre-filling the struct before unmarshaling.
|
||||
|
||||
[go-defaults]: https://github.com/mcuadros/go-defaults
|
||||
|
||||
#### `toml.Tree` replacement
|
||||
|
||||
This structure was the initial attempt at providing a document model for
|
||||
go-toml. It allows manipulating the structure of any document, encoding and
|
||||
decoding from their TOML representation. While a more robust feature was
|
||||
initially planned in go-toml v2, this has been ultimately [removed from
|
||||
scope][nodoc] of this library, with no plan to add it back at the moment. The
|
||||
closest equivalent at the moment would be to unmarshal into an `interface{}` and
|
||||
use type assertions and/or reflection to manipulate the arbitrary
|
||||
structure. However this would fall short of providing all of the TOML features
|
||||
such as adding comments and be specific about whitespace.
|
||||
|
||||
|
||||
#### `toml.Position` are not retrievable anymore
|
||||
|
||||
The API for retrieving the position (line, column) of a specific TOML element do
|
||||
not exist anymore. This was done to minimize the amount of concepts introduced
|
||||
by the library (query path), and avoid the performance hit related to storing
|
||||
positions in the absence of a document model, for a feature that seemed to have
|
||||
little use. Errors however have gained more detailed position
|
||||
information. Position retrieval seems better fitted for a document model, which
|
||||
has been [removed from the scope][nodoc] of go-toml v2 at the moment.
|
||||
|
||||
### Encoding / Marshal
|
||||
|
||||
#### Default struct fields order
|
||||
|
||||
V1 emits struct fields order alphabetically by default. V2 struct fields are
|
||||
emitted in order they are defined. For example:
|
||||
|
||||
```go
|
||||
type S struct {
|
||||
B string
|
||||
A string
|
||||
}
|
||||
|
||||
data := S{
|
||||
B: "B",
|
||||
A: "A",
|
||||
}
|
||||
|
||||
b, _ := tomlv1.Marshal(data)
|
||||
fmt.Println("v1:\n" + string(b))
|
||||
|
||||
b, _ = tomlv2.Marshal(data)
|
||||
fmt.Println("v2:\n" + string(b))
|
||||
|
||||
// Output:
|
||||
// v1:
|
||||
// A = "A"
|
||||
// B = "B"
|
||||
|
||||
// v2:
|
||||
// B = 'B'
|
||||
// A = 'A'
|
||||
```
|
||||
|
||||
There is no way to make v2 encoder behave like v1. A workaround could be to
|
||||
manually sort the fields alphabetically in the struct definition, or generate
|
||||
struct types using `reflect.StructOf`.
|
||||
|
||||
#### No indentation by default
|
||||
|
||||
V1 automatically indents content of tables by default. V2 does not. However the
|
||||
same behavior can be obtained using [`Encoder.SetIndentTables`][sit]. For example:
|
||||
|
||||
```go
|
||||
data := map[string]interface{}{
|
||||
"table": map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
}
|
||||
|
||||
b, _ := tomlv1.Marshal(data)
|
||||
fmt.Println("v1:\n" + string(b))
|
||||
|
||||
b, _ = tomlv2.Marshal(data)
|
||||
fmt.Println("v2:\n" + string(b))
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
enc := tomlv2.NewEncoder(&buf)
|
||||
enc.SetIndentTables(true)
|
||||
enc.Encode(data)
|
||||
fmt.Println("v2 Encoder:\n" + string(buf.Bytes()))
|
||||
|
||||
// Output:
|
||||
// v1:
|
||||
//
|
||||
// [table]
|
||||
// key = "value"
|
||||
//
|
||||
// v2:
|
||||
// [table]
|
||||
// key = 'value'
|
||||
//
|
||||
//
|
||||
// v2 Encoder:
|
||||
// [table]
|
||||
// key = 'value'
|
||||
```
|
||||
|
||||
[sit]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Encoder.SetIndentTables
|
||||
|
||||
#### Keys and strings are single quoted
|
||||
|
||||
V1 always uses double quotes (`"`) around strings and keys that cannot be
|
||||
represented bare (unquoted). V2 uses single quotes instead by default (`'`),
|
||||
unless a character cannot be represented, then falls back to double quotes. As a
|
||||
result of this change, `Encoder.QuoteMapKeys` has been removed, as it is not
|
||||
useful anymore.
|
||||
|
||||
There is no way to make v2 encoder behave like v1.
|
||||
|
||||
#### `TextMarshaler` emits as a string, not TOML
|
||||
|
||||
Types that implement [`encoding.TextMarshaler`][tm] can emit arbitrary TOML in
|
||||
v1. The encoder would append the result to the output directly. In v2 the result
|
||||
is wrapped in a string. As a result, this interface cannot be implemented by the
|
||||
root object.
|
||||
|
||||
There is no way to make v2 encoder behave like v1.
|
||||
|
||||
[tm]: https://golang.org/pkg/encoding/#TextMarshaler
|
||||
|
||||
#### `Encoder.CompactComments` has been removed
|
||||
|
||||
Emitting compact comments is now the default behavior of go-toml. This option
|
||||
is not necessary anymore.
|
||||
|
||||
#### Struct tags have been merged
|
||||
|
||||
V1 used to provide multiple struct tags: `comment`, `commented`, `multiline`,
|
||||
`toml`, and `omitempty`. To behave more like the standard library, v2 has merged
|
||||
`toml`, `multiline`, `commented`, and `omitempty`. For example:
|
||||
|
||||
```go
|
||||
type doc struct {
|
||||
// v1
|
||||
F string `toml:"field" multiline:"true" omitempty:"true" commented:"true"`
|
||||
// v2
|
||||
F string `toml:"field,multiline,omitempty,commented"`
|
||||
}
|
||||
```
|
||||
|
||||
Has a result, the `Encoder.SetTag*` methods have been removed, as there is just
|
||||
one tag now.
|
||||
|
||||
#### `Encoder.ArraysWithOneElementPerLine` has been renamed
|
||||
|
||||
The new name is `Encoder.SetArraysMultiline`. The behavior should be the same.
|
||||
|
||||
#### `Encoder.Indentation` has been renamed
|
||||
|
||||
The new name is `Encoder.SetIndentSymbol`. The behavior should be the same.
|
||||
|
||||
|
||||
#### Embedded structs behave like stdlib
|
||||
|
||||
V1 defaults to merging embedded struct fields into the embedding struct. This
|
||||
behavior was unexpected because it does not follow the standard library. To
|
||||
avoid breaking backward compatibility, the `Encoder.PromoteAnonymous` method was
|
||||
added to make the encoder behave correctly. Given backward compatibility is not
|
||||
a problem anymore, v2 does the right thing by default: it follows the behavior
|
||||
of `encoding/json`. `Encoder.PromoteAnonymous` has been removed.
|
||||
|
||||
[nodoc]: https://github.com/pelletier/go-toml/discussions/506#discussioncomment-1526038
|
||||
|
||||
### `query`
|
||||
|
||||
go-toml v1 provided the [`go-toml/query`][query] package. It allowed to run
|
||||
JSONPath-style queries on TOML files. This feature is not available in v2. For a
|
||||
replacement, check out [dasel][dasel].
|
||||
|
||||
This package has been removed because it was essentially not supported anymore
|
||||
(last commit May 2020), increased the complexity of the code base, and more
|
||||
complete solutions exist out there.
|
||||
|
||||
[query]: https://github.com/pelletier/go-toml/tree/f99d6bbca119636aeafcf351ee52b3d202782627/query
|
||||
[dasel]: https://github.com/TomWright/dasel
|
||||
|
||||
## Versioning
|
||||
|
||||
Expect for parts explicitly marked otherwise, go-toml follows [Semantic
|
||||
Versioning](https://semver.org). The supported version of
|
||||
[TOML](https://github.com/toml-lang/toml) is indicated at the beginning of this
|
||||
document. The last two major versions of Go are supported (see [Go Release
|
||||
Policy](https://golang.org/doc/devel/release.html#policy)).
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2013, 2014 Thomas Pelletier, Eric Anderton
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
The MIT License (MIT). Read [LICENSE](LICENSE).
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ---------- | ------------------ |
|
||||
| Latest 2.x | :white_check_mark: |
|
||||
| All 1.x | :x: |
|
||||
| All 0.x | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Email a vulnerability report to `security@pelletier.codes`. Make sure to include
|
||||
as many details as possible to reproduce the vulnerability. This is a
|
||||
side-project: I will try to get back to you as quickly as possible, time
|
||||
permitting in my personal life. Providing a working patch helps very much!
|
||||
@@ -0,0 +1,80 @@
|
||||
package benchmark_test
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/pelletier/go-toml/v2/internal/assert"
|
||||
)
|
||||
|
||||
var benchInputs = []struct {
|
||||
name string
|
||||
jsonLen int
|
||||
}{
|
||||
// from https://gist.githubusercontent.com/feeeper/2197d6d734729625a037af1df14cf2aa/raw/2f22b120e476d897179be3c1e2483d18067aa7df/config.toml
|
||||
{"config", 806507},
|
||||
|
||||
// converted from https://github.com/miloyip/nativejson-benchmark
|
||||
{"canada", 2090234},
|
||||
{"citm_catalog", 479897},
|
||||
{"twitter", 428778},
|
||||
{"code", 1940472},
|
||||
|
||||
// converted from https://raw.githubusercontent.com/mailru/easyjson/master/benchmark/example.json
|
||||
{"example", 7779},
|
||||
}
|
||||
|
||||
func TestUnmarshalDatasetCode(t *testing.T) {
|
||||
for _, tc := range benchInputs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
buf := fixture(t, tc.name)
|
||||
|
||||
var v interface{}
|
||||
assert.NoError(t, toml.Unmarshal(buf, &v))
|
||||
|
||||
b, err := json.Marshal(v)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(b), tc.jsonLen)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshalDataset(b *testing.B) {
|
||||
for _, tc := range benchInputs {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
buf := fixture(b, tc.name)
|
||||
b.SetBytes(int64(len(buf)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var v interface{}
|
||||
assert.NoError(b, toml.Unmarshal(buf, &v))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// fixture returns the uncompressed contents of path.
|
||||
func fixture(tb testing.TB, path string) []byte {
|
||||
tb.Helper()
|
||||
|
||||
file := path + ".toml.gz"
|
||||
f, err := os.Open(filepath.Join("testdata", file))
|
||||
if os.IsNotExist(err) {
|
||||
tb.Skip("benchmark fixture not found:", file)
|
||||
}
|
||||
assert.NoError(tb, err)
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
gz, err := gzip.NewReader(f)
|
||||
assert.NoError(tb, err)
|
||||
|
||||
buf, err := io.ReadAll(gz)
|
||||
assert.NoError(tb, err)
|
||||
return buf
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
################################################################################
|
||||
## Comment
|
||||
|
||||
# Speak your mind with the hash symbol. They go from the symbol to the end of
|
||||
# the line.
|
||||
|
||||
|
||||
################################################################################
|
||||
## Table
|
||||
|
||||
# Tables (also known as hash tables or dictionaries) are collections of
|
||||
# key/value pairs. They appear in square brackets on a line by themselves.
|
||||
|
||||
[table]
|
||||
|
||||
key = "value" # Yeah, you can do this.
|
||||
|
||||
# Nested tables are denoted by table names with dots in them. Name your tables
|
||||
# whatever crap you please, just don't use #, ., [ or ].
|
||||
|
||||
[table.subtable]
|
||||
|
||||
key = "another value"
|
||||
|
||||
# You don't need to specify all the super-tables if you don't want to. TOML
|
||||
# knows how to do it for you.
|
||||
|
||||
# [x] you
|
||||
# [x.y] don't
|
||||
# [x.y.z] need these
|
||||
[x.y.z.w] # for this to work
|
||||
|
||||
|
||||
################################################################################
|
||||
## Inline Table
|
||||
|
||||
# Inline tables provide a more compact syntax for expressing tables. They are
|
||||
# especially useful for grouped data that can otherwise quickly become verbose.
|
||||
# Inline tables are enclosed in curly braces `{` and `}`. No newlines are
|
||||
# allowed between the curly braces unless they are valid within a value.
|
||||
|
||||
[table.inline]
|
||||
|
||||
name = { first = "Tom", last = "Preston-Werner" }
|
||||
point = { x = 1, y = 2 }
|
||||
|
||||
|
||||
################################################################################
|
||||
## String
|
||||
|
||||
# There are four ways to express strings: basic, multi-line basic, literal, and
|
||||
# multi-line literal. All strings must contain only valid UTF-8 characters.
|
||||
|
||||
[string.basic]
|
||||
|
||||
basic = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF."
|
||||
|
||||
[string.multiline]
|
||||
|
||||
# The following strings are byte-for-byte equivalent:
|
||||
key1 = "One\nTwo"
|
||||
key2 = """One\nTwo"""
|
||||
key3 = """
|
||||
One
|
||||
Two"""
|
||||
|
||||
[string.multiline.continued]
|
||||
|
||||
# The following strings are byte-for-byte equivalent:
|
||||
key1 = "The quick brown fox jumps over the lazy dog."
|
||||
|
||||
key2 = """
|
||||
The quick brown \
|
||||
|
||||
|
||||
fox jumps over \
|
||||
the lazy dog."""
|
||||
|
||||
key3 = """\
|
||||
The quick brown \
|
||||
fox jumps over \
|
||||
the lazy dog.\
|
||||
"""
|
||||
|
||||
[string.literal]
|
||||
|
||||
# What you see is what you get.
|
||||
winpath = 'C:\Users\nodejs\templates'
|
||||
winpath2 = '\\ServerX\admin$\system32\'
|
||||
quoted = 'Tom "Dubs" Preston-Werner'
|
||||
regex = '<\i\c*\s*>'
|
||||
|
||||
|
||||
[string.literal.multiline]
|
||||
|
||||
regex2 = '''I [dw]on't need \d{2} apples'''
|
||||
lines = '''
|
||||
The first newline is
|
||||
trimmed in raw strings.
|
||||
All other whitespace
|
||||
is preserved.
|
||||
'''
|
||||
|
||||
|
||||
################################################################################
|
||||
## Integer
|
||||
|
||||
# Integers are whole numbers. Positive numbers may be prefixed with a plus sign.
|
||||
# Negative numbers are prefixed with a minus sign.
|
||||
|
||||
[integer]
|
||||
|
||||
key1 = +99
|
||||
key2 = 42
|
||||
key3 = 0
|
||||
key4 = -17
|
||||
|
||||
[integer.underscores]
|
||||
|
||||
# For large numbers, you may use underscores to enhance readability. Each
|
||||
# underscore must be surrounded by at least one digit.
|
||||
key1 = 1_000
|
||||
key2 = 5_349_221
|
||||
key3 = 1_2_3_4_5 # valid but inadvisable
|
||||
|
||||
|
||||
################################################################################
|
||||
## Float
|
||||
|
||||
# A float consists of an integer part (which may be prefixed with a plus or
|
||||
# minus sign) followed by a fractional part and/or an exponent part.
|
||||
|
||||
[float.fractional]
|
||||
|
||||
key1 = +1.0
|
||||
key2 = 3.1415
|
||||
key3 = -0.01
|
||||
|
||||
[float.exponent]
|
||||
|
||||
key1 = 5e+22
|
||||
key2 = 1e6
|
||||
key3 = -2E-2
|
||||
|
||||
[float.both]
|
||||
|
||||
key = 6.626e-34
|
||||
|
||||
[float.underscores]
|
||||
|
||||
key1 = 9_224_617.445_991_228_313
|
||||
key2 = 1e1_00
|
||||
|
||||
|
||||
################################################################################
|
||||
## Boolean
|
||||
|
||||
# Booleans are just the tokens you're used to. Always lowercase.
|
||||
|
||||
[boolean]
|
||||
|
||||
True = true
|
||||
False = false
|
||||
|
||||
|
||||
################################################################################
|
||||
## Datetime
|
||||
|
||||
# Datetimes are RFC 3339 dates.
|
||||
|
||||
[datetime]
|
||||
|
||||
key1 = 1979-05-27T07:32:00Z
|
||||
key2 = 1979-05-27T00:32:00-07:00
|
||||
key3 = 1979-05-27T00:32:00.999999-07:00
|
||||
|
||||
|
||||
################################################################################
|
||||
## Array
|
||||
|
||||
# Arrays are square brackets with other primitives inside. Whitespace is
|
||||
# ignored. Elements are separated by commas. Data types may not be mixed.
|
||||
|
||||
[array]
|
||||
|
||||
key1 = [ 1, 2, 3 ]
|
||||
key2 = [ "red", "yellow", "green" ]
|
||||
key3 = [ [ 1, 2 ], [3, 4, 5] ]
|
||||
key4 = [ [ 1, 2 ], ["a", "b", "c"] ] # this is ok
|
||||
|
||||
# Arrays can also be multiline. So in addition to ignoring whitespace, arrays
|
||||
# also ignore newlines between the brackets. Terminating commas are ok before
|
||||
# the closing bracket.
|
||||
|
||||
key5 = [
|
||||
1, 2, 3
|
||||
]
|
||||
key6 = [
|
||||
1,
|
||||
2, # this is ok
|
||||
]
|
||||
|
||||
|
||||
################################################################################
|
||||
## Array of Tables
|
||||
|
||||
# These can be expressed by using a table name in double brackets. Each table
|
||||
# with the same double bracketed name will be an element in the array. The
|
||||
# tables are inserted in the order encountered.
|
||||
|
||||
[[products]]
|
||||
|
||||
name = "Hammer"
|
||||
sku = 738594937
|
||||
|
||||
[[products]]
|
||||
|
||||
[[products]]
|
||||
|
||||
name = "Nail"
|
||||
sku = 284758393
|
||||
color = "gray"
|
||||
|
||||
|
||||
# You can create nested arrays of tables as well.
|
||||
|
||||
[[fruit]]
|
||||
name = "apple"
|
||||
|
||||
[fruit.physical]
|
||||
color = "red"
|
||||
shape = "round"
|
||||
|
||||
[[fruit.variety]]
|
||||
name = "red delicious"
|
||||
|
||||
[[fruit.variety]]
|
||||
name = "granny smith"
|
||||
|
||||
[[fruit]]
|
||||
name = "banana"
|
||||
|
||||
[[fruit.variety]]
|
||||
name = "plantain"
|
||||
@@ -0,0 +1,654 @@
|
||||
package benchmark_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/pelletier/go-toml/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestUnmarshalSimple(t *testing.T) {
|
||||
doc := []byte(`A = "hello"`)
|
||||
d := struct {
|
||||
A string
|
||||
}{}
|
||||
|
||||
err := toml.Unmarshal(doc, &d)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshal(b *testing.B) {
|
||||
b.Run("SimpleDocument", func(b *testing.B) {
|
||||
doc := []byte(`A = "hello"`)
|
||||
|
||||
b.Run("struct", func(b *testing.B) {
|
||||
b.SetBytes(int64(len(doc)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
d := struct {
|
||||
A string
|
||||
}{}
|
||||
|
||||
err := toml.Unmarshal(doc, &d)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("map", func(b *testing.B) {
|
||||
b.SetBytes(int64(len(doc)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
d := map[string]interface{}{}
|
||||
err := toml.Unmarshal(doc, &d)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("ReferenceFile", func(b *testing.B) {
|
||||
bytes, err := os.ReadFile("benchmark.toml")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.Run("struct", func(b *testing.B) {
|
||||
b.SetBytes(int64(len(bytes)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
d := benchmarkDoc{}
|
||||
err := toml.Unmarshal(bytes, &d)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("map", func(b *testing.B) {
|
||||
b.SetBytes(int64(len(bytes)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
d := map[string]interface{}{}
|
||||
err := toml.Unmarshal(bytes, &d)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("HugoFrontMatter", func(b *testing.B) {
|
||||
b.SetBytes(int64(len(hugoFrontMatterbytes)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
d := map[string]interface{}{}
|
||||
err := toml.Unmarshal(hugoFrontMatterbytes, &d)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func marshal(v interface{}) ([]byte, error) {
|
||||
var b bytes.Buffer
|
||||
enc := toml.NewEncoder(&b)
|
||||
err := enc.Encode(v)
|
||||
return b.Bytes(), err
|
||||
}
|
||||
|
||||
func BenchmarkMarshal(b *testing.B) {
|
||||
b.Run("SimpleDocument", func(b *testing.B) {
|
||||
doc := []byte(`A = "hello"`)
|
||||
|
||||
b.Run("struct", func(b *testing.B) {
|
||||
d := struct {
|
||||
A string
|
||||
}{}
|
||||
|
||||
err := toml.Unmarshal(doc, &d)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
var out []byte
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
out, err = marshal(d)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
b.SetBytes(int64(len(out)))
|
||||
})
|
||||
|
||||
b.Run("map", func(b *testing.B) {
|
||||
d := map[string]interface{}{}
|
||||
err := toml.Unmarshal(doc, &d)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
var out []byte
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
out, err = marshal(d)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
b.SetBytes(int64(len(out)))
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("ReferenceFile", func(b *testing.B) {
|
||||
bytes, err := os.ReadFile("benchmark.toml")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.Run("struct", func(b *testing.B) {
|
||||
d := benchmarkDoc{}
|
||||
err := toml.Unmarshal(bytes, &d)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
var out []byte
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
out, err = marshal(d)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
b.SetBytes(int64(len(out)))
|
||||
})
|
||||
|
||||
b.Run("map", func(b *testing.B) {
|
||||
d := map[string]interface{}{}
|
||||
err := toml.Unmarshal(bytes, &d)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
var out []byte
|
||||
for i := 0; i < b.N; i++ {
|
||||
out, err = marshal(d)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
b.SetBytes(int64(len(out)))
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("HugoFrontMatter", func(b *testing.B) {
|
||||
d := map[string]interface{}{}
|
||||
err := toml.Unmarshal(hugoFrontMatterbytes, &d)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
var out []byte
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
out, err = marshal(d)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
b.SetBytes(int64(len(out)))
|
||||
})
|
||||
}
|
||||
|
||||
type benchmarkDoc struct {
|
||||
Table struct {
|
||||
Key string
|
||||
Subtable struct {
|
||||
Key string
|
||||
}
|
||||
Inline struct {
|
||||
Name struct {
|
||||
First string
|
||||
Last string
|
||||
}
|
||||
Point struct {
|
||||
X int64
|
||||
Y int64
|
||||
}
|
||||
}
|
||||
}
|
||||
String struct {
|
||||
Basic struct {
|
||||
Basic string
|
||||
}
|
||||
Multiline struct {
|
||||
Key1 string
|
||||
Key2 string
|
||||
Key3 string
|
||||
Continued struct {
|
||||
Key1 string
|
||||
Key2 string
|
||||
Key3 string
|
||||
}
|
||||
}
|
||||
Literal struct {
|
||||
Winpath string
|
||||
Winpath2 string
|
||||
Quoted string
|
||||
Regex string
|
||||
Multiline struct {
|
||||
Regex2 string
|
||||
Lines string
|
||||
}
|
||||
}
|
||||
}
|
||||
Integer struct {
|
||||
Key1 int64
|
||||
Key2 int64
|
||||
Key3 int64
|
||||
Key4 int64
|
||||
Underscores struct {
|
||||
Key1 int64
|
||||
Key2 int64
|
||||
Key3 int64
|
||||
}
|
||||
}
|
||||
Float struct {
|
||||
Fractional struct {
|
||||
Key1 float64
|
||||
Key2 float64
|
||||
Key3 float64
|
||||
}
|
||||
Exponent struct {
|
||||
Key1 float64
|
||||
Key2 float64
|
||||
Key3 float64
|
||||
}
|
||||
Both struct {
|
||||
Key float64
|
||||
}
|
||||
Underscores struct {
|
||||
Key1 float64
|
||||
Key2 float64
|
||||
}
|
||||
}
|
||||
Boolean struct {
|
||||
True bool
|
||||
False bool
|
||||
}
|
||||
Datetime struct {
|
||||
Key1 time.Time
|
||||
Key2 time.Time
|
||||
Key3 time.Time
|
||||
}
|
||||
Array struct {
|
||||
Key1 []int64
|
||||
Key2 []string
|
||||
Key3 [][]int64
|
||||
// TODO: Key4 not supported by go-toml's Unmarshal
|
||||
Key4 []interface{}
|
||||
Key5 []int64
|
||||
Key6 []int64
|
||||
}
|
||||
Products []struct {
|
||||
Name string
|
||||
Sku int64
|
||||
Color string
|
||||
}
|
||||
Fruit []struct {
|
||||
Name string
|
||||
Physical struct {
|
||||
Color string
|
||||
Shape string
|
||||
}
|
||||
Variety []struct {
|
||||
Name string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalReferenceFile(t *testing.T) {
|
||||
bytes, err := os.ReadFile("benchmark.toml")
|
||||
assert.NoError(t, err)
|
||||
d := benchmarkDoc{}
|
||||
err = toml.Unmarshal(bytes, &d)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expected := benchmarkDoc{
|
||||
Table: struct {
|
||||
Key string
|
||||
Subtable struct{ Key string }
|
||||
Inline struct {
|
||||
Name struct {
|
||||
First string
|
||||
Last string
|
||||
}
|
||||
Point struct {
|
||||
X int64
|
||||
Y int64
|
||||
}
|
||||
}
|
||||
}{
|
||||
Key: "value",
|
||||
Subtable: struct{ Key string }{
|
||||
Key: "another value",
|
||||
},
|
||||
// note: x.y.z.w is purposefully missing
|
||||
Inline: struct {
|
||||
Name struct {
|
||||
First string
|
||||
Last string
|
||||
}
|
||||
Point struct {
|
||||
X int64
|
||||
Y int64
|
||||
}
|
||||
}{
|
||||
Name: struct {
|
||||
First string
|
||||
Last string
|
||||
}{
|
||||
First: "Tom",
|
||||
Last: "Preston-Werner",
|
||||
},
|
||||
Point: struct {
|
||||
X int64
|
||||
Y int64
|
||||
}{
|
||||
X: 1,
|
||||
Y: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
String: struct {
|
||||
Basic struct{ Basic string }
|
||||
Multiline struct {
|
||||
Key1 string
|
||||
Key2 string
|
||||
Key3 string
|
||||
Continued struct {
|
||||
Key1 string
|
||||
Key2 string
|
||||
Key3 string
|
||||
}
|
||||
}
|
||||
Literal struct {
|
||||
Winpath string
|
||||
Winpath2 string
|
||||
Quoted string
|
||||
Regex string
|
||||
Multiline struct {
|
||||
Regex2 string
|
||||
Lines string
|
||||
}
|
||||
}
|
||||
}{
|
||||
Basic: struct{ Basic string }{
|
||||
Basic: "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF.",
|
||||
},
|
||||
Multiline: struct {
|
||||
Key1 string
|
||||
Key2 string
|
||||
Key3 string
|
||||
Continued struct {
|
||||
Key1 string
|
||||
Key2 string
|
||||
Key3 string
|
||||
}
|
||||
}{
|
||||
Key1: "One\nTwo",
|
||||
Key2: "One\nTwo",
|
||||
Key3: "One\nTwo",
|
||||
|
||||
Continued: struct {
|
||||
Key1 string
|
||||
Key2 string
|
||||
Key3 string
|
||||
}{
|
||||
Key1: `The quick brown fox jumps over the lazy dog.`,
|
||||
Key2: `The quick brown fox jumps over the lazy dog.`,
|
||||
Key3: `The quick brown fox jumps over the lazy dog.`,
|
||||
},
|
||||
},
|
||||
Literal: struct {
|
||||
Winpath string
|
||||
Winpath2 string
|
||||
Quoted string
|
||||
Regex string
|
||||
Multiline struct {
|
||||
Regex2 string
|
||||
Lines string
|
||||
}
|
||||
}{
|
||||
Winpath: `C:\Users\nodejs\templates`,
|
||||
Winpath2: `\\ServerX\admin$\system32\`,
|
||||
Quoted: `Tom "Dubs" Preston-Werner`,
|
||||
Regex: `<\i\c*\s*>`,
|
||||
|
||||
Multiline: struct {
|
||||
Regex2 string
|
||||
Lines string
|
||||
}{
|
||||
Regex2: `I [dw]on't need \d{2} apples`,
|
||||
Lines: `The first newline is
|
||||
trimmed in raw strings.
|
||||
All other whitespace
|
||||
is preserved.
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
Integer: struct {
|
||||
Key1 int64
|
||||
Key2 int64
|
||||
Key3 int64
|
||||
Key4 int64
|
||||
Underscores struct {
|
||||
Key1 int64
|
||||
Key2 int64
|
||||
Key3 int64
|
||||
}
|
||||
}{
|
||||
Key1: 99,
|
||||
Key2: 42,
|
||||
Key3: 0,
|
||||
Key4: -17,
|
||||
|
||||
Underscores: struct {
|
||||
Key1 int64
|
||||
Key2 int64
|
||||
Key3 int64
|
||||
}{
|
||||
Key1: 1000,
|
||||
Key2: 5349221,
|
||||
Key3: 12345,
|
||||
},
|
||||
},
|
||||
Float: struct {
|
||||
Fractional struct {
|
||||
Key1 float64
|
||||
Key2 float64
|
||||
Key3 float64
|
||||
}
|
||||
Exponent struct {
|
||||
Key1 float64
|
||||
Key2 float64
|
||||
Key3 float64
|
||||
}
|
||||
Both struct{ Key float64 }
|
||||
Underscores struct {
|
||||
Key1 float64
|
||||
Key2 float64
|
||||
}
|
||||
}{
|
||||
Fractional: struct {
|
||||
Key1 float64
|
||||
Key2 float64
|
||||
Key3 float64
|
||||
}{
|
||||
Key1: 1.0,
|
||||
Key2: 3.1415,
|
||||
Key3: -0.01,
|
||||
},
|
||||
Exponent: struct {
|
||||
Key1 float64
|
||||
Key2 float64
|
||||
Key3 float64
|
||||
}{
|
||||
Key1: 5e+22,
|
||||
Key2: 1e6,
|
||||
Key3: -2e-2,
|
||||
},
|
||||
Both: struct{ Key float64 }{
|
||||
Key: 6.626e-34,
|
||||
},
|
||||
Underscores: struct {
|
||||
Key1 float64
|
||||
Key2 float64
|
||||
}{
|
||||
Key1: 9224617.445991228313,
|
||||
Key2: 1e100,
|
||||
},
|
||||
},
|
||||
Boolean: struct {
|
||||
True bool
|
||||
False bool
|
||||
}{
|
||||
True: true,
|
||||
False: false,
|
||||
},
|
||||
Datetime: struct {
|
||||
Key1 time.Time
|
||||
Key2 time.Time
|
||||
Key3 time.Time
|
||||
}{
|
||||
Key1: time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC),
|
||||
Key2: time.Date(1979, 5, 27, 0, 32, 0, 0, time.FixedZone("", -7*3600)),
|
||||
Key3: time.Date(1979, 5, 27, 0, 32, 0, 999999000, time.FixedZone("", -7*3600)),
|
||||
},
|
||||
Array: struct {
|
||||
Key1 []int64
|
||||
Key2 []string
|
||||
Key3 [][]int64
|
||||
Key4 []interface{}
|
||||
Key5 []int64
|
||||
Key6 []int64
|
||||
}{
|
||||
Key1: []int64{1, 2, 3},
|
||||
Key2: []string{"red", "yellow", "green"},
|
||||
Key3: [][]int64{{1, 2}, {3, 4, 5}},
|
||||
Key4: []interface{}{
|
||||
[]interface{}{int64(1), int64(2)},
|
||||
[]interface{}{"a", "b", "c"},
|
||||
},
|
||||
Key5: []int64{1, 2, 3},
|
||||
Key6: []int64{1, 2},
|
||||
},
|
||||
Products: []struct {
|
||||
Name string
|
||||
Sku int64
|
||||
Color string
|
||||
}{
|
||||
{
|
||||
Name: "Hammer",
|
||||
Sku: 738594937,
|
||||
},
|
||||
{},
|
||||
{
|
||||
Name: "Nail",
|
||||
Sku: 284758393,
|
||||
Color: "gray",
|
||||
},
|
||||
},
|
||||
Fruit: []struct {
|
||||
Name string
|
||||
Physical struct {
|
||||
Color string
|
||||
Shape string
|
||||
}
|
||||
Variety []struct{ Name string }
|
||||
}{
|
||||
{
|
||||
Name: "apple",
|
||||
Physical: struct {
|
||||
Color string
|
||||
Shape string
|
||||
}{
|
||||
Color: "red",
|
||||
Shape: "round",
|
||||
},
|
||||
Variety: []struct{ Name string }{
|
||||
{Name: "red delicious"},
|
||||
{Name: "granny smith"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "banana",
|
||||
Variety: []struct{ Name string }{
|
||||
{Name: "plantain"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, d)
|
||||
}
|
||||
|
||||
var hugoFrontMatterbytes = []byte(`
|
||||
categories = ["Development", "VIM"]
|
||||
date = "2012-04-06"
|
||||
description = "spf13-vim is a cross platform distribution of vim plugins and resources for Vim."
|
||||
slug = "spf13-vim-3-0-release-and-new-website"
|
||||
tags = [".vimrc", "plugins", "spf13-vim", "vim"]
|
||||
title = "spf13-vim 3.0 release and new website"
|
||||
include_toc = true
|
||||
show_comments = false
|
||||
|
||||
[[cascade]]
|
||||
background = "yosemite.jpg"
|
||||
[cascade._target]
|
||||
kind = "page"
|
||||
lang = "en"
|
||||
path = "/blog/**"
|
||||
|
||||
[[cascade]]
|
||||
background = "goldenbridge.jpg"
|
||||
[cascade._target]
|
||||
kind = "section"
|
||||
`)
|
||||
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,284 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
stderr() {
|
||||
echo "$@" 1>&2
|
||||
}
|
||||
|
||||
usage() {
|
||||
b=$(basename "$0")
|
||||
echo $b: ERROR: "$@" 1>&2
|
||||
|
||||
cat 1>&2 <<EOF
|
||||
|
||||
DESCRIPTION
|
||||
|
||||
$(basename "$0") is the script to run continuous integration commands for
|
||||
go-toml on unix.
|
||||
|
||||
Requires Go and Git to be available in the PATH. Expects to be ran from the
|
||||
root of go-toml's Git repository.
|
||||
|
||||
USAGE
|
||||
|
||||
$b COMMAND [OPTIONS...]
|
||||
|
||||
COMMANDS
|
||||
|
||||
benchmark [OPTIONS...] [BRANCH]
|
||||
|
||||
Run benchmarks.
|
||||
|
||||
ARGUMENTS
|
||||
|
||||
BRANCH Optional. Defines which Git branch to use when running
|
||||
benchmarks.
|
||||
|
||||
OPTIONS
|
||||
|
||||
-d Compare benchmarks of HEAD with BRANCH using benchstats. In
|
||||
this form the BRANCH argument is required.
|
||||
|
||||
-a Compare benchmarks of HEAD against go-toml v1 and
|
||||
BurntSushi/toml.
|
||||
|
||||
-html When used with -a, emits the output as HTML, ready to be
|
||||
embedded in the README.
|
||||
|
||||
coverage [OPTIONS...] [BRANCH]
|
||||
|
||||
Generates code coverage.
|
||||
|
||||
ARGUMENTS
|
||||
|
||||
BRANCH Optional. Defines which Git branch to use when reporting
|
||||
coverage. Defaults to HEAD.
|
||||
|
||||
OPTIONS
|
||||
|
||||
-d Compare coverage of HEAD with the one of BRANCH. In this form,
|
||||
the BRANCH argument is required. Exit code is non-zero when
|
||||
coverage percentage decreased.
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
cover() {
|
||||
branch="${1}"
|
||||
dir="$(mktemp -d)"
|
||||
|
||||
stderr "Executing coverage for ${branch} at ${dir}"
|
||||
|
||||
if [ "${branch}" = "HEAD" ]; then
|
||||
cp -r . "${dir}/"
|
||||
else
|
||||
git worktree add "$dir" "$branch"
|
||||
fi
|
||||
|
||||
pushd "$dir"
|
||||
go test -covermode=atomic -coverpkg=./... -coverprofile=coverage.out.tmp ./...
|
||||
grep -Ev '(fuzz|testsuite|tomltestgen|gotoml-test-decoder|gotoml-test-encoder)' coverage.out.tmp > coverage.out
|
||||
go tool cover -func=coverage.out
|
||||
echo "Coverage profile for ${branch}: ${dir}/coverage.out" >&2
|
||||
popd
|
||||
|
||||
if [ "${branch}" != "HEAD" ]; then
|
||||
git worktree remove --force "$dir"
|
||||
fi
|
||||
}
|
||||
|
||||
coverage() {
|
||||
case "$1" in
|
||||
-d)
|
||||
shift
|
||||
target="${1?Need to provide a target branch argument}"
|
||||
|
||||
output_dir="$(mktemp -d)"
|
||||
target_out="${output_dir}/target.txt"
|
||||
head_out="${output_dir}/head.txt"
|
||||
|
||||
cover "${target}" > "${target_out}"
|
||||
cover "HEAD" > "${head_out}"
|
||||
|
||||
cat "${target_out}"
|
||||
cat "${head_out}"
|
||||
|
||||
echo ""
|
||||
|
||||
target_pct="$(tail -n2 ${target_out} | head -n1 | sed -E 's/.*total.*\t([0-9.]+)%.*/\1/')"
|
||||
head_pct="$(tail -n2 ${head_out} | head -n1 | sed -E 's/.*total.*\t([0-9.]+)%/\1/')"
|
||||
echo "Results: ${target} ${target_pct}% HEAD ${head_pct}%"
|
||||
|
||||
delta_pct=$(echo "$head_pct - $target_pct" | bc -l)
|
||||
echo "Delta: ${delta_pct}"
|
||||
|
||||
if [[ $delta_pct = \-* ]]; then
|
||||
echo "Regression!";
|
||||
|
||||
target_diff="${output_dir}/target.diff.txt"
|
||||
head_diff="${output_dir}/head.diff.txt"
|
||||
cat "${target_out}" | grep -E '^github.com/pelletier/go-toml' | tr -s "\t " | cut -f 2,3 | sort > "${target_diff}"
|
||||
cat "${head_out}" | grep -E '^github.com/pelletier/go-toml' | tr -s "\t " | cut -f 2,3 | sort > "${head_diff}"
|
||||
|
||||
diff --side-by-side --suppress-common-lines "${target_diff}" "${head_diff}"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
cover "${1-HEAD}"
|
||||
}
|
||||
|
||||
bench() {
|
||||
branch="${1}"
|
||||
out="${2}"
|
||||
replace="${3}"
|
||||
dir="$(mktemp -d)"
|
||||
|
||||
stderr "Executing benchmark for ${branch} at ${dir}"
|
||||
|
||||
if [ "${branch}" = "HEAD" ]; then
|
||||
cp -r . "${dir}/"
|
||||
else
|
||||
git worktree add "$dir" "$branch"
|
||||
fi
|
||||
|
||||
pushd "$dir"
|
||||
|
||||
if [ "${replace}" != "" ]; then
|
||||
find ./benchmark/ -iname '*.go' -exec sed -i -E "s|github.com/pelletier/go-toml/v2|${replace}|g" {} \;
|
||||
go get "${replace}"
|
||||
fi
|
||||
|
||||
export GOMAXPROCS=2
|
||||
go test '-bench=^Benchmark(Un)?[mM]arshal' -count=10 -run=Nothing ./... | tee "${out}"
|
||||
popd
|
||||
|
||||
if [ "${branch}" != "HEAD" ]; then
|
||||
git worktree remove --force "$dir"
|
||||
fi
|
||||
}
|
||||
|
||||
fmktemp() {
|
||||
if mktemp --version &> /dev/null; then
|
||||
# GNU
|
||||
mktemp --suffix=-$1
|
||||
else
|
||||
# BSD
|
||||
mktemp -t $1
|
||||
fi
|
||||
}
|
||||
|
||||
benchstathtml() {
|
||||
python3 - $1 <<'EOF'
|
||||
import sys
|
||||
|
||||
lines = []
|
||||
stop = False
|
||||
|
||||
with open(sys.argv[1]) as f:
|
||||
for line in f.readlines():
|
||||
line = line.strip()
|
||||
if line == "":
|
||||
stop = True
|
||||
if not stop:
|
||||
lines.append(line.split(','))
|
||||
|
||||
results = []
|
||||
for line in reversed(lines[2:]):
|
||||
if len(line) < 8 or line[0] == "":
|
||||
continue
|
||||
v2 = float(line[1])
|
||||
results.append([
|
||||
line[0].replace("-32", ""),
|
||||
"%.1fx" % (float(line[3])/v2), # v1
|
||||
"%.1fx" % (float(line[7])/v2), # bs
|
||||
])
|
||||
# move geomean to the end
|
||||
results.append(results[0])
|
||||
del results[0]
|
||||
|
||||
|
||||
def printtable(data):
|
||||
print("""
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
|
||||
</thead>
|
||||
<tbody>""")
|
||||
|
||||
for r in data:
|
||||
print(" <tr><td>{}</td><td>{}</td><td>{}</td></tr>".format(*r))
|
||||
|
||||
print(""" </tbody>
|
||||
</table>""")
|
||||
|
||||
|
||||
def match(x):
|
||||
return "ReferenceFile" in x[0] or "HugoFrontMatter" in x[0]
|
||||
|
||||
above = [x for x in results if match(x)]
|
||||
below = [x for x in results if not match(x)]
|
||||
|
||||
printtable(above)
|
||||
print("<details><summary>See more</summary>")
|
||||
print("""<p>The table above has the results of the most common use-cases. The table below
|
||||
contains the results of all benchmarks, including unrealistic ones. It is
|
||||
provided for completeness.</p>""")
|
||||
printtable(below)
|
||||
print('<p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p>')
|
||||
print("</details>")
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
benchmark() {
|
||||
case "$1" in
|
||||
-d)
|
||||
shift
|
||||
target="${1?Need to provide a target branch argument}"
|
||||
|
||||
old=`fmktemp ${target}`
|
||||
bench "${target}" "${old}"
|
||||
|
||||
new=`fmktemp HEAD`
|
||||
bench HEAD "${new}"
|
||||
|
||||
benchstat "${old}" "${new}"
|
||||
return 0
|
||||
;;
|
||||
-a)
|
||||
shift
|
||||
|
||||
v2stats=`fmktemp go-toml-v2`
|
||||
bench HEAD "${v2stats}" "github.com/pelletier/go-toml/v2"
|
||||
v1stats=`fmktemp go-toml-v1`
|
||||
bench HEAD "${v1stats}" "github.com/pelletier/go-toml"
|
||||
bsstats=`fmktemp bs-toml`
|
||||
bench HEAD "${bsstats}" "github.com/BurntSushi/toml"
|
||||
|
||||
cp "${v2stats}" go-toml-v2.txt
|
||||
cp "${v1stats}" go-toml-v1.txt
|
||||
cp "${bsstats}" bs-toml.txt
|
||||
|
||||
if [ "$1" = "-html" ]; then
|
||||
tmpcsv=`fmktemp csv`
|
||||
benchstat -format csv go-toml-v2.txt go-toml-v1.txt bs-toml.txt > $tmpcsv
|
||||
benchstathtml $tmpcsv
|
||||
else
|
||||
benchstat go-toml-v2.txt go-toml-v1.txt bs-toml.txt
|
||||
fi
|
||||
|
||||
rm -f go-toml-v2.txt go-toml-v1.txt bs-toml.txt
|
||||
return $?
|
||||
esac
|
||||
|
||||
bench "${1-HEAD}" `mktemp`
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
coverage) shift; coverage $@;;
|
||||
benchmark) shift; benchmark $@;;
|
||||
*) usage "bad argument $1";;
|
||||
esac
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/bin/bash
|
||||
# fail out of the script if anything here fails
|
||||
set -e
|
||||
|
||||
# clear out stuff generated by test.sh
|
||||
rm -rf src test_program_bin toml-test
|
||||
@@ -0,0 +1,31 @@
|
||||
// Package gotoml-test-decoder is a minimal decoder program used to compare this library with other TOML implementations.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/internal/testsuite"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
if flag.NArg() != 0 {
|
||||
flag.Usage()
|
||||
}
|
||||
|
||||
err := testsuite.DecodeStdin()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
log.Printf("Usage: %s < toml-file\n", path.Base(os.Args[0]))
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Package gotoml-test-encoder is a minimal encoder program used to compare this library with other TOML implementations.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/internal/testsuite"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
if flag.NArg() != 0 {
|
||||
flag.Usage()
|
||||
}
|
||||
|
||||
err := testsuite.EncodeStdin()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
log.Printf("Usage: %s < json-file\n", path.Base(os.Args[0]))
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Package jsontoml is a program that converts JSON to TOML.
|
||||
//
|
||||
// # Usage
|
||||
//
|
||||
// Reading from stdin:
|
||||
//
|
||||
// cat file.json | jsontoml > file.toml
|
||||
//
|
||||
// Reading from a file:
|
||||
//
|
||||
// jsontoml file.json > file.toml
|
||||
//
|
||||
// # Installation
|
||||
//
|
||||
// Using Go:
|
||||
//
|
||||
// go install github.com/pelletier/go-toml/v2/cmd/jsontoml@latest
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"io"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/pelletier/go-toml/v2/internal/cli"
|
||||
)
|
||||
|
||||
const usage = `jsontoml can be used in two ways:
|
||||
Reading from stdin:
|
||||
cat file.json | jsontoml > file.toml
|
||||
|
||||
Reading from a file:
|
||||
jsontoml file.json > file.toml
|
||||
`
|
||||
|
||||
var useJSONNumber bool
|
||||
|
||||
func main() {
|
||||
flag.BoolVar(&useJSONNumber, "use-json-number", false, "unmarshal numbers into `json.Number` type instead of as `float64`")
|
||||
|
||||
p := cli.Program{
|
||||
Usage: usage,
|
||||
Fn: convert,
|
||||
}
|
||||
p.Execute()
|
||||
}
|
||||
|
||||
func convert(r io.Reader, w io.Writer) error {
|
||||
var v interface{}
|
||||
|
||||
d := json.NewDecoder(r)
|
||||
e := toml.NewEncoder(w)
|
||||
|
||||
if useJSONNumber {
|
||||
d.UseNumber()
|
||||
e.SetMarshalJSONNumbers(true)
|
||||
}
|
||||
|
||||
err := d.Decode(&v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.Encode(v)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestConvert(t *testing.T) {
|
||||
examples := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
errors bool
|
||||
useJSONNumber bool
|
||||
}{
|
||||
{
|
||||
name: "valid json",
|
||||
input: `
|
||||
{
|
||||
"mytoml": {
|
||||
"a": 42
|
||||
}
|
||||
}`,
|
||||
expected: `[mytoml]
|
||||
a = 42.0
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "use json number",
|
||||
useJSONNumber: true,
|
||||
input: `
|
||||
{
|
||||
"mytoml": {
|
||||
"a": 42
|
||||
}
|
||||
}`,
|
||||
expected: `[mytoml]
|
||||
a = 42
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "invalid json",
|
||||
input: `{ foo`,
|
||||
errors: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
b := new(bytes.Buffer)
|
||||
useJSONNumber = e.useJSONNumber
|
||||
err := convert(strings.NewReader(e.input), b)
|
||||
if e.errors {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, e.expected, b.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/pelletier/go-toml"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
bytes, err := ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
os.Exit(2)
|
||||
}
|
||||
tree, err := toml.Load(string(bytes))
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
typedTree := translate((map[string]interface{})(*tree))
|
||||
|
||||
if err := json.NewEncoder(os.Stdout).Encode(typedTree); err != nil {
|
||||
log.Fatalf("Error encoding JSON: %s", err)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func translate(tomlData interface{}) interface{} {
|
||||
|
||||
switch orig := tomlData.(type) {
|
||||
case map[string]interface{}:
|
||||
typed := make(map[string]interface{}, len(orig))
|
||||
for k, v := range orig {
|
||||
typed[k] = translate(v)
|
||||
}
|
||||
return typed
|
||||
case *toml.TomlTree:
|
||||
return translate((map[string]interface{})(*orig))
|
||||
case []*toml.TomlTree:
|
||||
typed := make([]map[string]interface{}, len(orig))
|
||||
for i, v := range orig {
|
||||
typed[i] = translate(v).(map[string]interface{})
|
||||
}
|
||||
return typed
|
||||
case []map[string]interface{}:
|
||||
typed := make([]map[string]interface{}, len(orig))
|
||||
for i, v := range orig {
|
||||
typed[i] = translate(v).(map[string]interface{})
|
||||
}
|
||||
return typed
|
||||
case []interface{}:
|
||||
typed := make([]interface{}, len(orig))
|
||||
for i, v := range orig {
|
||||
typed[i] = translate(v)
|
||||
}
|
||||
return tag("array", typed)
|
||||
case time.Time:
|
||||
return tag("datetime", orig.Format("2006-01-02T15:04:05Z"))
|
||||
case bool:
|
||||
return tag("bool", fmt.Sprintf("%v", orig))
|
||||
case int64:
|
||||
return tag("integer", fmt.Sprintf("%d", orig))
|
||||
case float64:
|
||||
return tag("float", fmt.Sprintf("%v", orig))
|
||||
case string:
|
||||
return tag("string", orig)
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("Unknown type: %T", tomlData))
|
||||
}
|
||||
|
||||
func tag(typeName string, data interface{}) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"type": typeName,
|
||||
"value": data,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Package tomljson is a program that converts TOML to JSON.
|
||||
//
|
||||
// # Usage
|
||||
//
|
||||
// Reading from stdin:
|
||||
//
|
||||
// cat file.toml | tomljson > file.json
|
||||
//
|
||||
// Reading from a file:
|
||||
//
|
||||
// tomljson file.toml > file.json
|
||||
//
|
||||
// # Installation
|
||||
//
|
||||
// Using Go:
|
||||
//
|
||||
// go install github.com/pelletier/go-toml/v2/cmd/tomljson@latest
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/pelletier/go-toml/v2/internal/cli"
|
||||
)
|
||||
|
||||
const usage = `tomljson can be used in two ways:
|
||||
Reading from stdin:
|
||||
cat file.toml | tomljson > file.json
|
||||
|
||||
Reading from a file:
|
||||
tomljson file.toml > file.json
|
||||
`
|
||||
|
||||
func main() {
|
||||
p := cli.Program{
|
||||
Usage: usage,
|
||||
Fn: convert,
|
||||
}
|
||||
p.Execute()
|
||||
}
|
||||
|
||||
func convert(r io.Reader, w io.Writer) error {
|
||||
var v interface{}
|
||||
|
||||
d := toml.NewDecoder(r)
|
||||
err := d.Decode(&v)
|
||||
if err != nil {
|
||||
var derr *toml.DecodeError
|
||||
if errors.As(err, &derr) {
|
||||
row, col := derr.Position()
|
||||
return fmt.Errorf("%s\nerror occurred at row %d column %d", derr.String(), row, col)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
e := json.NewEncoder(w)
|
||||
e.SetIndent("", " ")
|
||||
return e.Encode(v)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestConvert(t *testing.T) {
|
||||
examples := []struct {
|
||||
name string
|
||||
input io.Reader
|
||||
expected string
|
||||
errors bool
|
||||
}{
|
||||
{
|
||||
name: "valid toml",
|
||||
input: strings.NewReader(`
|
||||
[mytoml]
|
||||
a = 42`),
|
||||
expected: `{
|
||||
"mytoml": {
|
||||
"a": 42
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "invalid toml",
|
||||
input: strings.NewReader(`bad = []]`),
|
||||
errors: true,
|
||||
},
|
||||
{
|
||||
name: "bad reader",
|
||||
input: &badReader{},
|
||||
errors: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
b := new(bytes.Buffer)
|
||||
err := convert(e.input, b)
|
||||
if e.errors {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, e.expected, b.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type badReader struct{}
|
||||
|
||||
func (r *badReader) Read([]byte) (int, error) {
|
||||
return 0, errors.New("reader failed on purpose")
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Package tomll is a linter program for TOML.
|
||||
//
|
||||
// # Usage
|
||||
//
|
||||
// Reading from stdin, writing to stdout:
|
||||
//
|
||||
// cat file.toml | tomll
|
||||
//
|
||||
// Reading and updating a list of files in place:
|
||||
//
|
||||
// tomll a.toml b.toml c.toml
|
||||
//
|
||||
// # Installation
|
||||
//
|
||||
// Using Go:
|
||||
//
|
||||
// go install github.com/pelletier/go-toml/v2/cmd/tomll@latest
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/pelletier/go-toml/v2/internal/cli"
|
||||
)
|
||||
|
||||
const usage = `tomll can be used in two ways:
|
||||
|
||||
Reading from stdin, writing to stdout:
|
||||
cat file.toml | tomll > file.toml
|
||||
|
||||
Reading and updating a list of files in place:
|
||||
tomll a.toml b.toml c.toml
|
||||
|
||||
When given a list of files, tomll will modify all files in place without asking.
|
||||
`
|
||||
|
||||
func main() {
|
||||
p := cli.Program{
|
||||
Usage: usage,
|
||||
Fn: convert,
|
||||
Inplace: true,
|
||||
}
|
||||
p.Execute()
|
||||
}
|
||||
|
||||
func convert(r io.Reader, w io.Writer) error {
|
||||
var v interface{}
|
||||
|
||||
d := toml.NewDecoder(r)
|
||||
err := d.Decode(&v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e := toml.NewEncoder(w)
|
||||
return e.Encode(v)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestConvert(t *testing.T) {
|
||||
examples := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
errors bool
|
||||
}{
|
||||
{
|
||||
name: "valid toml",
|
||||
input: `
|
||||
mytoml.a = 42.0
|
||||
`,
|
||||
expected: `[mytoml]
|
||||
a = 42.0
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "invalid toml",
|
||||
input: `[what`,
|
||||
errors: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
b := new(bytes.Buffer)
|
||||
err := convert(strings.NewReader(e.input), b)
|
||||
if e.errors {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, e.expected, b.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// tomltestgen retrieves a given version of the language-agnostic TOML test suite in
|
||||
// https://github.com/BurntSushi/toml-test and generates go-toml unit tests.
|
||||
//
|
||||
// Within the go-toml package, run `go generate`. Otherwise, use:
|
||||
//
|
||||
// go run github.com/pelletier/go-toml/cmd/tomltestgen -o toml_testgen_test.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type invalid struct {
|
||||
Name string
|
||||
Input string
|
||||
}
|
||||
|
||||
type valid struct {
|
||||
Name string
|
||||
Input string
|
||||
JSONRef string
|
||||
}
|
||||
|
||||
type testsCollection struct {
|
||||
Ref string
|
||||
Timestamp string
|
||||
Invalid []invalid
|
||||
Valid []valid
|
||||
Count int
|
||||
}
|
||||
|
||||
const srcTemplate = "// Code generated by tomltestgen for toml-test ref {{.Ref}} on {{.Timestamp}}. DO NOT EDIT.\n" +
|
||||
"package toml_test\n" +
|
||||
" import (\n" +
|
||||
" \"testing\"\n" +
|
||||
")\n" +
|
||||
"{{range .Invalid}}\n" +
|
||||
"func TestTOMLTest_Invalid_{{.Name}}(t *testing.T) {\n" +
|
||||
" input := {{.Input|gostr}}\n" +
|
||||
" testgenInvalid(t, input)\n" +
|
||||
"}\n" +
|
||||
"{{end}}\n" +
|
||||
"\n" +
|
||||
"{{range .Valid}}\n" +
|
||||
"func TestTOMLTest_Valid_{{.Name}}(t *testing.T) {\n" +
|
||||
" input := {{.Input|gostr}}\n" +
|
||||
" jsonRef := {{.JSONRef|gostr}}\n" +
|
||||
" testgenValid(t, input, jsonRef)\n" +
|
||||
"}\n" +
|
||||
"{{end}}\n"
|
||||
|
||||
func kebabToCamel(kebab string) string {
|
||||
var buf strings.Builder
|
||||
nextUpper := true
|
||||
for _, c := range kebab {
|
||||
if nextUpper {
|
||||
buf.WriteRune(unicode.ToUpper(c))
|
||||
nextUpper = false
|
||||
} else {
|
||||
switch c {
|
||||
case '-':
|
||||
nextUpper = true
|
||||
case '/':
|
||||
nextUpper = true
|
||||
buf.WriteByte('_')
|
||||
default:
|
||||
buf.WriteRune(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func templateGoStr(input string) string {
|
||||
return strconv.Quote(input)
|
||||
}
|
||||
|
||||
var (
|
||||
ref = flag.String("r", "master", "git reference")
|
||||
out = flag.String("o", "", "output file")
|
||||
)
|
||||
|
||||
func usage() {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "usage: tomltestgen [flags]\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
collection := testsCollection{
|
||||
Ref: *ref,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
dirContent, _ := filepath.Glob("tests/invalid/**/*.toml")
|
||||
for _, f := range dirContent {
|
||||
filename := strings.TrimPrefix(f, "tests/valid/")
|
||||
name := kebabToCamel(strings.TrimSuffix(filename, ".toml"))
|
||||
name = strings.ReplaceAll(name, ".", "_")
|
||||
|
||||
log.Printf("> [%s] %s\n", "invalid", name)
|
||||
|
||||
tomlContent, err := os.ReadFile(f) // #nosec G304
|
||||
if err != nil {
|
||||
fmt.Printf("failed to read test file: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
collection.Invalid = append(collection.Invalid, invalid{
|
||||
Name: name,
|
||||
Input: string(tomlContent),
|
||||
})
|
||||
collection.Count++
|
||||
}
|
||||
|
||||
dirContent, _ = filepath.Glob("tests/valid/**/*.toml")
|
||||
for _, f := range dirContent {
|
||||
filename := strings.TrimPrefix(f, "tests/valid/")
|
||||
name := kebabToCamel(strings.TrimSuffix(filename, ".toml"))
|
||||
name = strings.ReplaceAll(name, ".", "_")
|
||||
|
||||
log.Printf("> [%s] %s\n", "valid", name)
|
||||
|
||||
tomlContent, err := os.ReadFile(f) // #nosec G304
|
||||
if err != nil {
|
||||
fmt.Printf("failed reading test file: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
filename = strings.TrimSuffix(f, ".toml")
|
||||
jsonContent, err := os.ReadFile(filename + ".json") // #nosec G304
|
||||
if err != nil {
|
||||
fmt.Printf("failed reading validation json: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
collection.Valid = append(collection.Valid, valid{
|
||||
Name: name,
|
||||
Input: string(tomlContent),
|
||||
JSONRef: string(jsonContent),
|
||||
})
|
||||
collection.Count++
|
||||
}
|
||||
|
||||
log.Printf("Collected %d tests from toml-test\n", collection.Count)
|
||||
|
||||
funcMap := template.FuncMap{
|
||||
"gostr": templateGoStr,
|
||||
}
|
||||
t := template.Must(template.New("src").Funcs(funcMap).Parse(srcTemplate))
|
||||
buf := new(bytes.Buffer)
|
||||
err := t.Execute(buf, collection)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
outputBytes, err := format.Source(buf.Bytes())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if *out == "" {
|
||||
fmt.Println(string(outputBytes))
|
||||
return
|
||||
}
|
||||
|
||||
err = os.WriteFile(*out, outputBytes, 0o600)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,549 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/unstable"
|
||||
)
|
||||
|
||||
func parseInteger(b []byte) (int64, error) {
|
||||
if len(b) > 2 && b[0] == '0' {
|
||||
switch b[1] {
|
||||
case 'x':
|
||||
return parseIntHex(b)
|
||||
case 'b':
|
||||
return parseIntBin(b)
|
||||
case 'o':
|
||||
return parseIntOct(b)
|
||||
default:
|
||||
panic(fmt.Errorf("invalid base '%c', should have been checked by scanIntOrFloat", b[1]))
|
||||
}
|
||||
}
|
||||
|
||||
return parseIntDec(b)
|
||||
}
|
||||
|
||||
func parseLocalDate(b []byte) (LocalDate, error) {
|
||||
// full-date = date-fullyear "-" date-month "-" date-mday
|
||||
// date-fullyear = 4DIGIT
|
||||
// date-month = 2DIGIT ; 01-12
|
||||
// date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year
|
||||
var date LocalDate
|
||||
|
||||
if len(b) != 10 || b[4] != '-' || b[7] != '-' {
|
||||
return date, unstable.NewParserError(b, "dates are expected to have the format YYYY-MM-DD")
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
date.Year, err = parseDecimalDigits(b[0:4])
|
||||
if err != nil {
|
||||
return LocalDate{}, err
|
||||
}
|
||||
|
||||
date.Month, err = parseDecimalDigits(b[5:7])
|
||||
if err != nil {
|
||||
return LocalDate{}, err
|
||||
}
|
||||
|
||||
date.Day, err = parseDecimalDigits(b[8:10])
|
||||
if err != nil {
|
||||
return LocalDate{}, err
|
||||
}
|
||||
|
||||
if !isValidDate(date.Year, date.Month, date.Day) {
|
||||
return LocalDate{}, unstable.NewParserError(b, "impossible date")
|
||||
}
|
||||
|
||||
return date, nil
|
||||
}
|
||||
|
||||
func parseDecimalDigits(b []byte) (int, error) {
|
||||
v := 0
|
||||
|
||||
for i, c := range b {
|
||||
if c < '0' || c > '9' {
|
||||
return 0, unstable.NewParserError(b[i:i+1], "expected digit (0-9)")
|
||||
}
|
||||
v *= 10
|
||||
v += int(c - '0')
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func parseDateTime(b []byte) (time.Time, error) {
|
||||
// offset-date-time = full-date time-delim full-time
|
||||
// full-time = partial-time time-offset
|
||||
// time-offset = "Z" / time-numoffset
|
||||
// time-numoffset = ( "+" / "-" ) time-hour ":" time-minute
|
||||
|
||||
dt, b, err := parseLocalDateTime(b)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
var zone *time.Location
|
||||
|
||||
if len(b) == 0 {
|
||||
// parser should have checked that when assigning the date time node
|
||||
panic("date time should have a timezone")
|
||||
}
|
||||
|
||||
if b[0] == 'Z' || b[0] == 'z' {
|
||||
b = b[1:]
|
||||
zone = time.UTC
|
||||
} else {
|
||||
const dateTimeByteLen = 6
|
||||
if len(b) != dateTimeByteLen {
|
||||
return time.Time{}, unstable.NewParserError(b, "invalid date-time timezone")
|
||||
}
|
||||
var direction int
|
||||
switch b[0] {
|
||||
case '-':
|
||||
direction = -1
|
||||
case '+':
|
||||
direction = +1
|
||||
default:
|
||||
return time.Time{}, unstable.NewParserError(b[:1], "invalid timezone offset character")
|
||||
}
|
||||
|
||||
if b[3] != ':' {
|
||||
return time.Time{}, unstable.NewParserError(b[3:4], "expected a : separator")
|
||||
}
|
||||
|
||||
hours, err := parseDecimalDigits(b[1:3])
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
if hours > 23 {
|
||||
return time.Time{}, unstable.NewParserError(b[:1], "invalid timezone offset hours")
|
||||
}
|
||||
|
||||
minutes, err := parseDecimalDigits(b[4:6])
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
if minutes > 59 {
|
||||
return time.Time{}, unstable.NewParserError(b[:1], "invalid timezone offset minutes")
|
||||
}
|
||||
|
||||
seconds := direction * (hours*3600 + minutes*60)
|
||||
if seconds == 0 {
|
||||
zone = time.UTC
|
||||
} else {
|
||||
zone = time.FixedZone("", seconds)
|
||||
}
|
||||
b = b[dateTimeByteLen:]
|
||||
}
|
||||
|
||||
if len(b) > 0 {
|
||||
return time.Time{}, unstable.NewParserError(b, "extra bytes at the end of the timezone")
|
||||
}
|
||||
|
||||
t := time.Date(
|
||||
dt.Year,
|
||||
time.Month(dt.Month),
|
||||
dt.Day,
|
||||
dt.Hour,
|
||||
dt.Minute,
|
||||
dt.Second,
|
||||
dt.Nanosecond,
|
||||
zone)
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) {
|
||||
var dt LocalDateTime
|
||||
|
||||
const localDateTimeByteMinLen = 11
|
||||
if len(b) < localDateTimeByteMinLen {
|
||||
return dt, nil, unstable.NewParserError(b, "local datetimes are expected to have the format YYYY-MM-DDTHH:MM:SS[.NNNNNNNNN]")
|
||||
}
|
||||
|
||||
date, err := parseLocalDate(b[:10])
|
||||
if err != nil {
|
||||
return dt, nil, err
|
||||
}
|
||||
dt.LocalDate = date
|
||||
|
||||
sep := b[10]
|
||||
if sep != 'T' && sep != ' ' && sep != 't' {
|
||||
return dt, nil, unstable.NewParserError(b[10:11], "datetime separator is expected to be T or a space")
|
||||
}
|
||||
|
||||
t, rest, err := parseLocalTime(b[11:])
|
||||
if err != nil {
|
||||
return dt, nil, err
|
||||
}
|
||||
dt.LocalTime = t
|
||||
|
||||
return dt, rest, nil
|
||||
}
|
||||
|
||||
// parseLocalTime is a bit different because it also returns the remaining
|
||||
// []byte that is didn't need. This is to allow parseDateTime to parse those
|
||||
// remaining bytes as a timezone.
|
||||
func parseLocalTime(b []byte) (LocalTime, []byte, error) {
|
||||
var (
|
||||
nspow = [10]int{0, 1e8, 1e7, 1e6, 1e5, 1e4, 1e3, 1e2, 1e1, 1e0}
|
||||
t LocalTime
|
||||
)
|
||||
|
||||
// check if b matches to have expected format HH:MM:SS[.NNNNNN]
|
||||
const localTimeByteLen = 8
|
||||
if len(b) < localTimeByteLen {
|
||||
return t, nil, unstable.NewParserError(b, "times are expected to have the format HH:MM:SS[.NNNNNN]")
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
t.Hour, err = parseDecimalDigits(b[0:2])
|
||||
if err != nil {
|
||||
return t, nil, err
|
||||
}
|
||||
|
||||
if t.Hour > 23 {
|
||||
return t, nil, unstable.NewParserError(b[0:2], "hour cannot be greater 23")
|
||||
}
|
||||
if b[2] != ':' {
|
||||
return t, nil, unstable.NewParserError(b[2:3], "expecting colon between hours and minutes")
|
||||
}
|
||||
|
||||
t.Minute, err = parseDecimalDigits(b[3:5])
|
||||
if err != nil {
|
||||
return t, nil, err
|
||||
}
|
||||
if t.Minute > 59 {
|
||||
return t, nil, unstable.NewParserError(b[3:5], "minutes cannot be greater 59")
|
||||
}
|
||||
if b[5] != ':' {
|
||||
return t, nil, unstable.NewParserError(b[5:6], "expecting colon between minutes and seconds")
|
||||
}
|
||||
|
||||
t.Second, err = parseDecimalDigits(b[6:8])
|
||||
if err != nil {
|
||||
return t, nil, err
|
||||
}
|
||||
|
||||
if t.Second > 59 {
|
||||
return t, nil, unstable.NewParserError(b[6:8], "seconds cannot be greater than 59")
|
||||
}
|
||||
|
||||
b = b[8:]
|
||||
|
||||
if len(b) >= 1 && b[0] == '.' {
|
||||
frac := 0
|
||||
precision := 0
|
||||
digits := 0
|
||||
|
||||
for i, c := range b[1:] {
|
||||
if !isDigit(c) {
|
||||
if i == 0 {
|
||||
return t, nil, unstable.NewParserError(b[0:1], "need at least one digit after fraction point")
|
||||
}
|
||||
break
|
||||
}
|
||||
digits++
|
||||
|
||||
const maxFracPrecision = 9
|
||||
if i >= maxFracPrecision {
|
||||
// go-toml allows decoding fractional seconds
|
||||
// beyond the supported precision of 9
|
||||
// digits. It truncates the fractional component
|
||||
// to the supported precision and ignores the
|
||||
// remaining digits.
|
||||
//
|
||||
// https://github.com/pelletier/go-toml/discussions/707
|
||||
continue
|
||||
}
|
||||
|
||||
frac *= 10
|
||||
frac += int(c - '0')
|
||||
precision++
|
||||
}
|
||||
|
||||
if precision == 0 {
|
||||
return t, nil, unstable.NewParserError(b[:1], "nanoseconds need at least one digit")
|
||||
}
|
||||
|
||||
t.Nanosecond = frac * nspow[precision]
|
||||
t.Precision = precision
|
||||
|
||||
return t, b[1+digits:], nil
|
||||
}
|
||||
return t, b, nil
|
||||
}
|
||||
|
||||
func parseFloat(b []byte) (float64, error) {
|
||||
if len(b) == 4 && (b[0] == '+' || b[0] == '-') && b[1] == 'n' && b[2] == 'a' && b[3] == 'n' {
|
||||
return math.NaN(), nil
|
||||
}
|
||||
|
||||
cleaned, err := checkAndRemoveUnderscoresFloats(b)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if cleaned[0] == '.' {
|
||||
return 0, unstable.NewParserError(b, "float cannot start with a dot")
|
||||
}
|
||||
|
||||
if cleaned[len(cleaned)-1] == '.' {
|
||||
return 0, unstable.NewParserError(b, "float cannot end with a dot")
|
||||
}
|
||||
|
||||
dotAlreadySeen := false
|
||||
for i, c := range cleaned {
|
||||
if c == '.' {
|
||||
if dotAlreadySeen {
|
||||
return 0, unstable.NewParserError(b[i:i+1], "float can have at most one decimal point")
|
||||
}
|
||||
if !isDigit(cleaned[i-1]) {
|
||||
return 0, unstable.NewParserError(b[i-1:i+1], "float decimal point must be preceded by a digit")
|
||||
}
|
||||
if !isDigit(cleaned[i+1]) {
|
||||
return 0, unstable.NewParserError(b[i:i+2], "float decimal point must be followed by a digit")
|
||||
}
|
||||
dotAlreadySeen = true
|
||||
}
|
||||
}
|
||||
|
||||
start := 0
|
||||
if cleaned[0] == '+' || cleaned[0] == '-' {
|
||||
start = 1
|
||||
}
|
||||
if cleaned[start] == '0' && len(cleaned) > start+1 && isDigit(cleaned[start+1]) {
|
||||
return 0, unstable.NewParserError(b, "float integer part cannot have leading zeroes")
|
||||
}
|
||||
|
||||
f, err := strconv.ParseFloat(string(cleaned), 64)
|
||||
if err != nil {
|
||||
return 0, unstable.NewParserError(b, "unable to parse float: %w", err)
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func parseIntHex(b []byte) (int64, error) {
|
||||
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
i, err := strconv.ParseInt(string(cleaned), 16, 64)
|
||||
if err != nil {
|
||||
return 0, unstable.NewParserError(b, "couldn't parse hexadecimal number: %w", err)
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func parseIntOct(b []byte) (int64, error) {
|
||||
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
i, err := strconv.ParseInt(string(cleaned), 8, 64)
|
||||
if err != nil {
|
||||
return 0, unstable.NewParserError(b, "couldn't parse octal number: %w", err)
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func parseIntBin(b []byte) (int64, error) {
|
||||
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
i, err := strconv.ParseInt(string(cleaned), 2, 64)
|
||||
if err != nil {
|
||||
return 0, unstable.NewParserError(b, "couldn't parse binary number: %w", err)
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func isSign(b byte) bool {
|
||||
return b == '+' || b == '-'
|
||||
}
|
||||
|
||||
func parseIntDec(b []byte) (int64, error) {
|
||||
cleaned, err := checkAndRemoveUnderscoresIntegers(b)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
startIdx := 0
|
||||
|
||||
if isSign(cleaned[0]) {
|
||||
startIdx++
|
||||
}
|
||||
|
||||
if len(cleaned) > startIdx+1 && cleaned[startIdx] == '0' {
|
||||
return 0, unstable.NewParserError(b, "leading zero not allowed on decimal number")
|
||||
}
|
||||
|
||||
i, err := strconv.ParseInt(string(cleaned), 10, 64)
|
||||
if err != nil {
|
||||
return 0, unstable.NewParserError(b, "couldn't parse decimal number: %w", err)
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func checkAndRemoveUnderscoresIntegers(b []byte) ([]byte, error) {
|
||||
start := 0
|
||||
if b[start] == '+' || b[start] == '-' {
|
||||
start++
|
||||
}
|
||||
|
||||
if len(b) == start {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
if b[start] == '_' {
|
||||
return nil, unstable.NewParserError(b[start:start+1], "number cannot start with underscore")
|
||||
}
|
||||
|
||||
if b[len(b)-1] == '_' {
|
||||
return nil, unstable.NewParserError(b[len(b)-1:], "number cannot end with underscore")
|
||||
}
|
||||
|
||||
// fast path
|
||||
i := 0
|
||||
for ; i < len(b); i++ {
|
||||
if b[i] == '_' {
|
||||
break
|
||||
}
|
||||
}
|
||||
if i == len(b) {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
before := false
|
||||
cleaned := make([]byte, i, len(b))
|
||||
copy(cleaned, b)
|
||||
|
||||
for i++; i < len(b); i++ {
|
||||
c := b[i]
|
||||
if c == '_' {
|
||||
if !before {
|
||||
return nil, unstable.NewParserError(b[i-1:i+1], "number must have at least one digit between underscores")
|
||||
}
|
||||
before = false
|
||||
} else {
|
||||
before = true
|
||||
cleaned = append(cleaned, c)
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned, nil
|
||||
}
|
||||
|
||||
func checkAndRemoveUnderscoresFloats(b []byte) ([]byte, error) {
|
||||
if b[0] == '_' {
|
||||
return nil, unstable.NewParserError(b[0:1], "number cannot start with underscore")
|
||||
}
|
||||
|
||||
if b[len(b)-1] == '_' {
|
||||
return nil, unstable.NewParserError(b[len(b)-1:], "number cannot end with underscore")
|
||||
}
|
||||
|
||||
// fast path
|
||||
i := 0
|
||||
for ; i < len(b); i++ {
|
||||
if b[i] == '_' {
|
||||
break
|
||||
}
|
||||
}
|
||||
if i == len(b) {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
before := false
|
||||
cleaned := make([]byte, 0, len(b))
|
||||
|
||||
for i := 0; i < len(b); i++ {
|
||||
c := b[i]
|
||||
|
||||
switch c {
|
||||
case '_':
|
||||
if !before {
|
||||
return nil, unstable.NewParserError(b[i-1:i+1], "number must have at least one digit between underscores")
|
||||
}
|
||||
if i < len(b)-1 && (b[i+1] == 'e' || b[i+1] == 'E') {
|
||||
return nil, unstable.NewParserError(b[i+1:i+2], "cannot have underscore before exponent")
|
||||
}
|
||||
before = false
|
||||
case '+', '-':
|
||||
// signed exponents
|
||||
cleaned = append(cleaned, c)
|
||||
before = false
|
||||
case 'e', 'E':
|
||||
if i < len(b)-1 && b[i+1] == '_' {
|
||||
return nil, unstable.NewParserError(b[i+1:i+2], "cannot have underscore after exponent")
|
||||
}
|
||||
cleaned = append(cleaned, c)
|
||||
case '.':
|
||||
if i < len(b)-1 && b[i+1] == '_' {
|
||||
return nil, unstable.NewParserError(b[i+1:i+2], "cannot have underscore after decimal point")
|
||||
}
|
||||
if i > 0 && b[i-1] == '_' {
|
||||
return nil, unstable.NewParserError(b[i-1:i], "cannot have underscore before decimal point")
|
||||
}
|
||||
cleaned = append(cleaned, c)
|
||||
default:
|
||||
before = true
|
||||
cleaned = append(cleaned, c)
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned, nil
|
||||
}
|
||||
|
||||
// isValidDate checks if a provided date is a date that exists.
|
||||
func isValidDate(year int, month int, day int) bool {
|
||||
return month > 0 && month < 13 && day > 0 && day <= daysIn(month, year)
|
||||
}
|
||||
|
||||
// daysBefore[m] counts the number of days in a non-leap year
|
||||
// before month m begins. There is an entry for m=12, counting
|
||||
// the number of days before January of next year (365).
|
||||
var daysBefore = [...]int32{
|
||||
0,
|
||||
31,
|
||||
31 + 28,
|
||||
31 + 28 + 31,
|
||||
31 + 28 + 31 + 30,
|
||||
31 + 28 + 31 + 30 + 31,
|
||||
31 + 28 + 31 + 30 + 31 + 30,
|
||||
31 + 28 + 31 + 30 + 31 + 30 + 31,
|
||||
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31,
|
||||
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30,
|
||||
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31,
|
||||
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30,
|
||||
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31,
|
||||
}
|
||||
|
||||
func daysIn(m int, year int) int {
|
||||
if m == 2 && isLeap(year) {
|
||||
return 29
|
||||
}
|
||||
return int(daysBefore[m] - daysBefore[m-1])
|
||||
}
|
||||
|
||||
func isLeap(year int) bool {
|
||||
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
|
||||
}
|
||||
|
||||
func isDigit(r byte) bool {
|
||||
return r >= '0' && r <= '9'
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package toml is a library to read and write TOML documents.
|
||||
package toml
|
||||
@@ -0,0 +1,283 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/unstable"
|
||||
)
|
||||
|
||||
// DecodeError represents an error encountered during the parsing or decoding
|
||||
// of a TOML document.
|
||||
//
|
||||
// In addition to the error message, it contains the position in the document
|
||||
// where it happened, as well as a human-readable representation that shows
|
||||
// where the error occurred in the document.
|
||||
type DecodeError struct {
|
||||
message string
|
||||
line int
|
||||
column int
|
||||
key Key
|
||||
|
||||
human string
|
||||
}
|
||||
|
||||
// StrictMissingError occurs in a TOML document that does not have a
|
||||
// corresponding field in the target value. It contains all the missing fields
|
||||
// in Errors.
|
||||
//
|
||||
// Emitted by Decoder when DisallowUnknownFields() was called.
|
||||
type StrictMissingError struct {
|
||||
// One error per field that could not be found.
|
||||
Errors []DecodeError
|
||||
}
|
||||
|
||||
// Error returns the canonical string for this error.
|
||||
func (s *StrictMissingError) Error() string {
|
||||
return "strict mode: fields in the document are missing in the target struct"
|
||||
}
|
||||
|
||||
// String returns a human readable description of all errors.
|
||||
func (s *StrictMissingError) String() string {
|
||||
var buf strings.Builder
|
||||
|
||||
for i, e := range s.Errors {
|
||||
if i > 0 {
|
||||
buf.WriteString("\n---\n")
|
||||
}
|
||||
|
||||
buf.WriteString(e.String())
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Unwrap returns wrapped decode errors
|
||||
//
|
||||
// Implements errors.Join() interface.
|
||||
func (s *StrictMissingError) Unwrap() []error {
|
||||
errs := make([]error, len(s.Errors))
|
||||
for i := range s.Errors {
|
||||
errs[i] = &s.Errors[i]
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// Key represents a TOML key as a sequence of key parts.
|
||||
type Key []string
|
||||
|
||||
// Error returns the error message contained in the DecodeError.
|
||||
func (e *DecodeError) Error() string {
|
||||
return "toml: " + e.message
|
||||
}
|
||||
|
||||
// String returns the human-readable contextualized error. This string is multi-line.
|
||||
func (e *DecodeError) String() string {
|
||||
return e.human
|
||||
}
|
||||
|
||||
// Position returns the (line, column) pair indicating where the error
|
||||
// occurred in the document. Positions are 1-indexed.
|
||||
func (e *DecodeError) Position() (row int, column int) {
|
||||
return e.line, e.column
|
||||
}
|
||||
|
||||
// Key that was being processed when the error occurred. The key is present only
|
||||
// if this DecodeError is part of a StrictMissingError.
|
||||
func (e *DecodeError) Key() Key {
|
||||
return e.key
|
||||
}
|
||||
|
||||
// wrapDecodeError creates a DecodeError referencing a highlighted
|
||||
// range of bytes from document.
|
||||
//
|
||||
// highlight needs to be a sub-slice of document, or this function panics.
|
||||
//
|
||||
// The function copies all bytes used in DecodeError, so that document and
|
||||
// highlight can be freely deallocated.
|
||||
//
|
||||
//nolint:funlen
|
||||
func wrapDecodeError(document []byte, de *unstable.ParserError) *DecodeError {
|
||||
offset := subsliceOffset(document, de.Highlight)
|
||||
|
||||
errMessage := de.Error()
|
||||
errLine, errColumn := positionAtEnd(document[:offset])
|
||||
before, after := linesOfContext(document, de.Highlight, offset, 3)
|
||||
|
||||
var buf strings.Builder
|
||||
|
||||
maxLine := errLine + len(after) - 1
|
||||
lineColumnWidth := len(strconv.Itoa(maxLine))
|
||||
|
||||
// Write the lines of context strictly before the error.
|
||||
for i := len(before) - 1; i > 0; i-- {
|
||||
line := errLine - i
|
||||
buf.WriteString(formatLineNumber(line, lineColumnWidth))
|
||||
buf.WriteString("|")
|
||||
|
||||
if len(before[i]) > 0 {
|
||||
buf.WriteString(" ")
|
||||
buf.Write(before[i])
|
||||
}
|
||||
|
||||
buf.WriteRune('\n')
|
||||
}
|
||||
|
||||
// Write the document line that contains the error.
|
||||
|
||||
buf.WriteString(formatLineNumber(errLine, lineColumnWidth))
|
||||
buf.WriteString("| ")
|
||||
|
||||
if len(before) > 0 {
|
||||
buf.Write(before[0])
|
||||
}
|
||||
|
||||
buf.Write(de.Highlight)
|
||||
|
||||
if len(after) > 0 {
|
||||
buf.Write(after[0])
|
||||
}
|
||||
|
||||
buf.WriteRune('\n')
|
||||
|
||||
// Write the line with the error message itself (so it does not have a line
|
||||
// number).
|
||||
|
||||
buf.WriteString(strings.Repeat(" ", lineColumnWidth))
|
||||
buf.WriteString("| ")
|
||||
|
||||
if len(before) > 0 {
|
||||
buf.WriteString(strings.Repeat(" ", len(before[0])))
|
||||
}
|
||||
|
||||
buf.WriteString(strings.Repeat("~", len(de.Highlight)))
|
||||
|
||||
if len(errMessage) > 0 {
|
||||
buf.WriteString(" ")
|
||||
buf.WriteString(errMessage)
|
||||
}
|
||||
|
||||
// Write the lines of context strictly after the error.
|
||||
|
||||
for i := 1; i < len(after); i++ {
|
||||
buf.WriteRune('\n')
|
||||
line := errLine + i
|
||||
buf.WriteString(formatLineNumber(line, lineColumnWidth))
|
||||
buf.WriteString("|")
|
||||
|
||||
if len(after[i]) > 0 {
|
||||
buf.WriteString(" ")
|
||||
buf.Write(after[i])
|
||||
}
|
||||
}
|
||||
|
||||
return &DecodeError{
|
||||
message: errMessage,
|
||||
line: errLine,
|
||||
column: errColumn,
|
||||
key: de.Key,
|
||||
human: buf.String(),
|
||||
}
|
||||
}
|
||||
|
||||
func formatLineNumber(line int, width int) string {
|
||||
format := "%" + strconv.Itoa(width) + "d"
|
||||
|
||||
return fmt.Sprintf(format, line)
|
||||
}
|
||||
|
||||
func linesOfContext(document []byte, highlight []byte, offset int, linesAround int) ([][]byte, [][]byte) {
|
||||
return beforeLines(document, offset, linesAround), afterLines(document, highlight, offset, linesAround)
|
||||
}
|
||||
|
||||
func beforeLines(document []byte, offset int, linesAround int) [][]byte {
|
||||
var beforeLines [][]byte
|
||||
|
||||
// Walk the document backward from the highlight to find previous lines
|
||||
// of context.
|
||||
rest := document[:offset]
|
||||
backward:
|
||||
for o := len(rest) - 1; o >= 0 && len(beforeLines) <= linesAround && len(rest) > 0; {
|
||||
switch {
|
||||
case rest[o] == '\n':
|
||||
// handle individual lines
|
||||
beforeLines = append(beforeLines, rest[o+1:])
|
||||
rest = rest[:o]
|
||||
o = len(rest) - 1
|
||||
case o == 0:
|
||||
// add the first line only if it's non-empty
|
||||
beforeLines = append(beforeLines, rest)
|
||||
|
||||
break backward
|
||||
default:
|
||||
o--
|
||||
}
|
||||
}
|
||||
|
||||
return beforeLines
|
||||
}
|
||||
|
||||
func afterLines(document []byte, highlight []byte, offset int, linesAround int) [][]byte {
|
||||
var afterLines [][]byte
|
||||
|
||||
// Walk the document forward from the highlight to find the following
|
||||
// lines of context.
|
||||
rest := document[offset+len(highlight):]
|
||||
forward:
|
||||
for o := 0; o < len(rest) && len(afterLines) <= linesAround; {
|
||||
switch {
|
||||
case rest[o] == '\n':
|
||||
// handle individual lines
|
||||
afterLines = append(afterLines, rest[:o])
|
||||
rest = rest[o+1:]
|
||||
o = 0
|
||||
|
||||
case o == len(rest)-1:
|
||||
// add last line only if it's non-empty
|
||||
afterLines = append(afterLines, rest)
|
||||
|
||||
break forward
|
||||
default:
|
||||
o++
|
||||
}
|
||||
}
|
||||
|
||||
return afterLines
|
||||
}
|
||||
|
||||
func positionAtEnd(b []byte) (row int, column int) {
|
||||
row = 1
|
||||
column = 1
|
||||
|
||||
for _, c := range b {
|
||||
if c == '\n' {
|
||||
row++
|
||||
column = 1
|
||||
} else {
|
||||
column++
|
||||
}
|
||||
}
|
||||
|
||||
return row, column
|
||||
}
|
||||
|
||||
// subsliceOffset returns the byte offset of subslice within data.
|
||||
// subslice must share the same backing array as data.
|
||||
func subsliceOffset(data []byte, subslice []byte) int {
|
||||
if len(subslice) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Use reflect to get the data pointers of both slices.
|
||||
// This is safe because we're only reading the pointer values for comparison.
|
||||
dataPtr := reflect.ValueOf(data).Pointer()
|
||||
subPtr := reflect.ValueOf(subslice).Pointer()
|
||||
|
||||
offset := int(subPtr - dataPtr)
|
||||
if offset < 0 || offset > len(data) {
|
||||
panic("subslice is not within data")
|
||||
}
|
||||
return offset
|
||||
}
|
||||
+317
@@ -0,0 +1,317 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/internal/assert"
|
||||
"github.com/pelletier/go-toml/v2/unstable"
|
||||
)
|
||||
|
||||
//nolint:funlen
|
||||
func TestDecodeError(t *testing.T) {
|
||||
examples := []struct {
|
||||
desc string
|
||||
doc [3]string
|
||||
msg string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "no context",
|
||||
doc: [3]string{"", "morning", ""},
|
||||
msg: "this is wrong",
|
||||
expected: `
|
||||
1| morning
|
||||
| ~~~~~~~ this is wrong`,
|
||||
},
|
||||
{
|
||||
desc: "one line",
|
||||
doc: [3]string{"good ", "morning", " everyone"},
|
||||
msg: "this is wrong",
|
||||
expected: `
|
||||
1| good morning everyone
|
||||
| ~~~~~~~ this is wrong`,
|
||||
},
|
||||
{
|
||||
desc: "exactly 3 lines",
|
||||
doc: [3]string{`line1
|
||||
line2
|
||||
line3
|
||||
before `, "highlighted", ` after
|
||||
post line 1
|
||||
post line 2
|
||||
post line 3`},
|
||||
msg: "this is wrong",
|
||||
expected: `
|
||||
1| line1
|
||||
2| line2
|
||||
3| line3
|
||||
4| before highlighted after
|
||||
| ~~~~~~~~~~~ this is wrong
|
||||
5| post line 1
|
||||
6| post line 2
|
||||
7| post line 3`,
|
||||
},
|
||||
{
|
||||
desc: "more than 3 lines",
|
||||
doc: [3]string{`should not be seen1
|
||||
should not be seen2
|
||||
line1
|
||||
line2
|
||||
line3
|
||||
before `, "highlighted", ` after
|
||||
post line 1
|
||||
post line 2
|
||||
post line 3
|
||||
should not be seen3
|
||||
should not be seen4`},
|
||||
msg: "this is wrong",
|
||||
expected: `
|
||||
3| line1
|
||||
4| line2
|
||||
5| line3
|
||||
6| before highlighted after
|
||||
| ~~~~~~~~~~~ this is wrong
|
||||
7| post line 1
|
||||
8| post line 2
|
||||
9| post line 3`,
|
||||
},
|
||||
{
|
||||
desc: "more than 10 total lines",
|
||||
doc: [3]string{`should not be seen 0
|
||||
should not be seen1
|
||||
should not be seen2
|
||||
should not be seen3
|
||||
line1
|
||||
line2
|
||||
line3
|
||||
before `, "highlighted", ` after
|
||||
post line 1
|
||||
post line 2
|
||||
post line 3
|
||||
should not be seen3
|
||||
should not be seen4`},
|
||||
msg: "this is wrong",
|
||||
expected: `
|
||||
5| line1
|
||||
6| line2
|
||||
7| line3
|
||||
8| before highlighted after
|
||||
| ~~~~~~~~~~~ this is wrong
|
||||
9| post line 1
|
||||
10| post line 2
|
||||
11| post line 3`,
|
||||
},
|
||||
{
|
||||
desc: "last line of more than 10",
|
||||
doc: [3]string{`should not be seen
|
||||
should not be seen
|
||||
should not be seen
|
||||
should not be seen
|
||||
should not be seen
|
||||
should not be seen
|
||||
should not be seen
|
||||
line1
|
||||
line2
|
||||
line3
|
||||
before `, "highlighted", ``},
|
||||
msg: "this is wrong",
|
||||
expected: `
|
||||
8| line1
|
||||
9| line2
|
||||
10| line3
|
||||
11| before highlighted
|
||||
| ~~~~~~~~~~~ this is wrong
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "handle empty lines in the before/after blocks",
|
||||
doc: [3]string{
|
||||
`line1
|
||||
|
||||
line 2
|
||||
before `, "highlighted", ` after
|
||||
line 3
|
||||
|
||||
line 4
|
||||
line 5`,
|
||||
},
|
||||
expected: `1| line1
|
||||
2|
|
||||
3| line 2
|
||||
4| before highlighted after
|
||||
| ~~~~~~~~~~~
|
||||
5| line 3
|
||||
6|
|
||||
7| line 4`,
|
||||
},
|
||||
{
|
||||
desc: "handle remainder of the error line when there is only one line",
|
||||
doc: [3]string{`P=`, `[`, `#`},
|
||||
msg: "array is incomplete",
|
||||
expected: `1| P=[#
|
||||
| ~ array is incomplete`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
e := e
|
||||
t.Run(e.desc, func(t *testing.T) {
|
||||
b := bytes.Buffer{}
|
||||
b.WriteString(e.doc[0])
|
||||
start := b.Len()
|
||||
b.WriteString(e.doc[1])
|
||||
end := b.Len()
|
||||
b.WriteString(e.doc[2])
|
||||
doc := b.Bytes()
|
||||
hl := doc[start:end]
|
||||
|
||||
err := wrapDecodeError(doc, &unstable.ParserError{
|
||||
Highlight: hl,
|
||||
Message: e.msg,
|
||||
})
|
||||
|
||||
var derr *DecodeError
|
||||
if !errors.As(err, &derr) {
|
||||
t.Errorf("error not in expected format")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, strings.Trim(e.expected, "\n"), derr.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeError_Accessors(t *testing.T) {
|
||||
e := DecodeError{
|
||||
message: "foo",
|
||||
line: 1,
|
||||
column: 2,
|
||||
key: []string{"one", "two"},
|
||||
human: "bar",
|
||||
}
|
||||
assert.Equal(t, "toml: foo", e.Error())
|
||||
r, c := e.Position()
|
||||
assert.Equal(t, 1, r)
|
||||
assert.Equal(t, 2, c)
|
||||
assert.Equal(t, Key{"one", "two"}, e.Key())
|
||||
assert.Equal(t, "bar", e.String())
|
||||
}
|
||||
|
||||
func TestDecodeError_DuplicateContent(t *testing.T) {
|
||||
// This test verifies that when the same content appears multiple times
|
||||
// in the document, the error correctly points to the actual location
|
||||
// of the error, not the first occurrence of the content.
|
||||
//
|
||||
// The document has "1__2" on line 1 and "3__4" on line 2.
|
||||
// Both have "__" which is invalid, but we want to ensure errors
|
||||
// on line 2 report line 2, not line 1.
|
||||
|
||||
doc := `a = 1
|
||||
b = 3__4`
|
||||
|
||||
var v map[string]int
|
||||
err := Unmarshal([]byte(doc), &v)
|
||||
|
||||
var derr *DecodeError
|
||||
if !errors.As(err, &derr) {
|
||||
t.Fatal("error not in expected format")
|
||||
}
|
||||
|
||||
row, col := derr.Position()
|
||||
// The error should be on line 2 where "3__4" is
|
||||
if row != 2 {
|
||||
t.Errorf("expected error on row 2, got row %d", row)
|
||||
}
|
||||
// Column should point to the "__" part (after "3")
|
||||
if col < 5 {
|
||||
t.Errorf("expected error at column >= 5, got column %d", col)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeError_Position(t *testing.T) {
|
||||
// Test that error positions are correctly reported for various error locations
|
||||
examples := []struct {
|
||||
name string
|
||||
doc string
|
||||
expectedRow int
|
||||
minCol int
|
||||
}{
|
||||
{
|
||||
name: "error on first line",
|
||||
doc: `a = 1__2`,
|
||||
expectedRow: 1,
|
||||
minCol: 5,
|
||||
},
|
||||
{
|
||||
name: "error on second line",
|
||||
doc: "a = 1\nb = 2__3",
|
||||
expectedRow: 2,
|
||||
minCol: 5,
|
||||
},
|
||||
{
|
||||
name: "error on third line",
|
||||
doc: "a = 1\nb = 2\nc = 3__4",
|
||||
expectedRow: 3,
|
||||
minCol: 5,
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
t.Run(e.name, func(t *testing.T) {
|
||||
var v map[string]int
|
||||
err := Unmarshal([]byte(e.doc), &v)
|
||||
|
||||
var derr *DecodeError
|
||||
if !errors.As(err, &derr) {
|
||||
t.Fatal("error not in expected format")
|
||||
}
|
||||
|
||||
row, col := derr.Position()
|
||||
assert.Equal(t, e.expectedRow, row)
|
||||
if col < e.minCol {
|
||||
t.Errorf("expected column >= %d, got %d", e.minCol, col)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrictErrorUnwrap(t *testing.T) {
|
||||
fo := bytes.NewBufferString(`
|
||||
Missing = 1
|
||||
OtherMissing = 1
|
||||
`)
|
||||
var out struct{}
|
||||
err := NewDecoder(fo).DisallowUnknownFields().Decode(&out)
|
||||
assert.Error(t, err)
|
||||
|
||||
strictErr := &StrictMissingError{}
|
||||
assert.True(t, errors.As(err, &strictErr))
|
||||
|
||||
assert.Equal(t, 2, len(strictErr.Unwrap()))
|
||||
}
|
||||
|
||||
func ExampleDecodeError() {
|
||||
doc := `name = 123__456`
|
||||
|
||||
s := map[string]interface{}{}
|
||||
err := Unmarshal([]byte(doc), &s)
|
||||
|
||||
fmt.Println(err)
|
||||
|
||||
var derr *DecodeError
|
||||
if errors.As(err, &derr) {
|
||||
fmt.Println(derr.String())
|
||||
row, col := derr.Position()
|
||||
fmt.Println("error occurred at row", row, "column", col)
|
||||
}
|
||||
// Output:
|
||||
// toml: number must have at least one digit between underscores
|
||||
// 1| name = 123__456
|
||||
// | ~~ number must have at least one digit between underscores
|
||||
// error occurred at row 1 column 11
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
# This is a TOML document. Boom.
|
||||
|
||||
title = "TOML Example"
|
||||
|
||||
[owner]
|
||||
name = "Tom Preston-Werner"
|
||||
organization = "GitHub"
|
||||
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
|
||||
dob = 1979-05-27T07:32:00Z # First class dates? Why not?
|
||||
|
||||
[database]
|
||||
server = "192.168.1.1"
|
||||
ports = [ 8001, 8001, 8002 ]
|
||||
connection_max = 5000
|
||||
enabled = true
|
||||
|
||||
[servers]
|
||||
|
||||
# You can indent as you please. Tabs or spaces. TOML don't care.
|
||||
[servers.alpha]
|
||||
ip = "10.0.0.1"
|
||||
dc = "eqdc10"
|
||||
|
||||
[servers.beta]
|
||||
ip = "10.0.0.2"
|
||||
dc = "eqdc10"
|
||||
|
||||
[clients]
|
||||
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
|
||||
@@ -0,0 +1,37 @@
|
||||
package toml_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
type customInt int
|
||||
|
||||
func (i *customInt) UnmarshalText(b []byte) error {
|
||||
x, err := strconv.ParseInt(string(b), 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*i = customInt(x * 100)
|
||||
return nil
|
||||
}
|
||||
|
||||
type doc struct {
|
||||
Value customInt
|
||||
}
|
||||
|
||||
func ExampleUnmarshal_textUnmarshal() {
|
||||
var x doc
|
||||
|
||||
data := []byte(`value = "42"`)
|
||||
err := toml.Unmarshal(data, &x)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(x)
|
||||
// Output:
|
||||
// {4200}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package toml_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/pelletier/go-toml/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestFastSimpleInt(t *testing.T) {
|
||||
m := map[string]int64{}
|
||||
err := toml.Unmarshal([]byte(`a = 42`), &m)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, map[string]int64{"a": 42}, m)
|
||||
}
|
||||
|
||||
func TestFastSimpleFloat(t *testing.T) {
|
||||
m := map[string]float64{}
|
||||
err := toml.Unmarshal([]byte("a = 42\nb = 1.1\nc = 12341234123412341234123412341234"), &m)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, map[string]float64{"a": 42, "b": 1.1, "c": 1.2341234123412342e+31}, m)
|
||||
}
|
||||
|
||||
func TestFastSimpleString(t *testing.T) {
|
||||
m := map[string]string{}
|
||||
err := toml.Unmarshal([]byte(`a = "hello"`), &m)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, map[string]string{"a": "hello"}, m)
|
||||
}
|
||||
|
||||
func TestFastSimpleInterface(t *testing.T) {
|
||||
m := map[string]interface{}{}
|
||||
err := toml.Unmarshal([]byte(`
|
||||
a = "hello"
|
||||
b = 42`), &m)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, map[string]interface{}{
|
||||
"a": "hello",
|
||||
"b": int64(42),
|
||||
}, m)
|
||||
}
|
||||
|
||||
func TestFastMultipartKeyInterface(t *testing.T) {
|
||||
m := map[string]interface{}{}
|
||||
err := toml.Unmarshal([]byte(`
|
||||
a.interim = "test"
|
||||
a.b.c = "hello"
|
||||
b = 42`), &m)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, map[string]interface{}{
|
||||
"a": map[string]interface{}{
|
||||
"interim": "test",
|
||||
"b": map[string]interface{}{
|
||||
"c": "hello",
|
||||
},
|
||||
},
|
||||
"b": int64(42),
|
||||
}, m)
|
||||
}
|
||||
|
||||
func TestFastExistingMap(t *testing.T) {
|
||||
m := map[string]interface{}{
|
||||
"ints": map[string]int{},
|
||||
}
|
||||
err := toml.Unmarshal([]byte(`
|
||||
ints.one = 1
|
||||
ints.two = 2
|
||||
strings.yo = "hello"`), &m)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, map[string]interface{}{
|
||||
"ints": map[string]interface{}{
|
||||
"one": int64(1),
|
||||
"two": int64(2),
|
||||
},
|
||||
"strings": map[string]interface{}{
|
||||
"yo": "hello",
|
||||
},
|
||||
}, m)
|
||||
}
|
||||
|
||||
func TestFastArrayTable(t *testing.T) {
|
||||
b := []byte(`
|
||||
[root]
|
||||
[[root.nested]]
|
||||
name = 'Bob'
|
||||
[[root.nested]]
|
||||
name = 'Alice'
|
||||
`)
|
||||
|
||||
m := map[string]interface{}{}
|
||||
|
||||
err := toml.Unmarshal(b, &m)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, map[string]interface{}{
|
||||
"root": map[string]interface{}{
|
||||
"nested": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "Bob",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"name": "Alice",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, m)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package toml_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/pelletier/go-toml/v2/internal/assert"
|
||||
)
|
||||
|
||||
func FuzzUnmarshal(f *testing.F) {
|
||||
file, err := os.ReadFile("benchmark/benchmark.toml")
|
||||
if err != nil {
|
||||
f.Error(err)
|
||||
}
|
||||
f.Add(file)
|
||||
|
||||
f.Fuzz(func(t *testing.T, b []byte) {
|
||||
if strings.Contains(string(b), "nan") {
|
||||
// Current limitation of testify.
|
||||
// https://github.com/stretchr/testify/issues/624
|
||||
t.Skip("can't compare NaNs")
|
||||
}
|
||||
|
||||
t.Log("INITIAL DOCUMENT ===========================")
|
||||
t.Log(string(b))
|
||||
|
||||
var v interface{}
|
||||
err := toml.Unmarshal(b, &v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
t.Log("DECODED VALUE ===========================")
|
||||
t.Logf("%#+v", v)
|
||||
|
||||
encoded, err := toml.Marshal(v)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot marshal unmarshaled document: %s", err)
|
||||
}
|
||||
|
||||
t.Log("ENCODED DOCUMENT ===========================")
|
||||
t.Log(string(encoded))
|
||||
|
||||
var v2 interface{}
|
||||
err = toml.Unmarshal(encoded, &v2)
|
||||
if err != nil {
|
||||
t.Fatalf("failed round trip: %s", err)
|
||||
}
|
||||
assert.Equal(t, v, v2)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Package assert provides assertion functions for unit testing.
|
||||
package assert
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// True asserts that an expression is true.
|
||||
func True(tb testing.TB, ok bool, msgAndArgs ...any) {
|
||||
tb.Helper()
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
tb.Fatal(formatMsgAndArgs("Expected expression to be true", msgAndArgs...))
|
||||
}
|
||||
|
||||
// False asserts that an expression is false.
|
||||
func False(tb testing.TB, ok bool, msgAndArgs ...any) {
|
||||
tb.Helper()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
tb.Fatal(formatMsgAndArgs("Expected expression to be false", msgAndArgs...))
|
||||
}
|
||||
|
||||
// Equal asserts that "expected" and "actual" are equal.
|
||||
func Equal[T any](tb testing.TB, expected, actual T, msgAndArgs ...any) {
|
||||
tb.Helper()
|
||||
if objectsAreEqual(expected, actual) {
|
||||
return
|
||||
}
|
||||
msg := formatMsgAndArgs("Expected values to be equal:", msgAndArgs...)
|
||||
tb.Fatalf("%s\n%s", msg, diff(expected, actual))
|
||||
}
|
||||
|
||||
// Error asserts that an error is not nil.
|
||||
func Error(tb testing.TB, err error, msgAndArgs ...any) {
|
||||
tb.Helper()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tb.Fatal(formatMsgAndArgs("Expected an error", msgAndArgs...))
|
||||
}
|
||||
|
||||
// NoError asserts that an error is nil.
|
||||
func NoError(tb testing.TB, err error, msgAndArgs ...any) {
|
||||
tb.Helper()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
msg := formatMsgAndArgs("Unexpected error:", msgAndArgs...)
|
||||
tb.Fatalf("%s\n%+v", msg, err)
|
||||
}
|
||||
|
||||
// Panics asserts that the given function panics.
|
||||
func Panics(tb testing.TB, fn func(), msgAndArgs ...any) {
|
||||
tb.Helper()
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
msg := formatMsgAndArgs("Expected function to panic", msgAndArgs...)
|
||||
tb.Fatal(msg)
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
}
|
||||
|
||||
// Zero asserts that a value is its zero value.
|
||||
func Zero[T any](tb testing.TB, value T, msgAndArgs ...any) {
|
||||
tb.Helper()
|
||||
var zero T
|
||||
if objectsAreEqual(value, zero) {
|
||||
return
|
||||
}
|
||||
val := reflect.ValueOf(value)
|
||||
if (val.Kind() == reflect.Slice || val.Kind() == reflect.Map || val.Kind() == reflect.Array) && val.Len() == 0 {
|
||||
return
|
||||
}
|
||||
msg := formatMsgAndArgs("Expected zero value but got:", msgAndArgs...)
|
||||
tb.Fatalf("%s\n%v", msg, value)
|
||||
}
|
||||
|
||||
func NotZero[T any](tb testing.TB, value T, msgAndArgs ...any) {
|
||||
tb.Helper()
|
||||
var zero T
|
||||
if !objectsAreEqual(value, zero) {
|
||||
val := reflect.ValueOf(value)
|
||||
switch val.Kind() {
|
||||
case reflect.Slice, reflect.Map, reflect.Array:
|
||||
if val.Len() > 0 {
|
||||
return
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
msg := formatMsgAndArgs("Unexpected zero value:", msgAndArgs...)
|
||||
tb.Fatalf("%s\n%v", msg, value)
|
||||
}
|
||||
|
||||
func formatMsgAndArgs(msg string, args ...any) string {
|
||||
if len(args) == 0 {
|
||||
return msg
|
||||
}
|
||||
format, ok := args[0].(string)
|
||||
if !ok {
|
||||
panic("message argument must be a fmt string")
|
||||
}
|
||||
return fmt.Sprintf(format, args[1:]...)
|
||||
}
|
||||
|
||||
func diff(expected, actual any) string {
|
||||
lines := []string{
|
||||
"expected:",
|
||||
fmt.Sprintf("%v", expected),
|
||||
"actual:",
|
||||
fmt.Sprintf("%v", actual),
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func objectsAreEqual(expected, actual any) bool {
|
||||
if expected == nil || actual == nil {
|
||||
return expected == actual
|
||||
}
|
||||
if exp, eok := expected.([]byte); eok {
|
||||
if act, aok := actual.([]byte); aok {
|
||||
return bytes.Equal(exp, act)
|
||||
}
|
||||
}
|
||||
if exp, eok := expected.(string); eok {
|
||||
if act, aok := actual.(string); aok {
|
||||
return exp == act
|
||||
}
|
||||
}
|
||||
|
||||
return reflect.DeepEqual(expected, actual)
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package assert
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type Data struct {
|
||||
Label string
|
||||
Value int64
|
||||
}
|
||||
|
||||
func TestBadMessage(t *testing.T) {
|
||||
invalidMessage := func() { True(t, false, 1234) }
|
||||
assertOk(t, "Non-fmt message value", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
Panics(tb, invalidMessage)
|
||||
})
|
||||
assertFail(t, "Non-fmt message value", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
True(tb, false, "example %s", "message")
|
||||
})
|
||||
}
|
||||
|
||||
func TestTrue(t *testing.T) {
|
||||
assertOk(t, "Succeed", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
True(tb, 1 > 0)
|
||||
})
|
||||
assertFail(t, "Fail", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
True(tb, 1 < 0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFalse(t *testing.T) {
|
||||
assertOk(t, "Succeed", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
False(tb, 1 < 0)
|
||||
})
|
||||
assertFail(t, "Fail", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
False(tb, 1 > 0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEqual(t *testing.T) {
|
||||
assertOk(t, "Nil", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
Equal(tb, interface{}(nil), interface{}(nil))
|
||||
})
|
||||
assertOk(t, "Identical structs", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
Equal(tb, Data{"expected", 1234}, Data{"expected", 1234})
|
||||
})
|
||||
assertFail(t, "Different structs", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
Equal(tb, Data{"expected", 1234}, Data{"actual", 1234})
|
||||
})
|
||||
assertOk(t, "Identical numbers", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
Equal(tb, 1234, 1234)
|
||||
})
|
||||
assertFail(t, "Identical numbers", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
Equal(tb, 1234, 1324)
|
||||
})
|
||||
assertOk(t, "Zero-length byte arrays", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
Equal(tb, []byte(nil), []byte(""))
|
||||
})
|
||||
assertOk(t, "Identical byte arrays", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
Equal(tb, []byte{1, 2, 3, 4}, []byte{1, 2, 3, 4})
|
||||
})
|
||||
assertFail(t, "Different byte arrays", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
Equal(tb, []byte{1, 2, 3, 4}, []byte{1, 3, 2, 4})
|
||||
})
|
||||
assertOk(t, "Identical strings", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
Equal(tb, "example", "example")
|
||||
})
|
||||
assertFail(t, "Identical strings", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
Equal(tb, "example", "elpmaxe")
|
||||
})
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
assertOk(t, "Error", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
Error(tb, errors.New("example"))
|
||||
})
|
||||
assertFail(t, "Nil", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
Error(tb, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNoError(t *testing.T) {
|
||||
assertFail(t, "Error", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
NoError(tb, errors.New("example"))
|
||||
})
|
||||
assertOk(t, "Nil", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
NoError(tb, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPanics(t *testing.T) {
|
||||
willPanic := func() { panic("example") }
|
||||
wontPanic := func() {}
|
||||
assertOk(t, "Will panic", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
Panics(tb, willPanic)
|
||||
})
|
||||
assertFail(t, "Won't panic", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
Panics(tb, wontPanic)
|
||||
})
|
||||
}
|
||||
|
||||
func TestZero(t *testing.T) {
|
||||
assertOk(t, "Empty struct", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
Zero(tb, Data{})
|
||||
})
|
||||
assertFail(t, "Non-empty struct", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
Zero(tb, Data{Label: "example"})
|
||||
})
|
||||
assertOk(t, "Nil slice", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
var slice []int
|
||||
Zero(tb, slice)
|
||||
})
|
||||
assertFail(t, "Non-empty slice", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
slice := []int{1, 2, 3, 4}
|
||||
Zero(tb, slice)
|
||||
})
|
||||
assertOk(t, "Zero-length slice", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
slice := []int{}
|
||||
Zero(tb, slice)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNotZero(t *testing.T) {
|
||||
assertFail(t, "Empty struct", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
zero := Data{}
|
||||
NotZero(tb, zero)
|
||||
})
|
||||
assertOk(t, "Non-empty struct", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
notZero := Data{Label: "example"}
|
||||
NotZero(tb, notZero)
|
||||
})
|
||||
assertFail(t, "Nil slice", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
var slice []int
|
||||
NotZero(tb, slice)
|
||||
})
|
||||
assertFail(t, "Zero-length slice", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
slice := []int{}
|
||||
NotZero(tb, slice)
|
||||
})
|
||||
assertOk(t, "Non-empty slice", func(tb testing.TB) {
|
||||
tb.Helper()
|
||||
slice := []int{1, 2, 3, 4}
|
||||
NotZero(tb, slice)
|
||||
})
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
*testing.T
|
||||
failed string
|
||||
}
|
||||
|
||||
func (t *testCase) Fatal(args ...interface{}) {
|
||||
t.failed = fmt.Sprint(args...)
|
||||
}
|
||||
|
||||
func (t *testCase) Fatalf(message string, args ...interface{}) {
|
||||
t.failed = fmt.Sprintf(message, args...)
|
||||
}
|
||||
|
||||
func assertFail(t *testing.T, name string, fn func(testing.TB)) {
|
||||
t.Helper()
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Helper()
|
||||
test := &testCase{T: t}
|
||||
fn(test)
|
||||
if test.failed == "" {
|
||||
t.Fatal("Test expected to fail but did not")
|
||||
} else {
|
||||
t.Log(test.failed)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func assertOk(t *testing.T, name string, fn func(testing.TB)) {
|
||||
t.Helper()
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Helper()
|
||||
test := &testCase{T: t}
|
||||
fn(test)
|
||||
if test.failed != "" {
|
||||
t.Fatal("Test expected to succeed but did not:\n", test.failed)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package characters
|
||||
|
||||
var invalidASCIITable = [256]bool{
|
||||
0x00: true,
|
||||
0x01: true,
|
||||
0x02: true,
|
||||
0x03: true,
|
||||
0x04: true,
|
||||
0x05: true,
|
||||
0x06: true,
|
||||
0x07: true,
|
||||
0x08: true,
|
||||
// 0x09 TAB
|
||||
// 0x0A LF
|
||||
0x0B: true,
|
||||
0x0C: true,
|
||||
// 0x0D CR
|
||||
0x0E: true,
|
||||
0x0F: true,
|
||||
0x10: true,
|
||||
0x11: true,
|
||||
0x12: true,
|
||||
0x13: true,
|
||||
0x14: true,
|
||||
0x15: true,
|
||||
0x16: true,
|
||||
0x17: true,
|
||||
0x18: true,
|
||||
0x19: true,
|
||||
0x1A: true,
|
||||
0x1B: true,
|
||||
0x1C: true,
|
||||
0x1D: true,
|
||||
0x1E: true,
|
||||
0x1F: true,
|
||||
// 0x20 - 0x7E Printable ASCII characters
|
||||
0x7F: true,
|
||||
}
|
||||
|
||||
func InvalidASCII(b byte) bool {
|
||||
return invalidASCIITable[b]
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// Package characters provides functions for working with string encodings.
|
||||
package characters
|
||||
|
||||
import (
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Utf8TomlValidAlreadyEscaped verifies that a given string is only made of
|
||||
// valid UTF-8 characters allowed by the TOML spec:
|
||||
//
|
||||
// Any Unicode character may be used except those that must be escaped:
|
||||
// quotation mark, backslash, and the control characters other than tab (U+0000
|
||||
// to U+0008, U+000A to U+001F, U+007F).
|
||||
//
|
||||
// It is a copy of the Go 1.17 utf8.Valid implementation, tweaked to exit early
|
||||
// when a character is not allowed.
|
||||
//
|
||||
// The returned slice is empty if the string is valid, or contains the bytes
|
||||
// of the invalid character.
|
||||
//
|
||||
// quotation mark => already checked
|
||||
// backslash => already checked
|
||||
// 0-0x8 => invalid
|
||||
// 0x9 => tab, ok
|
||||
// 0xA - 0x1F => invalid
|
||||
// 0x7F => invalid
|
||||
func Utf8TomlValidAlreadyEscaped(p []byte) []byte {
|
||||
// Fast path. Check for and skip 8 bytes of ASCII characters per iteration.
|
||||
for len(p) >= 8 {
|
||||
// Combining two 32 bit loads allows the same code to be used
|
||||
// for 32 and 64 bit platforms.
|
||||
// The compiler can generate a 32bit load for first32 and second32
|
||||
// on many platforms. See test/codegen/memcombine.go.
|
||||
first32 := uint32(p[0]) | uint32(p[1])<<8 | uint32(p[2])<<16 | uint32(p[3])<<24
|
||||
second32 := uint32(p[4]) | uint32(p[5])<<8 | uint32(p[6])<<16 | uint32(p[7])<<24
|
||||
if (first32|second32)&0x80808080 != 0 {
|
||||
// Found a non ASCII byte (>= RuneSelf).
|
||||
break
|
||||
}
|
||||
|
||||
for i, b := range p[:8] {
|
||||
if InvalidASCII(b) {
|
||||
return p[i : i+1]
|
||||
}
|
||||
}
|
||||
|
||||
p = p[8:]
|
||||
}
|
||||
n := len(p)
|
||||
for i := 0; i < n; {
|
||||
pi := p[i]
|
||||
if pi < utf8.RuneSelf {
|
||||
if InvalidASCII(pi) {
|
||||
return p[i : i+1]
|
||||
}
|
||||
i++
|
||||
continue
|
||||
}
|
||||
x := first[pi]
|
||||
if x == xx {
|
||||
// Illegal starter byte.
|
||||
return p[i : i+1]
|
||||
}
|
||||
size := int(x & 7)
|
||||
if i+size > n {
|
||||
// Short or invalid.
|
||||
return p[i:n]
|
||||
}
|
||||
accept := acceptRanges[x>>4]
|
||||
if c := p[i+1]; c < accept.lo || accept.hi < c {
|
||||
return p[i : i+2]
|
||||
} else if size == 2 { //revive:disable:empty-block
|
||||
} else if c := p[i+2]; c < locb || hicb < c {
|
||||
return p[i : i+3]
|
||||
} else if size == 3 { //revive:disable:empty-block
|
||||
} else if c := p[i+3]; c < locb || hicb < c {
|
||||
return p[i : i+4]
|
||||
}
|
||||
i += size
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Utf8ValidNext returns the size of the next rune if valid, 0 otherwise.
|
||||
func Utf8ValidNext(p []byte) int {
|
||||
c := p[0]
|
||||
|
||||
if c < utf8.RuneSelf {
|
||||
if InvalidASCII(c) {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
x := first[c]
|
||||
if x == xx {
|
||||
// Illegal starter byte.
|
||||
return 0
|
||||
}
|
||||
size := int(x & 7)
|
||||
if size > len(p) {
|
||||
// Short or invalid.
|
||||
return 0
|
||||
}
|
||||
accept := acceptRanges[x>>4]
|
||||
if c := p[1]; c < accept.lo || accept.hi < c {
|
||||
return 0
|
||||
} else if size == 2 { //nolint:revive
|
||||
} else if c := p[2]; c < locb || hicb < c {
|
||||
return 0
|
||||
} else if size == 3 { //nolint:revive
|
||||
} else if c := p[3]; c < locb || hicb < c {
|
||||
return 0
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
// acceptRange gives the range of valid values for the second byte in a UTF-8
|
||||
// sequence.
|
||||
type acceptRange struct {
|
||||
lo uint8 // lowest value for second byte.
|
||||
hi uint8 // highest value for second byte.
|
||||
}
|
||||
|
||||
// acceptRanges has size 16 to avoid bounds checks in the code that uses it.
|
||||
var acceptRanges = [16]acceptRange{
|
||||
0: {locb, hicb},
|
||||
1: {0xA0, hicb},
|
||||
2: {locb, 0x9F},
|
||||
3: {0x90, hicb},
|
||||
4: {locb, 0x8F},
|
||||
}
|
||||
|
||||
// first is information about the first byte in a UTF-8 sequence.
|
||||
var first = [256]uint8{
|
||||
// 1 2 3 4 5 6 7 8 9 A B C D E F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x00-0x0F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x10-0x1F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x20-0x2F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x30-0x3F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x40-0x4F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x50-0x5F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x60-0x6F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x70-0x7F
|
||||
// 1 2 3 4 5 6 7 8 9 A B C D E F
|
||||
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x80-0x8F
|
||||
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x90-0x9F
|
||||
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xA0-0xAF
|
||||
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xB0-0xBF
|
||||
xx, xx, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, // 0xC0-0xCF
|
||||
s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, // 0xD0-0xDF
|
||||
s2, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s4, s3, s3, // 0xE0-0xEF
|
||||
s5, s6, s6, s6, s7, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xF0-0xFF
|
||||
}
|
||||
|
||||
const (
|
||||
// The default lowest and highest continuation byte.
|
||||
locb = 0b10000000
|
||||
hicb = 0b10111111
|
||||
|
||||
// These names of these constants are chosen to give nice alignment in the
|
||||
// table below. The first nibble is an index into acceptRanges or F for
|
||||
// special one-byte cases. The second nibble is the Rune length or the
|
||||
// Status for the special one-byte case.
|
||||
xx = 0xF1 // invalid: size 1
|
||||
as = 0xF0 // ASCII: size 1
|
||||
s1 = 0x02 // accept 0, size 2
|
||||
s2 = 0x13 // accept 1, size 3
|
||||
s3 = 0x03 // accept 0, size 3
|
||||
s4 = 0x23 // accept 2, size 3
|
||||
s5 = 0x34 // accept 3, size 4
|
||||
s6 = 0x04 // accept 0, size 4
|
||||
s7 = 0x44 // accept 4, size 4
|
||||
)
|
||||
@@ -0,0 +1,87 @@
|
||||
// Package cli provides common functions for command-line programs.
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
type ConvertFn func(r io.Reader, w io.Writer) error
|
||||
|
||||
type Program struct {
|
||||
Usage string
|
||||
Fn ConvertFn
|
||||
// Inplace allows the command to take more than one file as argument and
|
||||
// perform conversion in place on each provided file.
|
||||
Inplace bool
|
||||
}
|
||||
|
||||
func (p *Program) Execute() {
|
||||
flag.Usage = func() { fmt.Fprint(os.Stderr, p.Usage) }
|
||||
flag.Parse()
|
||||
os.Exit(p.main(flag.Args(), os.Stdin, os.Stdout, os.Stderr))
|
||||
}
|
||||
|
||||
func (p *Program) main(files []string, input io.Reader, output, stderr io.Writer) int {
|
||||
err := p.run(files, input, output)
|
||||
if err != nil {
|
||||
var derr *toml.DecodeError
|
||||
if errors.As(err, &derr) {
|
||||
_, _ = fmt.Fprintln(stderr, derr.String())
|
||||
row, col := derr.Position()
|
||||
_, _ = fmt.Fprintln(stderr, "error occurred at row", row, "column", col)
|
||||
} else {
|
||||
_, _ = fmt.Fprintln(stderr, err.Error())
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (p *Program) run(files []string, input io.Reader, output io.Writer) error {
|
||||
if len(files) > 0 {
|
||||
if p.Inplace {
|
||||
return p.runAllFilesInPlace(files)
|
||||
}
|
||||
f, err := os.Open(files[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
input = f
|
||||
}
|
||||
return p.Fn(input, output)
|
||||
}
|
||||
|
||||
func (p *Program) runAllFilesInPlace(files []string) error {
|
||||
for _, path := range files {
|
||||
err := p.runFileInPlace(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Program) runFileInPlace(path string) error {
|
||||
in, err := os.ReadFile(path) // #nosec G304
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out := new(bytes.Buffer)
|
||||
|
||||
err = p.Fn(bytes.NewReader(in), out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(path, out.Bytes(), 0o600)
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/pelletier/go-toml/v2/internal/assert"
|
||||
)
|
||||
|
||||
func processMain(args []string, input io.Reader, stdout, stderr io.Writer, f ConvertFn) int {
|
||||
p := Program{Fn: f}
|
||||
return p.main(args, input, stdout, stderr)
|
||||
}
|
||||
|
||||
func TestProcessMainStdin(t *testing.T) {
|
||||
stdout := new(bytes.Buffer)
|
||||
stderr := new(bytes.Buffer)
|
||||
input := strings.NewReader("this is the input")
|
||||
|
||||
exit := processMain([]string{}, input, stdout, stderr, func(io.Reader, io.Writer) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
assert.Equal(t, 0, exit)
|
||||
assert.Zero(t, stdout.String())
|
||||
assert.Zero(t, stderr.String())
|
||||
}
|
||||
|
||||
func TestProcessMainStdinErr(t *testing.T) {
|
||||
stdout := new(bytes.Buffer)
|
||||
stderr := new(bytes.Buffer)
|
||||
input := strings.NewReader("this is the input")
|
||||
|
||||
exit := processMain([]string{}, input, stdout, stderr, func(io.Reader, io.Writer) error {
|
||||
return errors.New("something bad")
|
||||
})
|
||||
|
||||
assert.Equal(t, -1, exit)
|
||||
assert.Zero(t, stdout.String())
|
||||
assert.NotZero(t, stderr.String())
|
||||
}
|
||||
|
||||
func TestProcessMainStdinDecodeErr(t *testing.T) {
|
||||
stdout := new(bytes.Buffer)
|
||||
stderr := new(bytes.Buffer)
|
||||
input := strings.NewReader("this is the input")
|
||||
|
||||
exit := processMain([]string{}, input, stdout, stderr, func(io.Reader, io.Writer) error {
|
||||
var v interface{}
|
||||
return toml.Unmarshal([]byte(`qwe = 001`), &v)
|
||||
})
|
||||
|
||||
assert.Equal(t, -1, exit)
|
||||
assert.Zero(t, stdout.String())
|
||||
assert.True(t, strings.Contains(stderr.String(), "error occurred at"))
|
||||
}
|
||||
|
||||
func TestProcessMainFileExists(t *testing.T) {
|
||||
tmpfile, err := os.CreateTemp(t.TempDir(), "example")
|
||||
assert.NoError(t, err)
|
||||
_, err = tmpfile.WriteString(`some data`)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, tmpfile.Close())
|
||||
|
||||
stdout := new(bytes.Buffer)
|
||||
stderr := new(bytes.Buffer)
|
||||
|
||||
exit := processMain([]string{tmpfile.Name()}, nil, stdout, stderr, func(io.Reader, io.Writer) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
assert.Equal(t, 0, exit)
|
||||
assert.Zero(t, stdout.String())
|
||||
assert.Zero(t, stderr.String())
|
||||
}
|
||||
|
||||
func TestProcessMainFileDoesNotExist(t *testing.T) {
|
||||
stdout := new(bytes.Buffer)
|
||||
stderr := new(bytes.Buffer)
|
||||
|
||||
exit := processMain([]string{"/lets/hope/this/does/not/exist"}, nil, stdout, stderr, func(io.Reader, io.Writer) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
assert.Equal(t, -1, exit)
|
||||
assert.Zero(t, stdout.String())
|
||||
assert.NotZero(t, stderr.String())
|
||||
}
|
||||
|
||||
func TestProcessMainFilesInPlace(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
path1 := path.Join(dir, "file1")
|
||||
path2 := path.Join(dir, "file2")
|
||||
|
||||
err := os.WriteFile(path1, []byte("content 1"), 0o600)
|
||||
assert.NoError(t, err)
|
||||
err = os.WriteFile(path2, []byte("content 2"), 0o600)
|
||||
assert.NoError(t, err)
|
||||
|
||||
p := Program{
|
||||
Fn: dummyFileFn,
|
||||
Inplace: true,
|
||||
}
|
||||
|
||||
exit := p.main([]string{path1, path2}, os.Stdin, os.Stdout, os.Stderr)
|
||||
|
||||
assert.Equal(t, 0, exit)
|
||||
|
||||
v1, err := os.ReadFile(path1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "1", string(v1))
|
||||
|
||||
v2, err := os.ReadFile(path2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "2", string(v2))
|
||||
}
|
||||
|
||||
func TestProcessMainFilesInPlaceErrRead(t *testing.T) {
|
||||
p := Program{
|
||||
Fn: dummyFileFn,
|
||||
Inplace: true,
|
||||
}
|
||||
|
||||
exit := p.main([]string{"/this/path/is/invalid"}, os.Stdin, os.Stdout, os.Stderr)
|
||||
|
||||
assert.Equal(t, -1, exit)
|
||||
}
|
||||
|
||||
func TestProcessMainFilesInPlaceFailFn(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
path1 := path.Join(dir, "file1")
|
||||
|
||||
err := os.WriteFile(path1, []byte("content 1"), 0o600)
|
||||
assert.NoError(t, err)
|
||||
|
||||
p := Program{
|
||||
Fn: func(io.Reader, io.Writer) error { return errors.New("oh no") },
|
||||
Inplace: true,
|
||||
}
|
||||
|
||||
exit := p.main([]string{path1}, os.Stdin, os.Stdout, os.Stderr)
|
||||
|
||||
assert.Equal(t, -1, exit)
|
||||
|
||||
v1, err := os.ReadFile(path1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "content 1", string(v1))
|
||||
}
|
||||
|
||||
func dummyFileFn(r io.Reader, w io.Writer) error {
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v := strings.SplitN(string(b), " ", 2)[1]
|
||||
_, err = w.Write([]byte(v))
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package imported_tests //revive:disable:var-naming
|
||||
|
||||
// Those tests have been imported from v1, but adjust to match the new
|
||||
// defaults of v2.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/pelletier/go-toml/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestDocMarshal(t *testing.T) {
|
||||
type testDoc struct {
|
||||
Title string `toml:"title"`
|
||||
BasicLists testDocBasicLists `toml:"basic_lists"`
|
||||
SubDocPtrs []*testSubDoc `toml:"subdocptrs"`
|
||||
BasicMap map[string]string `toml:"basic_map"`
|
||||
Subdocs testDocSubs `toml:"subdoc"`
|
||||
Basics testDocBasics `toml:"basic"`
|
||||
SubDocList []testSubDoc `toml:"subdoclist"`
|
||||
err int `toml:"shouldntBeHere"` //nolint:unused
|
||||
unexported int `toml:"shouldntBeHere"`
|
||||
Unexported2 int `toml:"-"`
|
||||
}
|
||||
|
||||
docData := testDoc{
|
||||
Title: "TOML Marshal Testing",
|
||||
unexported: 0,
|
||||
Unexported2: 0,
|
||||
Basics: testDocBasics{
|
||||
Bool: true,
|
||||
Date: time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC),
|
||||
Float32: 123.4,
|
||||
Float64: 123.456782132399,
|
||||
Int: 5000,
|
||||
Uint: 5001,
|
||||
String: &biteMe,
|
||||
unexported: 0,
|
||||
},
|
||||
BasicLists: testDocBasicLists{
|
||||
Floats: []*float32{&float1, &float2, &float3},
|
||||
Bools: []bool{true, false, true},
|
||||
Dates: []time.Time{
|
||||
time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC),
|
||||
time.Date(1980, 5, 27, 7, 32, 0, 0, time.UTC),
|
||||
},
|
||||
Ints: []int{8001, 8001, 8002},
|
||||
Strings: []string{"One", "Two", "Three"},
|
||||
UInts: []uint{5002, 5003},
|
||||
},
|
||||
BasicMap: map[string]string{
|
||||
"one": "one",
|
||||
"two": "two",
|
||||
},
|
||||
Subdocs: testDocSubs{
|
||||
First: testSubDoc{"First", 0},
|
||||
Second: &subdoc,
|
||||
},
|
||||
SubDocList: []testSubDoc{
|
||||
{"List.First", 0},
|
||||
{"List.Second", 0},
|
||||
},
|
||||
SubDocPtrs: []*testSubDoc{&subdoc},
|
||||
}
|
||||
|
||||
marshalTestToml := `title = 'TOML Marshal Testing'
|
||||
|
||||
[basic_lists]
|
||||
floats = [12.3, 45.6, 78.9]
|
||||
bools = [true, false, true]
|
||||
dates = [1979-05-27T07:32:00Z, 1980-05-27T07:32:00Z]
|
||||
ints = [8001, 8001, 8002]
|
||||
uints = [5002, 5003]
|
||||
strings = ['One', 'Two', 'Three']
|
||||
|
||||
[[subdocptrs]]
|
||||
name = 'Second'
|
||||
|
||||
[basic_map]
|
||||
one = 'one'
|
||||
two = 'two'
|
||||
|
||||
[subdoc]
|
||||
[subdoc.second]
|
||||
name = 'Second'
|
||||
|
||||
[subdoc.first]
|
||||
name = 'First'
|
||||
|
||||
[basic]
|
||||
uint = 5001
|
||||
bool = true
|
||||
float = 123.4
|
||||
float64 = 123.456782132399
|
||||
int = 5000
|
||||
string = 'Bite me'
|
||||
date = 1979-05-27T07:32:00Z
|
||||
|
||||
[[subdoclist]]
|
||||
name = 'List.First'
|
||||
|
||||
[[subdoclist]]
|
||||
name = 'List.Second'
|
||||
`
|
||||
|
||||
result, err := toml.Marshal(docData)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, marshalTestToml, string(result))
|
||||
}
|
||||
|
||||
func TestBasicMarshalQuotedKey(t *testing.T) {
|
||||
result, err := toml.Marshal(quotedKeyMarshalTestData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expected := `'Z.string-àéù' = 'Hello'
|
||||
'Yfloat-𝟘' = 3.5
|
||||
|
||||
['Xsubdoc-àéù']
|
||||
String2 = 'One'
|
||||
|
||||
[['W.sublist-𝟘']]
|
||||
String2 = 'Two'
|
||||
|
||||
[['W.sublist-𝟘']]
|
||||
String2 = 'Three'
|
||||
`
|
||||
|
||||
assert.Equal(t, expected, string(result))
|
||||
}
|
||||
|
||||
func TestEmptyMarshal(t *testing.T) {
|
||||
type emptyMarshalTestStruct struct {
|
||||
Title string `toml:"title"`
|
||||
Bool bool `toml:"bool"`
|
||||
Int int `toml:"int"`
|
||||
String string `toml:"string"`
|
||||
StringList []string `toml:"stringlist"`
|
||||
Ptr *basicMarshalTestStruct `toml:"ptr"`
|
||||
Map map[string]string `toml:"map"`
|
||||
}
|
||||
|
||||
doc := emptyMarshalTestStruct{
|
||||
Title: "Placeholder",
|
||||
Bool: false,
|
||||
Int: 0,
|
||||
String: "",
|
||||
StringList: []string{},
|
||||
Ptr: nil,
|
||||
Map: map[string]string{},
|
||||
}
|
||||
result, err := toml.Marshal(doc)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expected := `title = 'Placeholder'
|
||||
bool = false
|
||||
int = 0
|
||||
string = ''
|
||||
stringlist = []
|
||||
|
||||
[map]
|
||||
`
|
||||
|
||||
assert.Equal(t, expected, string(result))
|
||||
}
|
||||
|
||||
type textMarshaler struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
}
|
||||
|
||||
func (m textMarshaler) MarshalText() ([]byte, error) {
|
||||
fullName := fmt.Sprintf("%s %s", m.FirstName, m.LastName)
|
||||
return []byte(fullName), nil
|
||||
}
|
||||
|
||||
func TestTextMarshaler(t *testing.T) {
|
||||
type wrap struct {
|
||||
TM textMarshaler
|
||||
}
|
||||
|
||||
m := textMarshaler{FirstName: "Sally", LastName: "Fields"}
|
||||
|
||||
t.Run("at root", func(t *testing.T) {
|
||||
_, err := toml.Marshal(m)
|
||||
// in v2 we do not allow TextMarshaler at root
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("leaf", func(t *testing.T) {
|
||||
res, err := toml.Marshal(wrap{m})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "TM = 'Sally Fields'\n", string(res))
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,75 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
// addTag adds JSON tags to a data structure as expected by toml-test.
|
||||
func addTag(tomlData interface{}) interface{} {
|
||||
// Switch on the data type.
|
||||
switch orig := tomlData.(type) {
|
||||
default:
|
||||
// return map[string]interface{}{}
|
||||
panic(fmt.Sprintf("Unknown type: %T", tomlData))
|
||||
|
||||
// A table: we don't need to add any tags, just recurse for every table
|
||||
// entry.
|
||||
case map[string]interface{}:
|
||||
typed := make(map[string]interface{}, len(orig))
|
||||
for k, v := range orig {
|
||||
typed[k] = addTag(v)
|
||||
}
|
||||
return typed
|
||||
|
||||
// An array: we don't need to add any tags, just recurse for every table
|
||||
// entry.
|
||||
case []map[string]interface{}:
|
||||
typed := make([]map[string]interface{}, len(orig))
|
||||
for i, v := range orig {
|
||||
typed[i] = addTag(v).(map[string]interface{})
|
||||
}
|
||||
return typed
|
||||
case []interface{}:
|
||||
typed := make([]interface{}, len(orig))
|
||||
for i, v := range orig {
|
||||
typed[i] = addTag(v)
|
||||
}
|
||||
return typed
|
||||
|
||||
// Datetime: tag as datetime.
|
||||
case toml.LocalTime:
|
||||
return tag("time-local", orig.String())
|
||||
case toml.LocalDate:
|
||||
return tag("date-local", orig.String())
|
||||
case toml.LocalDateTime:
|
||||
return tag("datetime-local", orig.String())
|
||||
case time.Time:
|
||||
return tag("datetime", orig.Format("2006-01-02T15:04:05.999999999Z07:00"))
|
||||
|
||||
// Tag primitive values: bool, string, int, and float64.
|
||||
case bool:
|
||||
return tag("bool", strconv.FormatBool(orig))
|
||||
case string:
|
||||
return tag("string", orig)
|
||||
case int64:
|
||||
return tag("integer", strconv.FormatInt(orig, 10))
|
||||
case float64:
|
||||
// Special case for nan since NaN == NaN is false.
|
||||
if math.IsNaN(orig) {
|
||||
return tag("float", "nan")
|
||||
}
|
||||
return tag("float", fmt.Sprintf("%v", orig))
|
||||
}
|
||||
}
|
||||
|
||||
func tag(typeName string, data interface{}) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"type": typeName,
|
||||
"value": data,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CmpJSON(t *testing.T, key string, want, have interface{}) {
|
||||
t.Helper()
|
||||
switch w := want.(type) {
|
||||
case map[string]interface{}:
|
||||
cmpJSONMaps(t, key, w, have)
|
||||
case []interface{}:
|
||||
cmpJSONArrays(t, key, w, have)
|
||||
default:
|
||||
t.Errorf(
|
||||
"Key '%s' in expected output should be a map or a list of maps, but it's a %T",
|
||||
key, want)
|
||||
}
|
||||
}
|
||||
|
||||
func cmpJSONMaps(t *testing.T, key string, want map[string]interface{}, have interface{}) {
|
||||
t.Helper()
|
||||
haveMap, ok := have.(map[string]interface{})
|
||||
if !ok {
|
||||
mismatch(t, key, "table", want, haveMap)
|
||||
return
|
||||
}
|
||||
|
||||
// Check to make sure both or neither are values.
|
||||
if isValue(want) && !isValue(haveMap) {
|
||||
t.Fatalf("Key '%s' is supposed to be a value, but the parser reports it as a table", key)
|
||||
}
|
||||
if !isValue(want) && isValue(haveMap) {
|
||||
t.Fatalf("Key '%s' is supposed to be a table, but the parser reports it as a value", key)
|
||||
}
|
||||
if isValue(want) && isValue(haveMap) {
|
||||
cmpJSONValues(t, key, want, haveMap)
|
||||
return
|
||||
}
|
||||
|
||||
// Check that the keys of each map are equivalent.
|
||||
for k := range want {
|
||||
if _, ok := haveMap[k]; !ok {
|
||||
bunk := kjoin(key, k)
|
||||
t.Fatalf("Could not find key '%s' in parser output.", bunk)
|
||||
}
|
||||
}
|
||||
for k := range haveMap {
|
||||
if _, ok := want[k]; !ok {
|
||||
bunk := kjoin(key, k)
|
||||
t.Fatalf("Could not find key '%s' in expected output.", bunk)
|
||||
}
|
||||
}
|
||||
|
||||
// Okay, now make sure that each value is equivalent.
|
||||
for k := range want {
|
||||
CmpJSON(t, kjoin(key, k), want[k], haveMap[k])
|
||||
}
|
||||
}
|
||||
|
||||
func cmpJSONArrays(t *testing.T, key string, want, have interface{}) {
|
||||
t.Helper()
|
||||
wantSlice, ok := want.([]interface{})
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("'value' should be a JSON array when 'type=array', but it is a %T", want))
|
||||
}
|
||||
|
||||
haveSlice, ok := have.([]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Malformed output from your encoder: 'value' is not a JSON array: %T", have)
|
||||
}
|
||||
|
||||
if len(wantSlice) != len(haveSlice) {
|
||||
t.Fatalf("Array lengths differ for key '%s':\n"+
|
||||
" Expected: %d\n"+
|
||||
" Your encoder: %d",
|
||||
key, len(wantSlice), len(haveSlice))
|
||||
}
|
||||
for i := 0; i < len(wantSlice); i++ {
|
||||
CmpJSON(t, key, wantSlice[i], haveSlice[i])
|
||||
}
|
||||
}
|
||||
|
||||
func cmpJSONValues(t *testing.T, key string, want, have map[string]interface{}) {
|
||||
t.Helper()
|
||||
wantType, ok := want["type"].(string)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("'type' should be a string, but it is a %T", want["type"]))
|
||||
}
|
||||
|
||||
haveType, ok := have["type"].(string)
|
||||
if !ok {
|
||||
t.Fatalf("Malformed output from your encoder: 'type' is not a string: %T", have["type"])
|
||||
}
|
||||
|
||||
if wantType != haveType {
|
||||
valMismatch(t, key, wantType, haveType, want, have)
|
||||
}
|
||||
|
||||
// If this is an array, then we've got to do some work to check equality.
|
||||
if wantType == "array" {
|
||||
cmpJSONArrays(t, key, want, have)
|
||||
return
|
||||
}
|
||||
|
||||
// Atomic values are always strings
|
||||
wantVal, ok := want["value"].(string)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("'value' %v should be a string, but it is a %[1]T", want["value"]))
|
||||
}
|
||||
|
||||
haveVal, ok := have["value"].(string)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("Malformed output from your encoder: %T is not a string", have["value"]))
|
||||
}
|
||||
|
||||
// Excepting floats and datetimes, other values can be compared as strings.
|
||||
switch wantType {
|
||||
case "float":
|
||||
cmpFloats(t, key, wantVal, haveVal)
|
||||
case "datetime", "datetime-local", "date-local", "time-local":
|
||||
cmpAsDatetimes(t, key, wantType, wantVal, haveVal)
|
||||
default:
|
||||
cmpAsStrings(t, key, wantVal, haveVal)
|
||||
}
|
||||
}
|
||||
|
||||
func cmpAsStrings(t *testing.T, key string, want, have string) {
|
||||
t.Helper()
|
||||
if want != have {
|
||||
t.Fatalf("Values for key '%s' don't match:\n"+
|
||||
" Expected: %s\n"+
|
||||
" Your encoder: %s",
|
||||
key, want, have)
|
||||
}
|
||||
}
|
||||
|
||||
func cmpFloats(t *testing.T, key string, want, have string) {
|
||||
t.Helper()
|
||||
// Special case for NaN, since NaN != NaN.
|
||||
if strings.HasSuffix(want, "nan") || strings.HasSuffix(have, "nan") {
|
||||
if want != have {
|
||||
t.Fatalf("Values for key '%s' don't match:\n"+
|
||||
" Expected: %v\n"+
|
||||
" Your encoder: %v",
|
||||
key, want, have)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
wantF, err := strconv.ParseFloat(want, 64)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Could not read '%s' as a float value for key '%s'", want, key))
|
||||
}
|
||||
|
||||
haveF, err := strconv.ParseFloat(have, 64)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Malformed output from your encoder: key '%s' is not a float: '%s'", key, have))
|
||||
}
|
||||
|
||||
if wantF != haveF {
|
||||
t.Fatalf("Values for key '%s' don't match:\n"+
|
||||
" Expected: %v\n"+
|
||||
" Your encoder: %v",
|
||||
key, wantF, haveF)
|
||||
}
|
||||
}
|
||||
|
||||
var datetimeRepl = strings.NewReplacer(
|
||||
" ", "T",
|
||||
"t", "T",
|
||||
"z", "Z")
|
||||
|
||||
var layouts = map[string]string{
|
||||
"datetime": time.RFC3339Nano,
|
||||
"datetime-local": "2006-01-02T15:04:05.999999999",
|
||||
"date-local": "2006-01-02",
|
||||
"time-local": "15:04:05",
|
||||
}
|
||||
|
||||
func cmpAsDatetimes(t *testing.T, key string, kind, want, have string) {
|
||||
t.Helper()
|
||||
layout, ok := layouts[kind]
|
||||
if !ok {
|
||||
panic("should never happen")
|
||||
}
|
||||
|
||||
wantT, err := time.Parse(layout, datetimeRepl.Replace(want))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Could not read '%s' as a datetime value for key '%s'", want, key))
|
||||
}
|
||||
|
||||
haveT, err := time.Parse(layout, datetimeRepl.Replace(want))
|
||||
if err != nil {
|
||||
t.Fatalf("Malformed output from your encoder: key '%s' is not a datetime: '%s'", key, have)
|
||||
return
|
||||
}
|
||||
if !wantT.Equal(haveT) {
|
||||
t.Fatalf("Values for key '%s' don't match:\n"+
|
||||
" Expected: %v\n"+
|
||||
" Your encoder: %v",
|
||||
key, wantT, haveT)
|
||||
}
|
||||
}
|
||||
|
||||
func kjoin(old, key string) string {
|
||||
if len(old) == 0 {
|
||||
return key
|
||||
}
|
||||
return old + "." + key
|
||||
}
|
||||
|
||||
func isValue(m map[string]interface{}) bool {
|
||||
if len(m) != 2 {
|
||||
return false
|
||||
}
|
||||
if _, ok := m["type"]; !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := m["value"]; !ok {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func mismatch(t *testing.T, key string, wantType string, want, have interface{}) {
|
||||
t.Helper()
|
||||
t.Fatalf("Key '%s' is not an %s but %[4]T:\n"+
|
||||
" Expected: %#[3]v\n"+
|
||||
" Your encoder: %#[4]v",
|
||||
key, wantType, want, have)
|
||||
}
|
||||
|
||||
func valMismatch(t *testing.T, key string, wantType, haveType string, want, have interface{}) {
|
||||
t.Helper()
|
||||
t.Fatalf("Key '%s' is not an %s but %s:\n"+
|
||||
" Expected: %#[3]v\n"+
|
||||
" Your encoder: %#[4]v",
|
||||
key, wantType, haveType, want, have)
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
// Remove JSON tags to a data structure as returned by toml-test.
|
||||
func rmTag(typedJSON interface{}) (interface{}, error) {
|
||||
// Check if key is in the table m.
|
||||
in := func(key string, m map[string]interface{}) bool {
|
||||
_, ok := m[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Switch on the data type.
|
||||
switch v := typedJSON.(type) {
|
||||
// Object: this can either be a TOML table or a primitive with tags.
|
||||
case map[string]interface{}:
|
||||
// This value represents a primitive: remove the tags and return just
|
||||
// the primitive value.
|
||||
if len(v) == 2 && in("type", v) && in("value", v) {
|
||||
ut, err := untag(v)
|
||||
if err != nil {
|
||||
return ut, fmt.Errorf("tag.Remove: %w", err)
|
||||
}
|
||||
return ut, nil
|
||||
}
|
||||
|
||||
// Table: remove tags on all children.
|
||||
m := make(map[string]interface{}, len(v))
|
||||
for k, v2 := range v {
|
||||
var err error
|
||||
m[k], err = rmTag(v2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
|
||||
// Array: remove tags from all items.
|
||||
case []interface{}:
|
||||
a := make([]interface{}, len(v))
|
||||
for i := range v {
|
||||
var err error
|
||||
a[i], err = rmTag(v[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// The top level must be an object or array.
|
||||
return nil, fmt.Errorf("unrecognized JSON format '%T'", typedJSON)
|
||||
}
|
||||
|
||||
// Return a primitive: read the "type" and convert the "value" to that.
|
||||
func untag(typed map[string]interface{}) (interface{}, error) {
|
||||
t := typed["type"].(string)
|
||||
v := typed["value"].(string)
|
||||
switch t {
|
||||
case "string":
|
||||
return v, nil
|
||||
case "integer":
|
||||
n, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("untag: %w", err)
|
||||
}
|
||||
return n, nil
|
||||
case "float":
|
||||
f, err := strconv.ParseFloat(v, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("untag: %w", err)
|
||||
}
|
||||
return f, nil
|
||||
|
||||
// toml.LocalDate{Year:2020, Month:12, Day:12}
|
||||
case "datetime":
|
||||
return time.Parse("2006-01-02T15:04:05.999999999Z07:00", v)
|
||||
case "datetime-local":
|
||||
var t toml.LocalDateTime
|
||||
err := t.UnmarshalText([]byte(v))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("untag: %w", err)
|
||||
}
|
||||
return t, nil
|
||||
case "date-local":
|
||||
var t toml.LocalDate
|
||||
err := t.UnmarshalText([]byte(v))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("untag: %w", err)
|
||||
}
|
||||
return t, nil
|
||||
case "time-local":
|
||||
var t toml.LocalTime
|
||||
err := t.UnmarshalText([]byte(v))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("untag: %w", err)
|
||||
}
|
||||
return t, nil
|
||||
case "bool":
|
||||
switch v {
|
||||
case "true":
|
||||
return true, nil
|
||||
case "false":
|
||||
return false, nil
|
||||
}
|
||||
return nil, fmt.Errorf("untag: could not parse %q as a boolean", v)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("untag: unrecognized tag type %q", t)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Package testsuite provides helper functions for interoperating with the
|
||||
// language-agnostic TOML test suite at github.com/BurntSushi/toml-test.
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
// Marshal is a helper function for calling toml.Marshal
|
||||
//
|
||||
// Only needed to avoid package import loops.
|
||||
func Marshal(v interface{}) ([]byte, error) {
|
||||
return toml.Marshal(v)
|
||||
}
|
||||
|
||||
// Unmarshal is a helper function for calling toml.Unmarshal.
|
||||
//
|
||||
// Only needed to avoid package import loops.
|
||||
func Unmarshal(data []byte, v interface{}) error {
|
||||
return toml.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// ValueToTaggedJSON takes a data structure and returns the tagged JSON
|
||||
// representation.
|
||||
func ValueToTaggedJSON(doc interface{}) ([]byte, error) {
|
||||
return json.MarshalIndent(addTag(doc), "", " ")
|
||||
}
|
||||
|
||||
// DecodeStdin is a helper function for the toml-test binary interface. TOML input
|
||||
// is read from STDIN and a resulting tagged JSON representation is written to
|
||||
// STDOUT.
|
||||
func DecodeStdin() error {
|
||||
var decoded map[string]interface{}
|
||||
|
||||
if err := toml.NewDecoder(os.Stdin).Decode(&decoded); err != nil {
|
||||
return fmt.Errorf("error decoding TOML: %w", err)
|
||||
}
|
||||
|
||||
j := json.NewEncoder(os.Stdout)
|
||||
j.SetIndent("", " ")
|
||||
if err := j.Encode(addTag(decoded)); err != nil {
|
||||
return fmt.Errorf("error encoding JSON: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeStdin is a helper function for the toml-test binary interface. Tagged
|
||||
// JSON is read from STDIN and a resulting TOML representation is written to
|
||||
// STDOUT.
|
||||
func EncodeStdin() error {
|
||||
var j interface{}
|
||||
err := json.NewDecoder(os.Stdin).Decode(&j)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rm, err := rmTag(j)
|
||||
if err != nil {
|
||||
return fmt.Errorf("removing tags: %w", err)
|
||||
}
|
||||
|
||||
return toml.NewEncoder(os.Stdout).Encode(rm)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package tracker
|
||||
|
||||
import "github.com/pelletier/go-toml/v2/unstable"
|
||||
|
||||
// KeyTracker is a tracker that keeps track of the current Key as the AST is
|
||||
// walked.
|
||||
type KeyTracker struct {
|
||||
k []string
|
||||
}
|
||||
|
||||
// UpdateTable sets the state of the tracker with the AST table node.
|
||||
func (t *KeyTracker) UpdateTable(node *unstable.Node) {
|
||||
t.reset()
|
||||
t.Push(node)
|
||||
}
|
||||
|
||||
// UpdateArrayTable sets the state of the tracker with the AST array table node.
|
||||
func (t *KeyTracker) UpdateArrayTable(node *unstable.Node) {
|
||||
t.reset()
|
||||
t.Push(node)
|
||||
}
|
||||
|
||||
// Push the given key on the stack.
|
||||
func (t *KeyTracker) Push(node *unstable.Node) {
|
||||
it := node.Key()
|
||||
for it.Next() {
|
||||
t.k = append(t.k, string(it.Node().Data))
|
||||
}
|
||||
}
|
||||
|
||||
// Pop key from stack.
|
||||
func (t *KeyTracker) Pop(node *unstable.Node) {
|
||||
it := node.Key()
|
||||
for it.Next() {
|
||||
t.k = t.k[:len(t.k)-1]
|
||||
}
|
||||
}
|
||||
|
||||
// Key returns the current key.
|
||||
func (t *KeyTracker) Key() []string {
|
||||
k := make([]string, len(t.k))
|
||||
copy(k, t.k)
|
||||
return k
|
||||
}
|
||||
|
||||
func (t *KeyTracker) reset() {
|
||||
t.k = t.k[:0]
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/unstable"
|
||||
)
|
||||
|
||||
type keyKind uint8
|
||||
|
||||
const (
|
||||
invalidKind keyKind = iota
|
||||
valueKind
|
||||
tableKind
|
||||
arrayTableKind
|
||||
)
|
||||
|
||||
func (k keyKind) String() string {
|
||||
switch k {
|
||||
case invalidKind:
|
||||
return "invalid"
|
||||
case valueKind:
|
||||
return "value"
|
||||
case tableKind:
|
||||
return "table"
|
||||
case arrayTableKind:
|
||||
return "array table"
|
||||
}
|
||||
panic("missing keyKind string mapping")
|
||||
}
|
||||
|
||||
// SeenTracker tracks which keys have been seen with which TOML type to flag
|
||||
// duplicates and mismatches according to the spec.
|
||||
//
|
||||
// Each node in the visited tree is represented by an entry. Each entry has an
|
||||
// identifier, which is provided by a counter. Entries are stored in the array
|
||||
// entries. As new nodes are discovered (referenced for the first time in the
|
||||
// TOML document), entries are created and appended to the array. An entry
|
||||
// points to its parent using its id.
|
||||
//
|
||||
// To find whether a given key (sequence of []byte) has already been visited,
|
||||
// the entries are linearly searched, looking for one with the right name and
|
||||
// parent id.
|
||||
//
|
||||
// Given that all keys appear in the document after their parent, it is
|
||||
// guaranteed that all descendants of a node are stored after the node, this
|
||||
// speeds up the search process.
|
||||
//
|
||||
// When encountering [[array tables]], the descendants of that node are removed
|
||||
// to allow that branch of the tree to be "rediscovered". To maintain the
|
||||
// invariant above, the deletion process needs to keep the order of entries.
|
||||
// This results in more copies in that case.
|
||||
type SeenTracker struct {
|
||||
entries []entry
|
||||
currentIdx int
|
||||
}
|
||||
|
||||
var pool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &SeenTracker{}
|
||||
},
|
||||
}
|
||||
|
||||
func (s *SeenTracker) reset() {
|
||||
// Always contains a root element at index 0.
|
||||
s.currentIdx = 0
|
||||
if len(s.entries) == 0 {
|
||||
s.entries = make([]entry, 1, 2)
|
||||
} else {
|
||||
s.entries = s.entries[:1]
|
||||
}
|
||||
s.entries[0].child = -1
|
||||
s.entries[0].next = -1
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
// Use -1 to indicate no child or no sibling.
|
||||
child int
|
||||
next int
|
||||
|
||||
name []byte
|
||||
kind keyKind
|
||||
explicit bool
|
||||
kv bool
|
||||
}
|
||||
|
||||
// Find the index of the child of parentIdx with key k. Returns -1 if
|
||||
// it does not exist.
|
||||
func (s *SeenTracker) find(parentIdx int, k []byte) int {
|
||||
for i := s.entries[parentIdx].child; i >= 0; i = s.entries[i].next {
|
||||
if bytes.Equal(s.entries[i].name, k) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Remove all descendants of node at position idx.
|
||||
func (s *SeenTracker) clear(idx int) {
|
||||
if idx >= len(s.entries) {
|
||||
return
|
||||
}
|
||||
|
||||
for i := s.entries[idx].child; i >= 0; {
|
||||
next := s.entries[i].next
|
||||
n := s.entries[0].next
|
||||
s.entries[0].next = i
|
||||
s.entries[i].next = n
|
||||
s.entries[i].name = nil
|
||||
s.clear(i)
|
||||
i = next
|
||||
}
|
||||
|
||||
s.entries[idx].child = -1
|
||||
}
|
||||
|
||||
func (s *SeenTracker) create(parentIdx int, name []byte, kind keyKind, explicit bool, kv bool) int {
|
||||
e := entry{
|
||||
child: -1,
|
||||
next: s.entries[parentIdx].child,
|
||||
|
||||
name: name,
|
||||
kind: kind,
|
||||
explicit: explicit,
|
||||
kv: kv,
|
||||
}
|
||||
var idx int
|
||||
if s.entries[0].next >= 0 {
|
||||
idx = s.entries[0].next
|
||||
s.entries[0].next = s.entries[idx].next
|
||||
s.entries[idx] = e
|
||||
} else {
|
||||
idx = len(s.entries)
|
||||
s.entries = append(s.entries, e)
|
||||
}
|
||||
|
||||
s.entries[parentIdx].child = idx
|
||||
|
||||
return idx
|
||||
}
|
||||
|
||||
func (s *SeenTracker) setExplicitFlag(parentIdx int) {
|
||||
for i := s.entries[parentIdx].child; i >= 0; i = s.entries[i].next {
|
||||
if s.entries[i].kv {
|
||||
s.entries[i].explicit = true
|
||||
s.entries[i].kv = false
|
||||
}
|
||||
s.setExplicitFlag(i)
|
||||
}
|
||||
}
|
||||
|
||||
// CheckExpression takes a top-level node and checks that it does not contain
|
||||
// keys that have been seen in previous calls, and validates that types are
|
||||
// consistent. It returns true if it is the first time this node's key is seen.
|
||||
// Useful to clear array tables on first use.
|
||||
func (s *SeenTracker) CheckExpression(node *unstable.Node) (bool, error) {
|
||||
if s.entries == nil {
|
||||
s.reset()
|
||||
}
|
||||
switch node.Kind {
|
||||
case unstable.KeyValue:
|
||||
return s.checkKeyValue(node)
|
||||
case unstable.Table:
|
||||
return s.checkTable(node)
|
||||
case unstable.ArrayTable:
|
||||
return s.checkArrayTable(node)
|
||||
default:
|
||||
panic(fmt.Errorf("this should not be a top level node type: %s", node.Kind))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SeenTracker) checkTable(node *unstable.Node) (bool, error) {
|
||||
if s.currentIdx >= 0 {
|
||||
s.setExplicitFlag(s.currentIdx)
|
||||
}
|
||||
|
||||
it := node.Key()
|
||||
|
||||
parentIdx := 0
|
||||
|
||||
// This code is duplicated in checkArrayTable. This is because factoring
|
||||
// it in a function requires to copy the iterator, or allocate it to the
|
||||
// heap, which is not cheap.
|
||||
for it.Next() {
|
||||
if it.IsLast() {
|
||||
break
|
||||
}
|
||||
|
||||
k := it.Node().Data
|
||||
|
||||
idx := s.find(parentIdx, k)
|
||||
|
||||
if idx < 0 {
|
||||
idx = s.create(parentIdx, k, tableKind, false, false)
|
||||
} else {
|
||||
entry := s.entries[idx]
|
||||
if entry.kind == valueKind {
|
||||
return false, fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
|
||||
}
|
||||
}
|
||||
parentIdx = idx
|
||||
}
|
||||
|
||||
k := it.Node().Data
|
||||
idx := s.find(parentIdx, k)
|
||||
|
||||
first := false
|
||||
if idx >= 0 {
|
||||
kind := s.entries[idx].kind
|
||||
if kind != tableKind {
|
||||
return false, fmt.Errorf("toml: key %s should be a table, not a %s", string(k), kind)
|
||||
}
|
||||
if s.entries[idx].explicit {
|
||||
return false, fmt.Errorf("toml: table %s already exists", string(k))
|
||||
}
|
||||
s.entries[idx].explicit = true
|
||||
} else {
|
||||
idx = s.create(parentIdx, k, tableKind, true, false)
|
||||
first = true
|
||||
}
|
||||
|
||||
s.currentIdx = idx
|
||||
|
||||
return first, nil
|
||||
}
|
||||
|
||||
func (s *SeenTracker) checkArrayTable(node *unstable.Node) (bool, error) {
|
||||
if s.currentIdx >= 0 {
|
||||
s.setExplicitFlag(s.currentIdx)
|
||||
}
|
||||
|
||||
it := node.Key()
|
||||
|
||||
parentIdx := 0
|
||||
|
||||
for it.Next() {
|
||||
if it.IsLast() {
|
||||
break
|
||||
}
|
||||
|
||||
k := it.Node().Data
|
||||
|
||||
idx := s.find(parentIdx, k)
|
||||
|
||||
if idx < 0 {
|
||||
idx = s.create(parentIdx, k, tableKind, false, false)
|
||||
} else {
|
||||
entry := s.entries[idx]
|
||||
if entry.kind == valueKind {
|
||||
return false, fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
|
||||
}
|
||||
}
|
||||
|
||||
parentIdx = idx
|
||||
}
|
||||
|
||||
k := it.Node().Data
|
||||
idx := s.find(parentIdx, k)
|
||||
|
||||
firstTime := idx < 0
|
||||
if firstTime {
|
||||
idx = s.create(parentIdx, k, arrayTableKind, true, false)
|
||||
} else {
|
||||
kind := s.entries[idx].kind
|
||||
if kind != arrayTableKind {
|
||||
return false, fmt.Errorf("toml: key %s already exists as a %s, but should be an array table", kind, string(k))
|
||||
}
|
||||
s.clear(idx)
|
||||
}
|
||||
|
||||
s.currentIdx = idx
|
||||
|
||||
return firstTime, nil
|
||||
}
|
||||
|
||||
func (s *SeenTracker) checkKeyValue(node *unstable.Node) (bool, error) {
|
||||
parentIdx := s.currentIdx
|
||||
it := node.Key()
|
||||
|
||||
for it.Next() {
|
||||
k := it.Node().Data
|
||||
|
||||
idx := s.find(parentIdx, k)
|
||||
|
||||
if idx < 0 {
|
||||
idx = s.create(parentIdx, k, tableKind, false, true)
|
||||
} else {
|
||||
entry := s.entries[idx]
|
||||
switch {
|
||||
case it.IsLast():
|
||||
return false, fmt.Errorf("toml: key %s is already defined", string(k))
|
||||
case entry.kind != tableKind:
|
||||
return false, fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
|
||||
case entry.explicit:
|
||||
return false, fmt.Errorf("toml: cannot redefine table %s that has already been explicitly defined", string(k))
|
||||
}
|
||||
}
|
||||
|
||||
parentIdx = idx
|
||||
}
|
||||
|
||||
s.entries[parentIdx].kind = valueKind
|
||||
|
||||
value := node.Value()
|
||||
|
||||
switch value.Kind {
|
||||
case unstable.InlineTable:
|
||||
return s.checkInlineTable(value)
|
||||
case unstable.Array:
|
||||
return s.checkArray(value)
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SeenTracker) checkArray(node *unstable.Node) (first bool, err error) {
|
||||
it := node.Children()
|
||||
for it.Next() {
|
||||
n := it.Node()
|
||||
switch n.Kind { //nolint:exhaustive
|
||||
case unstable.InlineTable:
|
||||
first, err = s.checkInlineTable(n)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
case unstable.Array:
|
||||
first, err = s.checkArray(n)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return first, nil
|
||||
}
|
||||
|
||||
func (s *SeenTracker) checkInlineTable(node *unstable.Node) (first bool, err error) {
|
||||
s = pool.Get().(*SeenTracker)
|
||||
s.reset()
|
||||
|
||||
it := node.Children()
|
||||
for it.Next() {
|
||||
n := it.Node()
|
||||
first, err = s.checkKeyValue(n)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// As inline tables are self-contained, the tracker does not
|
||||
// need to retain the details of what they contain. The
|
||||
// keyValue element that creates the inline table is kept to
|
||||
// mark the presence of the inline table and prevent
|
||||
// redefinition of its keys: check* functions cannot walk into
|
||||
// a value.
|
||||
pool.Put(s)
|
||||
return first, nil
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestEntrySize(t *testing.T) {
|
||||
// Validate no regression on the size of entry{}. This is a critical bit for
|
||||
// performance of unmarshaling documents. Should only be increased with care
|
||||
// and a very good reason.
|
||||
maxExpectedEntrySize := 48
|
||||
entrySize := int(reflect.TypeOf(entry{}).Size())
|
||||
assert.True(t,
|
||||
entrySize <= maxExpectedEntrySize,
|
||||
"Expected entry to be less than or equal to %d, got: %d",
|
||||
maxExpectedEntrySize, entrySize,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package tracker provides functions for keeping track of AST nodes.
|
||||
package tracker
|
||||
@@ -1,510 +0,0 @@
|
||||
// TOML lexer.// Written using the principles developped by Rob Pike in
|
||||
// http://www.youtube.com/watch?v=HxaD_trXwRE
|
||||
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var dateRegexp *regexp.Regexp
|
||||
|
||||
// Define tokens
|
||||
type tokenType int
|
||||
|
||||
const (
|
||||
eof = -(iota + 1)
|
||||
)
|
||||
|
||||
const (
|
||||
tokenError tokenType = iota
|
||||
tokenEOF
|
||||
tokenComment
|
||||
tokenKey
|
||||
tokenEqual
|
||||
tokenString
|
||||
tokenInteger
|
||||
tokenTrue
|
||||
tokenFalse
|
||||
tokenFloat
|
||||
tokenLeftBracket
|
||||
tokenRightBracket
|
||||
tokenDoubleLeftBracket
|
||||
tokenDoubleRightBracket
|
||||
tokenDate
|
||||
tokenKeyGroup
|
||||
tokenKeyGroupArray
|
||||
tokenComma
|
||||
tokenEOL
|
||||
)
|
||||
|
||||
type token struct {
|
||||
typ tokenType
|
||||
val string
|
||||
}
|
||||
|
||||
func (i token) String() string {
|
||||
switch i.typ {
|
||||
case tokenEOF:
|
||||
return "EOF"
|
||||
case tokenError:
|
||||
return i.val
|
||||
}
|
||||
|
||||
if len(i.val) > 10 {
|
||||
return fmt.Sprintf("%.10q...", i.val)
|
||||
}
|
||||
return fmt.Sprintf("%q", i.val)
|
||||
}
|
||||
|
||||
func isSpace(r rune) bool {
|
||||
return r == ' ' || r == '\t'
|
||||
}
|
||||
|
||||
func isAlphanumeric(r rune) bool {
|
||||
return unicode.IsLetter(r) || r == '_'
|
||||
}
|
||||
|
||||
func isKeyChar(r rune) bool {
|
||||
// "Keys start with the first non-whitespace character and end with the last
|
||||
// non-whitespace character before the equals sign."
|
||||
return !(isSpace(r) || r == '\r' || r == '\n' || r == eof || r == '=')
|
||||
}
|
||||
|
||||
func isDigit(r rune) bool {
|
||||
return unicode.IsNumber(r)
|
||||
}
|
||||
|
||||
func isHexDigit(r rune) bool {
|
||||
return isDigit(r) ||
|
||||
r == 'A' || r == 'B' || r == 'C' || r == 'D' || r == 'E' || r == 'F'
|
||||
}
|
||||
|
||||
// Define lexer
|
||||
type lexer struct {
|
||||
input string
|
||||
start int
|
||||
pos int
|
||||
width int
|
||||
tokens chan token
|
||||
depth int
|
||||
}
|
||||
|
||||
func (l *lexer) run() {
|
||||
for state := lexVoid; state != nil; {
|
||||
state = state(l)
|
||||
}
|
||||
close(l.tokens)
|
||||
}
|
||||
|
||||
func (l *lexer) emit(t tokenType) {
|
||||
l.tokens <- token{t, l.input[l.start:l.pos]}
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
func (l *lexer) emitWithValue(t tokenType, value string) {
|
||||
l.tokens <- token{t, value}
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
func (l *lexer) next() rune {
|
||||
if l.pos >= len(l.input) {
|
||||
l.width = 0
|
||||
return eof
|
||||
}
|
||||
var r rune
|
||||
r, l.width = utf8.DecodeRuneInString(l.input[l.pos:])
|
||||
l.pos += l.width
|
||||
return r
|
||||
}
|
||||
|
||||
func (l *lexer) ignore() {
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
func (l *lexer) backup() {
|
||||
l.pos -= l.width
|
||||
}
|
||||
|
||||
func (l *lexer) errorf(format string, args ...interface{}) stateFn {
|
||||
l.tokens <- token{
|
||||
tokenError,
|
||||
fmt.Sprintf(format, args...),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *lexer) peek() rune {
|
||||
r := l.next()
|
||||
l.backup()
|
||||
return r
|
||||
}
|
||||
|
||||
func (l *lexer) accept(valid string) bool {
|
||||
if strings.IndexRune(valid, l.next()) >= 0 {
|
||||
return true
|
||||
}
|
||||
l.backup()
|
||||
return false
|
||||
}
|
||||
|
||||
func (l *lexer) follow(next string) bool {
|
||||
return strings.HasPrefix(l.input[l.pos:], next)
|
||||
}
|
||||
|
||||
// Define state functions
|
||||
type stateFn func(*lexer) stateFn
|
||||
|
||||
func lexVoid(l *lexer) stateFn {
|
||||
for {
|
||||
next := l.peek()
|
||||
switch next {
|
||||
case '[':
|
||||
return lexKeyGroup
|
||||
case '#':
|
||||
return lexComment
|
||||
case '=':
|
||||
return lexEqual
|
||||
}
|
||||
|
||||
if isSpace(next) {
|
||||
l.ignore()
|
||||
}
|
||||
|
||||
if l.depth > 0 {
|
||||
return lexRvalue
|
||||
}
|
||||
|
||||
if isKeyChar(next) {
|
||||
return lexKey
|
||||
}
|
||||
|
||||
if l.next() == eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
l.emit(tokenEOF)
|
||||
return nil
|
||||
}
|
||||
|
||||
func lexRvalue(l *lexer) stateFn {
|
||||
for {
|
||||
next := l.peek()
|
||||
switch next {
|
||||
case '.':
|
||||
return l.errorf("cannot start float with a dot")
|
||||
case '=':
|
||||
return l.errorf("cannot have multiple equals for the same key")
|
||||
case '[':
|
||||
l.depth += 1
|
||||
return lexLeftBracket
|
||||
case ']':
|
||||
l.depth -= 1
|
||||
return lexRightBracket
|
||||
case '#':
|
||||
return lexComment
|
||||
case '"':
|
||||
return lexString
|
||||
case ',':
|
||||
return lexComma
|
||||
case '\n':
|
||||
l.ignore()
|
||||
l.pos += 1
|
||||
if l.depth == 0 {
|
||||
return lexVoid
|
||||
} else {
|
||||
return lexRvalue
|
||||
}
|
||||
}
|
||||
|
||||
if l.follow("true") {
|
||||
return lexTrue
|
||||
}
|
||||
|
||||
if l.follow("false") {
|
||||
return lexFalse
|
||||
}
|
||||
|
||||
if isAlphanumeric(next) {
|
||||
return lexKey
|
||||
}
|
||||
|
||||
if dateRegexp.FindString(l.input[l.pos:]) != "" {
|
||||
return lexDate
|
||||
}
|
||||
|
||||
if next == '+' || next == '-' || isDigit(next) {
|
||||
return lexNumber
|
||||
}
|
||||
|
||||
if isSpace(next) {
|
||||
l.ignore()
|
||||
}
|
||||
|
||||
if l.next() == eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
l.emit(tokenEOF)
|
||||
return nil
|
||||
}
|
||||
|
||||
func lexDate(l *lexer) stateFn {
|
||||
l.ignore()
|
||||
l.pos += 20 // Fixed size of a date in TOML
|
||||
l.emit(tokenDate)
|
||||
return lexRvalue
|
||||
}
|
||||
|
||||
func lexTrue(l *lexer) stateFn {
|
||||
l.ignore()
|
||||
l.pos += 4
|
||||
l.emit(tokenTrue)
|
||||
return lexRvalue
|
||||
}
|
||||
|
||||
func lexFalse(l *lexer) stateFn {
|
||||
l.ignore()
|
||||
l.pos += 5
|
||||
l.emit(tokenFalse)
|
||||
return lexRvalue
|
||||
}
|
||||
|
||||
func lexEqual(l *lexer) stateFn {
|
||||
l.ignore()
|
||||
l.accept("=")
|
||||
l.emit(tokenEqual)
|
||||
return lexRvalue
|
||||
}
|
||||
|
||||
func lexComma(l *lexer) stateFn {
|
||||
l.ignore()
|
||||
l.accept(",")
|
||||
l.emit(tokenComma)
|
||||
return lexRvalue
|
||||
}
|
||||
|
||||
func lexKey(l *lexer) stateFn {
|
||||
l.ignore()
|
||||
for isKeyChar(l.next()) {
|
||||
}
|
||||
l.backup()
|
||||
l.emit(tokenKey)
|
||||
return lexVoid
|
||||
}
|
||||
|
||||
func lexComment(l *lexer) stateFn {
|
||||
for {
|
||||
next := l.next()
|
||||
if next == '\n' || next == eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
l.ignore()
|
||||
return lexVoid
|
||||
}
|
||||
|
||||
func lexLeftBracket(l *lexer) stateFn {
|
||||
l.ignore()
|
||||
l.pos += 1
|
||||
l.emit(tokenLeftBracket)
|
||||
return lexRvalue
|
||||
}
|
||||
|
||||
func lexString(l *lexer) stateFn {
|
||||
l.pos += 1
|
||||
l.ignore()
|
||||
growing_string := ""
|
||||
|
||||
for {
|
||||
if l.peek() == '"' {
|
||||
l.emitWithValue(tokenString, growing_string)
|
||||
l.pos += 1
|
||||
l.ignore()
|
||||
return lexRvalue
|
||||
}
|
||||
|
||||
if l.follow("\\\"") {
|
||||
l.pos += 1
|
||||
growing_string += "\""
|
||||
} else if l.follow("\\n") {
|
||||
l.pos += 1
|
||||
growing_string += "\n"
|
||||
} else if l.follow("\\b") {
|
||||
l.pos += 1
|
||||
growing_string += "\b"
|
||||
} else if l.follow("\\f") {
|
||||
l.pos += 1
|
||||
growing_string += "\f"
|
||||
} else if l.follow("\\/") {
|
||||
l.pos += 1
|
||||
growing_string += "/"
|
||||
} else if l.follow("\\t") {
|
||||
l.pos += 1
|
||||
growing_string += "\t"
|
||||
} else if l.follow("\\r") {
|
||||
l.pos += 1
|
||||
growing_string += "\r"
|
||||
} else if l.follow("\\\\") {
|
||||
l.pos += 1
|
||||
growing_string += "\\"
|
||||
} else if l.follow("\\u") {
|
||||
l.pos += 2
|
||||
code := ""
|
||||
for i := 0; i < 4; i++ {
|
||||
c := l.peek()
|
||||
l.pos += 1
|
||||
if !isHexDigit(c) {
|
||||
return l.errorf("unfinished unicode escape")
|
||||
}
|
||||
code = code + string(c)
|
||||
}
|
||||
l.pos -= 1
|
||||
intcode, err := strconv.ParseInt(code, 16, 32)
|
||||
if err != nil {
|
||||
return l.errorf("invalid unicode escape: \\u" + code)
|
||||
}
|
||||
growing_string += string(rune(intcode))
|
||||
} else if l.follow("\\") {
|
||||
l.pos += 1
|
||||
return l.errorf("invalid escape sequence: \\" + string(l.peek()))
|
||||
} else {
|
||||
growing_string += string(l.peek())
|
||||
}
|
||||
|
||||
if l.next() == eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return l.errorf("unclosed string")
|
||||
}
|
||||
|
||||
func lexKeyGroup(l *lexer) stateFn {
|
||||
l.ignore()
|
||||
l.pos += 1
|
||||
|
||||
if l.peek() == '[' {
|
||||
// token '[[' signifies an array of anonymous key groups
|
||||
l.pos += 1
|
||||
l.emit(tokenDoubleLeftBracket)
|
||||
return lexInsideKeyGroupArray
|
||||
} else {
|
||||
// vanilla key group
|
||||
l.emit(tokenLeftBracket)
|
||||
return lexInsideKeyGroup
|
||||
}
|
||||
}
|
||||
|
||||
func lexInsideKeyGroupArray(l *lexer) stateFn {
|
||||
for {
|
||||
if l.peek() == ']' {
|
||||
if l.pos > l.start {
|
||||
l.emit(tokenKeyGroupArray)
|
||||
}
|
||||
l.ignore()
|
||||
l.pos += 1
|
||||
if l.peek() != ']' {
|
||||
break // error
|
||||
}
|
||||
l.pos += 1
|
||||
l.emit(tokenDoubleRightBracket)
|
||||
return lexVoid
|
||||
} else if l.peek() == '[' {
|
||||
return l.errorf("group name cannot contain ']'")
|
||||
}
|
||||
|
||||
if l.next() == eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
return l.errorf("unclosed key group array")
|
||||
}
|
||||
|
||||
func lexInsideKeyGroup(l *lexer) stateFn {
|
||||
for {
|
||||
if l.peek() == ']' {
|
||||
if l.pos > l.start {
|
||||
l.emit(tokenKeyGroup)
|
||||
}
|
||||
l.ignore()
|
||||
l.pos += 1
|
||||
l.emit(tokenRightBracket)
|
||||
return lexVoid
|
||||
} else if l.peek() == '[' {
|
||||
return l.errorf("group name cannot contain ']'")
|
||||
}
|
||||
|
||||
if l.next() == eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
return l.errorf("unclosed key group")
|
||||
}
|
||||
|
||||
func lexRightBracket(l *lexer) stateFn {
|
||||
l.ignore()
|
||||
l.pos += 1
|
||||
l.emit(tokenRightBracket)
|
||||
return lexRvalue
|
||||
}
|
||||
|
||||
func lexNumber(l *lexer) stateFn {
|
||||
l.ignore()
|
||||
if !l.accept("+") {
|
||||
l.accept("-")
|
||||
}
|
||||
point_seen := false
|
||||
digit_seen := false
|
||||
for {
|
||||
next := l.next()
|
||||
if next == '.' {
|
||||
if point_seen {
|
||||
return l.errorf("cannot have two dots in one float")
|
||||
}
|
||||
if !isDigit(l.peek()) {
|
||||
return l.errorf("float cannot end with a dot")
|
||||
}
|
||||
point_seen = true
|
||||
} else if isDigit(next) {
|
||||
digit_seen = true
|
||||
} else {
|
||||
l.backup()
|
||||
break
|
||||
}
|
||||
if point_seen && !digit_seen {
|
||||
return l.errorf("cannot start float with a dot")
|
||||
}
|
||||
}
|
||||
|
||||
if !digit_seen {
|
||||
return l.errorf("no digit in that number")
|
||||
}
|
||||
if point_seen {
|
||||
l.emit(tokenFloat)
|
||||
} else {
|
||||
l.emit(tokenInteger)
|
||||
}
|
||||
return lexRvalue
|
||||
}
|
||||
|
||||
func init() {
|
||||
dateRegexp = regexp.MustCompile("^\\d{1,4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z")
|
||||
}
|
||||
|
||||
// Entry point
|
||||
func lex(input string) (*lexer, chan token) {
|
||||
l := &lexer{
|
||||
input: input,
|
||||
tokens: make(chan token),
|
||||
}
|
||||
go l.run()
|
||||
return l, l.tokens
|
||||
}
|
||||
-415
@@ -1,415 +0,0 @@
|
||||
package toml
|
||||
|
||||
import "testing"
|
||||
|
||||
func testFlow(t *testing.T, input string, expectedFlow []token) {
|
||||
_, ch := lex(input)
|
||||
for _, expected := range expectedFlow {
|
||||
token := <-ch
|
||||
if token != expected {
|
||||
t.Log("compared", token, "to", expected)
|
||||
t.Log(token.val, "<->", expected.val)
|
||||
t.Log(token.typ, "<->", expected.typ)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
tok, ok := <-ch
|
||||
if ok {
|
||||
t.Log("channel is not closed!")
|
||||
t.Log(len(ch)+1, "tokens remaining:")
|
||||
|
||||
t.Log("token ->", tok)
|
||||
for token := range ch {
|
||||
t.Log("token ->", token)
|
||||
}
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidKeyGroup(t *testing.T) {
|
||||
testFlow(t, "[hello world]", []token{
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenKeyGroup, "hello world"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnclosedKeyGroup(t *testing.T) {
|
||||
testFlow(t, "[hello world", []token{
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenError, "unclosed key group"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestComment(t *testing.T) {
|
||||
testFlow(t, "# blahblah", []token{
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyGroupComment(t *testing.T) {
|
||||
testFlow(t, "[hello world] # blahblah", []token{
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenKeyGroup, "hello world"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultipleKeyGroupsComment(t *testing.T) {
|
||||
testFlow(t, "[hello world] # blahblah\n[test]", []token{
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenKeyGroup, "hello world"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenKeyGroup, "test"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicKey(t *testing.T) {
|
||||
testFlow(t, "hello", []token{
|
||||
token{tokenKey, "hello"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicKeyWithUnderscore(t *testing.T) {
|
||||
testFlow(t, "hello_hello", []token{
|
||||
token{tokenKey, "hello_hello"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicKeyWithDash(t *testing.T) {
|
||||
testFlow(t, "hello-world", []token{
|
||||
token{tokenKey, "hello-world"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicKeyWithUppercaseMix(t *testing.T) {
|
||||
testFlow(t, "helloHELLOHello", []token{
|
||||
token{tokenKey, "helloHELLOHello"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicKeyWithInternationalCharacters(t *testing.T) {
|
||||
testFlow(t, "héllÖ", []token{
|
||||
token{tokenKey, "héllÖ"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicKeyAndEqual(t *testing.T) {
|
||||
testFlow(t, "hello =", []token{
|
||||
token{tokenKey, "hello"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyWithSharpAndEqual(t *testing.T) {
|
||||
testFlow(t, "key#name = 5", []token{
|
||||
token{tokenKey, "key#name"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenInteger, "5"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
func TestKeyWithSymbolsAndEqual(t *testing.T) {
|
||||
testFlow(t, "~!@#$^&*()_+-`1234567890[]\\|/?><.,;:' = 5", []token{
|
||||
token{tokenKey, "~!@#$^&*()_+-`1234567890[]\\|/?><.,;:'"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenInteger, "5"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualStringEscape(t *testing.T) {
|
||||
testFlow(t, "foo = \"hello\\\"\"", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenString, "hello\""},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualStringUnfinished(t *testing.T) {
|
||||
testFlow(t, "foo = \"bar", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenError, "unclosed string"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualString(t *testing.T) {
|
||||
testFlow(t, "foo = \"bar\"", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenString, "bar"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualTrue(t *testing.T) {
|
||||
testFlow(t, "foo = true", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenTrue, "true"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualFalse(t *testing.T) {
|
||||
testFlow(t, "foo = false", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenFalse, "false"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayNestedString(t *testing.T) {
|
||||
testFlow(t, "a = [ [\"hello\", \"world\"] ]", []token{
|
||||
token{tokenKey, "a"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenString, "hello"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenString, "world"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayNestedInts(t *testing.T) {
|
||||
testFlow(t, "a = [ [42, 21], [10] ]", []token{
|
||||
token{tokenKey, "a"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenInteger, "42"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenInteger, "21"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenInteger, "10"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayInts(t *testing.T) {
|
||||
testFlow(t, "a = [ 42, 21, 10, ]", []token{
|
||||
token{tokenKey, "a"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenInteger, "42"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenInteger, "21"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenInteger, "10"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultilineArrayComments(t *testing.T) {
|
||||
testFlow(t, "a = [1, # wow\n2, # such items\n3, # so array\n]", []token{
|
||||
token{tokenKey, "a"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenInteger, "1"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenInteger, "2"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenInteger, "3"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualArrayBools(t *testing.T) {
|
||||
testFlow(t, "foo = [true, false, true]", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenTrue, "true"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenFalse, "false"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenTrue, "true"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualArrayBoolsWithComments(t *testing.T) {
|
||||
testFlow(t, "foo = [true, false, true] # YEAH", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenTrue, "true"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenFalse, "false"},
|
||||
token{tokenComma, ","},
|
||||
token{tokenTrue, "true"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestDateRegexp(t *testing.T) {
|
||||
if dateRegexp.FindString("1979-05-27T07:32:00Z") == "" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyEqualDate(t *testing.T) {
|
||||
testFlow(t, "foo = 1979-05-27T07:32:00Z", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenDate, "1979-05-27T07:32:00Z"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestFloatEndingWithDot(t *testing.T) {
|
||||
testFlow(t, "foo = 42.", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenError, "float cannot end with a dot"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestFloatWithTwoDots(t *testing.T) {
|
||||
testFlow(t, "foo = 4.2.", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenError, "cannot have two dots in one float"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestDoubleEqualKey(t *testing.T) {
|
||||
testFlow(t, "foo= = 2", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenError, "cannot have multiple equals for the same key"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestInvalidEsquapeSequence(t *testing.T) {
|
||||
testFlow(t, "foo = \"\\x\"", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenError, "invalid escape sequence: \\x"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestNestedArrays(t *testing.T) {
|
||||
testFlow(t, "foo = [[[]]]", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenLeftBracket, "["},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenRightBracket, "]"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualNumber(t *testing.T) {
|
||||
testFlow(t, "foo = 42", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenInteger, "42"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
|
||||
testFlow(t, "foo = +42", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenInteger, "+42"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
|
||||
testFlow(t, "foo = -42", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenInteger, "-42"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
|
||||
testFlow(t, "foo = 4.2", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenFloat, "4.2"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
|
||||
testFlow(t, "foo = +4.2", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenFloat, "+4.2"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
|
||||
testFlow(t, "foo = -4.2", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenFloat, "-4.2"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultiline(t *testing.T) {
|
||||
testFlow(t, "foo = 42\nbar=21", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenInteger, "42"},
|
||||
token{tokenKey, "bar"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenInteger, "21"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualStringUnicodeEscape(t *testing.T) {
|
||||
testFlow(t, "foo = \"hello \\u2665\"", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenString, "hello ♥"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnicodeString(t *testing.T) {
|
||||
testFlow(t, "foo = \"hello ♥ world\"", []token{
|
||||
token{tokenKey, "foo"},
|
||||
token{tokenEqual, "="},
|
||||
token{tokenString, "hello ♥ world"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyGroupArray(t *testing.T) {
|
||||
testFlow(t, "[[foo]]", []token{
|
||||
token{tokenDoubleLeftBracket, "[["},
|
||||
token{tokenKeyGroupArray, "foo"},
|
||||
token{tokenDoubleRightBracket, "]]"},
|
||||
token{tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/unstable"
|
||||
)
|
||||
|
||||
// LocalDate represents a calendar day in no specific timezone.
|
||||
type LocalDate struct {
|
||||
Year int
|
||||
Month int
|
||||
Day int
|
||||
}
|
||||
|
||||
// AsTime converts d into a specific time instance at midnight in zone.
|
||||
func (d LocalDate) AsTime(zone *time.Location) time.Time {
|
||||
return time.Date(d.Year, time.Month(d.Month), d.Day, 0, 0, 0, 0, zone)
|
||||
}
|
||||
|
||||
// String returns RFC 3339 representation of d.
|
||||
func (d LocalDate) String() string {
|
||||
return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day)
|
||||
}
|
||||
|
||||
// MarshalText returns RFC 3339 representation of d.
|
||||
func (d LocalDate) MarshalText() ([]byte, error) {
|
||||
return []byte(d.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText parses b using RFC 3339 to fill d.
|
||||
func (d *LocalDate) UnmarshalText(b []byte) error {
|
||||
res, err := parseLocalDate(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*d = res
|
||||
return nil
|
||||
}
|
||||
|
||||
// LocalTime represents a time of day of no specific day in no specific
|
||||
// timezone.
|
||||
type LocalTime struct {
|
||||
Hour int // Hour of the day: [0; 24[
|
||||
Minute int // Minute of the hour: [0; 60[
|
||||
Second int // Second of the minute: [0; 59]
|
||||
Nanosecond int // Nanoseconds within the second: [0, 1000000000[
|
||||
Precision int // Number of digits to display for Nanosecond.
|
||||
}
|
||||
|
||||
// String returns RFC 3339 representation of d.
|
||||
// If d.Nanosecond and d.Precision are zero, the time won't have a nanosecond
|
||||
// component. If d.Nanosecond > 0 but d.Precision = 0, then the minimum number
|
||||
// of digits for nanoseconds is provided.
|
||||
func (d LocalTime) String() string {
|
||||
s := fmt.Sprintf("%02d:%02d:%02d", d.Hour, d.Minute, d.Second)
|
||||
|
||||
if d.Precision > 0 {
|
||||
s += fmt.Sprintf(".%09d", d.Nanosecond)[:d.Precision+1]
|
||||
} else if d.Nanosecond > 0 {
|
||||
// Nanoseconds are specified, but precision is not provided. Use the
|
||||
// minimum.
|
||||
s += strings.Trim(fmt.Sprintf(".%09d", d.Nanosecond), "0")
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// MarshalText returns RFC 3339 representation of d.
|
||||
func (d LocalTime) MarshalText() ([]byte, error) {
|
||||
return []byte(d.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText parses b using RFC 3339 to fill d.
|
||||
func (d *LocalTime) UnmarshalText(b []byte) error {
|
||||
res, left, err := parseLocalTime(b)
|
||||
if err == nil && len(left) != 0 {
|
||||
err = unstable.NewParserError(left, "extra characters")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*d = res
|
||||
return nil
|
||||
}
|
||||
|
||||
// LocalDateTime represents a time of a specific day in no specific timezone.
|
||||
type LocalDateTime struct {
|
||||
LocalDate
|
||||
LocalTime
|
||||
}
|
||||
|
||||
// AsTime converts d into a specific time instance in zone.
|
||||
func (d LocalDateTime) AsTime(zone *time.Location) time.Time {
|
||||
return time.Date(d.Year, time.Month(d.Month), d.Day, d.Hour, d.Minute, d.Second, d.Nanosecond, zone)
|
||||
}
|
||||
|
||||
// String returns RFC 3339 representation of d.
|
||||
func (d LocalDateTime) String() string {
|
||||
return d.LocalDate.String() + "T" + d.LocalTime.String()
|
||||
}
|
||||
|
||||
// MarshalText returns RFC 3339 representation of d.
|
||||
func (d LocalDateTime) MarshalText() ([]byte, error) {
|
||||
return []byte(d.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText parses b using RFC 3339 to fill d.
|
||||
func (d *LocalDateTime) UnmarshalText(data []byte) error {
|
||||
res, left, err := parseLocalDateTime(data)
|
||||
if err == nil && len(left) != 0 {
|
||||
err = unstable.NewParserError(left, "extra characters")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*d = res
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package toml_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/pelletier/go-toml/v2/internal/assert"
|
||||
)
|
||||
|
||||
func TestLocalDate_AsTime(t *testing.T) {
|
||||
d := toml.LocalDate{2021, 6, 8}
|
||||
cast := d.AsTime(time.UTC)
|
||||
assert.Equal(t, time.Date(2021, time.June, 8, 0, 0, 0, 0, time.UTC), cast)
|
||||
}
|
||||
|
||||
func TestLocalDate_String(t *testing.T) {
|
||||
d := toml.LocalDate{2021, 6, 8}
|
||||
assert.Equal(t, "2021-06-08", d.String())
|
||||
}
|
||||
|
||||
func TestLocalDate_MarshalText(t *testing.T) {
|
||||
d := toml.LocalDate{2021, 6, 8}
|
||||
b, err := d.MarshalText()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("2021-06-08"), b)
|
||||
}
|
||||
|
||||
func TestLocalDate_UnmarshalMarshalText(t *testing.T) {
|
||||
d := toml.LocalDate{}
|
||||
err := d.UnmarshalText([]byte("2021-06-08"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, toml.LocalDate{2021, 6, 8}, d)
|
||||
|
||||
err = d.UnmarshalText([]byte("what"))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestLocalTime_String(t *testing.T) {
|
||||
d := toml.LocalTime{20, 12, 1, 2, 9}
|
||||
assert.Equal(t, "20:12:01.000000002", d.String())
|
||||
d = toml.LocalTime{20, 12, 1, 0, 0}
|
||||
assert.Equal(t, "20:12:01", d.String())
|
||||
d = toml.LocalTime{20, 12, 1, 0, 9}
|
||||
assert.Equal(t, "20:12:01.000000000", d.String())
|
||||
d = toml.LocalTime{20, 12, 1, 100, 0}
|
||||
assert.Equal(t, "20:12:01.0000001", d.String())
|
||||
}
|
||||
|
||||
func TestLocalTime_MarshalText(t *testing.T) {
|
||||
d := toml.LocalTime{20, 12, 1, 2, 9}
|
||||
b, err := d.MarshalText()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("20:12:01.000000002"), b)
|
||||
}
|
||||
|
||||
func TestLocalTime_UnmarshalMarshalText(t *testing.T) {
|
||||
d := toml.LocalTime{}
|
||||
err := d.UnmarshalText([]byte("20:12:01.000000002"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, toml.LocalTime{20, 12, 1, 2, 9}, d)
|
||||
|
||||
err = d.UnmarshalText([]byte("what"))
|
||||
assert.Error(t, err)
|
||||
|
||||
err = d.UnmarshalText([]byte("20:12:01.000000002 bad"))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestLocalTime_RoundTrip(t *testing.T) {
|
||||
var d struct{ A toml.LocalTime }
|
||||
err := toml.Unmarshal([]byte("a=20:12:01.500"), &d)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "20:12:01.500", d.A.String())
|
||||
}
|
||||
|
||||
func TestLocalDateTime_AsTime(t *testing.T) {
|
||||
d := toml.LocalDateTime{
|
||||
toml.LocalDate{2021, 6, 8},
|
||||
toml.LocalTime{20, 12, 1, 2, 9},
|
||||
}
|
||||
cast := d.AsTime(time.UTC)
|
||||
assert.Equal(t, time.Date(2021, time.June, 8, 20, 12, 1, 2, time.UTC), cast)
|
||||
}
|
||||
|
||||
func TestLocalDateTime_String(t *testing.T) {
|
||||
d := toml.LocalDateTime{
|
||||
toml.LocalDate{2021, 6, 8},
|
||||
toml.LocalTime{20, 12, 1, 2, 9},
|
||||
}
|
||||
assert.Equal(t, "2021-06-08T20:12:01.000000002", d.String())
|
||||
}
|
||||
|
||||
func TestLocalDateTime_MarshalText(t *testing.T) {
|
||||
d := toml.LocalDateTime{
|
||||
toml.LocalDate{2021, 6, 8},
|
||||
toml.LocalTime{20, 12, 1, 2, 9},
|
||||
}
|
||||
b, err := d.MarshalText()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("2021-06-08T20:12:01.000000002"), b)
|
||||
}
|
||||
|
||||
func TestLocalDateTime_UnmarshalMarshalText(t *testing.T) {
|
||||
d := toml.LocalDateTime{}
|
||||
err := d.UnmarshalText([]byte("2021-06-08 20:12:01.000000002"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, toml.LocalDateTime{
|
||||
toml.LocalDate{2021, 6, 8},
|
||||
toml.LocalTime{20, 12, 1, 2, 9},
|
||||
}, d)
|
||||
|
||||
err = d.UnmarshalText([]byte("what"))
|
||||
assert.Error(t, err)
|
||||
|
||||
err = d.UnmarshalText([]byte("2021-06-08 20:12:01.000000002 bad"))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
+1199
File diff suppressed because it is too large
Load Diff
+2241
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
// Package ossfuzz provides a fuzzing target for OSS-Fuzz.
|
||||
package ossfuzz
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
// FuzzToml is the fuzzing target.
|
||||
func FuzzToml(data []byte) int {
|
||||
if len(data) >= 2048 {
|
||||
return 0
|
||||
}
|
||||
|
||||
if strings.Contains(string(data), "nan") {
|
||||
return 0
|
||||
}
|
||||
|
||||
var v interface{}
|
||||
err := toml.Unmarshal(data, &v)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
encoded, err := toml.Marshal(v)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to marshal unmarshaled document: %s", err))
|
||||
}
|
||||
|
||||
var v2 interface{}
|
||||
err = toml.Unmarshal(encoded, &v2)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed round trip: %s", err))
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(v, v2) {
|
||||
panic(fmt.Sprintf("not equal: %#+v %#+v", v, v2))
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
// TOML Parser.
|
||||
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type parser struct {
|
||||
flow chan token
|
||||
tree *TomlTree
|
||||
tokensBuffer []token
|
||||
currentGroup []string
|
||||
seenGroupKeys []string
|
||||
}
|
||||
|
||||
type parserStateFn func(*parser) parserStateFn
|
||||
|
||||
func (p *parser) run() {
|
||||
for state := parseStart; state != nil; {
|
||||
state = state(p)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parser) peek() *token {
|
||||
if len(p.tokensBuffer) != 0 {
|
||||
return &(p.tokensBuffer[0])
|
||||
}
|
||||
|
||||
tok, ok := <-p.flow
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
p.tokensBuffer = append(p.tokensBuffer, tok)
|
||||
return &tok
|
||||
}
|
||||
|
||||
func (p *parser) assume(typ tokenType) {
|
||||
tok := p.getToken()
|
||||
if tok == nil {
|
||||
panic(fmt.Sprintf("was expecting token %s, but token stream is empty", typ))
|
||||
}
|
||||
if tok.typ != typ {
|
||||
panic(fmt.Sprintf("was expecting token %s, but got %s", typ, tok.typ))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parser) getToken() *token {
|
||||
if len(p.tokensBuffer) != 0 {
|
||||
tok := p.tokensBuffer[0]
|
||||
p.tokensBuffer = p.tokensBuffer[1:]
|
||||
return &tok
|
||||
}
|
||||
tok, ok := <-p.flow
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &tok
|
||||
}
|
||||
|
||||
func parseStart(p *parser) parserStateFn {
|
||||
tok := p.peek()
|
||||
|
||||
// end of stream, parsing is finished
|
||||
if tok == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch tok.typ {
|
||||
case tokenDoubleLeftBracket:
|
||||
return parseGroupArray
|
||||
case tokenLeftBracket:
|
||||
return parseGroup
|
||||
case tokenKey:
|
||||
return parseAssign
|
||||
case tokenEOF:
|
||||
return nil
|
||||
default:
|
||||
panic("unexpected token")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseGroupArray(p *parser) parserStateFn {
|
||||
p.getToken() // discard the [[
|
||||
key := p.getToken()
|
||||
if key.typ != tokenKeyGroupArray {
|
||||
panic(fmt.Sprintf("unexpected token %s, was expecting a key group array", key))
|
||||
}
|
||||
|
||||
// get or create group array element at the indicated part in the path
|
||||
p.currentGroup = strings.Split(key.val, ".")
|
||||
dest_tree := p.tree.GetPath(p.currentGroup)
|
||||
var array []*TomlTree
|
||||
if dest_tree == nil {
|
||||
array = make([]*TomlTree, 0)
|
||||
} else if dest_tree.([]*TomlTree) != nil {
|
||||
array = dest_tree.([]*TomlTree)
|
||||
} else {
|
||||
panic(fmt.Sprintf("key %s is already assigned and not of type group array", key))
|
||||
}
|
||||
|
||||
// add a new tree to the end of the group array
|
||||
new_tree := make(TomlTree)
|
||||
array = append(array, &new_tree)
|
||||
p.tree.SetPath(p.currentGroup, array)
|
||||
|
||||
// keep this key name from use by other kinds of assignments
|
||||
p.seenGroupKeys = append(p.seenGroupKeys, key.val)
|
||||
|
||||
// move to next parser state
|
||||
p.assume(tokenDoubleRightBracket)
|
||||
return parseStart(p)
|
||||
}
|
||||
|
||||
func parseGroup(p *parser) parserStateFn {
|
||||
p.getToken() // discard the [
|
||||
key := p.getToken()
|
||||
if key.typ != tokenKeyGroup {
|
||||
panic(fmt.Sprintf("unexpected token %s, was expecting a key group", key))
|
||||
}
|
||||
for _, item := range p.seenGroupKeys {
|
||||
if item == key.val {
|
||||
panic("duplicated tables")
|
||||
}
|
||||
}
|
||||
p.seenGroupKeys = append(p.seenGroupKeys, key.val)
|
||||
p.tree.createSubTree(key.val)
|
||||
p.assume(tokenRightBracket)
|
||||
p.currentGroup = strings.Split(key.val, ".")
|
||||
return parseStart(p)
|
||||
}
|
||||
|
||||
func parseAssign(p *parser) parserStateFn {
|
||||
key := p.getToken()
|
||||
p.assume(tokenEqual)
|
||||
value := parseRvalue(p)
|
||||
var group_key []string
|
||||
if len(p.currentGroup) > 0 {
|
||||
group_key = p.currentGroup
|
||||
} else {
|
||||
group_key = make([]string, 0)
|
||||
}
|
||||
|
||||
// find the group to assign, looking out for arrays of groups
|
||||
var target_node *TomlTree
|
||||
switch node := p.tree.GetPath(group_key).(type) {
|
||||
case []*TomlTree:
|
||||
target_node = node[len(node)-1]
|
||||
case *TomlTree:
|
||||
target_node = node
|
||||
default:
|
||||
panic(fmt.Sprintf("Unknown group type for path %v", group_key))
|
||||
}
|
||||
|
||||
// assign value to the found group
|
||||
local_key := []string{key.val}
|
||||
final_key := append(group_key, key.val)
|
||||
if target_node.GetPath(local_key) != nil {
|
||||
panic(fmt.Sprintf("the following key was defined twice: %s", strings.Join(final_key, ".")))
|
||||
}
|
||||
target_node.SetPath(local_key, value)
|
||||
return parseStart(p)
|
||||
}
|
||||
|
||||
func parseRvalue(p *parser) interface{} {
|
||||
tok := p.getToken()
|
||||
if tok == nil || tok.typ == tokenEOF {
|
||||
panic("expecting a value")
|
||||
}
|
||||
|
||||
switch tok.typ {
|
||||
case tokenString:
|
||||
return tok.val
|
||||
case tokenTrue:
|
||||
return true
|
||||
case tokenFalse:
|
||||
return false
|
||||
case tokenInteger:
|
||||
val, err := strconv.ParseInt(tok.val, 10, 64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return val
|
||||
case tokenFloat:
|
||||
val, err := strconv.ParseFloat(tok.val, 64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return val
|
||||
case tokenDate:
|
||||
val, err := time.Parse(time.RFC3339, tok.val)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return val
|
||||
case tokenLeftBracket:
|
||||
return parseArray(p)
|
||||
case tokenError:
|
||||
panic(tok.val)
|
||||
}
|
||||
|
||||
panic("never reached")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseArray(p *parser) []interface{} {
|
||||
array := make([]interface{}, 0)
|
||||
arrayType := reflect.TypeOf(nil)
|
||||
for {
|
||||
follow := p.peek()
|
||||
if follow == nil || follow.typ == tokenEOF {
|
||||
panic("unterminated array")
|
||||
}
|
||||
if follow.typ == tokenRightBracket {
|
||||
p.getToken()
|
||||
return array
|
||||
}
|
||||
val := parseRvalue(p)
|
||||
if arrayType == nil {
|
||||
arrayType = reflect.TypeOf(val)
|
||||
}
|
||||
if reflect.TypeOf(val) != arrayType {
|
||||
panic("mixed types in array")
|
||||
}
|
||||
array = append(array, val)
|
||||
follow = p.peek()
|
||||
if follow == nil {
|
||||
panic("unterminated array")
|
||||
}
|
||||
if follow.typ != tokenRightBracket && follow.typ != tokenComma {
|
||||
panic("missing comma")
|
||||
}
|
||||
if follow.typ == tokenComma {
|
||||
p.getToken()
|
||||
}
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
func parse(flow chan token) *TomlTree {
|
||||
result := make(TomlTree)
|
||||
parser := &parser{
|
||||
flow: flow,
|
||||
tree: &result,
|
||||
tokensBuffer: make([]token, 0),
|
||||
currentGroup: make([]string, 0),
|
||||
seenGroupKeys: make([]string, 0),
|
||||
}
|
||||
parser.run()
|
||||
return parser.tree
|
||||
}
|
||||
-339
@@ -1,339 +0,0 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func assertTree(t *testing.T, tree *TomlTree, err error, ref map[string]interface{}) {
|
||||
if err != nil {
|
||||
t.Error("Non-nil error:", err.Error())
|
||||
return
|
||||
}
|
||||
for k, v := range ref {
|
||||
node := tree.Get(k)
|
||||
switch cast_node := node.(type) {
|
||||
case []*TomlTree:
|
||||
for idx, item := range cast_node {
|
||||
assertTree(t, item, err, v.([]map[string]interface{})[idx])
|
||||
}
|
||||
case *TomlTree:
|
||||
assertTree(t, cast_node, err, v.(map[string]interface{}))
|
||||
default:
|
||||
if fmt.Sprintf("%v", node) != fmt.Sprintf("%v", v) {
|
||||
t.Errorf("was expecting %v at %v but got %v", v, k, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateSubTree(t *testing.T) {
|
||||
tree := make(TomlTree)
|
||||
tree.createSubTree("a.b.c")
|
||||
tree.Set("a.b.c", 42)
|
||||
if tree.Get("a.b.c") != 42 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleKV(t *testing.T) {
|
||||
tree, err := Load("a = 42")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": int64(42),
|
||||
})
|
||||
|
||||
tree, _ = Load("a = 42\nb = 21")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": int64(42),
|
||||
"b": int64(21),
|
||||
})
|
||||
}
|
||||
|
||||
func TestSimpleNumbers(t *testing.T) {
|
||||
tree, err := Load("a = +42\nb = -21\nc = +4.2\nd = -2.1")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": int64(42),
|
||||
"b": int64(-21),
|
||||
"c": float64(4.2),
|
||||
"d": float64(-2.1),
|
||||
})
|
||||
}
|
||||
|
||||
func TestSimpleDate(t *testing.T) {
|
||||
tree, err := Load("a = 1979-05-27T07:32:00Z")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC),
|
||||
})
|
||||
}
|
||||
|
||||
func TestSimpleString(t *testing.T) {
|
||||
tree, err := Load("a = \"hello world\"")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": "hello world",
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringEscapables(t *testing.T) {
|
||||
tree, err := Load("a = \"a \\n b\"")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": "a \n b",
|
||||
})
|
||||
|
||||
tree, err = Load("a = \"a \\t b\"")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": "a \t b",
|
||||
})
|
||||
|
||||
tree, err = Load("a = \"a \\r b\"")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": "a \r b",
|
||||
})
|
||||
|
||||
tree, err = Load("a = \"a \\\\ b\"")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": "a \\ b",
|
||||
})
|
||||
}
|
||||
|
||||
func TestBools(t *testing.T) {
|
||||
tree, err := Load("a = true\nb = false")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": true,
|
||||
"b": false,
|
||||
})
|
||||
}
|
||||
|
||||
func TestNestedKeys(t *testing.T) {
|
||||
tree, err := Load("[a.b.c]\nd = 42")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a.b.c.d": int64(42),
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayOne(t *testing.T) {
|
||||
tree, err := Load("a = [1]")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": []int64{int64(1)},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayZero(t *testing.T) {
|
||||
tree, err := Load("a = []")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": []interface{}{},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArraySimple(t *testing.T) {
|
||||
tree, err := Load("a = [42, 21, 10]")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": []int64{int64(42), int64(21), int64(10)},
|
||||
})
|
||||
|
||||
tree, _ = Load("a = [42, 21, 10,]")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": []int64{int64(42), int64(21), int64(10)},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayMultiline(t *testing.T) {
|
||||
tree, err := Load("a = [42,\n21, 10,]")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": []int64{int64(42), int64(21), int64(10)},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayNested(t *testing.T) {
|
||||
tree, err := Load("a = [[42, 21], [10]]")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": [][]int64{[]int64{int64(42), int64(21)}, []int64{int64(10)}},
|
||||
})
|
||||
}
|
||||
|
||||
func TestNestedEmptyArrays(t *testing.T) {
|
||||
tree, err := Load("a = [[[]]]")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": [][][]interface{}{[][]interface{}{[]interface{}{}}},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayMixedTypes(t *testing.T) {
|
||||
_, err := Load("a = [42, 16.0]")
|
||||
if err.Error() != "mixed types in array" {
|
||||
t.Error("Bad error message:", err.Error())
|
||||
}
|
||||
|
||||
_, err = Load("a = [42, \"hello\"]")
|
||||
if err.Error() != "mixed types in array" {
|
||||
t.Error("Bad error message:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestArrayNestedStrings(t *testing.T) {
|
||||
tree, err := Load("data = [ [\"gamma\", \"delta\"], [\"Foo\"] ]")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"data": [][]string{[]string{"gamma", "delta"}, []string{"Foo"}},
|
||||
})
|
||||
}
|
||||
|
||||
func TestMissingValue(t *testing.T) {
|
||||
_, err := Load("a = ")
|
||||
if err.Error() != "expecting a value" {
|
||||
t.Error("Bad error message:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnterminatedArray(t *testing.T) {
|
||||
_, err := Load("a = [1,")
|
||||
if err.Error() != "unterminated array" {
|
||||
t.Error("Bad error message:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewlinesInArrays(t *testing.T) {
|
||||
tree, err := Load("a = [1,\n2,\n3]")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": []int64{int64(1), int64(2), int64(3)},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayWithExtraComma(t *testing.T) {
|
||||
tree, err := Load("a = [1,\n2,\n3,\n]")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": []int64{int64(1), int64(2), int64(3)},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayWithExtraCommaComment(t *testing.T) {
|
||||
tree, err := Load("a = [1, # wow\n2, # such items\n3, # so array\n]")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": []int64{int64(1), int64(2), int64(3)},
|
||||
})
|
||||
}
|
||||
|
||||
func TestDuplicateGroups(t *testing.T) {
|
||||
_, err := Load("[foo]\na=2\n[foo]b=3")
|
||||
if err.Error() != "duplicated tables" {
|
||||
t.Error("Bad error message:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDuplicateKeys(t *testing.T) {
|
||||
_, err := Load("foo = 2\nfoo = 3")
|
||||
if err.Error() != "the following key was defined twice: foo" {
|
||||
t.Error("Bad error message:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyIntermediateTable(t *testing.T) {
|
||||
_, err := Load("[foo..bar]")
|
||||
if err.Error() != "empty intermediate table" {
|
||||
t.Error("Bad error message:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestImplicitDeclarationBefore(t *testing.T) {
|
||||
tree, err := Load("[a.b.c]\nanswer = 42\n[a]\nbetter = 43")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"a": map[string]interface{}{
|
||||
"b": map[string]interface{}{
|
||||
"c": map[string]interface{}{
|
||||
"answer": int64(42),
|
||||
},
|
||||
},
|
||||
"better": int64(43),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestFloatsWithoutLeadingZeros(t *testing.T) {
|
||||
_, err := Load("a = .42")
|
||||
if err.Error() != "cannot start float with a dot" {
|
||||
t.Error("Bad error message:", err.Error())
|
||||
}
|
||||
|
||||
_, err = Load("a = -.42")
|
||||
if err.Error() != "cannot start float with a dot" {
|
||||
t.Error("Bad error message:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingFile(t *testing.T) {
|
||||
_, err := LoadFile("foo.toml")
|
||||
if err.Error() != "open foo.toml: no such file or directory" {
|
||||
t.Error("Bad error message:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFile(t *testing.T) {
|
||||
tree, err := LoadFile("example.toml")
|
||||
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"title": "TOML Example",
|
||||
"owner.name": "Tom Preston-Werner",
|
||||
"owner.organization": "GitHub",
|
||||
"owner.bio": "GitHub Cofounder & CEO\nLikes tater tots and beer.",
|
||||
"owner.dob": time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC),
|
||||
"database.server": "192.168.1.1",
|
||||
"database.ports": []int64{8001, 8001, 8002},
|
||||
"database.connection_max": 5000,
|
||||
"database.enabled": true,
|
||||
"servers.alpha.ip": "10.0.0.1",
|
||||
"servers.alpha.dc": "eqdc10",
|
||||
"servers.beta.ip": "10.0.0.2",
|
||||
"servers.beta.dc": "eqdc10",
|
||||
"clients.data": []interface{}{[]string{"gamma", "delta"}, []int64{1, 2}},
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseKeyGroupArray(t *testing.T) {
|
||||
tree, err := Load("[[foo.bar]] a = 42\n[[foo.bar]] a = 69")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"bar": []map[string]interface{}{
|
||||
{"a": int64(42)},
|
||||
{"a": int64(69)},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestToTomlValue(t *testing.T) {
|
||||
for idx, item := range []struct {
|
||||
Value interface{}
|
||||
Expect string
|
||||
}{
|
||||
{int64(12345), "12345"},
|
||||
{float64(123.45), "123.45"},
|
||||
{bool(true), "true"},
|
||||
{"hello world", "\"hello world\""},
|
||||
{"\b\t\n\f\r\"\\", "\"\\b\\t\\n\\f\\r\\\"\\\\\""},
|
||||
{"\x05", "\"\\u0005\""},
|
||||
{time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC),
|
||||
"1979-05-27T07:32:00Z"},
|
||||
{[]interface{}{"gamma", "delta"},
|
||||
"[\n \"gamma\",\n \"delta\",\n]"},
|
||||
} {
|
||||
result := toTomlValue(item.Value, 0)
|
||||
if result != item.Expect {
|
||||
t.Errorf("Test %d - got '%s', expected '%s'", idx, result, item.Expect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestToString(t *testing.T) {
|
||||
tree := &TomlTree{
|
||||
"foo": &TomlTree{
|
||||
"bar": []*TomlTree{
|
||||
{"a": int64(42)},
|
||||
{"a": int64(69)},
|
||||
},
|
||||
},
|
||||
}
|
||||
result := tree.ToString()
|
||||
expected := "\n[foo]\n\n[[foo.bar]]\na = 42\n\n[[foo.bar]]\na = 69\n"
|
||||
if result != expected {
|
||||
t.Errorf("Expected got '%s', expected '%s'", result, expected)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"github.com/pelletier/go-toml/v2/internal/tracker"
|
||||
"github.com/pelletier/go-toml/v2/unstable"
|
||||
)
|
||||
|
||||
type strict struct {
|
||||
Enabled bool
|
||||
|
||||
// Tracks the current key being processed.
|
||||
key tracker.KeyTracker
|
||||
|
||||
missing []unstable.ParserError
|
||||
|
||||
// Reference to the document for computing key ranges.
|
||||
doc []byte
|
||||
}
|
||||
|
||||
func (s *strict) EnterTable(node *unstable.Node) {
|
||||
if !s.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
s.key.UpdateTable(node)
|
||||
}
|
||||
|
||||
func (s *strict) EnterArrayTable(node *unstable.Node) {
|
||||
if !s.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
s.key.UpdateArrayTable(node)
|
||||
}
|
||||
|
||||
func (s *strict) EnterKeyValue(node *unstable.Node) {
|
||||
if !s.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
s.key.Push(node)
|
||||
}
|
||||
|
||||
func (s *strict) ExitKeyValue(node *unstable.Node) {
|
||||
if !s.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
s.key.Pop(node)
|
||||
}
|
||||
|
||||
func (s *strict) MissingTable(node *unstable.Node) {
|
||||
if !s.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
s.missing = append(s.missing, unstable.ParserError{
|
||||
Highlight: s.keyLocation(node),
|
||||
Message: "missing table",
|
||||
Key: s.key.Key(),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *strict) MissingField(node *unstable.Node) {
|
||||
if !s.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
s.missing = append(s.missing, unstable.ParserError{
|
||||
Highlight: s.keyLocation(node),
|
||||
Message: "missing field",
|
||||
Key: s.key.Key(),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *strict) Error(doc []byte) error {
|
||||
if !s.Enabled || len(s.missing) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := &StrictMissingError{
|
||||
Errors: make([]DecodeError, 0, len(s.missing)),
|
||||
}
|
||||
|
||||
for _, derr := range s.missing {
|
||||
derr := derr
|
||||
err.Errors = append(err.Errors, *wrapDecodeError(doc, &derr))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *strict) keyLocation(node *unstable.Node) []byte {
|
||||
k := node.Key()
|
||||
|
||||
hasOne := k.Next()
|
||||
if !hasOne {
|
||||
panic("should not be called with empty key")
|
||||
}
|
||||
|
||||
// Get the range from the first key to the last key.
|
||||
firstRaw := k.Node().Raw
|
||||
lastRaw := firstRaw
|
||||
|
||||
for k.Next() {
|
||||
lastRaw = k.Node().Raw
|
||||
}
|
||||
|
||||
// Compute the slice from the document using the ranges.
|
||||
start := firstRaw.Offset
|
||||
end := lastRaw.Offset + lastRaw.Length
|
||||
|
||||
return s.doc[start:end]
|
||||
}
|
||||
Executable
+596
@@ -0,0 +1,596 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Go versions to test (1.11 through 1.25)
|
||||
GO_VERSIONS=(
|
||||
"1.11"
|
||||
"1.12"
|
||||
"1.13"
|
||||
"1.14"
|
||||
"1.15"
|
||||
"1.16"
|
||||
"1.17"
|
||||
"1.18"
|
||||
"1.19"
|
||||
"1.20"
|
||||
"1.21"
|
||||
"1.22"
|
||||
"1.23"
|
||||
"1.24"
|
||||
"1.25"
|
||||
)
|
||||
|
||||
# Default values
|
||||
PARALLEL=true
|
||||
VERBOSE=false
|
||||
OUTPUT_DIR="test-results"
|
||||
DOCKER_TIMEOUT="10m"
|
||||
|
||||
usage() {
|
||||
cat << EOF
|
||||
Usage: $0 [OPTIONS] [GO_VERSIONS...]
|
||||
|
||||
Test go-toml across multiple Go versions using Docker containers.
|
||||
|
||||
The script reports the lowest continuous supported Go version (where all subsequent
|
||||
versions pass) and only exits with non-zero status if either of the two most recent
|
||||
Go versions fail, indicating immediate attention is needed.
|
||||
|
||||
Note: For Go versions < 1.21, the script automatically updates go.mod to match the
|
||||
target version, but older versions may still fail due to missing standard library
|
||||
features (e.g., the 'slices' package introduced in Go 1.21).
|
||||
|
||||
OPTIONS:
|
||||
-h, --help Show this help message
|
||||
-s, --sequential Run tests sequentially instead of in parallel
|
||||
-v, --verbose Enable verbose output
|
||||
-o, --output DIR Output directory for test results (default: test-results)
|
||||
-t, --timeout TIME Docker timeout for each test (default: 10m)
|
||||
--list List available Go versions and exit
|
||||
|
||||
ARGUMENTS:
|
||||
GO_VERSIONS Specific Go versions to test (default: all supported versions)
|
||||
Examples: 1.21 1.22 1.23
|
||||
|
||||
EXAMPLES:
|
||||
$0 # Test all Go versions in parallel
|
||||
$0 --sequential # Test all Go versions sequentially
|
||||
$0 1.21 1.22 1.23 # Test specific versions
|
||||
$0 --verbose --output ./results 1.24 1.25 # Verbose output to custom directory
|
||||
|
||||
EXIT CODES:
|
||||
0 Recent Go versions pass (good compatibility)
|
||||
1 Recent Go versions fail (needs attention) or script error
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
echo -e "${BLUE}[$(date +'%H:%M:%S')]${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[$(date +'%H:%M:%S')] ✓${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[$(date +'%H:%M:%S')] ✗${NC} $*" >&2
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[$(date +'%H:%M:%S')] ⚠${NC} $*" >&2
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
-s|--sequential)
|
||||
PARALLEL=false
|
||||
shift
|
||||
;;
|
||||
-v|--verbose)
|
||||
VERBOSE=true
|
||||
shift
|
||||
;;
|
||||
-o|--output)
|
||||
OUTPUT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
-t|--timeout)
|
||||
DOCKER_TIMEOUT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--list)
|
||||
echo "Available Go versions:"
|
||||
printf '%s\n' "${GO_VERSIONS[@]}"
|
||||
exit 0
|
||||
;;
|
||||
-*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
# Remaining arguments are Go versions
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# If specific versions provided, use those instead of defaults
|
||||
if [[ $# -gt 0 ]]; then
|
||||
GO_VERSIONS=("$@")
|
||||
fi
|
||||
|
||||
# Validate Go versions
|
||||
for version in "${GO_VERSIONS[@]}"; do
|
||||
if ! [[ "$version" =~ ^1\.(1[1-9]|2[0-5])$ ]]; then
|
||||
log_error "Invalid Go version: $version. Supported versions: 1.11-1.25"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if Docker is available
|
||||
if ! command -v docker &> /dev/null; then
|
||||
log_error "Docker is required but not installed or not in PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Docker daemon is running
|
||||
if ! docker info &> /dev/null; then
|
||||
log_error "Docker daemon is not running"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Function to test a single Go version
|
||||
test_go_version() {
|
||||
local go_version="$1"
|
||||
local container_name="go-toml-test-${go_version}"
|
||||
local result_file="${OUTPUT_DIR}/go-${go_version}.txt"
|
||||
local dockerfile_content
|
||||
|
||||
log "Testing Go $go_version..."
|
||||
|
||||
# Create a temporary Dockerfile for this version
|
||||
# For Go versions < 1.21, we need to update go.mod to match the Go version
|
||||
local needs_go_mod_update=false
|
||||
if [[ $(echo "$go_version 1.21" | tr ' ' '\n' | sort -V | head -n1) == "$go_version" && "$go_version" != "1.21" ]]; then
|
||||
needs_go_mod_update=true
|
||||
fi
|
||||
|
||||
dockerfile_content="FROM golang:${go_version}-alpine
|
||||
|
||||
# Install git (required for go mod)
|
||||
RUN apk add --no-cache git
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy source code
|
||||
COPY . ."
|
||||
|
||||
# Add go.mod update step for older Go versions
|
||||
if [[ "$needs_go_mod_update" == true ]]; then
|
||||
dockerfile_content="$dockerfile_content
|
||||
|
||||
# Update go.mod to match Go version (required for Go < 1.21)
|
||||
RUN if [ -f go.mod ]; then sed -i 's/^go [0-9]\\+\\.[0-9]\\+\\(\\.[0-9]\\+\\)\\?/go $go_version/' go.mod; fi
|
||||
|
||||
# Note: Go versions < 1.21 may fail due to missing standard library packages (e.g., slices)
|
||||
# This is expected for projects that use Go 1.21+ features"
|
||||
fi
|
||||
|
||||
dockerfile_content="$dockerfile_content
|
||||
|
||||
# Run tests
|
||||
CMD [\"sh\", \"-c\", \"go version && echo '--- Running go test ./... ---' && go test ./...\"]"
|
||||
|
||||
# Create temporary directory for this test
|
||||
local temp_dir
|
||||
temp_dir=$(mktemp -d)
|
||||
|
||||
# Copy source to temp directory (excluding test results and git)
|
||||
rsync -a --exclude="$OUTPUT_DIR" --exclude=".git" --exclude="*.test" . "$temp_dir/"
|
||||
|
||||
# Create Dockerfile in temp directory
|
||||
echo "$dockerfile_content" > "$temp_dir/Dockerfile"
|
||||
|
||||
# Build and run container
|
||||
local exit_code=0
|
||||
local output
|
||||
|
||||
if $VERBOSE; then
|
||||
log "Building Docker image for Go $go_version..."
|
||||
fi
|
||||
|
||||
# Capture both stdout and stderr, and the exit code
|
||||
if output=$(cd "$temp_dir" && timeout "$DOCKER_TIMEOUT" docker build -t "$container_name" . 2>&1 && \
|
||||
timeout "$DOCKER_TIMEOUT" docker run --rm "$container_name" 2>&1); then
|
||||
log_success "Go $go_version: PASSED"
|
||||
echo "PASSED" > "${result_file}.status"
|
||||
else
|
||||
exit_code=$?
|
||||
log_error "Go $go_version: FAILED (exit code: $exit_code)"
|
||||
echo "FAILED" > "${result_file}.status"
|
||||
fi
|
||||
|
||||
# Save full output
|
||||
echo "$output" > "$result_file"
|
||||
|
||||
# Clean up
|
||||
docker rmi "$container_name" &> /dev/null || true
|
||||
rm -rf "$temp_dir"
|
||||
|
||||
if $VERBOSE; then
|
||||
echo "--- Go $go_version output ---"
|
||||
echo "$output"
|
||||
echo "--- End Go $go_version output ---"
|
||||
fi
|
||||
|
||||
return $exit_code
|
||||
}
|
||||
|
||||
# Function to run tests in parallel
|
||||
run_parallel() {
|
||||
local pids=()
|
||||
local failed_versions=()
|
||||
|
||||
log "Starting parallel tests for ${#GO_VERSIONS[@]} Go versions..."
|
||||
|
||||
# Start all tests in background
|
||||
for version in "${GO_VERSIONS[@]}"; do
|
||||
test_go_version "$version" &
|
||||
pids+=($!)
|
||||
done
|
||||
|
||||
# Wait for all tests to complete
|
||||
for i in "${!pids[@]}"; do
|
||||
local pid=${pids[$i]}
|
||||
local version=${GO_VERSIONS[$i]}
|
||||
|
||||
if ! wait $pid; then
|
||||
failed_versions+=("$version")
|
||||
fi
|
||||
done
|
||||
|
||||
return ${#failed_versions[@]}
|
||||
}
|
||||
|
||||
# Function to run tests sequentially
|
||||
run_sequential() {
|
||||
local failed_versions=()
|
||||
|
||||
log "Starting sequential tests for ${#GO_VERSIONS[@]} Go versions..."
|
||||
|
||||
for version in "${GO_VERSIONS[@]}"; do
|
||||
if ! test_go_version "$version"; then
|
||||
failed_versions+=("$version")
|
||||
fi
|
||||
done
|
||||
|
||||
return ${#failed_versions[@]}
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
local start_time
|
||||
start_time=$(date +%s)
|
||||
|
||||
log "Starting Go version compatibility tests..."
|
||||
log "Testing versions: ${GO_VERSIONS[*]}"
|
||||
log "Output directory: $OUTPUT_DIR"
|
||||
log "Parallel execution: $PARALLEL"
|
||||
|
||||
local failed_count
|
||||
if $PARALLEL; then
|
||||
run_parallel
|
||||
failed_count=$?
|
||||
else
|
||||
run_sequential
|
||||
failed_count=$?
|
||||
fi
|
||||
|
||||
local end_time
|
||||
end_time=$(date +%s)
|
||||
local duration=$((end_time - start_time))
|
||||
|
||||
# Collect results for display
|
||||
local passed_versions=()
|
||||
local failed_versions=()
|
||||
local unknown_versions=()
|
||||
local passed_count=0
|
||||
|
||||
for version in "${GO_VERSIONS[@]}"; do
|
||||
local status_file="${OUTPUT_DIR}/go-${version}.txt.status"
|
||||
if [[ -f "$status_file" ]]; then
|
||||
local status
|
||||
status=$(cat "$status_file")
|
||||
if [[ "$status" == "PASSED" ]]; then
|
||||
passed_versions+=("$version")
|
||||
((passed_count++))
|
||||
else
|
||||
failed_versions+=("$version")
|
||||
fi
|
||||
else
|
||||
unknown_versions+=("$version")
|
||||
fi
|
||||
done
|
||||
|
||||
# Generate summary report
|
||||
local summary_file="${OUTPUT_DIR}/summary.txt"
|
||||
{
|
||||
echo "Go Version Compatibility Test Summary"
|
||||
echo "====================================="
|
||||
echo "Date: $(date)"
|
||||
echo "Duration: ${duration}s"
|
||||
echo "Parallel: $PARALLEL"
|
||||
echo ""
|
||||
echo "Results:"
|
||||
|
||||
for version in "${GO_VERSIONS[@]}"; do
|
||||
local status_file="${OUTPUT_DIR}/go-${version}.txt.status"
|
||||
if [[ -f "$status_file" ]]; then
|
||||
local status
|
||||
status=$(cat "$status_file")
|
||||
if [[ "$status" == "PASSED" ]]; then
|
||||
echo " Go $version: ✓ PASSED"
|
||||
else
|
||||
echo " Go $version: ✗ FAILED"
|
||||
fi
|
||||
else
|
||||
echo " Go $version: ? UNKNOWN (no status file)"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Summary: $passed_count/${#GO_VERSIONS[@]} versions passed"
|
||||
|
||||
if [[ $failed_count -gt 0 ]]; then
|
||||
echo ""
|
||||
echo "Failed versions details:"
|
||||
for version in "${failed_versions[@]}"; do
|
||||
echo ""
|
||||
echo "--- Go $version (FAILED) ---"
|
||||
local result_file="${OUTPUT_DIR}/go-${version}.txt"
|
||||
if [[ -f "$result_file" ]]; then
|
||||
tail -n 30 "$result_file"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
} > "$summary_file"
|
||||
|
||||
# Find lowest continuous supported version and check recent versions
|
||||
local lowest_continuous_version=""
|
||||
local recent_versions_failed=false
|
||||
|
||||
# Sort versions to ensure proper order
|
||||
local sorted_versions=()
|
||||
for version in "${GO_VERSIONS[@]}"; do
|
||||
sorted_versions+=("$version")
|
||||
done
|
||||
# Sort versions numerically (1.11, 1.12, ..., 1.25)
|
||||
IFS=$'\n' sorted_versions=($(sort -V <<< "${sorted_versions[*]}"))
|
||||
|
||||
# Find lowest continuous supported version (all versions from this point onwards pass)
|
||||
for version in "${sorted_versions[@]}"; do
|
||||
local status_file="${OUTPUT_DIR}/go-${version}.txt.status"
|
||||
local all_subsequent_pass=true
|
||||
|
||||
# Check if this version and all subsequent versions pass
|
||||
local found_current=false
|
||||
for check_version in "${sorted_versions[@]}"; do
|
||||
if [[ "$check_version" == "$version" ]]; then
|
||||
found_current=true
|
||||
fi
|
||||
|
||||
if [[ "$found_current" == true ]]; then
|
||||
local check_status_file="${OUTPUT_DIR}/go-${check_version}.txt.status"
|
||||
if [[ -f "$check_status_file" ]]; then
|
||||
local status
|
||||
status=$(cat "$check_status_file")
|
||||
if [[ "$status" != "PASSED" ]]; then
|
||||
all_subsequent_pass=false
|
||||
break
|
||||
fi
|
||||
else
|
||||
all_subsequent_pass=false
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$all_subsequent_pass" == true ]]; then
|
||||
lowest_continuous_version="$version"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if the two most recent versions failed
|
||||
local num_versions=${#sorted_versions[@]}
|
||||
if [[ $num_versions -ge 2 ]]; then
|
||||
local second_recent="${sorted_versions[$((num_versions-2))]}"
|
||||
local most_recent="${sorted_versions[$((num_versions-1))]}"
|
||||
|
||||
local second_recent_status_file="${OUTPUT_DIR}/go-${second_recent}.txt.status"
|
||||
local most_recent_status_file="${OUTPUT_DIR}/go-${most_recent}.txt.status"
|
||||
|
||||
local second_recent_failed=false
|
||||
local most_recent_failed=false
|
||||
|
||||
if [[ -f "$second_recent_status_file" ]]; then
|
||||
local status
|
||||
status=$(cat "$second_recent_status_file")
|
||||
if [[ "$status" != "PASSED" ]]; then
|
||||
second_recent_failed=true
|
||||
fi
|
||||
else
|
||||
second_recent_failed=true
|
||||
fi
|
||||
|
||||
if [[ -f "$most_recent_status_file" ]]; then
|
||||
local status
|
||||
status=$(cat "$most_recent_status_file")
|
||||
if [[ "$status" != "PASSED" ]]; then
|
||||
most_recent_failed=true
|
||||
fi
|
||||
else
|
||||
most_recent_failed=true
|
||||
fi
|
||||
|
||||
if [[ "$second_recent_failed" == true || "$most_recent_failed" == true ]]; then
|
||||
recent_versions_failed=true
|
||||
fi
|
||||
elif [[ $num_versions -eq 1 ]]; then
|
||||
# Only one version tested, check if it's the most recent and failed
|
||||
local only_version="${sorted_versions[0]}"
|
||||
local only_status_file="${OUTPUT_DIR}/go-${only_version}.txt.status"
|
||||
|
||||
if [[ -f "$only_status_file" ]]; then
|
||||
local status
|
||||
status=$(cat "$only_status_file")
|
||||
if [[ "$status" != "PASSED" ]]; then
|
||||
recent_versions_failed=true
|
||||
fi
|
||||
else
|
||||
recent_versions_failed=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Display summary
|
||||
echo ""
|
||||
log "Test completed in ${duration}s"
|
||||
log "Summary report: $summary_file"
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " FINAL RESULTS"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Display passed versions
|
||||
if [[ ${#passed_versions[@]} -gt 0 ]]; then
|
||||
log_success "PASSED (${#passed_versions[@]}/${#GO_VERSIONS[@]}):"
|
||||
# Sort passed versions for display
|
||||
local sorted_passed=()
|
||||
for version in "${sorted_versions[@]}"; do
|
||||
for passed_version in "${passed_versions[@]}"; do
|
||||
if [[ "$version" == "$passed_version" ]]; then
|
||||
sorted_passed+=("$version")
|
||||
break
|
||||
fi
|
||||
done
|
||||
done
|
||||
for version in "${sorted_passed[@]}"; do
|
||||
echo -e " ${GREEN}✓${NC} Go $version"
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Display failed versions
|
||||
if [[ ${#failed_versions[@]} -gt 0 ]]; then
|
||||
log_error "FAILED (${#failed_versions[@]}/${#GO_VERSIONS[@]}):"
|
||||
# Sort failed versions for display
|
||||
local sorted_failed=()
|
||||
for version in "${sorted_versions[@]}"; do
|
||||
for failed_version in "${failed_versions[@]}"; do
|
||||
if [[ "$version" == "$failed_version" ]]; then
|
||||
sorted_failed+=("$version")
|
||||
break
|
||||
fi
|
||||
done
|
||||
done
|
||||
for version in "${sorted_failed[@]}"; do
|
||||
echo -e " ${RED}✗${NC} Go $version"
|
||||
done
|
||||
echo ""
|
||||
|
||||
# Show failure details
|
||||
echo "========================================"
|
||||
echo " FAILURE DETAILS"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
for version in "${sorted_failed[@]}"; do
|
||||
echo -e "${RED}--- Go $version FAILURE LOGS (last 30 lines) ---${NC}"
|
||||
local result_file="${OUTPUT_DIR}/go-${version}.txt"
|
||||
if [[ -f "$result_file" ]]; then
|
||||
tail -n 30 "$result_file" | sed 's/^/ /'
|
||||
else
|
||||
echo " No log file found: $result_file"
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
fi
|
||||
|
||||
# Display unknown versions
|
||||
if [[ ${#unknown_versions[@]} -gt 0 ]]; then
|
||||
log_warning "UNKNOWN (${#unknown_versions[@]}/${#GO_VERSIONS[@]}):"
|
||||
for version in "${unknown_versions[@]}"; do
|
||||
echo -e " ${YELLOW}?${NC} Go $version (no status file)"
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "========================================"
|
||||
echo " COMPATIBILITY SUMMARY"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
if [[ -n "$lowest_continuous_version" ]]; then
|
||||
log_success "Lowest continuous supported version: Go $lowest_continuous_version"
|
||||
echo " (All versions from Go $lowest_continuous_version onwards pass)"
|
||||
else
|
||||
log_error "No continuous version support found"
|
||||
echo " (No version has all subsequent versions passing)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "Full detailed logs available in: $OUTPUT_DIR"
|
||||
echo "========================================"
|
||||
|
||||
# Determine exit code based on recent versions
|
||||
if [[ "$recent_versions_failed" == true ]]; then
|
||||
log_error "OVERALL RESULT: Recent Go versions failed - this needs attention!"
|
||||
if [[ -n "$lowest_continuous_version" ]]; then
|
||||
echo "Note: Continuous support starts from Go $lowest_continuous_version"
|
||||
fi
|
||||
exit 1
|
||||
else
|
||||
log_success "OVERALL RESULT: Recent Go versions pass - compatibility looks good!"
|
||||
if [[ -n "$lowest_continuous_version" ]]; then
|
||||
echo "Continuous support starts from Go $lowest_continuous_version"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Trap to clean up on exit
|
||||
cleanup() {
|
||||
# Kill any remaining background processes
|
||||
jobs -p | xargs -r kill 2>/dev/null || true
|
||||
|
||||
# Clean up any remaining Docker containers
|
||||
docker ps -q --filter "name=go-toml-test-" | xargs -r docker stop 2>/dev/null || true
|
||||
docker images -q --filter "reference=go-toml-test-*" | xargs -r docker rmi 2>/dev/null || true
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
# Run main function
|
||||
main
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/bin/bash
|
||||
# fail out of the script if anything here fails
|
||||
set -e
|
||||
|
||||
# set the path to the present working directory
|
||||
export GOPATH=`pwd`
|
||||
|
||||
# Vendorize the BurntSushi test suite
|
||||
# NOTE: this gets a specific release to avoid versioning issues
|
||||
if [ ! -d 'src/github.com/BurntSushi/toml-test' ]; then
|
||||
mkdir -p src/github.com/BurntSushi
|
||||
git clone https://github.com/BurntSushi/toml-test.git src/github.com/BurntSushi/toml-test
|
||||
fi
|
||||
pushd src/github.com/BurntSushi/toml-test
|
||||
git reset --hard '0.2.0' # use the released version, NOT tip
|
||||
popd
|
||||
go build -o toml-test github.com/BurntSushi/toml-test
|
||||
|
||||
# vendorize the current lib for testing
|
||||
# NOTE: this basically mocks an install without having to go back out to github for code
|
||||
mkdir -p src/github.com/pelletier/go-toml/cmd
|
||||
cp *.go *.toml src/github.com/pelletier/go-toml
|
||||
cp cmd/*.go src/github.com/pelletier/go-toml/cmd
|
||||
go build -o test_program_bin src/github.com/pelletier/go-toml/cmd/test_program.go
|
||||
|
||||
# Run basic unit tests and then the BurntSushi test suite
|
||||
go test -v github.com/pelletier/go-toml
|
||||
./toml-test ./test_program_bin | tee test_out
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("0=0000-01-01 00:00:00")
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("\"\\n\"=\"\"")
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("''=0")
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("0=0000-01-01")
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("0=\"\"\"\\U00000000\"\"\"")
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("0=[[{}]]")
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("\"\\b\"=\"\"")
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("0=inf")
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("0=0000-01-01 00:00:00+00:00")
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("0=[{}]")
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
[]byte("0=nan")
|
||||
@@ -0,0 +1,243 @@
|
||||
;; This document describes TOML's syntax, using the ABNF format (defined in
|
||||
;; RFC 5234 -- https://www.ietf.org/rfc/rfc5234.txt).
|
||||
;;
|
||||
;; All valid TOML documents will match this description, however certain
|
||||
;; invalid documents would need to be rejected as per the semantics described
|
||||
;; in the supporting text description.
|
||||
|
||||
;; It is possible to try this grammar interactively, using instaparse.
|
||||
;; http://instaparse.mojombo.com/
|
||||
;;
|
||||
;; To do so, in the lower right, click on Options and change `:input-format` to
|
||||
;; ':abnf'. Then paste this entire ABNF document into the grammar entry box
|
||||
;; (above the options). Then you can type or paste a sample TOML document into
|
||||
;; the beige box on the left. Tada!
|
||||
|
||||
;; Overall Structure
|
||||
|
||||
toml = expression *( newline expression )
|
||||
|
||||
expression = ws [ comment ]
|
||||
expression =/ ws keyval ws [ comment ]
|
||||
expression =/ ws table ws [ comment ]
|
||||
|
||||
;; Whitespace
|
||||
|
||||
ws = *wschar
|
||||
wschar = %x20 ; Space
|
||||
wschar =/ %x09 ; Horizontal tab
|
||||
|
||||
;; Newline
|
||||
|
||||
newline = %x0A ; LF
|
||||
newline =/ %x0D.0A ; CRLF
|
||||
|
||||
;; Comment
|
||||
|
||||
comment-start-symbol = %x23 ; #
|
||||
non-ascii = %x80-D7FF / %xE000-10FFFF
|
||||
non-eol = %x09 / %x20-7F / non-ascii
|
||||
|
||||
comment = comment-start-symbol *non-eol
|
||||
|
||||
;; Key-Value pairs
|
||||
|
||||
keyval = key keyval-sep val
|
||||
|
||||
key = simple-key / dotted-key
|
||||
simple-key = quoted-key / unquoted-key
|
||||
|
||||
unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
|
||||
quoted-key = basic-string / literal-string
|
||||
dotted-key = simple-key 1*( dot-sep simple-key )
|
||||
|
||||
dot-sep = ws %x2E ws ; . Period
|
||||
keyval-sep = ws %x3D ws ; =
|
||||
|
||||
val = string / boolean / array / inline-table / date-time / float / integer
|
||||
|
||||
;; String
|
||||
|
||||
string = ml-basic-string / basic-string / ml-literal-string / literal-string
|
||||
|
||||
;; Basic String
|
||||
|
||||
basic-string = quotation-mark *basic-char quotation-mark
|
||||
|
||||
quotation-mark = %x22 ; "
|
||||
|
||||
basic-char = basic-unescaped / escaped
|
||||
basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
|
||||
escaped = escape escape-seq-char
|
||||
|
||||
escape = %x5C ; \
|
||||
escape-seq-char = %x22 ; " quotation mark U+0022
|
||||
escape-seq-char =/ %x5C ; \ reverse solidus U+005C
|
||||
escape-seq-char =/ %x62 ; b backspace U+0008
|
||||
escape-seq-char =/ %x66 ; f form feed U+000C
|
||||
escape-seq-char =/ %x6E ; n line feed U+000A
|
||||
escape-seq-char =/ %x72 ; r carriage return U+000D
|
||||
escape-seq-char =/ %x74 ; t tab U+0009
|
||||
escape-seq-char =/ %x75 4HEXDIG ; uXXXX U+XXXX
|
||||
escape-seq-char =/ %x55 8HEXDIG ; UXXXXXXXX U+XXXXXXXX
|
||||
|
||||
;; Multiline Basic String
|
||||
|
||||
ml-basic-string = ml-basic-string-delim [ newline ] ml-basic-body
|
||||
ml-basic-string-delim
|
||||
ml-basic-string-delim = 3quotation-mark
|
||||
ml-basic-body = *mlb-content *( mlb-quotes 1*mlb-content ) [ mlb-quotes ]
|
||||
|
||||
mlb-content = mlb-char / newline / mlb-escaped-nl
|
||||
mlb-char = mlb-unescaped / escaped
|
||||
mlb-quotes = 1*2quotation-mark
|
||||
mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
|
||||
mlb-escaped-nl = escape ws newline *( wschar / newline )
|
||||
|
||||
;; Literal String
|
||||
|
||||
literal-string = apostrophe *literal-char apostrophe
|
||||
|
||||
apostrophe = %x27 ; ' apostrophe
|
||||
|
||||
literal-char = %x09 / %x20-26 / %x28-7E / non-ascii
|
||||
|
||||
;; Multiline Literal String
|
||||
|
||||
ml-literal-string = ml-literal-string-delim [ newline ] ml-literal-body
|
||||
ml-literal-string-delim
|
||||
ml-literal-string-delim = 3apostrophe
|
||||
ml-literal-body = *mll-content *( mll-quotes 1*mll-content ) [ mll-quotes ]
|
||||
|
||||
mll-content = mll-char / newline
|
||||
mll-char = %x09 / %x20-26 / %x28-7E / non-ascii
|
||||
mll-quotes = 1*2apostrophe
|
||||
|
||||
;; Integer
|
||||
|
||||
integer = dec-int / hex-int / oct-int / bin-int
|
||||
|
||||
minus = %x2D ; -
|
||||
plus = %x2B ; +
|
||||
underscore = %x5F ; _
|
||||
digit1-9 = %x31-39 ; 1-9
|
||||
digit0-7 = %x30-37 ; 0-7
|
||||
digit0-1 = %x30-31 ; 0-1
|
||||
|
||||
hex-prefix = %x30.78 ; 0x
|
||||
oct-prefix = %x30.6F ; 0o
|
||||
bin-prefix = %x30.62 ; 0b
|
||||
|
||||
dec-int = [ minus / plus ] unsigned-dec-int
|
||||
unsigned-dec-int = DIGIT / digit1-9 1*( DIGIT / underscore DIGIT )
|
||||
|
||||
hex-int = hex-prefix HEXDIG *( HEXDIG / underscore HEXDIG )
|
||||
oct-int = oct-prefix digit0-7 *( digit0-7 / underscore digit0-7 )
|
||||
bin-int = bin-prefix digit0-1 *( digit0-1 / underscore digit0-1 )
|
||||
|
||||
;; Float
|
||||
|
||||
float = float-int-part ( exp / frac [ exp ] )
|
||||
float =/ special-float
|
||||
|
||||
float-int-part = dec-int
|
||||
frac = decimal-point zero-prefixable-int
|
||||
decimal-point = %x2E ; .
|
||||
zero-prefixable-int = DIGIT *( DIGIT / underscore DIGIT )
|
||||
|
||||
exp = "e" float-exp-part
|
||||
float-exp-part = [ minus / plus ] zero-prefixable-int
|
||||
|
||||
special-float = [ minus / plus ] ( inf / nan )
|
||||
inf = %x69.6e.66 ; inf
|
||||
nan = %x6e.61.6e ; nan
|
||||
|
||||
;; Boolean
|
||||
|
||||
boolean = true / false
|
||||
|
||||
true = %x74.72.75.65 ; true
|
||||
false = %x66.61.6C.73.65 ; false
|
||||
|
||||
;; Date and Time (as defined in RFC 3339)
|
||||
|
||||
date-time = offset-date-time / local-date-time / local-date / local-time
|
||||
|
||||
date-fullyear = 4DIGIT
|
||||
date-month = 2DIGIT ; 01-12
|
||||
date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year
|
||||
time-delim = "T" / %x20 ; T, t, or space
|
||||
time-hour = 2DIGIT ; 00-23
|
||||
time-minute = 2DIGIT ; 00-59
|
||||
time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second rules
|
||||
time-secfrac = "." 1*DIGIT
|
||||
time-numoffset = ( "+" / "-" ) time-hour ":" time-minute
|
||||
time-offset = "Z" / time-numoffset
|
||||
|
||||
partial-time = time-hour ":" time-minute ":" time-second [ time-secfrac ]
|
||||
full-date = date-fullyear "-" date-month "-" date-mday
|
||||
full-time = partial-time time-offset
|
||||
|
||||
;; Offset Date-Time
|
||||
|
||||
offset-date-time = full-date time-delim full-time
|
||||
|
||||
;; Local Date-Time
|
||||
|
||||
local-date-time = full-date time-delim partial-time
|
||||
|
||||
;; Local Date
|
||||
|
||||
local-date = full-date
|
||||
|
||||
;; Local Time
|
||||
|
||||
local-time = partial-time
|
||||
|
||||
;; Array
|
||||
|
||||
array = array-open [ array-values ] ws-comment-newline array-close
|
||||
|
||||
array-open = %x5B ; [
|
||||
array-close = %x5D ; ]
|
||||
|
||||
array-values = ws-comment-newline val ws-comment-newline array-sep array-values
|
||||
array-values =/ ws-comment-newline val ws-comment-newline [ array-sep ]
|
||||
|
||||
array-sep = %x2C ; , Comma
|
||||
|
||||
ws-comment-newline = *( wschar / [ comment ] newline )
|
||||
|
||||
;; Table
|
||||
|
||||
table = std-table / array-table
|
||||
|
||||
;; Standard Table
|
||||
|
||||
std-table = std-table-open key std-table-close
|
||||
|
||||
std-table-open = %x5B ws ; [ Left square bracket
|
||||
std-table-close = ws %x5D ; ] Right square bracket
|
||||
|
||||
;; Inline Table
|
||||
|
||||
inline-table = inline-table-open [ inline-table-keyvals ] inline-table-close
|
||||
|
||||
inline-table-open = %x7B ws ; {
|
||||
inline-table-close = ws %x7D ; }
|
||||
inline-table-sep = ws %x2C ws ; , Comma
|
||||
|
||||
inline-table-keyvals = keyval [ inline-table-sep inline-table-keyvals ]
|
||||
|
||||
;; Array Table
|
||||
|
||||
array-table = array-table-open key array-table-close
|
||||
|
||||
array-table-open = %x5B.5B ws ; [[ Double left square bracket
|
||||
array-table-close = ws %x5D.5D ; ]] Double right square bracket
|
||||
|
||||
;; Built-in ABNF terms, reproduced here for clarity
|
||||
|
||||
ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
|
||||
DIGIT = %x30-39 ; 0-9
|
||||
HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
|
||||
@@ -1,262 +0,0 @@
|
||||
// TOML markup language parser.
|
||||
//
|
||||
// This version supports the specification as described in
|
||||
// https://github.com/toml-lang/toml/blob/master/versions/toml-v0.2.0.md
|
||||
package toml
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Definition of a TomlTree.
|
||||
// This is the result of the parsing of a TOML file.
|
||||
type TomlTree map[string]interface{}
|
||||
|
||||
// Has returns a boolean indicating if the toplevel tree contains the given
|
||||
// key.
|
||||
func (t *TomlTree) Has(key string) bool {
|
||||
mp := (map[string]interface{})(*t)
|
||||
for k, _ := range mp {
|
||||
if k == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Keys returns the keys of the toplevel tree.
|
||||
// Warning: this is a costly operation.
|
||||
func (t *TomlTree) Keys() []string {
|
||||
keys := make([]string, 0)
|
||||
mp := (map[string]interface{})(*t)
|
||||
for k, _ := range mp {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// Get the value at key in the TomlTree.
|
||||
// Key is a dot-separated path (e.g. a.b.c).
|
||||
// Returns nil if the path does not exist in the tree.
|
||||
// If keys is of length zero, the current tree is returned.
|
||||
func (t *TomlTree) Get(key string) interface{} {
|
||||
if key == "" {
|
||||
return t
|
||||
}
|
||||
return t.GetPath(strings.Split(key, "."))
|
||||
}
|
||||
|
||||
// Returns the element in the tree indicated by 'keys'.
|
||||
// If keys is of length zero, the current tree is returned.
|
||||
func (t *TomlTree) GetPath(keys []string) interface{} {
|
||||
if len(keys) == 0 {
|
||||
return t
|
||||
}
|
||||
subtree := t
|
||||
for _, intermediate_key := range keys[:len(keys)-1] {
|
||||
_, exists := (*subtree)[intermediate_key]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
switch node := (*subtree)[intermediate_key].(type) {
|
||||
case *TomlTree:
|
||||
subtree = node
|
||||
case []*TomlTree:
|
||||
// go to most recent element
|
||||
if len(node) == 0 {
|
||||
return nil //(*subtree)[intermediate_key] = append(node, &TomlTree{})
|
||||
}
|
||||
subtree = node[len(node)-1]
|
||||
}
|
||||
}
|
||||
return (*subtree)[keys[len(keys)-1]]
|
||||
}
|
||||
|
||||
// Same as Get but with a default value
|
||||
func (t *TomlTree) GetDefault(key string, def interface{}) interface{} {
|
||||
val := t.Get(key)
|
||||
if val == nil {
|
||||
return def
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Set an element in the tree.
|
||||
// Key is a dot-separated path (e.g. a.b.c).
|
||||
// Creates all necessary intermediates trees, if needed.
|
||||
func (t *TomlTree) Set(key string, value interface{}) {
|
||||
t.SetPath(strings.Split(key, "."), value)
|
||||
}
|
||||
|
||||
func (t *TomlTree) SetPath(keys []string, value interface{}) {
|
||||
subtree := t
|
||||
for _, intermediate_key := range keys[:len(keys)-1] {
|
||||
_, exists := (*subtree)[intermediate_key]
|
||||
if !exists {
|
||||
var new_tree TomlTree = make(TomlTree)
|
||||
(*subtree)[intermediate_key] = &new_tree
|
||||
}
|
||||
switch node := (*subtree)[intermediate_key].(type) {
|
||||
case *TomlTree:
|
||||
subtree = node
|
||||
case []*TomlTree:
|
||||
// go to most recent element
|
||||
if len(node) == 0 {
|
||||
(*subtree)[intermediate_key] = append(node, &TomlTree{})
|
||||
}
|
||||
subtree = node[len(node)-1]
|
||||
}
|
||||
}
|
||||
(*subtree)[keys[len(keys)-1]] = value
|
||||
}
|
||||
|
||||
// createSubTree takes a tree and a key and create the necessary intermediate
|
||||
// subtrees to create a subtree at that point. In-place.
|
||||
//
|
||||
// e.g. passing a.b.c will create (assuming tree is empty) tree[a], tree[a][b]
|
||||
// and tree[a][b][c]
|
||||
func (t *TomlTree) createSubTree(key string) {
|
||||
subtree := t
|
||||
for _, intermediate_key := range strings.Split(key, ".") {
|
||||
if intermediate_key == "" {
|
||||
panic("empty intermediate table")
|
||||
}
|
||||
_, exists := (*subtree)[intermediate_key]
|
||||
if !exists {
|
||||
var new_tree TomlTree = make(TomlTree)
|
||||
(*subtree)[intermediate_key] = &new_tree
|
||||
}
|
||||
subtree = ((*subtree)[intermediate_key]).(*TomlTree)
|
||||
}
|
||||
}
|
||||
|
||||
// encodes a string to a TOML-compliant string value
|
||||
func encodeTomlString(value string) string {
|
||||
result := ""
|
||||
for _, rr := range value {
|
||||
int_rr := uint16(rr)
|
||||
switch rr {
|
||||
case '\b':
|
||||
result += "\\b"
|
||||
case '\t':
|
||||
result += "\\t"
|
||||
case '\n':
|
||||
result += "\\n"
|
||||
case '\f':
|
||||
result += "\\f"
|
||||
case '\r':
|
||||
result += "\\r"
|
||||
case '"':
|
||||
result += "\\\""
|
||||
case '\\':
|
||||
result += "\\\\"
|
||||
default:
|
||||
if int_rr < 0x001F {
|
||||
result += fmt.Sprintf("\\u%0.4X", int_rr)
|
||||
} else {
|
||||
result += string(rr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Value print support function for ToString()
|
||||
// Outputs the TOML compliant string representation of a value
|
||||
func toTomlValue(item interface{}, indent int) string {
|
||||
tab := strings.Repeat(" ", indent)
|
||||
switch value := item.(type) {
|
||||
case int64:
|
||||
return tab + strconv.FormatInt(value, 10)
|
||||
case float64:
|
||||
return tab + strconv.FormatFloat(value, 'f', -1, 64)
|
||||
case string:
|
||||
return tab + "\"" + encodeTomlString(value) + "\""
|
||||
case bool:
|
||||
if value {
|
||||
return "true"
|
||||
} else {
|
||||
return "false"
|
||||
}
|
||||
case time.Time:
|
||||
return tab + value.Format(time.RFC3339)
|
||||
case []interface{}:
|
||||
result := tab + "[\n"
|
||||
for _, item := range value {
|
||||
result += toTomlValue(item, indent+2) + ",\n"
|
||||
}
|
||||
return result + tab + "]"
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported value type: %v", value))
|
||||
}
|
||||
}
|
||||
|
||||
// Recursive support function for ToString()
|
||||
// Outputs a tree, using the provided keyspace to prefix group names
|
||||
func (t *TomlTree) toToml(keyspace string) string {
|
||||
result := ""
|
||||
for k, v := range (map[string]interface{})(*t) {
|
||||
// figure out the keyspace
|
||||
combined_key := k
|
||||
if keyspace != "" {
|
||||
combined_key = keyspace + "." + combined_key
|
||||
}
|
||||
// output based on type
|
||||
switch node := v.(type) {
|
||||
case []*TomlTree:
|
||||
for _, item := range node {
|
||||
if len(item.Keys()) > 0 {
|
||||
result += fmt.Sprintf("\n[[%s]]\n", combined_key)
|
||||
}
|
||||
result += item.toToml(combined_key)
|
||||
}
|
||||
case *TomlTree:
|
||||
if len(node.Keys()) > 0 {
|
||||
result += fmt.Sprintf("\n[%s]\n", combined_key)
|
||||
}
|
||||
result += node.toToml(combined_key)
|
||||
default:
|
||||
result += fmt.Sprintf("%s = %s\n", k, toTomlValue(node, 0))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Generates a human-readable representation of the current tree.
|
||||
// Output spans multiple lines, and is suitable for ingest by a TOML parser
|
||||
func (t *TomlTree) ToString() string {
|
||||
return t.toToml("")
|
||||
}
|
||||
|
||||
// Create a TomlTree from a string.
|
||||
func Load(content string) (tree *TomlTree, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if _, ok := r.(runtime.Error); ok {
|
||||
panic(r)
|
||||
}
|
||||
err = errors.New(r.(string))
|
||||
}
|
||||
}()
|
||||
_, flow := lex(content)
|
||||
tree = parse(flow)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a TomlTree from a file.
|
||||
func LoadFile(path string) (tree *TomlTree, err error) {
|
||||
buff, ferr := ioutil.ReadFile(path)
|
||||
if ferr != nil {
|
||||
err = ferr
|
||||
} else {
|
||||
s := string(buff)
|
||||
tree, err = Load(s)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTomlGetPath(t *testing.T) {
|
||||
node := make(TomlTree)
|
||||
//TODO: set other node data
|
||||
|
||||
for idx, item := range []struct {
|
||||
Path []string
|
||||
Expected interface{}
|
||||
}{
|
||||
{ // empty path test
|
||||
[]string{},
|
||||
&node,
|
||||
},
|
||||
} {
|
||||
result := node.GetPath(item.Path)
|
||||
if result != item.Expected {
|
||||
t.Errorf("GetPath[%d] %v - expected %v, got %v instead.", idx, item.Path, item.Expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//go:generate go run github.com/toml-lang/toml-test/cmd/toml-test@v1.6.0 -copy ./tests
|
||||
//go:generate go run ./cmd/tomltestgen/main.go -r v1.6.0 -o toml_testgen_test.go
|
||||
|
||||
package toml_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/pelletier/go-toml/v2/internal/assert"
|
||||
"github.com/pelletier/go-toml/v2/internal/testsuite"
|
||||
)
|
||||
|
||||
func testgenInvalid(t *testing.T, input string) {
|
||||
t.Helper()
|
||||
t.Logf("Input TOML:\n%s", input)
|
||||
|
||||
doc := map[string]interface{}{}
|
||||
err := testsuite.Unmarshal([]byte(input), &doc)
|
||||
|
||||
if err == nil {
|
||||
out, err := json.Marshal(doc)
|
||||
if err != nil {
|
||||
panic("could not marshal map to json")
|
||||
}
|
||||
t.Log("JSON output from unmarshal:", string(out))
|
||||
t.Fatalf("test did not fail")
|
||||
}
|
||||
}
|
||||
|
||||
func testgenValid(t *testing.T, input string, jsonRef string) {
|
||||
t.Helper()
|
||||
t.Logf("Input TOML:\n%s", input)
|
||||
|
||||
// TODO: change this to interface{}
|
||||
var doc map[string]interface{}
|
||||
|
||||
err := testsuite.Unmarshal([]byte(input), &doc)
|
||||
if err != nil {
|
||||
de := &toml.DecodeError{}
|
||||
if errors.As(err, &de) {
|
||||
t.Logf("%s\n%s", err, de)
|
||||
}
|
||||
t.Fatalf("failed parsing toml: %s", err)
|
||||
}
|
||||
j, err := testsuite.ValueToTaggedJSON(doc)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var ref interface{}
|
||||
err = json.Unmarshal([]byte(jsonRef), &ref)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var actual interface{}
|
||||
err = json.Unmarshal(j, &actual)
|
||||
assert.NoError(t, err)
|
||||
|
||||
testsuite.CmpJSON(t, "", ref, actual)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
// isZeroer is used to check if a type has a custom IsZero method.
|
||||
// This allows custom types to define their own zero-value semantics.
|
||||
type isZeroer interface {
|
||||
IsZero() bool
|
||||
}
|
||||
|
||||
var (
|
||||
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
|
||||
textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
|
||||
textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
|
||||
isZeroerType = reflect.TypeOf((*isZeroer)(nil)).Elem()
|
||||
mapStringInterfaceType = reflect.TypeOf(map[string]interface{}(nil))
|
||||
sliceInterfaceType = reflect.TypeOf([]interface{}(nil))
|
||||
stringType = reflect.TypeOf("")
|
||||
)
|
||||
+1458
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user