Compare commits
652 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b371733c67 | |||
| 64dcce07ea | |||
| 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 |
@@ -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,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@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
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@v2
|
||||
|
||||
# ℹ️ 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@v2
|
||||
@@ -0,0 +1,20 @@
|
||||
name: coverage
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- v2
|
||||
|
||||
jobs:
|
||||
report:
|
||||
runs-on: "ubuntu-latest"
|
||||
name: report
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@master
|
||||
with:
|
||||
go-version: 1.19
|
||||
- name: Run tests with coverage
|
||||
run: ./ci.sh coverage -d "${GITHUB_BASE_REF-HEAD}"
|
||||
@@ -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@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.19
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release ${{ inputs.args }} --rm-dist
|
||||
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']
|
||||
go: [ '1.18', '1.19' ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: ${{ matrix.go }}/${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup go ${{ matrix.go }}
|
||||
uses: actions/setup-go@master
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Run unit tests
|
||||
run: go test -race ./...
|
||||
release-check:
|
||||
if: ${{ github.ref != 'refs/heads/v2' }}
|
||||
uses: pelletier/go-toml/.github/workflows/release.yml@v2
|
||||
with:
|
||||
args: --snapshot
|
||||
@@ -1 +1,6 @@
|
||||
test_program/test_program_bin
|
||||
fuzz/
|
||||
cmd/tomll/tomll
|
||||
cmd/tomljson/tomljson
|
||||
cmd/tomltestgen/tomltestgen
|
||||
dist
|
||||
@@ -0,0 +1,84 @@
|
||||
[service]
|
||||
golangci-lint-version = "1.39.0"
|
||||
|
||||
[linters-settings.wsl]
|
||||
allow-assign-and-anything = true
|
||||
|
||||
[linters-settings.exhaustive]
|
||||
default-signifies-exhaustive = true
|
||||
|
||||
[linters]
|
||||
disable-all = true
|
||||
enable = [
|
||||
"asciicheck",
|
||||
"bodyclose",
|
||||
"cyclop",
|
||||
"deadcode",
|
||||
"depguard",
|
||||
"dogsled",
|
||||
"dupl",
|
||||
"durationcheck",
|
||||
"errcheck",
|
||||
"errorlint",
|
||||
"exhaustive",
|
||||
# "exhaustivestruct",
|
||||
"exportloopref",
|
||||
"forbidigo",
|
||||
# "forcetypeassert",
|
||||
"funlen",
|
||||
"gci",
|
||||
# "gochecknoglobals",
|
||||
"gochecknoinits",
|
||||
"gocognit",
|
||||
"goconst",
|
||||
"gocritic",
|
||||
"gocyclo",
|
||||
"godot",
|
||||
"godox",
|
||||
# "goerr113",
|
||||
"gofmt",
|
||||
"gofumpt",
|
||||
"goheader",
|
||||
"goimports",
|
||||
"golint",
|
||||
"gomnd",
|
||||
# "gomoddirectives",
|
||||
"gomodguard",
|
||||
"goprintffuncname",
|
||||
"gosec",
|
||||
"gosimple",
|
||||
"govet",
|
||||
# "ifshort",
|
||||
"importas",
|
||||
"ineffassign",
|
||||
"lll",
|
||||
"makezero",
|
||||
"misspell",
|
||||
"nakedret",
|
||||
"nestif",
|
||||
"nilerr",
|
||||
# "nlreturn",
|
||||
"noctx",
|
||||
"nolintlint",
|
||||
#"paralleltest",
|
||||
"prealloc",
|
||||
"predeclared",
|
||||
"revive",
|
||||
"rowserrcheck",
|
||||
"sqlclosecheck",
|
||||
"staticcheck",
|
||||
"structcheck",
|
||||
"stylecheck",
|
||||
# "testpackage",
|
||||
"thelper",
|
||||
"tparallel",
|
||||
"typecheck",
|
||||
"unconvert",
|
||||
"unparam",
|
||||
"unused",
|
||||
"varcheck",
|
||||
"wastedassign",
|
||||
"whitespace",
|
||||
# "wrapcheck",
|
||||
# "wsl"
|
||||
]
|
||||
@@ -0,0 +1,123 @@
|
||||
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
|
||||
- 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
|
||||
- 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_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:
|
||||
name_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,7 +0,0 @@
|
||||
language: go
|
||||
script: "./test.sh"
|
||||
go:
|
||||
- 1.1
|
||||
- 1.2
|
||||
- 1.3
|
||||
- tip
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
# 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.
|
||||
|
||||
### 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.
|
||||
2. Generate release notes using [`gh`][gh]. Example:
|
||||
```
|
||||
$ gh api -X POST \
|
||||
-F tag_name='v2.0.0-beta.5' \
|
||||
-F target_commitish='v2' \
|
||||
-F previous_tag_name='v2.0.0-beta.4' \
|
||||
--jq '.body' \
|
||||
repos/pelletier/go-toml/releases/generate-notes
|
||||
```
|
||||
3. 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. [Draft new release][new-release].
|
||||
5. Fill tag and target with the same value used to generate the changelog.
|
||||
6. Set title to the new tag value.
|
||||
7. Paste the generated changelog.
|
||||
8. Check "create discussion", in the "Releases" category.
|
||||
9. Check pre-release if new version is an alpha or beta.
|
||||
|
||||
[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,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 - 2022 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.
|
||||
@@ -1,118 +1,552 @@
|
||||
# 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.2.0](https://github.com/mojombo/toml/blob/master/versions/toml-v0.2.0.md)
|
||||
This library supports [TOML v1.0.0](https://toml.io/en/v1.0.0).
|
||||
|
||||
[](http://godoc.org/github.com/pelletier/go-toml)
|
||||
[](https://travis-ci.org/pelletier/go-toml)
|
||||
[🐞 Bug Reports](https://github.com/pelletier/go-toml/issues)
|
||||
|
||||
## Features
|
||||
|
||||
Go-toml provides the following features for using data parsed from TOML documents:
|
||||
|
||||
* Load TOML documents from files and string data
|
||||
* Easily navigate TOML structure using TomlTree
|
||||
* Line & column position data for all parsed elements
|
||||
* Query support similar to JSON-Path
|
||||
* Syntax errors contain line and column numbers
|
||||
|
||||
Go-toml is designed to help cover use-cases not covered by reflection-based TOML parsing:
|
||||
|
||||
* Semantic evaluation of parsed TOML
|
||||
* Informing a user of mistakes in the source document, after it has been parsed
|
||||
* Programatic handling of default values on a case-by-case basis
|
||||
* Using a TOML document as a flexible data-store
|
||||
|
||||
## Import
|
||||
|
||||
import "github.com/pelletier/go-toml"
|
||||
|
||||
## Usage
|
||||
|
||||
### Example
|
||||
|
||||
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)
|
||||
|
||||
// show where elements are in the file
|
||||
fmt.Println("User position: %v", configTree.GetPosition("user"))
|
||||
fmt.Println("Password position: %v", configTree.GetPosition("password"))
|
||||
|
||||
// use a query to gather elements without walking the tree
|
||||
results, _ := config.Query("$..[user,password]")
|
||||
for ii, item := range results.Values() {
|
||||
fmt.Println("Query result %d: %v", ii, item)
|
||||
}
|
||||
}
|
||||
```
|
||||
[💬 Anything else](https://github.com/pelletier/go-toml/discussions)
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation and additional examples are 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
|
||||
2. The TOML examples base
|
||||
## 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:
|
||||
|
||||
```
|
||||
2| key1 = "value1"
|
||||
3| key2 = "missing2"
|
||||
| ~~~~ missing field
|
||||
4| key3 = "missing3"
|
||||
5| key4 = "value4"
|
||||
```
|
||||
|
||||
[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
|
||||
|
||||
## 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. 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
|
||||
|
||||
### 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
|
||||
|
||||
## 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>1.9x</td></tr>
|
||||
<tr><td>Marshal/ReferenceFile/map-2</td><td>1.7x</td><td>1.8x</td></tr>
|
||||
<tr><td>Marshal/ReferenceFile/struct-2</td><td>2.2x</td><td>2.5x</td></tr>
|
||||
<tr><td>Unmarshal/HugoFrontMatter-2</td><td>2.9x</td><td>2.9x</td></tr>
|
||||
<tr><td>Unmarshal/ReferenceFile/map-2</td><td>2.6x</td><td>2.9x</td></tr>
|
||||
<tr><td>Unmarshal/ReferenceFile/struct-2</td><td>4.4x</td><td>5.3x</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.9x</td></tr>
|
||||
<tr><td>Marshal/SimpleDocument/struct-2</td><td>2.7x</td><td>4.2x</td></tr>
|
||||
<tr><td>Unmarshal/SimpleDocument/map-2</td><td>4.5x</td><td>3.1x</td></tr>
|
||||
<tr><td>Unmarshal/SimpleDocument/struct-2</td><td>6.2x</td><td>3.9x</td></tr>
|
||||
<tr><td>UnmarshalDataset/example-2</td><td>3.1x</td><td>3.5x</td></tr>
|
||||
<tr><td>UnmarshalDataset/code-2</td><td>2.3x</td><td>3.1x</td></tr>
|
||||
<tr><td>UnmarshalDataset/twitter-2</td><td>2.5x</td><td>2.6x</td></tr>
|
||||
<tr><td>UnmarshalDataset/citm_catalog-2</td><td>2.1x</td><td>2.2x</td></tr>
|
||||
<tr><td>UnmarshalDataset/canada-2</td><td>1.6x</td><td>1.3x</td></tr>
|
||||
<tr><td>UnmarshalDataset/config-2</td><td>4.3x</td><td>3.2x</td></tr>
|
||||
<tr><td>[Geo mean]</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 availble 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`, and `omitempty`. For example:
|
||||
|
||||
```go
|
||||
type doc struct {
|
||||
// v1
|
||||
F string `toml:"field" multiline:"true" omitempty:"true"`
|
||||
// v2
|
||||
F string `toml:"field,multiline,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
Has a result, the `Encoder.SetTag*` methods have been removed, as there is just
|
||||
one tag now.
|
||||
|
||||
|
||||
#### `commented` tag has been removed
|
||||
|
||||
There is no replacement for the `commented` tag. This feature would be better
|
||||
suited in a proper document model for go-toml v2, which has been [cut from
|
||||
scope][nodoc] at the moment.
|
||||
|
||||
#### `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
|
||||
|
||||
Go-toml follows [Semantic Versioning](http://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).
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
|
||||
| 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/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var bench_inputs = []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 bench_inputs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
buf := fixture(t, tc.name)
|
||||
|
||||
var v interface{}
|
||||
require.NoError(t, toml.Unmarshal(buf, &v))
|
||||
|
||||
b, err := json.Marshal(v)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(b), tc.jsonLen)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshalDataset(b *testing.B) {
|
||||
for _, tc := range bench_inputs {
|
||||
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{}
|
||||
require.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)
|
||||
}
|
||||
require.NoError(tb, err)
|
||||
defer f.Close()
|
||||
|
||||
gz, err := gzip.NewReader(f)
|
||||
require.NoError(tb, err)
|
||||
|
||||
buf, err := ioutil.ReadAll(gz)
|
||||
require.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"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUnmarshalSimple(t *testing.T) {
|
||||
doc := []byte(`A = "hello"`)
|
||||
d := struct {
|
||||
A string
|
||||
}{}
|
||||
|
||||
err := toml.Unmarshal(doc, &d)
|
||||
if err != nil {
|
||||
panic(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 {
|
||||
panic(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 {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("ReferenceFile", func(b *testing.B) {
|
||||
bytes, err := ioutil.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 {
|
||||
panic(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 {
|
||||
panic(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 {
|
||||
panic(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 {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
var out []byte
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
out, err = marshal(d)
|
||||
if err != nil {
|
||||
panic(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 {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
var out []byte
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
out, err = marshal(d)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
b.SetBytes(int64(len(out)))
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("ReferenceFile", func(b *testing.B) {
|
||||
bytes, err := ioutil.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 {
|
||||
panic(err)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
var out []byte
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
out, err = marshal(d)
|
||||
if err != nil {
|
||||
panic(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 {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
var out []byte
|
||||
for i := 0; i < b.N; i++ {
|
||||
out, err = marshal(d)
|
||||
if err != nil {
|
||||
panic(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 {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
var out []byte
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
out, err = marshal(d)
|
||||
if err != nil {
|
||||
panic(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 := ioutil.ReadFile("benchmark.toml")
|
||||
require.NoError(t, err)
|
||||
d := benchmarkDoc{}
|
||||
err = toml.Unmarshal(bytes, &d)
|
||||
require.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"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
require.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,71 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var valid10Ascii = []byte("1234567890")
|
||||
var valid10Utf8 = []byte("日本語a")
|
||||
var valid1kUtf8 = bytes.Repeat([]byte("0123456789日本語日本語日本語日abcdefghijklmnopqrstuvwx"), 16)
|
||||
var valid1MUtf8 = bytes.Repeat(valid1kUtf8, 1024)
|
||||
var valid1kAscii = bytes.Repeat([]byte("012345678998jhjklasDJKLAAdjdfjsdklfjdslkabcdefghijklmnopqrstuvwx"), 16)
|
||||
var valid1MAscii = bytes.Repeat(valid1kAscii, 1024)
|
||||
|
||||
func BenchmarkScanComments(b *testing.B) {
|
||||
wrap := func(x []byte) []byte {
|
||||
return []byte("# " + string(x) + "\n")
|
||||
}
|
||||
|
||||
inputs := map[string][]byte{
|
||||
"10Valid": wrap(valid10Ascii),
|
||||
"1kValid": wrap(valid1kAscii),
|
||||
"1MValid": wrap(valid1MAscii),
|
||||
"10ValidUtf8": wrap(valid10Utf8),
|
||||
"1kValidUtf8": wrap(valid1kUtf8),
|
||||
"1MValidUtf8": wrap(valid1MUtf8),
|
||||
}
|
||||
|
||||
for name, input := range inputs {
|
||||
b.Run(name, func(b *testing.B) {
|
||||
b.SetBytes(int64(len(input)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
scanComment(input)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParseLiteralStringValid(b *testing.B) {
|
||||
wrap := func(x []byte) []byte {
|
||||
return []byte("'" + string(x) + "'")
|
||||
}
|
||||
|
||||
inputs := map[string][]byte{
|
||||
"10Valid": wrap(valid10Ascii),
|
||||
"1kValid": wrap(valid1kAscii),
|
||||
"1MValid": wrap(valid1MAscii),
|
||||
"10ValidUtf8": wrap(valid10Utf8),
|
||||
"1kValidUtf8": wrap(valid1kUtf8),
|
||||
"1MValidUtf8": wrap(valid1MUtf8),
|
||||
}
|
||||
|
||||
for name, input := range inputs {
|
||||
b.Run(name, func(b *testing.B) {
|
||||
p := parser{}
|
||||
b.SetBytes(int64(len(input)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _, _, err := p.parseLiteralString(input)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
#!/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 ./...
|
||||
cat coverage.out.tmp | grep -v testsuite | grep -v tomltestgen | grep -v gotoml-test-decoder > coverage.out
|
||||
go tool cover -func=coverage.out
|
||||
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
|
||||
nice -n -19 taskset --cpu-list 0,1 go test '-bench=^Benchmark(Un)?[mM]arshal' -count=5 -run=Nothing ./... | tee "${out}"
|
||||
popd
|
||||
|
||||
if [ "${branch}" != "HEAD" ]; then
|
||||
git worktree remove --force "$dir"
|
||||
fi
|
||||
}
|
||||
|
||||
fmktemp() {
|
||||
if mktemp --version|grep GNU >/dev/null; then
|
||||
mktemp --suffix=-$1;
|
||||
else
|
||||
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[1:]):
|
||||
v2 = float(line[1])
|
||||
results.append([
|
||||
line[0].replace("-32", ""),
|
||||
"%.1fx" % (float(line[3])/v2), # v1
|
||||
"%.1fx" % (float(line[5])/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 -csv -geomean go-toml-v2.txt go-toml-v1.txt bs-toml.txt > $tmpcsv
|
||||
benchstathtml $tmpcsv
|
||||
else
|
||||
benchstat -geomean 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,30 @@
|
||||
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,55 @@
|
||||
// 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"
|
||||
"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
|
||||
`
|
||||
|
||||
func main() {
|
||||
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)
|
||||
err := d.Decode(&v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e := toml.NewEncoder(w)
|
||||
return e.Encode(v)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConvert(t *testing.T) {
|
||||
examples := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
errors bool
|
||||
}{
|
||||
{
|
||||
name: "valid json",
|
||||
input: `
|
||||
{
|
||||
"mytoml": {
|
||||
"a": 42
|
||||
}
|
||||
}`,
|
||||
expected: `[mytoml]
|
||||
a = 42.0
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "invalid json",
|
||||
input: `{ foo`,
|
||||
errors: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
b := new(bytes.Buffer)
|
||||
err := convert(strings.NewReader(e.input), b)
|
||||
if e.errors {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, e.expected, b.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +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(*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(*orig)
|
||||
case toml.TomlTree:
|
||||
keys := orig.Keys()
|
||||
typed := make(map[string]interface{}, len(keys))
|
||||
for _, k := range keys {
|
||||
typed[k] = translate(orig.GetPath([]string{k}))
|
||||
}
|
||||
return typed
|
||||
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,61 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
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 {
|
||||
require.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, fmt.Errorf("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,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
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 {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, e.expected, b.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// 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 (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
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 = "// Generated by tomltestgen for toml-test ref {{.Ref}} on {{.Timestamp}}\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 downloadTmpFile(url string) string {
|
||||
log.Println("starting to download file from", url)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
tmpfile, err := ioutil.TempFile("", "toml-test-*.zip")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer tmpfile.Close()
|
||||
|
||||
copiedLen, err := io.Copy(tmpfile, resp.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if resp.ContentLength > 0 && copiedLen != resp.ContentLength {
|
||||
panic(fmt.Errorf("copied %d bytes, request body had %d", copiedLen, resp.ContentLength))
|
||||
}
|
||||
return tmpfile.Name()
|
||||
}
|
||||
|
||||
func kebabToCamel(kebab string) string {
|
||||
camel := ""
|
||||
nextUpper := true
|
||||
for _, c := range kebab {
|
||||
if nextUpper {
|
||||
camel += strings.ToUpper(string(c))
|
||||
nextUpper = false
|
||||
} else if c == '-' {
|
||||
nextUpper = true
|
||||
} else if c == '/' {
|
||||
nextUpper = true
|
||||
camel += "_"
|
||||
} else {
|
||||
camel += string(c)
|
||||
}
|
||||
}
|
||||
return camel
|
||||
}
|
||||
|
||||
func readFileFromZip(f *zip.File) string {
|
||||
reader, err := f.Open()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer reader.Close()
|
||||
bytes, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
url := "https://codeload.github.com/BurntSushi/toml-test/zip/" + *ref
|
||||
resultFile := downloadTmpFile(url)
|
||||
defer os.Remove(resultFile)
|
||||
log.Println("file written to", resultFile)
|
||||
|
||||
zipReader, err := zip.OpenReader(resultFile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer zipReader.Close()
|
||||
|
||||
collection := testsCollection{
|
||||
Ref: *ref,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
zipFilesMap := map[string]*zip.File{}
|
||||
|
||||
for _, f := range zipReader.File {
|
||||
zipFilesMap[f.Name] = f
|
||||
}
|
||||
|
||||
testFileRegexp := regexp.MustCompile(`([^/]+/tests/(valid|invalid)/(.+))\.(toml)`)
|
||||
for _, f := range zipReader.File {
|
||||
groups := testFileRegexp.FindStringSubmatch(f.Name)
|
||||
if len(groups) > 0 {
|
||||
name := kebabToCamel(groups[3])
|
||||
testType := groups[2]
|
||||
|
||||
log.Printf("> [%s] %s\n", testType, name)
|
||||
|
||||
tomlContent := readFileFromZip(f)
|
||||
|
||||
switch testType {
|
||||
case "invalid":
|
||||
collection.Invalid = append(collection.Invalid, invalid{
|
||||
Name: name,
|
||||
Input: tomlContent,
|
||||
})
|
||||
collection.Count++
|
||||
case "valid":
|
||||
baseFilePath := groups[1]
|
||||
jsonFilePath := baseFilePath + ".json"
|
||||
jsonContent := readFileFromZip(zipFilesMap[jsonFilePath])
|
||||
|
||||
collection.Valid = append(collection.Valid, valid{
|
||||
Name: name,
|
||||
Input: tomlContent,
|
||||
JsonRef: jsonContent,
|
||||
})
|
||||
collection.Count++
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown test type: %s", testType))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, 0644)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,544 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
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, newDecodeError(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{}, newDecodeError(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, newDecodeError(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{}, newDecodeError(b, "invalid date-time timezone")
|
||||
}
|
||||
var direction int
|
||||
switch b[0] {
|
||||
case '-':
|
||||
direction = -1
|
||||
case '+':
|
||||
direction = +1
|
||||
default:
|
||||
return time.Time{}, newDecodeError(b[:1], "invalid timezone offset character")
|
||||
}
|
||||
|
||||
if b[3] != ':' {
|
||||
return time.Time{}, newDecodeError(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{}, newDecodeError(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{}, newDecodeError(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{}, newDecodeError(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, newDecodeError(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, newDecodeError(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, newDecodeError(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, newDecodeError(b[0:2], "hour cannot be greater 23")
|
||||
}
|
||||
if b[2] != ':' {
|
||||
return t, nil, newDecodeError(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, newDecodeError(b[3:5], "minutes cannot be greater 59")
|
||||
}
|
||||
if b[5] != ':' {
|
||||
return t, nil, newDecodeError(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 > 60 {
|
||||
return t, nil, newDecodeError(b[6:8], "seconds cannot be greater 60")
|
||||
}
|
||||
|
||||
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, newDecodeError(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, newDecodeError(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
|
||||
}
|
||||
|
||||
//nolint:cyclop
|
||||
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, newDecodeError(b, "float cannot start with a dot")
|
||||
}
|
||||
|
||||
if cleaned[len(cleaned)-1] == '.' {
|
||||
return 0, newDecodeError(b, "float cannot end with a dot")
|
||||
}
|
||||
|
||||
dotAlreadySeen := false
|
||||
for i, c := range cleaned {
|
||||
if c == '.' {
|
||||
if dotAlreadySeen {
|
||||
return 0, newDecodeError(b[i:i+1], "float can have at most one decimal point")
|
||||
}
|
||||
if !isDigit(cleaned[i-1]) {
|
||||
return 0, newDecodeError(b[i-1:i+1], "float decimal point must be preceded by a digit")
|
||||
}
|
||||
if !isDigit(cleaned[i+1]) {
|
||||
return 0, newDecodeError(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' && isDigit(cleaned[start+1]) {
|
||||
return 0, newDecodeError(b, "float integer part cannot have leading zeroes")
|
||||
}
|
||||
|
||||
f, err := strconv.ParseFloat(string(cleaned), 64)
|
||||
if err != nil {
|
||||
return 0, newDecodeError(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, newDecodeError(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, newDecodeError(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, newDecodeError(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, newDecodeError(b, "leading zero not allowed on decimal number")
|
||||
}
|
||||
|
||||
i, err := strconv.ParseInt(string(cleaned), 10, 64)
|
||||
if err != nil {
|
||||
return 0, newDecodeError(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, newDecodeError(b[start:start+1], "number cannot start with underscore")
|
||||
}
|
||||
|
||||
if b[len(b)-1] == '_' {
|
||||
return nil, newDecodeError(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, newDecodeError(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, newDecodeError(b[0:1], "number cannot start with underscore")
|
||||
}
|
||||
|
||||
if b[len(b)-1] == '_' {
|
||||
return nil, newDecodeError(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, newDecodeError(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, newDecodeError(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, newDecodeError(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, newDecodeError(b[i+1:i+2], "cannot have underscore after decimal point")
|
||||
}
|
||||
if i > 0 && b[i-1] == '_' {
|
||||
return nil, newDecodeError(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)
|
||||
}
|
||||
@@ -1,245 +1,2 @@
|
||||
// Package toml is a 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
|
||||
//
|
||||
// TOML Parsing
|
||||
//
|
||||
// TOML data may be parsed in two ways: by file, or by string.
|
||||
//
|
||||
// // load TOML data by filename
|
||||
// tree, err := toml.LoadFile("filename.toml")
|
||||
//
|
||||
// // load TOML data stored in a string
|
||||
// tree, err := toml.Load(stringContainingTomlData)
|
||||
//
|
||||
// Either way, the result is a TomlTree object that can be used to navigate the
|
||||
// structure and data within the original document.
|
||||
//
|
||||
//
|
||||
// Getting data from the TomlTree
|
||||
//
|
||||
// After parsing TOML data with Load() or LoadFile(), use the Has() and Get()
|
||||
// methods on the returned TomlTree, to find your way through the document data.
|
||||
//
|
||||
// if tree.Has('foo') {
|
||||
// fmt.Prinln("foo is: %v", tree.Get('foo'))
|
||||
// }
|
||||
//
|
||||
// Working with Paths
|
||||
//
|
||||
// Go-toml has support for basic dot-separated key paths on the Has(), Get(), Set()
|
||||
// and GetDefault() methods. These are the same kind of key paths used within the
|
||||
// TOML specification for struct tames.
|
||||
//
|
||||
// // looks for a key named 'baz', within struct 'bar', within struct 'foo'
|
||||
// tree.Has("foo.bar.baz")
|
||||
//
|
||||
// // returns the key at this path, if it is there
|
||||
// tree.Get("foo.bar.baz")
|
||||
//
|
||||
// TOML allows keys to contain '.', which can cause this syntax to be problematic
|
||||
// for some documents. In such cases, use the GetPath(), HasPath(), and SetPath(),
|
||||
// methods to explicitly define the path. This form is also faster, since
|
||||
// it avoids having to parse the passed key for '.' delimiters.
|
||||
//
|
||||
// // looks for a key named 'baz', within struct 'bar', within struct 'foo'
|
||||
// tree.HasPath(string{}{"foo","bar","baz"})
|
||||
//
|
||||
// // returns the key at this path, if it is there
|
||||
// tree.GetPath(string{}{"foo","bar","baz"})
|
||||
//
|
||||
// Note that this is distinct from the heavyweight query syntax supported by
|
||||
// TomlTree.Query() and the Query() struct (see below).
|
||||
//
|
||||
// Position Support
|
||||
//
|
||||
// Each element within the TomlTree is stored with position metadata, which is
|
||||
// invaluable for providing semantic feedback to a user. This helps in
|
||||
// situations where the TOML file parses correctly, but contains data that is
|
||||
// not correct for the application. In such cases, an error message can be
|
||||
// generated that indicates the problem line and column number in the source
|
||||
// TOML document.
|
||||
//
|
||||
// // load TOML data
|
||||
// tree, _ := toml.Load("filename.toml")
|
||||
//
|
||||
// // get an entry and report an error if it's the wrong type
|
||||
// element := tree.Get("foo")
|
||||
// if value, ok := element.(int64); !ok {
|
||||
// return fmt.Errorf("%v: Element 'foo' must be an integer", tree.GetPosition("foo"))
|
||||
// }
|
||||
//
|
||||
// // report an error if an expected element is missing
|
||||
// if !tree.Has("bar") {
|
||||
// return fmt.Errorf("%v: Expected 'bar' element", tree.GetPosition(""))
|
||||
// }
|
||||
//
|
||||
// Query Support
|
||||
//
|
||||
// The TOML query path implementation is based loosely on the JSONPath specification:
|
||||
// http://goessner.net/articles/JsonPath/
|
||||
//
|
||||
// The idea behind a query path is to allow quick access to any element, or set
|
||||
// of elements within TOML document, with a single expression.
|
||||
//
|
||||
// result := tree.Query("$.foo.bar.baz") // result is 'nil' if the path is not present
|
||||
//
|
||||
// This is equivalent to:
|
||||
//
|
||||
// next := tree.Get("foo")
|
||||
// if next != nil {
|
||||
// next = next.Get("bar")
|
||||
// if next != nil {
|
||||
// next = next.Get("baz")
|
||||
// }
|
||||
// }
|
||||
// result := next
|
||||
//
|
||||
// As illustrated above, the query path is much more efficient, especially since
|
||||
// the structure of the TOML file can vary. Rather than making assumptions about
|
||||
// a document's structure, a query allows the programmer to make structured
|
||||
// requests into the document, and get zero or more values as a result.
|
||||
//
|
||||
// The syntax of a query begins with a root token, followed by any number
|
||||
// sub-expressions:
|
||||
//
|
||||
// $
|
||||
// Root of the TOML tree. This must always come first.
|
||||
// .name
|
||||
// Selects child of this node, where 'name' is a TOML key
|
||||
// name.
|
||||
// ['name']
|
||||
// Selects child of this node, where 'name' is a string
|
||||
// containing a TOML key name.
|
||||
// [index]
|
||||
// Selcts child array element at 'index'.
|
||||
// ..expr
|
||||
// Recursively selects all children, filtered by an a union,
|
||||
// index, or slice expression.
|
||||
// ..*
|
||||
// Recursive selection of all nodes at this point in the
|
||||
// tree.
|
||||
// .*
|
||||
// Selects all children of the current node.
|
||||
// [expr,expr]
|
||||
// Union operator - a logical 'or' grouping of two or more
|
||||
// sub-expressions: index, key name, or filter.
|
||||
// [start:end:step]
|
||||
// Slice operator - selects array elements from start to
|
||||
// end-1, at the given step. All three arguments are
|
||||
// optional.
|
||||
// [?(filter)]
|
||||
// Named filter expression - the function 'filter' is
|
||||
// used to filter children at this node.
|
||||
//
|
||||
// Query Indexes And Slices
|
||||
//
|
||||
// Index expressions perform no bounds checking, and will contribute no
|
||||
// values to the result set if the provided index or index range is invalid.
|
||||
// Negative indexes represent values from the end of the array, counting backwards.
|
||||
//
|
||||
// // select the last index of the array named 'foo'
|
||||
// tree.Query("$.foo[-1]")
|
||||
//
|
||||
// Slice expressions are supported, by using ':' to separate a start/end index pair.
|
||||
//
|
||||
// // select up to the first five elements in the array
|
||||
// tree.Query("$.foo[0:5]")
|
||||
//
|
||||
// Slice expressions also allow negative indexes for the start and stop
|
||||
// arguments.
|
||||
//
|
||||
// // select all array elements.
|
||||
// tree.Query("$.foo[0:-1]")
|
||||
//
|
||||
// Slice expressions may have an optional stride/step parameter:
|
||||
//
|
||||
// // select every other element
|
||||
// tree.Query("$.foo[0:-1:2]")
|
||||
//
|
||||
// Slice start and end parameters are also optional:
|
||||
//
|
||||
// // these are all equivalent and select all the values in the array
|
||||
// tree.Query("$.foo[:]")
|
||||
// tree.Query("$.foo[0:]")
|
||||
// tree.Query("$.foo[:-1]")
|
||||
// tree.Query("$.foo[0:-1:]")
|
||||
// tree.Query("$.foo[::1]")
|
||||
// tree.Query("$.foo[0::1]")
|
||||
// tree.Query("$.foo[:-1:1]")
|
||||
// tree.Query("$.foo[0:-1:1]")
|
||||
//
|
||||
// Query Filters
|
||||
//
|
||||
// Query filters are used within a Union [,] or single Filter [] expression.
|
||||
// A filter only allows nodes that qualify through to the next expression,
|
||||
// and/or into the result set.
|
||||
//
|
||||
// // returns children of foo that are permitted by the 'bar' filter.
|
||||
// tree.Query("$.foo[?(bar)]")
|
||||
//
|
||||
// There are several filters provided with the library:
|
||||
//
|
||||
// tree
|
||||
// Allows nodes of type TomlTree.
|
||||
// int
|
||||
// Allows nodes of type int64.
|
||||
// float
|
||||
// Allows nodes of type float64.
|
||||
// string
|
||||
// Allows nodes of type string.
|
||||
// time
|
||||
// Allows nodes of type time.Time.
|
||||
// bool
|
||||
// Allows nodes of type bool.
|
||||
//
|
||||
// Query Results
|
||||
//
|
||||
// An executed query returns a QueryResult object. This contains the nodes
|
||||
// in the TOML tree that qualify the query expression. Position information
|
||||
// is also available for each value in the set.
|
||||
//
|
||||
// // display the results of a query
|
||||
// results := tree.Query("$.foo.bar.baz")
|
||||
// for idx, value := results.Values() {
|
||||
// fmt.Println("%v: %v", results.Positions()[idx], value)
|
||||
// }
|
||||
//
|
||||
// Compiled Queries
|
||||
//
|
||||
// Queries may be executed directly on a TomlTree object, or compiled ahead
|
||||
// of time and executed discretely. The former is more convienent, but has the
|
||||
// penalty of having to recompile the query expression each time.
|
||||
//
|
||||
// // basic query
|
||||
// results := tree.Query("$.foo.bar.baz")
|
||||
//
|
||||
// // compiled query
|
||||
// query := toml.CompileQuery("$.foo.bar.baz")
|
||||
// results := query.Execute(tree)
|
||||
//
|
||||
// // run the compiled query again on a different tree
|
||||
// moreResults := query.Execute(anotherTree)
|
||||
//
|
||||
// User Defined Query Filters
|
||||
//
|
||||
// Filter expressions may also be user defined by using the SetFilter()
|
||||
// function on the Query object. The function must return true/false, which
|
||||
// signifies if the passed node is kept or discarded, respectively.
|
||||
//
|
||||
// // create a query that references a user-defined filter
|
||||
// query, _ := CompileQuery("$[?(bazOnly)]")
|
||||
//
|
||||
// // define the filter, and assign it to the query
|
||||
// query.SetFilter("bazOnly", func(node interface{}) bool{
|
||||
// if tree, ok := node.(*TomlTree); ok {
|
||||
// return tree.Has("baz")
|
||||
// }
|
||||
// return false // reject all other node types
|
||||
// })
|
||||
//
|
||||
// // run the query
|
||||
// query.Execute(tree)
|
||||
//
|
||||
// Package toml is a library to read and write TOML documents.
|
||||
package toml
|
||||
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
// code examples for godoc
|
||||
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func ExampleNodeFilterFn_filterExample() {
|
||||
tree, _ := Load(`
|
||||
[struct_one]
|
||||
foo = "foo"
|
||||
bar = "bar"
|
||||
|
||||
[struct_two]
|
||||
baz = "baz"
|
||||
gorf = "gorf"
|
||||
`)
|
||||
|
||||
// create a query that references a user-defined-filter
|
||||
query, _ := CompileQuery("$[?(bazOnly)]")
|
||||
|
||||
// define the filter, and assign it to the query
|
||||
query.SetFilter("bazOnly", func(node interface{}) bool {
|
||||
if tree, ok := node.(*TomlTree); ok {
|
||||
return tree.Has("baz")
|
||||
}
|
||||
return false // reject all other node types
|
||||
})
|
||||
|
||||
// results contain only the 'struct_two' TomlTree
|
||||
query.Execute(tree)
|
||||
}
|
||||
|
||||
func ExampleQuery_queryExample() {
|
||||
config, _ := Load(`
|
||||
[[book]]
|
||||
title = "The Stand"
|
||||
author = "Stephen King"
|
||||
[[book]]
|
||||
title = "For Whom the Bell Tolls"
|
||||
author = "Ernest Hemmingway"
|
||||
[[book]]
|
||||
title = "Neuromancer"
|
||||
author = "William Gibson"
|
||||
`)
|
||||
|
||||
// find and print all the authors in the document
|
||||
authors, _ := config.Query("$.book.author")
|
||||
for _, name := range authors.Values() {
|
||||
fmt.Println(name)
|
||||
}
|
||||
}
|
||||
|
||||
func Example_comprehensiveExample() {
|
||||
config, err := 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").(*TomlTree)
|
||||
user = configTree.Get("user").(string)
|
||||
password = configTree.Get("password").(string)
|
||||
fmt.Println("User is ", user, ". Password is ", password)
|
||||
|
||||
// show where elements are in the file
|
||||
fmt.Println("User position: %v", configTree.GetPosition("user"))
|
||||
fmt.Println("Password position: %v", configTree.GetPosition("password"))
|
||||
|
||||
// use a query to gather elements without walking the tree
|
||||
results, _ := config.Query("$..[user,password]")
|
||||
for ii, item := range results.Values() {
|
||||
fmt.Println("Query result %d: %v", ii, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/internal/danger"
|
||||
)
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
type Key []string
|
||||
|
||||
// internal version of DecodeError that is used as the base to create a
|
||||
// DecodeError with full context.
|
||||
type decodeError struct {
|
||||
highlight []byte
|
||||
message string
|
||||
key Key // optional
|
||||
}
|
||||
|
||||
func (de *decodeError) Error() string {
|
||||
return de.message
|
||||
}
|
||||
|
||||
func newDecodeError(highlight []byte, format string, args ...interface{}) error {
|
||||
return &decodeError{
|
||||
highlight: highlight,
|
||||
message: fmt.Errorf(format, args...).Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// decodeErrorFromHighlight 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 *decodeError) *DecodeError {
|
||||
offset := danger.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
|
||||
}
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
//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.Write([]byte(e.doc[0]))
|
||||
start := b.Len()
|
||||
b.Write([]byte(e.doc[1]))
|
||||
end := b.Len()
|
||||
b.Write([]byte(e.doc[2]))
|
||||
doc := b.Bytes()
|
||||
hl := doc[start:end]
|
||||
|
||||
err := wrapDecodeError(doc, &decodeError{
|
||||
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 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
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package toml_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFastSimple(t *testing.T) {
|
||||
m := map[string]int64{}
|
||||
err := toml.Unmarshal([]byte(`a = 42`), &m)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]int64{"a": 42}, m)
|
||||
}
|
||||
|
||||
func TestFastSimpleString(t *testing.T) {
|
||||
m := map[string]string{}
|
||||
err := toml.Unmarshal([]byte(`a = "hello"`), &m)
|
||||
require.NoError(t, err)
|
||||
require.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)
|
||||
require.NoError(t, err)
|
||||
require.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)
|
||||
require.NoError(t, err)
|
||||
require.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)
|
||||
require.NoError(t, err)
|
||||
require.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)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.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,56 @@
|
||||
//go:build go1.18 || go1.19
|
||||
// +build go1.18 go1.19
|
||||
|
||||
package toml_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func FuzzUnmarshal(f *testing.F) {
|
||||
file, err := ioutil.ReadFile("benchmark/benchmark.toml")
|
||||
if err != nil {
|
||||
panic(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)
|
||||
}
|
||||
require.Equal(t, v, v2)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
module github.com/pelletier/go-toml/v2
|
||||
|
||||
go 1.16
|
||||
|
||||
require github.com/stretchr/testify v1.8.0
|
||||
@@ -0,0 +1,15 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,144 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/internal/danger"
|
||||
)
|
||||
|
||||
// Iterator starts uninitialized, you need to call Next() first.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// it := n.Children()
|
||||
// for it.Next() {
|
||||
// it.Node()
|
||||
// }
|
||||
type Iterator struct {
|
||||
started bool
|
||||
node *Node
|
||||
}
|
||||
|
||||
// Next moves the iterator forward and returns true if points to a
|
||||
// node, false otherwise.
|
||||
func (c *Iterator) Next() bool {
|
||||
if !c.started {
|
||||
c.started = true
|
||||
} else if c.node.Valid() {
|
||||
c.node = c.node.Next()
|
||||
}
|
||||
return c.node.Valid()
|
||||
}
|
||||
|
||||
// IsLast returns true if the current node of the iterator is the last
|
||||
// one. Subsequent call to Next() will return false.
|
||||
func (c *Iterator) IsLast() bool {
|
||||
return c.node.next == 0
|
||||
}
|
||||
|
||||
// Node returns a copy of the node pointed at by the iterator.
|
||||
func (c *Iterator) Node() *Node {
|
||||
return c.node
|
||||
}
|
||||
|
||||
// Root contains a full AST.
|
||||
//
|
||||
// It is immutable once constructed with Builder.
|
||||
type Root struct {
|
||||
nodes []Node
|
||||
}
|
||||
|
||||
// Iterator over the top level nodes.
|
||||
func (r *Root) Iterator() Iterator {
|
||||
it := Iterator{}
|
||||
if len(r.nodes) > 0 {
|
||||
it.node = &r.nodes[0]
|
||||
}
|
||||
return it
|
||||
}
|
||||
|
||||
func (r *Root) at(idx Reference) *Node {
|
||||
return &r.nodes[idx]
|
||||
}
|
||||
|
||||
// Arrays have one child per element in the array. InlineTables have
|
||||
// one child per key-value pair in the table. KeyValues have at least
|
||||
// two children. The first one is the value. The rest make a
|
||||
// potentially dotted key. Table and Array table have one child per
|
||||
// element of the key they represent (same as KeyValue, but without
|
||||
// the last node being the value).
|
||||
type Node struct {
|
||||
Kind Kind
|
||||
Raw Range // Raw bytes from the input.
|
||||
Data []byte // Node value (either allocated or referencing the input).
|
||||
|
||||
// References to other nodes, as offsets in the backing array
|
||||
// from this node. References can go backward, so those can be
|
||||
// negative.
|
||||
next int // 0 if last element
|
||||
child int // 0 if no child
|
||||
}
|
||||
|
||||
type Range struct {
|
||||
Offset uint32
|
||||
Length uint32
|
||||
}
|
||||
|
||||
// Next returns a copy of the next node, or an invalid Node if there
|
||||
// is no next node.
|
||||
func (n *Node) Next() *Node {
|
||||
if n.next == 0 {
|
||||
return nil
|
||||
}
|
||||
ptr := unsafe.Pointer(n)
|
||||
size := unsafe.Sizeof(Node{})
|
||||
return (*Node)(danger.Stride(ptr, size, n.next))
|
||||
}
|
||||
|
||||
// Child returns a copy of the first child node of this node. Other
|
||||
// children can be accessed calling Next on the first child. Returns
|
||||
// an invalid Node if there is none.
|
||||
func (n *Node) Child() *Node {
|
||||
if n.child == 0 {
|
||||
return nil
|
||||
}
|
||||
ptr := unsafe.Pointer(n)
|
||||
size := unsafe.Sizeof(Node{})
|
||||
return (*Node)(danger.Stride(ptr, size, n.child))
|
||||
}
|
||||
|
||||
// Valid returns true if the node's kind is set (not to Invalid).
|
||||
func (n *Node) Valid() bool {
|
||||
return n != nil
|
||||
}
|
||||
|
||||
// Key returns the child nodes making the Key on a supported
|
||||
// node. Panics otherwise. They are guaranteed to be all be of the
|
||||
// Kind Key. A simple key would return just one element.
|
||||
func (n *Node) Key() Iterator {
|
||||
switch n.Kind {
|
||||
case KeyValue:
|
||||
value := n.Child()
|
||||
if !value.Valid() {
|
||||
panic(fmt.Errorf("KeyValue should have at least two children"))
|
||||
}
|
||||
return Iterator{node: value.Next()}
|
||||
case Table, ArrayTable:
|
||||
return Iterator{node: n.Child()}
|
||||
default:
|
||||
panic(fmt.Errorf("Key() is not supported on a %s", n.Kind))
|
||||
}
|
||||
}
|
||||
|
||||
// Value returns a pointer to the value node of a KeyValue.
|
||||
// Guaranteed to be non-nil. Panics if not called on a KeyValue node,
|
||||
// or if the Children are malformed.
|
||||
func (n *Node) Value() *Node {
|
||||
return n.Child()
|
||||
}
|
||||
|
||||
// Children returns an iterator over a node's children.
|
||||
func (n *Node) Children() Iterator {
|
||||
return Iterator{node: n.Child()}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package ast
|
||||
|
||||
type Reference int
|
||||
|
||||
const InvalidReference Reference = -1
|
||||
|
||||
func (r Reference) Valid() bool {
|
||||
return r != InvalidReference
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
tree Root
|
||||
lastIdx int
|
||||
}
|
||||
|
||||
func (b *Builder) Tree() *Root {
|
||||
return &b.tree
|
||||
}
|
||||
|
||||
func (b *Builder) NodeAt(ref Reference) *Node {
|
||||
return b.tree.at(ref)
|
||||
}
|
||||
|
||||
func (b *Builder) Reset() {
|
||||
b.tree.nodes = b.tree.nodes[:0]
|
||||
b.lastIdx = 0
|
||||
}
|
||||
|
||||
func (b *Builder) Push(n Node) Reference {
|
||||
b.lastIdx = len(b.tree.nodes)
|
||||
b.tree.nodes = append(b.tree.nodes, n)
|
||||
return Reference(b.lastIdx)
|
||||
}
|
||||
|
||||
func (b *Builder) PushAndChain(n Node) Reference {
|
||||
newIdx := len(b.tree.nodes)
|
||||
b.tree.nodes = append(b.tree.nodes, n)
|
||||
if b.lastIdx >= 0 {
|
||||
b.tree.nodes[b.lastIdx].next = newIdx - b.lastIdx
|
||||
}
|
||||
b.lastIdx = newIdx
|
||||
return Reference(b.lastIdx)
|
||||
}
|
||||
|
||||
func (b *Builder) AttachChild(parent Reference, child Reference) {
|
||||
b.tree.nodes[parent].child = int(child) - int(parent)
|
||||
}
|
||||
|
||||
func (b *Builder) Chain(from Reference, to Reference) {
|
||||
b.tree.nodes[from].next = int(to) - int(from)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package ast
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Kind int
|
||||
|
||||
const (
|
||||
// meta
|
||||
Invalid Kind = iota
|
||||
Comment
|
||||
Key
|
||||
|
||||
// top level structures
|
||||
Table
|
||||
ArrayTable
|
||||
KeyValue
|
||||
|
||||
// containers values
|
||||
Array
|
||||
InlineTable
|
||||
|
||||
// values
|
||||
String
|
||||
Bool
|
||||
Float
|
||||
Integer
|
||||
LocalDate
|
||||
LocalTime
|
||||
LocalDateTime
|
||||
DateTime
|
||||
)
|
||||
|
||||
func (k Kind) String() string {
|
||||
switch k {
|
||||
case Invalid:
|
||||
return "Invalid"
|
||||
case Comment:
|
||||
return "Comment"
|
||||
case Key:
|
||||
return "Key"
|
||||
case Table:
|
||||
return "Table"
|
||||
case ArrayTable:
|
||||
return "ArrayTable"
|
||||
case KeyValue:
|
||||
return "KeyValue"
|
||||
case Array:
|
||||
return "Array"
|
||||
case InlineTable:
|
||||
return "InlineTable"
|
||||
case String:
|
||||
return "String"
|
||||
case Bool:
|
||||
return "Bool"
|
||||
case Float:
|
||||
return "Float"
|
||||
case Integer:
|
||||
return "Integer"
|
||||
case LocalDate:
|
||||
return "LocalDate"
|
||||
case LocalTime:
|
||||
return "LocalTime"
|
||||
case LocalDateTime:
|
||||
return "LocalDateTime"
|
||||
case DateTime:
|
||||
return "DateTime"
|
||||
}
|
||||
panic(fmt.Errorf("Kind.String() not implemented for '%d'", k))
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"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 convertion in place on each provided file.
|
||||
Inplace bool
|
||||
}
|
||||
|
||||
func (p *Program) Execute() {
|
||||
flag.Usage = func() { fmt.Fprintf(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, error io.Writer) int {
|
||||
err := p.run(files, input, output)
|
||||
if err != nil {
|
||||
|
||||
var derr *toml.DecodeError
|
||||
if errors.As(err, &derr) {
|
||||
fmt.Fprintln(error, derr.String())
|
||||
row, col := derr.Position()
|
||||
fmt.Fprintln(error, "error occurred at row", row, "column", col)
|
||||
} else {
|
||||
fmt.Fprintln(error, 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 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 := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out := new(bytes.Buffer)
|
||||
|
||||
err = p.Fn(bytes.NewReader(in), out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(path, out.Bytes(), 0600)
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
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(r io.Reader, w io.Writer) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
assert.Equal(t, 0, exit)
|
||||
assert.Empty(t, stdout.String())
|
||||
assert.Empty(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(r io.Reader, w io.Writer) error {
|
||||
return fmt.Errorf("something bad")
|
||||
})
|
||||
|
||||
assert.Equal(t, -1, exit)
|
||||
assert.Empty(t, stdout.String())
|
||||
assert.NotEmpty(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(r io.Reader, w io.Writer) error {
|
||||
var v interface{}
|
||||
return toml.Unmarshal([]byte(`qwe = 001`), &v)
|
||||
})
|
||||
|
||||
assert.Equal(t, -1, exit)
|
||||
assert.Empty(t, stdout.String())
|
||||
assert.Contains(t, stderr.String(), "error occurred at")
|
||||
}
|
||||
|
||||
func TestProcessMainFileExists(t *testing.T) {
|
||||
tmpfile, err := ioutil.TempFile("", "example")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(tmpfile.Name())
|
||||
_, err = tmpfile.Write([]byte(`some data`))
|
||||
require.NoError(t, err)
|
||||
|
||||
stdout := new(bytes.Buffer)
|
||||
stderr := new(bytes.Buffer)
|
||||
|
||||
exit := processMain([]string{tmpfile.Name()}, nil, stdout, stderr, func(r io.Reader, w io.Writer) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
assert.Equal(t, 0, exit)
|
||||
assert.Empty(t, stdout.String())
|
||||
assert.Empty(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(r io.Reader, w io.Writer) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
assert.Equal(t, -1, exit)
|
||||
assert.Empty(t, stdout.String())
|
||||
assert.NotEmpty(t, stderr.String())
|
||||
}
|
||||
|
||||
func TestProcessMainFilesInPlace(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
path1 := path.Join(dir, "file1")
|
||||
path2 := path.Join(dir, "file2")
|
||||
|
||||
err = ioutil.WriteFile(path1, []byte("content 1"), 0600)
|
||||
require.NoError(t, err)
|
||||
err = ioutil.WriteFile(path2, []byte("content 2"), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
p := Program{
|
||||
Fn: dummyFileFn,
|
||||
Inplace: true,
|
||||
}
|
||||
|
||||
exit := p.main([]string{path1, path2}, os.Stdin, os.Stdout, os.Stderr)
|
||||
|
||||
require.Equal(t, 0, exit)
|
||||
|
||||
v1, err := ioutil.ReadFile(path1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "1", string(v1))
|
||||
|
||||
v2, err := ioutil.ReadFile(path2)
|
||||
require.NoError(t, err)
|
||||
require.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)
|
||||
|
||||
require.Equal(t, -1, exit)
|
||||
}
|
||||
|
||||
func TestProcessMainFilesInPlaceFailFn(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
path1 := path.Join(dir, "file1")
|
||||
|
||||
err = ioutil.WriteFile(path1, []byte("content 1"), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
p := Program{
|
||||
Fn: func(io.Reader, io.Writer) error { return fmt.Errorf("oh no") },
|
||||
Inplace: true,
|
||||
}
|
||||
|
||||
exit := p.main([]string{path1}, os.Stdin, os.Stdout, os.Stderr)
|
||||
|
||||
require.Equal(t, -1, exit)
|
||||
|
||||
v1, err := ioutil.ReadFile(path1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "content 1", string(v1))
|
||||
}
|
||||
|
||||
func dummyFileFn(r io.Reader, w io.Writer) error {
|
||||
b, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v := strings.SplitN(string(b), " ", 2)[1]
|
||||
_, err = w.Write([]byte(v))
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package danger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const maxInt = uintptr(int(^uint(0) >> 1))
|
||||
|
||||
func SubsliceOffset(data []byte, subslice []byte) int {
|
||||
datap := (*reflect.SliceHeader)(unsafe.Pointer(&data))
|
||||
hlp := (*reflect.SliceHeader)(unsafe.Pointer(&subslice))
|
||||
|
||||
if hlp.Data < datap.Data {
|
||||
panic(fmt.Errorf("subslice address (%d) is before data address (%d)", hlp.Data, datap.Data))
|
||||
}
|
||||
offset := hlp.Data - datap.Data
|
||||
|
||||
if offset > maxInt {
|
||||
panic(fmt.Errorf("slice offset larger than int (%d)", offset))
|
||||
}
|
||||
|
||||
intoffset := int(offset)
|
||||
|
||||
if intoffset > datap.Len {
|
||||
panic(fmt.Errorf("slice offset (%d) is farther than data length (%d)", intoffset, datap.Len))
|
||||
}
|
||||
|
||||
if intoffset+hlp.Len > datap.Len {
|
||||
panic(fmt.Errorf("slice ends (%d+%d) is farther than data length (%d)", intoffset, hlp.Len, datap.Len))
|
||||
}
|
||||
|
||||
return intoffset
|
||||
}
|
||||
|
||||
func BytesRange(start []byte, end []byte) []byte {
|
||||
if start == nil || end == nil {
|
||||
panic("cannot call BytesRange with nil")
|
||||
}
|
||||
startp := (*reflect.SliceHeader)(unsafe.Pointer(&start))
|
||||
endp := (*reflect.SliceHeader)(unsafe.Pointer(&end))
|
||||
|
||||
if startp.Data > endp.Data {
|
||||
panic(fmt.Errorf("start pointer address (%d) is after end pointer address (%d)", startp.Data, endp.Data))
|
||||
}
|
||||
|
||||
l := startp.Len
|
||||
endLen := int(endp.Data-startp.Data) + endp.Len
|
||||
if endLen > l {
|
||||
l = endLen
|
||||
}
|
||||
|
||||
if l > startp.Cap {
|
||||
panic(fmt.Errorf("range length is larger than capacity"))
|
||||
}
|
||||
|
||||
return start[:l]
|
||||
}
|
||||
|
||||
func Stride(ptr unsafe.Pointer, size uintptr, offset int) unsafe.Pointer {
|
||||
// TODO: replace with unsafe.Add when Go 1.17 is released
|
||||
// https://github.com/golang/go/issues/40481
|
||||
return unsafe.Pointer(uintptr(ptr) + uintptr(int(size)*offset))
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package danger_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/internal/danger"
|
||||
)
|
||||
|
||||
func TestSubsliceOffsetValid(t *testing.T) {
|
||||
examples := []struct {
|
||||
desc string
|
||||
test func() ([]byte, []byte)
|
||||
offset int
|
||||
}{
|
||||
{
|
||||
desc: "simple",
|
||||
test: func() ([]byte, []byte) {
|
||||
data := []byte("hello")
|
||||
return data, data[1:]
|
||||
},
|
||||
offset: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
t.Run(e.desc, func(t *testing.T) {
|
||||
d, s := e.test()
|
||||
offset := danger.SubsliceOffset(d, s)
|
||||
assert.Equal(t, e.offset, offset)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubsliceOffsetInvalid(t *testing.T) {
|
||||
examples := []struct {
|
||||
desc string
|
||||
test func() ([]byte, []byte)
|
||||
}{
|
||||
{
|
||||
desc: "unrelated arrays",
|
||||
test: func() ([]byte, []byte) {
|
||||
return []byte("one"), []byte("two")
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "slice starts before data",
|
||||
test: func() ([]byte, []byte) {
|
||||
full := []byte("hello world")
|
||||
return full[5:], full[1:]
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "slice starts after data",
|
||||
test: func() ([]byte, []byte) {
|
||||
full := []byte("hello world")
|
||||
return full[:3], full[5:]
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "slice ends after data",
|
||||
test: func() ([]byte, []byte) {
|
||||
full := []byte("hello world")
|
||||
return full[:5], full[3:8]
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
t.Run(e.desc, func(t *testing.T) {
|
||||
d, s := e.test()
|
||||
require.Panics(t, func() {
|
||||
danger.SubsliceOffset(d, s)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStride(t *testing.T) {
|
||||
a := []byte{1, 2, 3, 4}
|
||||
x := &a[1]
|
||||
n := (*byte)(danger.Stride(unsafe.Pointer(x), unsafe.Sizeof(byte(0)), 1))
|
||||
require.Equal(t, &a[2], n)
|
||||
n = (*byte)(danger.Stride(unsafe.Pointer(x), unsafe.Sizeof(byte(0)), -1))
|
||||
require.Equal(t, &a[0], n)
|
||||
}
|
||||
|
||||
func TestBytesRange(t *testing.T) {
|
||||
type fn = func() ([]byte, []byte)
|
||||
examples := []struct {
|
||||
desc string
|
||||
test fn
|
||||
expected []byte
|
||||
}{
|
||||
{
|
||||
desc: "simple",
|
||||
test: func() ([]byte, []byte) {
|
||||
full := []byte("hello world")
|
||||
return full[1:3], full[6:8]
|
||||
},
|
||||
expected: []byte("ello wo"),
|
||||
},
|
||||
{
|
||||
desc: "full",
|
||||
test: func() ([]byte, []byte) {
|
||||
full := []byte("hello world")
|
||||
return full[0:1], full[len(full)-1:]
|
||||
},
|
||||
expected: []byte("hello world"),
|
||||
},
|
||||
{
|
||||
desc: "end before start",
|
||||
test: func() ([]byte, []byte) {
|
||||
full := []byte("hello world")
|
||||
return full[len(full)-1:], full[0:1]
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nils",
|
||||
test: func() ([]byte, []byte) {
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nils start",
|
||||
test: func() ([]byte, []byte) {
|
||||
return nil, []byte("foo")
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nils end",
|
||||
test: func() ([]byte, []byte) {
|
||||
return []byte("foo"), nil
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "start is end",
|
||||
test: func() ([]byte, []byte) {
|
||||
full := []byte("hello world")
|
||||
return full[1:3], full[1:3]
|
||||
},
|
||||
expected: []byte("el"),
|
||||
},
|
||||
{
|
||||
desc: "end contained in start",
|
||||
test: func() ([]byte, []byte) {
|
||||
full := []byte("hello world")
|
||||
return full[1:7], full[2:4]
|
||||
},
|
||||
expected: []byte("ello w"),
|
||||
},
|
||||
{
|
||||
desc: "different backing arrays",
|
||||
test: func() ([]byte, []byte) {
|
||||
one := []byte("hello world")
|
||||
two := []byte("hello world")
|
||||
return one, two
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
t.Run(e.desc, func(t *testing.T) {
|
||||
start, end := e.test()
|
||||
if e.expected == nil {
|
||||
require.Panics(t, func() {
|
||||
danger.BytesRange(start, end)
|
||||
})
|
||||
} else {
|
||||
res := danger.BytesRange(start, end)
|
||||
require.Equal(t, e.expected, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package danger
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// typeID is used as key in encoder and decoder caches to enable using
|
||||
// the optimize runtime.mapaccess2_fast64 function instead of the more
|
||||
// expensive lookup if we were to use reflect.Type as map key.
|
||||
//
|
||||
// typeID holds the pointer to the reflect.Type value, which is unique
|
||||
// in the program.
|
||||
//
|
||||
// https://github.com/segmentio/encoding/blob/master/json/codec.go#L59-L61
|
||||
type TypeID unsafe.Pointer
|
||||
|
||||
func MakeTypeID(t reflect.Type) TypeID {
|
||||
// reflect.Type has the fields:
|
||||
// typ unsafe.Pointer
|
||||
// ptr unsafe.Pointer
|
||||
return TypeID((*[2]unsafe.Pointer)(unsafe.Pointer(&t))[1])
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package imported_tests
|
||||
|
||||
// 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/stretchr/testify/require"
|
||||
)
|
||||
|
||||
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"`
|
||||
unexported int `toml:"shouldntBeHere"`
|
||||
Unexported2 int `toml:"-"`
|
||||
}
|
||||
|
||||
var 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)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, marshalTestToml, string(result))
|
||||
}
|
||||
|
||||
func TestBasicMarshalQuotedKey(t *testing.T) {
|
||||
result, err := toml.Marshal(quotedKeyMarshalTestData)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := `'Z.string-àéù' = 'Hello'
|
||||
'Yfloat-𝟘' = 3.5
|
||||
|
||||
['Xsubdoc-àéù']
|
||||
String2 = 'One'
|
||||
|
||||
[['W.sublist-𝟘']]
|
||||
String2 = 'Two'
|
||||
|
||||
[['W.sublist-𝟘']]
|
||||
String2 = 'Three'
|
||||
`
|
||||
|
||||
require.Equal(t, string(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)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := `title = 'Placeholder'
|
||||
bool = false
|
||||
int = 0
|
||||
string = ''
|
||||
stringlist = []
|
||||
|
||||
[map]
|
||||
`
|
||||
|
||||
require.Equal(t, string(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
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("leaf", func(t *testing.T) {
|
||||
res, err := toml.Marshal(wrap{m})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "TM = 'Sally Fields'\n", string(res))
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
// addTag adds JSON tags to a data structure as expected by toml-test.
|
||||
func addTag(key string, 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(k, 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", fmt.Sprintf("%v", orig))
|
||||
case string:
|
||||
return tag("string", orig)
|
||||
case int64:
|
||||
return tag("integer", fmt.Sprintf("%d", orig))
|
||||
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{}) {
|
||||
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{}) {
|
||||
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{}) {
|
||||
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{}) {
|
||||
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) {
|
||||
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) {
|
||||
// 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) {
|
||||
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 cmpAsDatetimesLocal(t *testing.T, key string, want, have string) {
|
||||
if datetimeRepl.Replace(want) != datetimeRepl.Replace(have) {
|
||||
t.Fatalf("Values for key '%s' don't match:\n"+
|
||||
" Expected: %v\n"+
|
||||
" Your encoder: %v",
|
||||
key, want, have)
|
||||
}
|
||||
}
|
||||
|
||||
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.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.Fatalf("Key '%s' is not an %s but %s:\n"+
|
||||
" Expected: %#[3]v\n"+
|
||||
" Your encoder: %#[4]v",
|
||||
key, wantType, want, have)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
type parser struct{}
|
||||
|
||||
func (p parser) Decode(input string) (output string, outputIsError bool, retErr error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
switch rr := r.(type) {
|
||||
case error:
|
||||
retErr = rr
|
||||
default:
|
||||
retErr = fmt.Errorf("%s", rr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var v interface{}
|
||||
|
||||
if err := toml.Unmarshal([]byte(input), &v); err != nil {
|
||||
return err.Error(), true, nil
|
||||
}
|
||||
|
||||
j, err := json.MarshalIndent(addTag("", v), "", " ")
|
||||
if err != nil {
|
||||
return "", false, retErr
|
||||
}
|
||||
|
||||
return string(j), false, retErr
|
||||
}
|
||||
|
||||
func (p parser) Encode(input string) (output string, outputIsError bool, retErr error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
switch rr := r.(type) {
|
||||
case error:
|
||||
retErr = rr
|
||||
default:
|
||||
retErr = fmt.Errorf("%s", rr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var tmp interface{}
|
||||
err := json.Unmarshal([]byte(input), &tmp)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
rm, err := rmTag(tmp)
|
||||
if err != nil {
|
||||
return err.Error(), true, retErr
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
err = toml.NewEncoder(buf).Encode(rm)
|
||||
if err != nil {
|
||||
return err.Error(), true, retErr
|
||||
}
|
||||
|
||||
return buf.String(), false, retErr
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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 itenm.
|
||||
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
|
||||
case "datetime":
|
||||
return parseTime(v, "2006-01-02T15:04:05.999999999Z07:00", false)
|
||||
case "datetime-local":
|
||||
return parseTime(v, "2006-01-02T15:04:05.999999999", true)
|
||||
case "date-local":
|
||||
return parseTime(v, "2006-01-02", true)
|
||||
case "time-local":
|
||||
return parseTime(v, "15:04:05.999999999", true)
|
||||
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)
|
||||
}
|
||||
|
||||
func parseTime(v, format string, local bool) (t time.Time, err error) {
|
||||
if local {
|
||||
t, err = time.ParseInLocation(format, v, time.Local)
|
||||
} else {
|
||||
t, err = time.Parse(format, v)
|
||||
}
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("Could not parse %q as a datetime: %w", v, err)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// 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 helpfer 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: %s", err)
|
||||
}
|
||||
|
||||
j := json.NewEncoder(os.Stdout)
|
||||
j.SetIndent("", " ")
|
||||
if err := j.Encode(addTag("", decoded)); err != nil {
|
||||
return fmt.Errorf("Error encoding JSON: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"github.com/pelletier/go-toml/v2/internal/ast"
|
||||
)
|
||||
|
||||
// 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 *ast.Node) {
|
||||
t.reset()
|
||||
t.Push(node)
|
||||
}
|
||||
|
||||
// UpdateArrayTable sets the state of the tracker with the AST array table node.
|
||||
func (t *KeyTracker) UpdateArrayTable(node *ast.Node) {
|
||||
t.reset()
|
||||
t.Push(node)
|
||||
}
|
||||
|
||||
// Push the given key on the stack.
|
||||
func (t *KeyTracker) Push(node *ast.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 *ast.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,356 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/internal/ast"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
func (s *SeenTracker) CheckExpression(node *ast.Node) error {
|
||||
if s.entries == nil {
|
||||
s.reset()
|
||||
}
|
||||
switch node.Kind {
|
||||
case ast.KeyValue:
|
||||
return s.checkKeyValue(node)
|
||||
case ast.Table:
|
||||
return s.checkTable(node)
|
||||
case ast.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 *ast.Node) 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 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)
|
||||
|
||||
if idx >= 0 {
|
||||
kind := s.entries[idx].kind
|
||||
if kind != tableKind {
|
||||
return fmt.Errorf("toml: key %s should be a table, not a %s", string(k), kind)
|
||||
}
|
||||
if s.entries[idx].explicit {
|
||||
return fmt.Errorf("toml: table %s already exists", string(k))
|
||||
}
|
||||
s.entries[idx].explicit = true
|
||||
} else {
|
||||
idx = s.create(parentIdx, k, tableKind, true, false)
|
||||
}
|
||||
|
||||
s.currentIdx = idx
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SeenTracker) checkArrayTable(node *ast.Node) 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 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)
|
||||
|
||||
if idx >= 0 {
|
||||
kind := s.entries[idx].kind
|
||||
if kind != arrayTableKind {
|
||||
return fmt.Errorf("toml: key %s already exists as a %s, but should be an array table", kind, string(k))
|
||||
}
|
||||
s.clear(idx)
|
||||
} else {
|
||||
idx = s.create(parentIdx, k, arrayTableKind, true, false)
|
||||
}
|
||||
|
||||
s.currentIdx = idx
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SeenTracker) checkKeyValue(node *ast.Node) 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]
|
||||
if it.IsLast() {
|
||||
return fmt.Errorf("toml: key %s is already defined", string(k))
|
||||
} else if entry.kind != tableKind {
|
||||
return fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
|
||||
} else if entry.explicit {
|
||||
return 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 ast.InlineTable:
|
||||
return s.checkInlineTable(value)
|
||||
case ast.Array:
|
||||
return s.checkArray(value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SeenTracker) checkArray(node *ast.Node) error {
|
||||
it := node.Children()
|
||||
for it.Next() {
|
||||
n := it.Node()
|
||||
switch n.Kind {
|
||||
case ast.InlineTable:
|
||||
err := s.checkInlineTable(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case ast.Array:
|
||||
err := s.checkArray(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SeenTracker) checkInlineTable(node *ast.Node) error {
|
||||
if pool.New == nil {
|
||||
pool.New = func() interface{} {
|
||||
return &SeenTracker{}
|
||||
}
|
||||
}
|
||||
|
||||
s = pool.Get().(*SeenTracker)
|
||||
s.reset()
|
||||
|
||||
it := node.Children()
|
||||
for it.Next() {
|
||||
n := it.Node()
|
||||
err := s.checkKeyValue(n)
|
||||
if err != nil {
|
||||
return 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 nil
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
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
|
||||
require.LessOrEqual(t, int(unsafe.Sizeof(entry{})), maxExpectedEntrySize)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package tracker
|
||||
@@ -1,468 +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/utf8"
|
||||
)
|
||||
|
||||
var dateRegexp *regexp.Regexp
|
||||
|
||||
// Define state functions
|
||||
type tomlLexStateFn func() tomlLexStateFn
|
||||
|
||||
// Define lexer
|
||||
type tomlLexer struct {
|
||||
input string
|
||||
start int
|
||||
pos int
|
||||
width int
|
||||
tokens chan token
|
||||
depth int
|
||||
line int
|
||||
col int
|
||||
}
|
||||
|
||||
func (l *tomlLexer) run() {
|
||||
for state := l.lexVoid; state != nil; {
|
||||
state = state()
|
||||
}
|
||||
close(l.tokens)
|
||||
}
|
||||
|
||||
func (l *tomlLexer) nextStart() {
|
||||
// iterate by runes (utf8 characters)
|
||||
// search for newlines and advance line/col counts
|
||||
for i := l.start; i < l.pos; {
|
||||
r, width := utf8.DecodeRuneInString(l.input[i:])
|
||||
if r == '\n' {
|
||||
l.line++
|
||||
l.col = 1
|
||||
} else {
|
||||
l.col++
|
||||
}
|
||||
i += width
|
||||
}
|
||||
// advance start position to next token
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
func (l *tomlLexer) emit(t tokenType) {
|
||||
l.tokens <- token{
|
||||
Position: Position{l.line, l.col},
|
||||
typ: t,
|
||||
val: l.input[l.start:l.pos],
|
||||
}
|
||||
l.nextStart()
|
||||
}
|
||||
|
||||
func (l *tomlLexer) emitWithValue(t tokenType, value string) {
|
||||
l.tokens <- token{
|
||||
Position: Position{l.line, l.col},
|
||||
typ: t,
|
||||
val: value,
|
||||
}
|
||||
l.nextStart()
|
||||
}
|
||||
|
||||
func (l *tomlLexer) 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 *tomlLexer) ignore() {
|
||||
l.nextStart()
|
||||
}
|
||||
|
||||
func (l *tomlLexer) backup() {
|
||||
l.pos -= l.width
|
||||
}
|
||||
|
||||
func (l *tomlLexer) errorf(format string, args ...interface{}) tomlLexStateFn {
|
||||
l.tokens <- token{
|
||||
Position: Position{l.line, l.col},
|
||||
typ: tokenError,
|
||||
val: fmt.Sprintf(format, args...),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *tomlLexer) peek() rune {
|
||||
r := l.next()
|
||||
l.backup()
|
||||
return r
|
||||
}
|
||||
|
||||
func (l *tomlLexer) accept(valid string) bool {
|
||||
if strings.IndexRune(valid, l.next()) >= 0 {
|
||||
return true
|
||||
}
|
||||
l.backup()
|
||||
return false
|
||||
}
|
||||
|
||||
func (l *tomlLexer) follow(next string) bool {
|
||||
return strings.HasPrefix(l.input[l.pos:], next)
|
||||
}
|
||||
|
||||
func (l *tomlLexer) lexVoid() tomlLexStateFn {
|
||||
for {
|
||||
next := l.peek()
|
||||
switch next {
|
||||
case '[':
|
||||
return l.lexKeyGroup
|
||||
case '#':
|
||||
return l.lexComment
|
||||
case '=':
|
||||
return l.lexEqual
|
||||
}
|
||||
|
||||
if isSpace(next) {
|
||||
l.ignore()
|
||||
}
|
||||
|
||||
if l.depth > 0 {
|
||||
return l.lexRvalue
|
||||
}
|
||||
|
||||
if isKeyChar(next) {
|
||||
return l.lexKey
|
||||
}
|
||||
|
||||
if l.next() == eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
l.emit(tokenEOF)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *tomlLexer) lexRvalue() tomlLexStateFn {
|
||||
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++
|
||||
return l.lexLeftBracket
|
||||
case ']':
|
||||
l.depth--
|
||||
return l.lexRightBracket
|
||||
case '#':
|
||||
return l.lexComment
|
||||
case '"':
|
||||
return l.lexString
|
||||
case ',':
|
||||
return l.lexComma
|
||||
case '\n':
|
||||
l.ignore()
|
||||
l.pos++
|
||||
if l.depth == 0 {
|
||||
return l.lexVoid
|
||||
}
|
||||
return l.lexRvalue
|
||||
}
|
||||
|
||||
if l.follow("true") {
|
||||
return l.lexTrue
|
||||
}
|
||||
|
||||
if l.follow("false") {
|
||||
return l.lexFalse
|
||||
}
|
||||
|
||||
if isAlphanumeric(next) {
|
||||
return l.lexKey
|
||||
}
|
||||
|
||||
if dateRegexp.FindString(l.input[l.pos:]) != "" {
|
||||
return l.lexDate
|
||||
}
|
||||
|
||||
if next == '+' || next == '-' || isDigit(next) {
|
||||
return l.lexNumber
|
||||
}
|
||||
|
||||
if isSpace(next) {
|
||||
l.ignore()
|
||||
}
|
||||
|
||||
if l.next() == eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
l.emit(tokenEOF)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *tomlLexer) lexDate() tomlLexStateFn {
|
||||
l.ignore()
|
||||
l.pos += 20 // Fixed size of a date in TOML
|
||||
l.emit(tokenDate)
|
||||
return l.lexRvalue
|
||||
}
|
||||
|
||||
func (l *tomlLexer) lexTrue() tomlLexStateFn {
|
||||
l.ignore()
|
||||
l.pos += 4
|
||||
l.emit(tokenTrue)
|
||||
return l.lexRvalue
|
||||
}
|
||||
|
||||
func (l *tomlLexer) lexFalse() tomlLexStateFn {
|
||||
l.ignore()
|
||||
l.pos += 5
|
||||
l.emit(tokenFalse)
|
||||
return l.lexRvalue
|
||||
}
|
||||
|
||||
func (l *tomlLexer) lexEqual() tomlLexStateFn {
|
||||
l.ignore()
|
||||
l.accept("=")
|
||||
l.emit(tokenEqual)
|
||||
return l.lexRvalue
|
||||
}
|
||||
|
||||
func (l *tomlLexer) lexComma() tomlLexStateFn {
|
||||
l.ignore()
|
||||
l.accept(",")
|
||||
l.emit(tokenComma)
|
||||
return l.lexRvalue
|
||||
}
|
||||
|
||||
func (l *tomlLexer) lexKey() tomlLexStateFn {
|
||||
l.ignore()
|
||||
for isKeyChar(l.next()) {
|
||||
}
|
||||
l.backup()
|
||||
l.emit(tokenKey)
|
||||
return l.lexVoid
|
||||
}
|
||||
|
||||
func (l *tomlLexer) lexComment() tomlLexStateFn {
|
||||
for {
|
||||
next := l.next()
|
||||
if next == '\n' || next == eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
l.ignore()
|
||||
return l.lexVoid
|
||||
}
|
||||
|
||||
func (l *tomlLexer) lexLeftBracket() tomlLexStateFn {
|
||||
l.ignore()
|
||||
l.pos++
|
||||
l.emit(tokenLeftBracket)
|
||||
return l.lexRvalue
|
||||
}
|
||||
|
||||
func (l *tomlLexer) lexString() tomlLexStateFn {
|
||||
l.pos++
|
||||
l.ignore()
|
||||
growingString := ""
|
||||
|
||||
for {
|
||||
if l.peek() == '"' {
|
||||
l.emitWithValue(tokenString, growingString)
|
||||
l.pos++
|
||||
l.ignore()
|
||||
return l.lexRvalue
|
||||
}
|
||||
|
||||
if l.follow("\\\"") {
|
||||
l.pos++
|
||||
growingString += "\""
|
||||
} else if l.follow("\\n") {
|
||||
l.pos++
|
||||
growingString += "\n"
|
||||
} else if l.follow("\\b") {
|
||||
l.pos++
|
||||
growingString += "\b"
|
||||
} else if l.follow("\\f") {
|
||||
l.pos++
|
||||
growingString += "\f"
|
||||
} else if l.follow("\\/") {
|
||||
l.pos++
|
||||
growingString += "/"
|
||||
} else if l.follow("\\t") {
|
||||
l.pos++
|
||||
growingString += "\t"
|
||||
} else if l.follow("\\r") {
|
||||
l.pos++
|
||||
growingString += "\r"
|
||||
} else if l.follow("\\\\") {
|
||||
l.pos++
|
||||
growingString += "\\"
|
||||
} else if l.follow("\\u") {
|
||||
l.pos += 2
|
||||
code := ""
|
||||
for i := 0; i < 4; i++ {
|
||||
c := l.peek()
|
||||
l.pos++
|
||||
if !isHexDigit(c) {
|
||||
return l.errorf("unfinished unicode escape")
|
||||
}
|
||||
code = code + string(c)
|
||||
}
|
||||
l.pos--
|
||||
intcode, err := strconv.ParseInt(code, 16, 32)
|
||||
if err != nil {
|
||||
return l.errorf("invalid unicode escape: \\u" + code)
|
||||
}
|
||||
growingString += string(rune(intcode))
|
||||
} else if l.follow("\\") {
|
||||
l.pos++
|
||||
return l.errorf("invalid escape sequence: \\" + string(l.peek()))
|
||||
} else {
|
||||
growingString += string(l.peek())
|
||||
}
|
||||
|
||||
if l.next() == eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return l.errorf("unclosed string")
|
||||
}
|
||||
|
||||
func (l *tomlLexer) lexKeyGroup() tomlLexStateFn {
|
||||
l.ignore()
|
||||
l.pos++
|
||||
|
||||
if l.peek() == '[' {
|
||||
// token '[[' signifies an array of anonymous key groups
|
||||
l.pos++
|
||||
l.emit(tokenDoubleLeftBracket)
|
||||
return l.lexInsideKeyGroupArray
|
||||
}
|
||||
// vanilla key group
|
||||
l.emit(tokenLeftBracket)
|
||||
return l.lexInsideKeyGroup
|
||||
}
|
||||
|
||||
func (l *tomlLexer) lexInsideKeyGroupArray() tomlLexStateFn {
|
||||
for {
|
||||
if l.peek() == ']' {
|
||||
if l.pos > l.start {
|
||||
l.emit(tokenKeyGroupArray)
|
||||
}
|
||||
l.ignore()
|
||||
l.pos++
|
||||
if l.peek() != ']' {
|
||||
break // error
|
||||
}
|
||||
l.pos++
|
||||
l.emit(tokenDoubleRightBracket)
|
||||
return l.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 (l *tomlLexer) lexInsideKeyGroup() tomlLexStateFn {
|
||||
for {
|
||||
if l.peek() == ']' {
|
||||
if l.pos > l.start {
|
||||
l.emit(tokenKeyGroup)
|
||||
}
|
||||
l.ignore()
|
||||
l.pos++
|
||||
l.emit(tokenRightBracket)
|
||||
return l.lexVoid
|
||||
} else if l.peek() == '[' {
|
||||
return l.errorf("group name cannot contain ']'")
|
||||
}
|
||||
|
||||
if l.next() == eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
return l.errorf("unclosed key group")
|
||||
}
|
||||
|
||||
func (l *tomlLexer) lexRightBracket() tomlLexStateFn {
|
||||
l.ignore()
|
||||
l.pos++
|
||||
l.emit(tokenRightBracket)
|
||||
return l.lexRvalue
|
||||
}
|
||||
|
||||
func (l *tomlLexer) lexNumber() tomlLexStateFn {
|
||||
l.ignore()
|
||||
if !l.accept("+") {
|
||||
l.accept("-")
|
||||
}
|
||||
pointSeen := false
|
||||
digitSeen := false
|
||||
for {
|
||||
next := l.next()
|
||||
if next == '.' {
|
||||
if pointSeen {
|
||||
return l.errorf("cannot have two dots in one float")
|
||||
}
|
||||
if !isDigit(l.peek()) {
|
||||
return l.errorf("float cannot end with a dot")
|
||||
}
|
||||
pointSeen = true
|
||||
} else if isDigit(next) {
|
||||
digitSeen = true
|
||||
} else {
|
||||
l.backup()
|
||||
break
|
||||
}
|
||||
if pointSeen && !digitSeen {
|
||||
return l.errorf("cannot start float with a dot")
|
||||
}
|
||||
}
|
||||
|
||||
if !digitSeen {
|
||||
return l.errorf("no digit in that number")
|
||||
}
|
||||
if pointSeen {
|
||||
l.emit(tokenFloat)
|
||||
} else {
|
||||
l.emit(tokenInteger)
|
||||
}
|
||||
return l.lexRvalue
|
||||
}
|
||||
|
||||
func init() {
|
||||
dateRegexp = regexp.MustCompile("^\\d{1,4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z")
|
||||
}
|
||||
|
||||
// Entry point
|
||||
func lexToml(input string) chan token {
|
||||
l := &tomlLexer{
|
||||
input: input,
|
||||
tokens: make(chan token),
|
||||
line: 1,
|
||||
col: 1,
|
||||
}
|
||||
go l.run()
|
||||
return l.tokens
|
||||
}
|
||||
-419
@@ -1,419 +0,0 @@
|
||||
package toml
|
||||
|
||||
import "testing"
|
||||
|
||||
func testFlow(t *testing.T, input string, expectedFlow []token) {
|
||||
ch := lexToml(input)
|
||||
for _, expected := range expectedFlow {
|
||||
token := <-ch
|
||||
if token != expected {
|
||||
t.Log("While testing: ", input)
|
||||
t.Log("compared", token, "to", expected)
|
||||
t.Log(token.val, "<->", expected.val)
|
||||
t.Log(token.typ, "<->", expected.typ)
|
||||
t.Log(token.Line, "<->", expected.Line)
|
||||
t.Log(token.Col, "<->", expected.Col)
|
||||
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{Position{1, 1}, tokenLeftBracket, "["},
|
||||
token{Position{1, 2}, tokenKeyGroup, "hello world"},
|
||||
token{Position{1, 13}, tokenRightBracket, "]"},
|
||||
token{Position{1, 14}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnclosedKeyGroup(t *testing.T) {
|
||||
testFlow(t, "[hello world", []token{
|
||||
token{Position{1, 1}, tokenLeftBracket, "["},
|
||||
token{Position{1, 2}, tokenError, "unclosed key group"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestComment(t *testing.T) {
|
||||
testFlow(t, "# blahblah", []token{
|
||||
token{Position{1, 11}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyGroupComment(t *testing.T) {
|
||||
testFlow(t, "[hello world] # blahblah", []token{
|
||||
token{Position{1, 1}, tokenLeftBracket, "["},
|
||||
token{Position{1, 2}, tokenKeyGroup, "hello world"},
|
||||
token{Position{1, 13}, tokenRightBracket, "]"},
|
||||
token{Position{1, 25}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultipleKeyGroupsComment(t *testing.T) {
|
||||
testFlow(t, "[hello world] # blahblah\n[test]", []token{
|
||||
token{Position{1, 1}, tokenLeftBracket, "["},
|
||||
token{Position{1, 2}, tokenKeyGroup, "hello world"},
|
||||
token{Position{1, 13}, tokenRightBracket, "]"},
|
||||
token{Position{2, 1}, tokenLeftBracket, "["},
|
||||
token{Position{2, 2}, tokenKeyGroup, "test"},
|
||||
token{Position{2, 6}, tokenRightBracket, "]"},
|
||||
token{Position{2, 7}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicKey(t *testing.T) {
|
||||
testFlow(t, "hello", []token{
|
||||
token{Position{1, 1}, tokenKey, "hello"},
|
||||
token{Position{1, 6}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicKeyWithUnderscore(t *testing.T) {
|
||||
testFlow(t, "hello_hello", []token{
|
||||
token{Position{1, 1}, tokenKey, "hello_hello"},
|
||||
token{Position{1, 12}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicKeyWithDash(t *testing.T) {
|
||||
testFlow(t, "hello-world", []token{
|
||||
token{Position{1, 1}, tokenKey, "hello-world"},
|
||||
token{Position{1, 12}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicKeyWithUppercaseMix(t *testing.T) {
|
||||
testFlow(t, "helloHELLOHello", []token{
|
||||
token{Position{1, 1}, tokenKey, "helloHELLOHello"},
|
||||
token{Position{1, 16}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicKeyWithInternationalCharacters(t *testing.T) {
|
||||
testFlow(t, "héllÖ", []token{
|
||||
token{Position{1, 1}, tokenKey, "héllÖ"},
|
||||
token{Position{1, 6}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicKeyAndEqual(t *testing.T) {
|
||||
testFlow(t, "hello =", []token{
|
||||
token{Position{1, 1}, tokenKey, "hello"},
|
||||
token{Position{1, 7}, tokenEqual, "="},
|
||||
token{Position{1, 8}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyWithSharpAndEqual(t *testing.T) {
|
||||
testFlow(t, "key#name = 5", []token{
|
||||
token{Position{1, 1}, tokenKey, "key#name"},
|
||||
token{Position{1, 10}, tokenEqual, "="},
|
||||
token{Position{1, 12}, tokenInteger, "5"},
|
||||
token{Position{1, 13}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyWithSymbolsAndEqual(t *testing.T) {
|
||||
testFlow(t, "~!@#$^&*()_+-`1234567890[]\\|/?><.,;:' = 5", []token{
|
||||
token{Position{1, 1}, tokenKey, "~!@#$^&*()_+-`1234567890[]\\|/?><.,;:'"},
|
||||
token{Position{1, 39}, tokenEqual, "="},
|
||||
token{Position{1, 41}, tokenInteger, "5"},
|
||||
token{Position{1, 42}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualStringEscape(t *testing.T) {
|
||||
testFlow(t, `foo = "hello\""`, []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 8}, tokenString, "hello\""},
|
||||
token{Position{1, 16}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualStringUnfinished(t *testing.T) {
|
||||
testFlow(t, `foo = "bar`, []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 8}, tokenError, "unclosed string"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualString(t *testing.T) {
|
||||
testFlow(t, `foo = "bar"`, []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 8}, tokenString, "bar"},
|
||||
token{Position{1, 12}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualTrue(t *testing.T) {
|
||||
testFlow(t, "foo = true", []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 7}, tokenTrue, "true"},
|
||||
token{Position{1, 11}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualFalse(t *testing.T) {
|
||||
testFlow(t, "foo = false", []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 7}, tokenFalse, "false"},
|
||||
token{Position{1, 12}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayNestedString(t *testing.T) {
|
||||
testFlow(t, `a = [ ["hello", "world"] ]`, []token{
|
||||
token{Position{1, 1}, tokenKey, "a"},
|
||||
token{Position{1, 3}, tokenEqual, "="},
|
||||
token{Position{1, 5}, tokenLeftBracket, "["},
|
||||
token{Position{1, 7}, tokenLeftBracket, "["},
|
||||
token{Position{1, 9}, tokenString, "hello"},
|
||||
token{Position{1, 15}, tokenComma, ","},
|
||||
token{Position{1, 18}, tokenString, "world"},
|
||||
token{Position{1, 24}, tokenRightBracket, "]"},
|
||||
token{Position{1, 26}, tokenRightBracket, "]"},
|
||||
token{Position{1, 27}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayNestedInts(t *testing.T) {
|
||||
testFlow(t, "a = [ [42, 21], [10] ]", []token{
|
||||
token{Position{1, 1}, tokenKey, "a"},
|
||||
token{Position{1, 3}, tokenEqual, "="},
|
||||
token{Position{1, 5}, tokenLeftBracket, "["},
|
||||
token{Position{1, 7}, tokenLeftBracket, "["},
|
||||
token{Position{1, 8}, tokenInteger, "42"},
|
||||
token{Position{1, 10}, tokenComma, ","},
|
||||
token{Position{1, 12}, tokenInteger, "21"},
|
||||
token{Position{1, 14}, tokenRightBracket, "]"},
|
||||
token{Position{1, 15}, tokenComma, ","},
|
||||
token{Position{1, 17}, tokenLeftBracket, "["},
|
||||
token{Position{1, 18}, tokenInteger, "10"},
|
||||
token{Position{1, 20}, tokenRightBracket, "]"},
|
||||
token{Position{1, 22}, tokenRightBracket, "]"},
|
||||
token{Position{1, 23}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayInts(t *testing.T) {
|
||||
testFlow(t, "a = [ 42, 21, 10, ]", []token{
|
||||
token{Position{1, 1}, tokenKey, "a"},
|
||||
token{Position{1, 3}, tokenEqual, "="},
|
||||
token{Position{1, 5}, tokenLeftBracket, "["},
|
||||
token{Position{1, 7}, tokenInteger, "42"},
|
||||
token{Position{1, 9}, tokenComma, ","},
|
||||
token{Position{1, 11}, tokenInteger, "21"},
|
||||
token{Position{1, 13}, tokenComma, ","},
|
||||
token{Position{1, 15}, tokenInteger, "10"},
|
||||
token{Position{1, 17}, tokenComma, ","},
|
||||
token{Position{1, 19}, tokenRightBracket, "]"},
|
||||
token{Position{1, 20}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultilineArrayComments(t *testing.T) {
|
||||
testFlow(t, "a = [1, # wow\n2, # such items\n3, # so array\n]", []token{
|
||||
token{Position{1, 1}, tokenKey, "a"},
|
||||
token{Position{1, 3}, tokenEqual, "="},
|
||||
token{Position{1, 5}, tokenLeftBracket, "["},
|
||||
token{Position{1, 6}, tokenInteger, "1"},
|
||||
token{Position{1, 7}, tokenComma, ","},
|
||||
token{Position{2, 1}, tokenInteger, "2"},
|
||||
token{Position{2, 2}, tokenComma, ","},
|
||||
token{Position{3, 1}, tokenInteger, "3"},
|
||||
token{Position{3, 2}, tokenComma, ","},
|
||||
token{Position{4, 1}, tokenRightBracket, "]"},
|
||||
token{Position{4, 2}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualArrayBools(t *testing.T) {
|
||||
testFlow(t, "foo = [true, false, true]", []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 7}, tokenLeftBracket, "["},
|
||||
token{Position{1, 8}, tokenTrue, "true"},
|
||||
token{Position{1, 12}, tokenComma, ","},
|
||||
token{Position{1, 14}, tokenFalse, "false"},
|
||||
token{Position{1, 19}, tokenComma, ","},
|
||||
token{Position{1, 21}, tokenTrue, "true"},
|
||||
token{Position{1, 25}, tokenRightBracket, "]"},
|
||||
token{Position{1, 26}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualArrayBoolsWithComments(t *testing.T) {
|
||||
testFlow(t, "foo = [true, false, true] # YEAH", []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 7}, tokenLeftBracket, "["},
|
||||
token{Position{1, 8}, tokenTrue, "true"},
|
||||
token{Position{1, 12}, tokenComma, ","},
|
||||
token{Position{1, 14}, tokenFalse, "false"},
|
||||
token{Position{1, 19}, tokenComma, ","},
|
||||
token{Position{1, 21}, tokenTrue, "true"},
|
||||
token{Position{1, 25}, tokenRightBracket, "]"},
|
||||
token{Position{1, 33}, 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{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 7}, tokenDate, "1979-05-27T07:32:00Z"},
|
||||
token{Position{1, 27}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestFloatEndingWithDot(t *testing.T) {
|
||||
testFlow(t, "foo = 42.", []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 7}, tokenError, "float cannot end with a dot"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestFloatWithTwoDots(t *testing.T) {
|
||||
testFlow(t, "foo = 4.2.", []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 7}, tokenError, "cannot have two dots in one float"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestDoubleEqualKey(t *testing.T) {
|
||||
testFlow(t, "foo= = 2", []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 4}, tokenEqual, "="},
|
||||
token{Position{1, 5}, tokenError, "cannot have multiple equals for the same key"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestInvalidEsquapeSequence(t *testing.T) {
|
||||
testFlow(t, `foo = "\x"`, []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 8}, tokenError, "invalid escape sequence: \\x"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestNestedArrays(t *testing.T) {
|
||||
testFlow(t, "foo = [[[]]]", []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 7}, tokenLeftBracket, "["},
|
||||
token{Position{1, 8}, tokenLeftBracket, "["},
|
||||
token{Position{1, 9}, tokenLeftBracket, "["},
|
||||
token{Position{1, 10}, tokenRightBracket, "]"},
|
||||
token{Position{1, 11}, tokenRightBracket, "]"},
|
||||
token{Position{1, 12}, tokenRightBracket, "]"},
|
||||
token{Position{1, 13}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualNumber(t *testing.T) {
|
||||
testFlow(t, "foo = 42", []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 7}, tokenInteger, "42"},
|
||||
token{Position{1, 9}, tokenEOF, ""},
|
||||
})
|
||||
|
||||
testFlow(t, "foo = +42", []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 7}, tokenInteger, "+42"},
|
||||
token{Position{1, 10}, tokenEOF, ""},
|
||||
})
|
||||
|
||||
testFlow(t, "foo = -42", []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 7}, tokenInteger, "-42"},
|
||||
token{Position{1, 10}, tokenEOF, ""},
|
||||
})
|
||||
|
||||
testFlow(t, "foo = 4.2", []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 7}, tokenFloat, "4.2"},
|
||||
token{Position{1, 10}, tokenEOF, ""},
|
||||
})
|
||||
|
||||
testFlow(t, "foo = +4.2", []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 7}, tokenFloat, "+4.2"},
|
||||
token{Position{1, 11}, tokenEOF, ""},
|
||||
})
|
||||
|
||||
testFlow(t, "foo = -4.2", []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 7}, tokenFloat, "-4.2"},
|
||||
token{Position{1, 11}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultiline(t *testing.T) {
|
||||
testFlow(t, "foo = 42\nbar=21", []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 7}, tokenInteger, "42"},
|
||||
token{Position{2, 1}, tokenKey, "bar"},
|
||||
token{Position{2, 4}, tokenEqual, "="},
|
||||
token{Position{2, 5}, tokenInteger, "21"},
|
||||
token{Position{2, 7}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyEqualStringUnicodeEscape(t *testing.T) {
|
||||
testFlow(t, `foo = "hello \u2665"`, []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 8}, tokenString, "hello ♥"},
|
||||
token{Position{1, 21}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnicodeString(t *testing.T) {
|
||||
testFlow(t, `foo = "hello ♥ world"`, []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenEqual, "="},
|
||||
token{Position{1, 8}, tokenString, "hello ♥ world"},
|
||||
token{Position{1, 22}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyGroupArray(t *testing.T) {
|
||||
testFlow(t, "[[foo]]", []token{
|
||||
token{Position{1, 1}, tokenDoubleLeftBracket, "[["},
|
||||
token{Position{1, 3}, tokenKeyGroupArray, "foo"},
|
||||
token{Position{1, 6}, tokenDoubleRightBracket, "]]"},
|
||||
token{Position{1, 8}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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; 60[
|
||||
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 = newDecodeError(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 = newDecodeError(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/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLocalDate_AsTime(t *testing.T) {
|
||||
d := toml.LocalDate{2021, 6, 8}
|
||||
cast := d.AsTime(time.UTC)
|
||||
require.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}
|
||||
require.Equal(t, "2021-06-08", d.String())
|
||||
}
|
||||
|
||||
func TestLocalDate_MarshalText(t *testing.T) {
|
||||
d := toml.LocalDate{2021, 6, 8}
|
||||
b, err := d.MarshalText()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("2021-06-08"), b)
|
||||
}
|
||||
|
||||
func TestLocalDate_UnmarshalMarshalText(t *testing.T) {
|
||||
d := toml.LocalDate{}
|
||||
err := d.UnmarshalText([]byte("2021-06-08"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, toml.LocalDate{2021, 6, 8}, d)
|
||||
|
||||
err = d.UnmarshalText([]byte("what"))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestLocalTime_String(t *testing.T) {
|
||||
d := toml.LocalTime{20, 12, 1, 2, 9}
|
||||
require.Equal(t, "20:12:01.000000002", d.String())
|
||||
d = toml.LocalTime{20, 12, 1, 0, 0}
|
||||
require.Equal(t, "20:12:01", d.String())
|
||||
d = toml.LocalTime{20, 12, 1, 0, 9}
|
||||
require.Equal(t, "20:12:01.000000000", d.String())
|
||||
d = toml.LocalTime{20, 12, 1, 100, 0}
|
||||
require.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()
|
||||
require.NoError(t, err)
|
||||
require.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"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, toml.LocalTime{20, 12, 1, 2, 9}, d)
|
||||
|
||||
err = d.UnmarshalText([]byte("what"))
|
||||
require.Error(t, err)
|
||||
|
||||
err = d.UnmarshalText([]byte("20:12:01.000000002 bad"))
|
||||
require.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)
|
||||
require.NoError(t, err)
|
||||
require.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)
|
||||
require.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},
|
||||
}
|
||||
require.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()
|
||||
require.NoError(t, err)
|
||||
require.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"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, toml.LocalDateTime{
|
||||
toml.LocalDate{2021, 6, 8},
|
||||
toml.LocalTime{20, 12, 1, 2, 9},
|
||||
}, d)
|
||||
|
||||
err = d.UnmarshalText([]byte("what"))
|
||||
require.Error(t, err)
|
||||
|
||||
err = d.UnmarshalText([]byte("2021-06-08 20:12:01.000000002 bad"))
|
||||
require.Error(t, err)
|
||||
}
|
||||
+1040
File diff suppressed because it is too large
Load Diff
+1216
File diff suppressed because it is too large
Load Diff
@@ -1,227 +0,0 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// support function to set positions for tomlValues
|
||||
// NOTE: this is done to allow ctx.lastPosition to indicate the start of any
|
||||
// values returned by the query engines
|
||||
func tomlValueCheck(node interface{}, ctx *queryContext) interface{} {
|
||||
switch castNode := node.(type) {
|
||||
case *tomlValue:
|
||||
ctx.lastPosition = castNode.position
|
||||
return castNode.value
|
||||
case []*TomlTree:
|
||||
if len(castNode) > 0 {
|
||||
ctx.lastPosition = castNode[0].position
|
||||
}
|
||||
return node
|
||||
default:
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
// base match
|
||||
type matchBase struct {
|
||||
next pathFn
|
||||
}
|
||||
|
||||
func (f *matchBase) setNext(next pathFn) {
|
||||
f.next = next
|
||||
}
|
||||
|
||||
// terminating functor - gathers results
|
||||
type terminatingFn struct {
|
||||
// empty
|
||||
}
|
||||
|
||||
func newTerminatingFn() *terminatingFn {
|
||||
return &terminatingFn{}
|
||||
}
|
||||
|
||||
func (f *terminatingFn) setNext(next pathFn) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func (f *terminatingFn) call(node interface{}, ctx *queryContext) {
|
||||
switch castNode := node.(type) {
|
||||
case *TomlTree:
|
||||
ctx.result.appendResult(node, castNode.position)
|
||||
case *tomlValue:
|
||||
ctx.result.appendResult(node, castNode.position)
|
||||
default:
|
||||
// use last position for scalars
|
||||
ctx.result.appendResult(node, ctx.lastPosition)
|
||||
}
|
||||
}
|
||||
|
||||
// match single key
|
||||
type matchKeyFn struct {
|
||||
matchBase
|
||||
Name string
|
||||
}
|
||||
|
||||
func newMatchKeyFn(name string) *matchKeyFn {
|
||||
return &matchKeyFn{Name: name}
|
||||
}
|
||||
|
||||
func (f *matchKeyFn) call(node interface{}, ctx *queryContext) {
|
||||
if tree, ok := node.(*TomlTree); ok {
|
||||
item := tree.values[f.Name]
|
||||
if item != nil {
|
||||
f.next.call(item, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// match single index
|
||||
type matchIndexFn struct {
|
||||
matchBase
|
||||
Idx int
|
||||
}
|
||||
|
||||
func newMatchIndexFn(idx int) *matchIndexFn {
|
||||
return &matchIndexFn{Idx: idx}
|
||||
}
|
||||
|
||||
func (f *matchIndexFn) call(node interface{}, ctx *queryContext) {
|
||||
if arr, ok := tomlValueCheck(node, ctx).([]interface{}); ok {
|
||||
if f.Idx < len(arr) && f.Idx >= 0 {
|
||||
f.next.call(arr[f.Idx], ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// filter by slicing
|
||||
type matchSliceFn struct {
|
||||
matchBase
|
||||
Start, End, Step int
|
||||
}
|
||||
|
||||
func newMatchSliceFn(start, end, step int) *matchSliceFn {
|
||||
return &matchSliceFn{Start: start, End: end, Step: step}
|
||||
}
|
||||
|
||||
func (f *matchSliceFn) call(node interface{}, ctx *queryContext) {
|
||||
if arr, ok := tomlValueCheck(node, ctx).([]interface{}); ok {
|
||||
// adjust indexes for negative values, reverse ordering
|
||||
realStart, realEnd := f.Start, f.End
|
||||
if realStart < 0 {
|
||||
realStart = len(arr) + realStart
|
||||
}
|
||||
if realEnd < 0 {
|
||||
realEnd = len(arr) + realEnd
|
||||
}
|
||||
if realEnd < realStart {
|
||||
realEnd, realStart = realStart, realEnd // swap
|
||||
}
|
||||
// loop and gather
|
||||
for idx := realStart; idx < realEnd; idx += f.Step {
|
||||
f.next.call(arr[idx], ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// match anything
|
||||
type matchAnyFn struct {
|
||||
matchBase
|
||||
}
|
||||
|
||||
func newMatchAnyFn() *matchAnyFn {
|
||||
return &matchAnyFn{}
|
||||
}
|
||||
|
||||
func (f *matchAnyFn) call(node interface{}, ctx *queryContext) {
|
||||
if tree, ok := node.(*TomlTree); ok {
|
||||
for _, v := range tree.values {
|
||||
f.next.call(v, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// filter through union
|
||||
type matchUnionFn struct {
|
||||
Union []pathFn
|
||||
}
|
||||
|
||||
func (f *matchUnionFn) setNext(next pathFn) {
|
||||
for _, fn := range f.Union {
|
||||
fn.setNext(next)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *matchUnionFn) call(node interface{}, ctx *queryContext) {
|
||||
for _, fn := range f.Union {
|
||||
fn.call(node, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// match every single last node in the tree
|
||||
type matchRecursiveFn struct {
|
||||
matchBase
|
||||
}
|
||||
|
||||
func newMatchRecursiveFn() *matchRecursiveFn {
|
||||
return &matchRecursiveFn{}
|
||||
}
|
||||
|
||||
func (f *matchRecursiveFn) call(node interface{}, ctx *queryContext) {
|
||||
if tree, ok := node.(*TomlTree); ok {
|
||||
var visit func(tree *TomlTree)
|
||||
visit = func(tree *TomlTree) {
|
||||
for _, v := range tree.values {
|
||||
f.next.call(v, ctx)
|
||||
switch node := v.(type) {
|
||||
case *TomlTree:
|
||||
visit(node)
|
||||
case []*TomlTree:
|
||||
for _, subtree := range node {
|
||||
visit(subtree)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
f.next.call(tree, ctx)
|
||||
visit(tree)
|
||||
}
|
||||
}
|
||||
|
||||
// match based on an externally provided functional filter
|
||||
type matchFilterFn struct {
|
||||
matchBase
|
||||
Pos Position
|
||||
Name string
|
||||
}
|
||||
|
||||
func newMatchFilterFn(name string, pos Position) *matchFilterFn {
|
||||
return &matchFilterFn{Name: name, Pos: pos}
|
||||
}
|
||||
|
||||
func (f *matchFilterFn) call(node interface{}, ctx *queryContext) {
|
||||
fn, ok := (*ctx.filters)[f.Name]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("%s: query context does not have filter '%s'",
|
||||
f.Pos, f.Name))
|
||||
}
|
||||
switch castNode := tomlValueCheck(node, ctx).(type) {
|
||||
case *TomlTree:
|
||||
for _, v := range castNode.values {
|
||||
if tv, ok := v.(*tomlValue); ok {
|
||||
if fn(tv.value) {
|
||||
f.next.call(v, ctx)
|
||||
}
|
||||
} else {
|
||||
if fn(v) {
|
||||
f.next.call(v, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
for _, v := range castNode {
|
||||
if fn(v) {
|
||||
f.next.call(v, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-202
@@ -1,202 +0,0 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// dump path tree to a string
|
||||
func pathString(root pathFn) string {
|
||||
result := fmt.Sprintf("%T:", root)
|
||||
switch fn := root.(type) {
|
||||
case *terminatingFn:
|
||||
result += "{}"
|
||||
case *matchKeyFn:
|
||||
result += fmt.Sprintf("{%s}", fn.Name)
|
||||
result += pathString(fn.next)
|
||||
case *matchIndexFn:
|
||||
result += fmt.Sprintf("{%d}", fn.Idx)
|
||||
result += pathString(fn.next)
|
||||
case *matchSliceFn:
|
||||
result += fmt.Sprintf("{%d:%d:%d}",
|
||||
fn.Start, fn.End, fn.Step)
|
||||
result += pathString(fn.next)
|
||||
case *matchAnyFn:
|
||||
result += "{}"
|
||||
result += pathString(fn.next)
|
||||
case *matchUnionFn:
|
||||
result += "{["
|
||||
for _, v := range fn.Union {
|
||||
result += pathString(v) + ", "
|
||||
}
|
||||
result += "]}"
|
||||
case *matchRecursiveFn:
|
||||
result += "{}"
|
||||
result += pathString(fn.next)
|
||||
case *matchFilterFn:
|
||||
result += fmt.Sprintf("{%s}", fn.Name)
|
||||
result += pathString(fn.next)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func assertPathMatch(t *testing.T, path, ref *Query) bool {
|
||||
pathStr := pathString(path.root)
|
||||
refStr := pathString(ref.root)
|
||||
if pathStr != refStr {
|
||||
t.Errorf("paths do not match")
|
||||
t.Log("test:", pathStr)
|
||||
t.Log("ref: ", refStr)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func assertPath(t *testing.T, query string, ref *Query) {
|
||||
path, _ := parseQuery(lexQuery(query))
|
||||
assertPathMatch(t, path, ref)
|
||||
}
|
||||
|
||||
func buildPath(parts ...pathFn) *Query {
|
||||
query := newQuery()
|
||||
for _, v := range parts {
|
||||
query.appendPath(v)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
func TestPathRoot(t *testing.T) {
|
||||
assertPath(t,
|
||||
"$",
|
||||
buildPath(
|
||||
// empty
|
||||
))
|
||||
}
|
||||
|
||||
func TestPathKey(t *testing.T) {
|
||||
assertPath(t,
|
||||
"$.foo",
|
||||
buildPath(
|
||||
newMatchKeyFn("foo"),
|
||||
))
|
||||
}
|
||||
|
||||
func TestPathBracketKey(t *testing.T) {
|
||||
assertPath(t,
|
||||
"$[foo]",
|
||||
buildPath(
|
||||
newMatchKeyFn("foo"),
|
||||
))
|
||||
}
|
||||
|
||||
func TestPathBracketStringKey(t *testing.T) {
|
||||
assertPath(t,
|
||||
"$['foo']",
|
||||
buildPath(
|
||||
newMatchKeyFn("foo"),
|
||||
))
|
||||
}
|
||||
|
||||
func TestPathIndex(t *testing.T) {
|
||||
assertPath(t,
|
||||
"$[123]",
|
||||
buildPath(
|
||||
newMatchIndexFn(123),
|
||||
))
|
||||
}
|
||||
|
||||
func TestPathSliceStart(t *testing.T) {
|
||||
assertPath(t,
|
||||
"$[123:]",
|
||||
buildPath(
|
||||
newMatchSliceFn(123, math.MaxInt64, 1),
|
||||
))
|
||||
}
|
||||
|
||||
func TestPathSliceStartEnd(t *testing.T) {
|
||||
assertPath(t,
|
||||
"$[123:456]",
|
||||
buildPath(
|
||||
newMatchSliceFn(123, 456, 1),
|
||||
))
|
||||
}
|
||||
|
||||
func TestPathSliceStartEndColon(t *testing.T) {
|
||||
assertPath(t,
|
||||
"$[123:456:]",
|
||||
buildPath(
|
||||
newMatchSliceFn(123, 456, 1),
|
||||
))
|
||||
}
|
||||
|
||||
func TestPathSliceStartStep(t *testing.T) {
|
||||
assertPath(t,
|
||||
"$[123::7]",
|
||||
buildPath(
|
||||
newMatchSliceFn(123, math.MaxInt64, 7),
|
||||
))
|
||||
}
|
||||
|
||||
func TestPathSliceEndStep(t *testing.T) {
|
||||
assertPath(t,
|
||||
"$[:456:7]",
|
||||
buildPath(
|
||||
newMatchSliceFn(0, 456, 7),
|
||||
))
|
||||
}
|
||||
|
||||
func TestPathSliceStep(t *testing.T) {
|
||||
assertPath(t,
|
||||
"$[::7]",
|
||||
buildPath(
|
||||
newMatchSliceFn(0, math.MaxInt64, 7),
|
||||
))
|
||||
}
|
||||
|
||||
func TestPathSliceAll(t *testing.T) {
|
||||
assertPath(t,
|
||||
"$[123:456:7]",
|
||||
buildPath(
|
||||
newMatchSliceFn(123, 456, 7),
|
||||
))
|
||||
}
|
||||
|
||||
func TestPathAny(t *testing.T) {
|
||||
assertPath(t,
|
||||
"$.*",
|
||||
buildPath(
|
||||
newMatchAnyFn(),
|
||||
))
|
||||
}
|
||||
|
||||
func TestPathUnion(t *testing.T) {
|
||||
assertPath(t,
|
||||
"$[foo, bar, baz]",
|
||||
buildPath(
|
||||
&matchUnionFn{[]pathFn{
|
||||
newMatchKeyFn("foo"),
|
||||
newMatchKeyFn("bar"),
|
||||
newMatchKeyFn("baz"),
|
||||
}},
|
||||
))
|
||||
}
|
||||
|
||||
func TestPathRecurse(t *testing.T) {
|
||||
assertPath(t,
|
||||
"$..*",
|
||||
buildPath(
|
||||
newMatchRecursiveFn(),
|
||||
))
|
||||
}
|
||||
|
||||
func TestPathFilterExpr(t *testing.T) {
|
||||
assertPath(t,
|
||||
"$[?('foo'),?(bar)]",
|
||||
buildPath(
|
||||
&matchUnionFn{[]pathFn{
|
||||
newMatchFilterFn("foo", Position{}),
|
||||
newMatchFilterFn("bar", Position{}),
|
||||
}},
|
||||
))
|
||||
}
|
||||
+421
-409
@@ -1,438 +1,450 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pelletier/go-toml/v2/internal/ast"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
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 {
|
||||
// NOTE: directly access key instead of resolve by path
|
||||
// NOTE: see TestSpecialKV
|
||||
switch node := tree.GetPath([]string{k}).(type) {
|
||||
case []*TomlTree:
|
||||
for idx, item := range node {
|
||||
assertTree(t, item, err, v.([]map[string]interface{})[idx])
|
||||
}
|
||||
case *TomlTree:
|
||||
assertTree(t, 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 := newTomlTree()
|
||||
tree.createSubTree([]string{"a", "b", "c"}, Position{})
|
||||
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),
|
||||
})
|
||||
}
|
||||
|
||||
// NOTE: from the BurntSushi test suite
|
||||
// NOTE: this test is pure evil due to the embedded '.'
|
||||
func TestSpecialKV(t *testing.T) {
|
||||
tree, err := Load("~!@#$^&*()_+-`1234567890[]\\|/?><.,;: = 1")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"~!@#$^&*()_+-`1234567890[]\\|/?><.,;:": int64(1),
|
||||
})
|
||||
}
|
||||
|
||||
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": map[string]interface{}{
|
||||
"b": map[string]interface{}{
|
||||
"c": map[string]interface{}{
|
||||
"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() != "(1, 10): mixed types in array" {
|
||||
t.Error("Bad error message:", err.Error())
|
||||
}
|
||||
|
||||
_, err = Load("a = [42, \"hello\"]")
|
||||
if err.Error() != "(1, 11): 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() != "(1, 4): expecting a value" {
|
||||
t.Error("Bad error message:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnterminatedArray(t *testing.T) {
|
||||
_, err := Load("a = [1,")
|
||||
if err.Error() != "(1, 8): 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() != "(3, 2): duplicated tables" {
|
||||
t.Error("Bad error message:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDuplicateKeys(t *testing.T) {
|
||||
_, err := Load("foo = 2\nfoo = 3")
|
||||
if err.Error() != "(2, 1): 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() != "(1, 2): 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() != "(1, 4): cannot start float with a dot" {
|
||||
t.Error("Bad error message:", err.Error())
|
||||
}
|
||||
|
||||
_, err = Load("a = -.42")
|
||||
if err.Error() != "(1, 5): 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": map[string]interface{}{
|
||||
"name": "Tom Preston-Werner",
|
||||
"organization": "GitHub",
|
||||
"bio": "GitHub Cofounder & CEO\nLikes tater tots and beer.",
|
||||
"dob": time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC),
|
||||
},
|
||||
"database": map[string]interface{}{
|
||||
"server": "192.168.1.1",
|
||||
"ports": []int64{8001, 8001, 8002},
|
||||
"connection_max": 5000,
|
||||
"enabled": true,
|
||||
},
|
||||
"servers": map[string]interface{}{
|
||||
"alpha": map[string]interface{}{
|
||||
"ip": "10.0.0.1",
|
||||
"dc": "eqdc10",
|
||||
},
|
||||
"beta": map[string]interface{}{
|
||||
"ip": "10.0.0.2",
|
||||
"dc": "eqdc10",
|
||||
},
|
||||
},
|
||||
"clients": map[string]interface{}{
|
||||
"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 TestParseKeyGroupArraySpec(t *testing.T) {
|
||||
tree, err := Load("[[fruit]]\n name=\"apple\"\n [fruit.physical]\n color=\"red\"\n shape=\"round\"\n [[fruit]]\n name=\"banana\"")
|
||||
assertTree(t, tree, err, map[string]interface{}{
|
||||
"fruit": []map[string]interface{}{
|
||||
{"name": "apple", "physical": map[string]interface{}{"color": "red", "shape": "round"}},
|
||||
{"name": "banana"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestToTomlValue(t *testing.T) {
|
||||
for idx, item := range []struct {
|
||||
Value interface{}
|
||||
Expect string
|
||||
//nolint:funlen
|
||||
func TestParser_AST_Numbers(t *testing.T) {
|
||||
examples := []struct {
|
||||
desc string
|
||||
input string
|
||||
kind ast.Kind
|
||||
err bool
|
||||
}{
|
||||
{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)
|
||||
{
|
||||
desc: "integer just digits",
|
||||
input: `1234`,
|
||||
kind: ast.Integer,
|
||||
},
|
||||
{
|
||||
desc: "integer zero",
|
||||
input: `0`,
|
||||
kind: ast.Integer,
|
||||
},
|
||||
{
|
||||
desc: "integer sign",
|
||||
input: `+99`,
|
||||
kind: ast.Integer,
|
||||
},
|
||||
{
|
||||
desc: "integer hex uppercase",
|
||||
input: `0xDEADBEEF`,
|
||||
kind: ast.Integer,
|
||||
},
|
||||
{
|
||||
desc: "integer hex lowercase",
|
||||
input: `0xdead_beef`,
|
||||
kind: ast.Integer,
|
||||
},
|
||||
{
|
||||
desc: "integer octal",
|
||||
input: `0o01234567`,
|
||||
kind: ast.Integer,
|
||||
},
|
||||
{
|
||||
desc: "integer binary",
|
||||
input: `0b11010110`,
|
||||
kind: ast.Integer,
|
||||
},
|
||||
{
|
||||
desc: "float zero",
|
||||
input: `0.0`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "float positive zero",
|
||||
input: `+0.0`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "float negative zero",
|
||||
input: `-0.0`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "float pi",
|
||||
input: `3.1415`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "float negative",
|
||||
input: `-0.01`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "float signed exponent",
|
||||
input: `5e+22`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "float exponent lowercase",
|
||||
input: `1e06`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "float exponent uppercase",
|
||||
input: `-2E-2`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "float fractional with exponent",
|
||||
input: `6.626e-34`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "float underscores",
|
||||
input: `224_617.445_991_228`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "inf",
|
||||
input: `inf`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "inf negative",
|
||||
input: `-inf`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "inf positive",
|
||||
input: `+inf`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "nan",
|
||||
input: `nan`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "nan negative",
|
||||
input: `-nan`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
{
|
||||
desc: "nan positive",
|
||||
input: `+nan`,
|
||||
kind: ast.Float,
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range examples {
|
||||
e := e
|
||||
t.Run(e.desc, func(t *testing.T) {
|
||||
p := parser{}
|
||||
p.Reset([]byte(`A = ` + e.input))
|
||||
p.NextExpression()
|
||||
err := p.Error()
|
||||
if e.err {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := astNode{
|
||||
Kind: ast.KeyValue,
|
||||
Children: []astNode{
|
||||
{Kind: e.kind, Data: []byte(e.input)},
|
||||
{Kind: ast.Key, Data: []byte(`A`)},
|
||||
},
|
||||
}
|
||||
compareNode(t, expected, p.Expression())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToString(t *testing.T) {
|
||||
tree, err := Load("[foo]\n\n[[foo.bar]]\na = 42\n\n[[foo.bar]]\na = 69\n")
|
||||
if err != nil {
|
||||
t.Errorf("Test failed to parse: %v", err)
|
||||
return
|
||||
type (
|
||||
astNode struct {
|
||||
Kind ast.Kind
|
||||
Data []byte
|
||||
Children []astNode
|
||||
}
|
||||
result := tree.ToString()
|
||||
expected := "\n[foo]\n\n [[foo.bar]]\n a = 42\n\n [[foo.bar]]\n a = 69\n"
|
||||
if result != expected {
|
||||
t.Errorf("Expected got '%s', expected '%s'", result, expected)
|
||||
)
|
||||
|
||||
func compareNode(t *testing.T, e astNode, n *ast.Node) {
|
||||
t.Helper()
|
||||
require.Equal(t, e.Kind, n.Kind)
|
||||
require.Equal(t, e.Data, n.Data)
|
||||
|
||||
compareIterator(t, e.Children, n.Children())
|
||||
}
|
||||
|
||||
func compareIterator(t *testing.T, expected []astNode, actual ast.Iterator) {
|
||||
t.Helper()
|
||||
idx := 0
|
||||
|
||||
for actual.Next() {
|
||||
n := actual.Node()
|
||||
|
||||
if idx >= len(expected) {
|
||||
t.Fatal("extra child in actual tree")
|
||||
}
|
||||
e := expected[idx]
|
||||
|
||||
compareNode(t, e, n)
|
||||
|
||||
idx++
|
||||
}
|
||||
|
||||
if idx < len(expected) {
|
||||
t.Fatal("missing children in actual", "idx =", idx, "expected =", len(expected))
|
||||
}
|
||||
}
|
||||
|
||||
func assertPosition(t *testing.T, text string, ref map[string]Position) {
|
||||
tree, err := Load(text)
|
||||
if err != nil {
|
||||
t.Errorf("Error loading document text: `%v`", text)
|
||||
t.Errorf("Error: %v", err)
|
||||
//nolint:funlen
|
||||
func TestParser_AST(t *testing.T) {
|
||||
examples := []struct {
|
||||
desc string
|
||||
input string
|
||||
ast astNode
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
desc: "simple string assignment",
|
||||
input: `A = "hello"`,
|
||||
ast: astNode{
|
||||
Kind: ast.KeyValue,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: ast.String,
|
||||
Data: []byte(`hello`),
|
||||
},
|
||||
{
|
||||
Kind: ast.Key,
|
||||
Data: []byte(`A`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "simple bool assignment",
|
||||
input: `A = true`,
|
||||
ast: astNode{
|
||||
Kind: ast.KeyValue,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: ast.Bool,
|
||||
Data: []byte(`true`),
|
||||
},
|
||||
{
|
||||
Kind: ast.Key,
|
||||
Data: []byte(`A`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "array of strings",
|
||||
input: `A = ["hello", ["world", "again"]]`,
|
||||
ast: astNode{
|
||||
Kind: ast.KeyValue,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: ast.Array,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: ast.String,
|
||||
Data: []byte(`hello`),
|
||||
},
|
||||
{
|
||||
Kind: ast.Array,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: ast.String,
|
||||
Data: []byte(`world`),
|
||||
},
|
||||
{
|
||||
Kind: ast.String,
|
||||
Data: []byte(`again`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: ast.Key,
|
||||
Data: []byte(`A`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "array of arrays of strings",
|
||||
input: `A = ["hello", "world"]`,
|
||||
ast: astNode{
|
||||
Kind: ast.KeyValue,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: ast.Array,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: ast.String,
|
||||
Data: []byte(`hello`),
|
||||
},
|
||||
{
|
||||
Kind: ast.String,
|
||||
Data: []byte(`world`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: ast.Key,
|
||||
Data: []byte(`A`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "inline table",
|
||||
input: `name = { first = "Tom", last = "Preston-Werner" }`,
|
||||
ast: astNode{
|
||||
Kind: ast.KeyValue,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: ast.InlineTable,
|
||||
Children: []astNode{
|
||||
{
|
||||
Kind: ast.KeyValue,
|
||||
Children: []astNode{
|
||||
{Kind: ast.String, Data: []byte(`Tom`)},
|
||||
{Kind: ast.Key, Data: []byte(`first`)},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: ast.KeyValue,
|
||||
Children: []astNode{
|
||||
{Kind: ast.String, Data: []byte(`Preston-Werner`)},
|
||||
{Kind: ast.Key, Data: []byte(`last`)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: ast.Key,
|
||||
Data: []byte(`name`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for path, pos := range ref {
|
||||
testPos := tree.GetPosition(path)
|
||||
if testPos.Invalid() {
|
||||
t.Errorf("Failed to query tree path or path has invalid position: %s", path)
|
||||
} else if pos != testPos {
|
||||
t.Errorf("Expected position %v, got %v instead", pos, testPos)
|
||||
|
||||
for _, e := range examples {
|
||||
e := e
|
||||
t.Run(e.desc, func(t *testing.T) {
|
||||
p := parser{}
|
||||
p.Reset([]byte(e.input))
|
||||
p.NextExpression()
|
||||
err := p.Error()
|
||||
if e.err {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
compareNode(t, e.ast, p.Expression())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentPositions(t *testing.T) {
|
||||
assertPosition(t,
|
||||
"[foo]\nbar=42\nbaz=69",
|
||||
map[string]Position{
|
||||
"": Position{1, 1},
|
||||
"foo": Position{1, 1},
|
||||
"foo.bar": Position{2, 1},
|
||||
"foo.baz": Position{3, 1},
|
||||
func BenchmarkParseBasicStringWithUnicode(b *testing.B) {
|
||||
p := &parser{}
|
||||
b.Run("4", func(b *testing.B) {
|
||||
input := []byte(`"\u1234\u5678\u9ABC\u1234\u5678\u9ABC"`)
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(input)))
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
p.parseBasicString(input)
|
||||
}
|
||||
})
|
||||
b.Run("8", func(b *testing.B) {
|
||||
input := []byte(`"\u12345678\u9ABCDEF0\u12345678\u9ABCDEF0"`)
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(input)))
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
p.parseBasicString(input)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDocumentPositionsWithSpaces(t *testing.T) {
|
||||
assertPosition(t,
|
||||
" [foo]\n bar=42\n baz=69",
|
||||
map[string]Position{
|
||||
"": Position{1, 1},
|
||||
"foo": Position{1, 3},
|
||||
"foo.bar": Position{2, 3},
|
||||
"foo.baz": Position{3, 3},
|
||||
func BenchmarkParseBasicStringsEasy(b *testing.B) {
|
||||
p := &parser{}
|
||||
|
||||
for _, size := range []int{1, 4, 8, 16, 21} {
|
||||
b.Run(strconv.Itoa(size), func(b *testing.B) {
|
||||
input := []byte(`"` + strings.Repeat("A", size) + `"`)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(input)))
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
p.parseBasicString(input)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentPositionsWithGroupArray(t *testing.T) {
|
||||
assertPosition(t,
|
||||
"[[foo]]\nbar=42\nbaz=69",
|
||||
map[string]Position{
|
||||
"": Position{1, 1},
|
||||
"foo": Position{1, 1},
|
||||
"foo.bar": Position{2, 1},
|
||||
"foo.baz": Position{3, 1},
|
||||
})
|
||||
}
|
||||
func TestParser_AST_DateTimes(t *testing.T) {
|
||||
examples := []struct {
|
||||
desc string
|
||||
input string
|
||||
kind ast.Kind
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
desc: "offset-date-time with delim 'T' and UTC offset",
|
||||
input: `2021-07-21T12:08:05Z`,
|
||||
kind: ast.DateTime,
|
||||
},
|
||||
{
|
||||
desc: "offset-date-time with space delim and +8hours offset",
|
||||
input: `2021-07-21 12:08:05+08:00`,
|
||||
kind: ast.DateTime,
|
||||
},
|
||||
{
|
||||
desc: "local-date-time with nano second",
|
||||
input: `2021-07-21T12:08:05.666666666`,
|
||||
kind: ast.LocalDateTime,
|
||||
},
|
||||
{
|
||||
desc: "local-date-time",
|
||||
input: `2021-07-21T12:08:05`,
|
||||
kind: ast.LocalDateTime,
|
||||
},
|
||||
{
|
||||
desc: "local-date",
|
||||
input: `2021-07-21`,
|
||||
kind: ast.LocalDate,
|
||||
},
|
||||
}
|
||||
|
||||
func TestNestedTreePosition(t *testing.T) {
|
||||
assertPosition(t,
|
||||
"[foo.bar]\na=42\nb=69",
|
||||
map[string]Position{
|
||||
"": Position{1, 1},
|
||||
"foo": Position{1, 1},
|
||||
"foo.bar": Position{1, 1},
|
||||
"foo.bar.a": Position{2, 1},
|
||||
"foo.bar.b": Position{3, 1},
|
||||
for _, e := range examples {
|
||||
e := e
|
||||
t.Run(e.desc, func(t *testing.T) {
|
||||
p := parser{}
|
||||
p.Reset([]byte(`A = ` + e.input))
|
||||
p.NextExpression()
|
||||
err := p.Error()
|
||||
if e.err {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := astNode{
|
||||
Kind: ast.KeyValue,
|
||||
Children: []astNode{
|
||||
{Kind: e.kind, Data: []byte(e.input)},
|
||||
{Kind: ast.Key, Data: []byte(`A`)},
|
||||
},
|
||||
}
|
||||
compareNode(t, expected, p.Expression())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
// Position support for go-toml
|
||||
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
/*
|
||||
Position of a document element within a TOML document.
|
||||
|
||||
Line and Col are both 1-indexed positions for the element's line number and
|
||||
column number, respectively. Values of zero or less will cause Invalid(),
|
||||
to return true.
|
||||
*/
|
||||
type Position struct {
|
||||
Line int // line within the document
|
||||
Col int // column within the line
|
||||
}
|
||||
|
||||
// String representation of the position.
|
||||
// Displays 1-indexed line and column numbers.
|
||||
func (p *Position) String() string {
|
||||
return fmt.Sprintf("(%d, %d)", p.Line, p.Col)
|
||||
}
|
||||
|
||||
// Returns whether or not the position is valid (i.e. with negative or
|
||||
// null values)
|
||||
func (p *Position) Invalid() bool {
|
||||
return p.Line <= 0 || p.Col <= 0
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// Testing support for go-toml
|
||||
|
||||
package toml
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPositionString(t *testing.T) {
|
||||
p := Position{123, 456}
|
||||
expected := "(123, 456)"
|
||||
value := p.String()
|
||||
|
||||
if value != expected {
|
||||
t.Errorf("Expected %v, got %v instead", expected, value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalid(t *testing.T) {
|
||||
for i, v := range []Position{
|
||||
Position{0, 1234},
|
||||
Position{1234, 0},
|
||||
Position{0, 0},
|
||||
} {
|
||||
if !v.Invalid() {
|
||||
t.Errorf("Position at %v is valid: %v", i, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Type of a user-defined filter function, for use with Query.SetFilter().
|
||||
//
|
||||
// The return value of the function must indicate if 'node' is to be included
|
||||
// at this stage of the TOML path. Returning true will include the node, and
|
||||
// returning false will exclude it.
|
||||
//
|
||||
// NOTE: Care should be taken to write script callbacks such that they are safe
|
||||
// to use from multiple goroutines.
|
||||
type NodeFilterFn func(node interface{}) bool
|
||||
|
||||
// The result of Executing a Query
|
||||
type QueryResult struct {
|
||||
items []interface{}
|
||||
positions []Position
|
||||
}
|
||||
|
||||
// appends a value/position pair to the result set
|
||||
func (r *QueryResult) appendResult(node interface{}, pos Position) {
|
||||
r.items = append(r.items, node)
|
||||
r.positions = append(r.positions, pos)
|
||||
}
|
||||
|
||||
// Set of values within a QueryResult. The order of values is not guaranteed
|
||||
// to be in document order, and may be different each time a query is executed.
|
||||
func (r *QueryResult) Values() []interface{} {
|
||||
return r.items
|
||||
}
|
||||
|
||||
// Set of positions for values within a QueryResult. Each index in Positions()
|
||||
// corresponds to the entry in Value() of the same index.
|
||||
func (r *QueryResult) Positions() []Position {
|
||||
return r.positions
|
||||
}
|
||||
|
||||
// runtime context for executing query paths
|
||||
type queryContext struct {
|
||||
result *QueryResult
|
||||
filters *map[string]NodeFilterFn
|
||||
lastPosition Position
|
||||
}
|
||||
|
||||
// generic path functor interface
|
||||
type pathFn interface {
|
||||
setNext(next pathFn)
|
||||
call(node interface{}, ctx *queryContext)
|
||||
}
|
||||
|
||||
// A Query is the representation of a compiled TOML path. A Query is safe
|
||||
// for concurrent use by multiple goroutines.
|
||||
type Query struct {
|
||||
root pathFn
|
||||
tail pathFn
|
||||
filters *map[string]NodeFilterFn
|
||||
}
|
||||
|
||||
func newQuery() *Query {
|
||||
return &Query{
|
||||
root: nil,
|
||||
tail: nil,
|
||||
filters: &defaultFilterFunctions,
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Query) appendPath(next pathFn) {
|
||||
if q.root == nil {
|
||||
q.root = next
|
||||
} else {
|
||||
q.tail.setNext(next)
|
||||
}
|
||||
q.tail = next
|
||||
next.setNext(newTerminatingFn()) // init the next functor
|
||||
}
|
||||
|
||||
// Compiles a TOML path expression. The returned Query can be used to match
|
||||
// elements within a TomlTree and its descendants.
|
||||
func CompileQuery(path string) (*Query, error) {
|
||||
return parseQuery(lexQuery(path))
|
||||
}
|
||||
|
||||
// Executes a query against a TomlTree, and returns the result of the query.
|
||||
func (q *Query) Execute(tree *TomlTree) *QueryResult {
|
||||
result := &QueryResult{
|
||||
items: []interface{}{},
|
||||
positions: []Position{},
|
||||
}
|
||||
if q.root == nil {
|
||||
result.appendResult(tree, tree.GetPosition(""))
|
||||
} else {
|
||||
ctx := &queryContext{
|
||||
result: result,
|
||||
filters: q.filters,
|
||||
}
|
||||
q.root.call(tree, ctx)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Sets a user-defined filter function. These may be used inside "?(..)" query
|
||||
// expressions to filter TOML document elements within a query.
|
||||
func (q *Query) SetFilter(name string, fn NodeFilterFn) {
|
||||
if q.filters == &defaultFilterFunctions {
|
||||
// clone the static table
|
||||
q.filters = &map[string]NodeFilterFn{}
|
||||
for k, v := range defaultFilterFunctions {
|
||||
(*q.filters)[k] = v
|
||||
}
|
||||
}
|
||||
(*q.filters)[name] = fn
|
||||
}
|
||||
|
||||
var defaultFilterFunctions = map[string]NodeFilterFn{
|
||||
"tree": func(node interface{}) bool {
|
||||
_, ok := node.(*TomlTree)
|
||||
return ok
|
||||
},
|
||||
"int": func(node interface{}) bool {
|
||||
_, ok := node.(int64)
|
||||
return ok
|
||||
},
|
||||
"float": func(node interface{}) bool {
|
||||
_, ok := node.(float64)
|
||||
return ok
|
||||
},
|
||||
"string": func(node interface{}) bool {
|
||||
_, ok := node.(string)
|
||||
return ok
|
||||
},
|
||||
"time": func(node interface{}) bool {
|
||||
_, ok := node.(time.Time)
|
||||
return ok
|
||||
},
|
||||
"bool": func(node interface{}) bool {
|
||||
_, ok := node.(bool)
|
||||
return ok
|
||||
},
|
||||
}
|
||||
-339
@@ -1,339 +0,0 @@
|
||||
// TOML JSONPath lexer.
|
||||
//
|
||||
// Written using the principles developed by Rob Pike in
|
||||
// http://www.youtube.com/watch?v=HxaD_trXwRE
|
||||
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Lexer state function
|
||||
type queryLexStateFn func() queryLexStateFn
|
||||
|
||||
// Lexer definition
|
||||
type queryLexer struct {
|
||||
input string
|
||||
start int
|
||||
pos int
|
||||
width int
|
||||
tokens chan token
|
||||
depth int
|
||||
line int
|
||||
col int
|
||||
stringTerm string
|
||||
}
|
||||
|
||||
func (l *queryLexer) run() {
|
||||
for state := l.lexVoid; state != nil; {
|
||||
state = state()
|
||||
}
|
||||
close(l.tokens)
|
||||
}
|
||||
|
||||
func (l *queryLexer) nextStart() {
|
||||
// iterate by runes (utf8 characters)
|
||||
// search for newlines and advance line/col counts
|
||||
for i := l.start; i < l.pos; {
|
||||
r, width := utf8.DecodeRuneInString(l.input[i:])
|
||||
if r == '\n' {
|
||||
l.line++
|
||||
l.col = 1
|
||||
} else {
|
||||
l.col++
|
||||
}
|
||||
i += width
|
||||
}
|
||||
// advance start position to next token
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
func (l *queryLexer) emit(t tokenType) {
|
||||
l.tokens <- token{
|
||||
Position: Position{l.line, l.col},
|
||||
typ: t,
|
||||
val: l.input[l.start:l.pos],
|
||||
}
|
||||
l.nextStart()
|
||||
}
|
||||
|
||||
func (l *queryLexer) emitWithValue(t tokenType, value string) {
|
||||
l.tokens <- token{
|
||||
Position: Position{l.line, l.col},
|
||||
typ: t,
|
||||
val: value,
|
||||
}
|
||||
l.nextStart()
|
||||
}
|
||||
|
||||
func (l *queryLexer) 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 *queryLexer) ignore() {
|
||||
l.nextStart()
|
||||
}
|
||||
|
||||
func (l *queryLexer) backup() {
|
||||
l.pos -= l.width
|
||||
}
|
||||
|
||||
func (l *queryLexer) errorf(format string, args ...interface{}) queryLexStateFn {
|
||||
l.tokens <- token{
|
||||
Position: Position{l.line, l.col},
|
||||
typ: tokenError,
|
||||
val: fmt.Sprintf(format, args...),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *queryLexer) peek() rune {
|
||||
r := l.next()
|
||||
l.backup()
|
||||
return r
|
||||
}
|
||||
|
||||
func (l *queryLexer) accept(valid string) bool {
|
||||
if strings.IndexRune(valid, l.next()) >= 0 {
|
||||
return true
|
||||
}
|
||||
l.backup()
|
||||
return false
|
||||
}
|
||||
|
||||
func (l *queryLexer) follow(next string) bool {
|
||||
return strings.HasPrefix(l.input[l.pos:], next)
|
||||
}
|
||||
|
||||
func (l *queryLexer) lexVoid() queryLexStateFn {
|
||||
for {
|
||||
next := l.peek()
|
||||
switch next {
|
||||
case '$':
|
||||
l.pos++
|
||||
l.emit(tokenDollar)
|
||||
continue
|
||||
case '.':
|
||||
if l.follow("..") {
|
||||
l.pos += 2
|
||||
l.emit(tokenDotDot)
|
||||
} else {
|
||||
l.pos++
|
||||
l.emit(tokenDot)
|
||||
}
|
||||
continue
|
||||
case '[':
|
||||
l.pos++
|
||||
l.emit(tokenLeftBracket)
|
||||
continue
|
||||
case ']':
|
||||
l.pos++
|
||||
l.emit(tokenRightBracket)
|
||||
continue
|
||||
case ',':
|
||||
l.pos++
|
||||
l.emit(tokenComma)
|
||||
continue
|
||||
case '*':
|
||||
l.pos++
|
||||
l.emit(tokenStar)
|
||||
continue
|
||||
case '(':
|
||||
l.pos++
|
||||
l.emit(tokenLeftParen)
|
||||
continue
|
||||
case ')':
|
||||
l.pos++
|
||||
l.emit(tokenRightParen)
|
||||
continue
|
||||
case '?':
|
||||
l.pos++
|
||||
l.emit(tokenQuestion)
|
||||
continue
|
||||
case ':':
|
||||
l.pos++
|
||||
l.emit(tokenColon)
|
||||
continue
|
||||
case '\'':
|
||||
l.ignore()
|
||||
l.stringTerm = string(next)
|
||||
return l.lexString
|
||||
case '"':
|
||||
l.ignore()
|
||||
l.stringTerm = string(next)
|
||||
return l.lexString
|
||||
}
|
||||
|
||||
if isSpace(next) {
|
||||
l.next()
|
||||
l.ignore()
|
||||
continue
|
||||
}
|
||||
|
||||
if isAlphanumeric(next) {
|
||||
return l.lexKey
|
||||
}
|
||||
|
||||
if next == '+' || next == '-' || isDigit(next) {
|
||||
return l.lexNumber
|
||||
}
|
||||
|
||||
if l.next() == eof {
|
||||
break
|
||||
}
|
||||
|
||||
return l.errorf("unexpected char: '%v'", next)
|
||||
}
|
||||
l.emit(tokenEOF)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *queryLexer) lexKey() queryLexStateFn {
|
||||
for {
|
||||
next := l.peek()
|
||||
if !isAlphanumeric(next) {
|
||||
l.emit(tokenKey)
|
||||
return l.lexVoid
|
||||
}
|
||||
|
||||
if l.next() == eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
l.emit(tokenEOF)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *queryLexer) lexString() queryLexStateFn {
|
||||
l.pos++
|
||||
l.ignore()
|
||||
growingString := ""
|
||||
|
||||
for {
|
||||
if l.follow(l.stringTerm) {
|
||||
l.emitWithValue(tokenString, growingString)
|
||||
l.pos++
|
||||
l.ignore()
|
||||
return l.lexVoid
|
||||
}
|
||||
|
||||
if l.follow("\\\"") {
|
||||
l.pos++
|
||||
growingString += "\""
|
||||
} else if l.follow("\\'") {
|
||||
l.pos++
|
||||
growingString += "'"
|
||||
} else if l.follow("\\n") {
|
||||
l.pos++
|
||||
growingString += "\n"
|
||||
} else if l.follow("\\b") {
|
||||
l.pos++
|
||||
growingString += "\b"
|
||||
} else if l.follow("\\f") {
|
||||
l.pos++
|
||||
growingString += "\f"
|
||||
} else if l.follow("\\/") {
|
||||
l.pos++
|
||||
growingString += "/"
|
||||
} else if l.follow("\\t") {
|
||||
l.pos++
|
||||
growingString += "\t"
|
||||
} else if l.follow("\\r") {
|
||||
l.pos++
|
||||
growingString += "\r"
|
||||
} else if l.follow("\\\\") {
|
||||
l.pos++
|
||||
growingString += "\\"
|
||||
} else if l.follow("\\u") {
|
||||
l.pos += 2
|
||||
code := ""
|
||||
for i := 0; i < 4; i++ {
|
||||
c := l.peek()
|
||||
l.pos++
|
||||
if !isHexDigit(c) {
|
||||
return l.errorf("unfinished unicode escape")
|
||||
}
|
||||
code = code + string(c)
|
||||
}
|
||||
l.pos--
|
||||
intcode, err := strconv.ParseInt(code, 16, 32)
|
||||
if err != nil {
|
||||
return l.errorf("invalid unicode escape: \\u" + code)
|
||||
}
|
||||
growingString += string(rune(intcode))
|
||||
} else if l.follow("\\") {
|
||||
l.pos++
|
||||
return l.errorf("invalid escape sequence: \\" + string(l.peek()))
|
||||
} else {
|
||||
growingString += string(l.peek())
|
||||
}
|
||||
|
||||
if l.next() == eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return l.errorf("unclosed string")
|
||||
}
|
||||
|
||||
func (l *queryLexer) lexNumber() queryLexStateFn {
|
||||
l.ignore()
|
||||
if !l.accept("+") {
|
||||
l.accept("-")
|
||||
}
|
||||
pointSeen := false
|
||||
digitSeen := false
|
||||
for {
|
||||
next := l.next()
|
||||
if next == '.' {
|
||||
if pointSeen {
|
||||
return l.errorf("cannot have two dots in one float")
|
||||
}
|
||||
if !isDigit(l.peek()) {
|
||||
return l.errorf("float cannot end with a dot")
|
||||
}
|
||||
pointSeen = true
|
||||
} else if isDigit(next) {
|
||||
digitSeen = true
|
||||
} else {
|
||||
l.backup()
|
||||
break
|
||||
}
|
||||
if pointSeen && !digitSeen {
|
||||
return l.errorf("cannot start float with a dot")
|
||||
}
|
||||
}
|
||||
|
||||
if !digitSeen {
|
||||
return l.errorf("no digit in that number")
|
||||
}
|
||||
if pointSeen {
|
||||
l.emit(tokenFloat)
|
||||
} else {
|
||||
l.emit(tokenInteger)
|
||||
}
|
||||
return l.lexVoid
|
||||
}
|
||||
|
||||
// Entry point
|
||||
func lexQuery(input string) chan token {
|
||||
l := &queryLexer{
|
||||
input: input,
|
||||
tokens: make(chan token),
|
||||
line: 1,
|
||||
col: 1,
|
||||
}
|
||||
go l.run()
|
||||
return l.tokens
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testQLFlow(t *testing.T, input string, expectedFlow []token) {
|
||||
ch := lexQuery(input)
|
||||
for idx, expected := range expectedFlow {
|
||||
token := <-ch
|
||||
if token != expected {
|
||||
t.Log("While testing #", idx, ":", input)
|
||||
t.Log("compared", token, "to", expected)
|
||||
t.Log(token.val, "<->", expected.val)
|
||||
t.Log(token.typ, "<->", expected.typ)
|
||||
t.Log(token.Line, "<->", expected.Line)
|
||||
t.Log(token.Col, "<->", expected.Col)
|
||||
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 TestLexSpecialChars(t *testing.T) {
|
||||
testQLFlow(t, " .$[]..()?*", []token{
|
||||
token{Position{1, 2}, tokenDot, "."},
|
||||
token{Position{1, 3}, tokenDollar, "$"},
|
||||
token{Position{1, 4}, tokenLeftBracket, "["},
|
||||
token{Position{1, 5}, tokenRightBracket, "]"},
|
||||
token{Position{1, 6}, tokenDotDot, ".."},
|
||||
token{Position{1, 8}, tokenLeftParen, "("},
|
||||
token{Position{1, 9}, tokenRightParen, ")"},
|
||||
token{Position{1, 10}, tokenQuestion, "?"},
|
||||
token{Position{1, 11}, tokenStar, "*"},
|
||||
token{Position{1, 12}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestLexString(t *testing.T) {
|
||||
testQLFlow(t, "'foo'", []token{
|
||||
token{Position{1, 2}, tokenString, "foo"},
|
||||
token{Position{1, 6}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestLexDoubleString(t *testing.T) {
|
||||
testQLFlow(t, `"bar"`, []token{
|
||||
token{Position{1, 2}, tokenString, "bar"},
|
||||
token{Position{1, 6}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestLexKey(t *testing.T) {
|
||||
testQLFlow(t, "foo", []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 4}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestLexRecurse(t *testing.T) {
|
||||
testQLFlow(t, "$..*", []token{
|
||||
token{Position{1, 1}, tokenDollar, "$"},
|
||||
token{Position{1, 2}, tokenDotDot, ".."},
|
||||
token{Position{1, 4}, tokenStar, "*"},
|
||||
token{Position{1, 5}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestLexBracketKey(t *testing.T) {
|
||||
testQLFlow(t, "$[foo]", []token{
|
||||
token{Position{1, 1}, tokenDollar, "$"},
|
||||
token{Position{1, 2}, tokenLeftBracket, "["},
|
||||
token{Position{1, 3}, tokenKey, "foo"},
|
||||
token{Position{1, 6}, tokenRightBracket, "]"},
|
||||
token{Position{1, 7}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
|
||||
func TestLexSpace(t *testing.T) {
|
||||
testQLFlow(t, "foo bar baz", []token{
|
||||
token{Position{1, 1}, tokenKey, "foo"},
|
||||
token{Position{1, 5}, tokenKey, "bar"},
|
||||
token{Position{1, 9}, tokenKey, "baz"},
|
||||
token{Position{1, 12}, tokenEOF, ""},
|
||||
})
|
||||
}
|
||||
-275
@@ -1,275 +0,0 @@
|
||||
/*
|
||||
Based on the "jsonpath" spec/concept.
|
||||
|
||||
http://goessner.net/articles/JsonPath/
|
||||
https://code.google.com/p/json-path/
|
||||
*/
|
||||
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
type queryParser struct {
|
||||
flow chan token
|
||||
tokensBuffer []token
|
||||
query *Query
|
||||
union []pathFn
|
||||
err error
|
||||
}
|
||||
|
||||
type queryParserStateFn func() queryParserStateFn
|
||||
|
||||
// Formats and panics an error message based on a token
|
||||
func (p *queryParser) parseError(tok *token, msg string, args ...interface{}) queryParserStateFn {
|
||||
p.err = fmt.Errorf(tok.Position.String()+": "+msg, args...)
|
||||
return nil // trigger parse to end
|
||||
}
|
||||
|
||||
func (p *queryParser) run() {
|
||||
for state := p.parseStart; state != nil; {
|
||||
state = state()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *queryParser) backup(tok *token) {
|
||||
p.tokensBuffer = append(p.tokensBuffer, *tok)
|
||||
}
|
||||
|
||||
func (p *queryParser) peek() *token {
|
||||
if len(p.tokensBuffer) != 0 {
|
||||
return &(p.tokensBuffer[0])
|
||||
}
|
||||
|
||||
tok, ok := <-p.flow
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
p.backup(&tok)
|
||||
return &tok
|
||||
}
|
||||
|
||||
func (p *queryParser) lookahead(types ...tokenType) bool {
|
||||
result := true
|
||||
buffer := []token{}
|
||||
|
||||
for _, typ := range types {
|
||||
tok := p.getToken()
|
||||
if tok == nil {
|
||||
result = false
|
||||
break
|
||||
}
|
||||
buffer = append(buffer, *tok)
|
||||
if tok.typ != typ {
|
||||
result = false
|
||||
break
|
||||
}
|
||||
}
|
||||
// add the tokens back to the buffer, and return
|
||||
p.tokensBuffer = append(p.tokensBuffer, buffer...)
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *queryParser) 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 (p *queryParser) parseStart() queryParserStateFn {
|
||||
tok := p.getToken()
|
||||
|
||||
if tok == nil || tok.typ == tokenEOF {
|
||||
return nil
|
||||
}
|
||||
|
||||
if tok.typ != tokenDollar {
|
||||
return p.parseError(tok, "Expected '$' at start of expression")
|
||||
}
|
||||
|
||||
return p.parseMatchExpr
|
||||
}
|
||||
|
||||
// handle '.' prefix, '[]', and '..'
|
||||
func (p *queryParser) parseMatchExpr() queryParserStateFn {
|
||||
tok := p.getToken()
|
||||
switch tok.typ {
|
||||
case tokenDotDot:
|
||||
p.query.appendPath(&matchRecursiveFn{})
|
||||
// nested parse for '..'
|
||||
tok := p.getToken()
|
||||
switch tok.typ {
|
||||
case tokenKey:
|
||||
p.query.appendPath(newMatchKeyFn(tok.val))
|
||||
return p.parseMatchExpr
|
||||
case tokenLeftBracket:
|
||||
return p.parseBracketExpr
|
||||
case tokenStar:
|
||||
// do nothing - the recursive predicate is enough
|
||||
return p.parseMatchExpr
|
||||
}
|
||||
|
||||
case tokenDot:
|
||||
// nested parse for '.'
|
||||
tok := p.getToken()
|
||||
switch tok.typ {
|
||||
case tokenKey:
|
||||
p.query.appendPath(newMatchKeyFn(tok.val))
|
||||
return p.parseMatchExpr
|
||||
case tokenStar:
|
||||
p.query.appendPath(&matchAnyFn{})
|
||||
return p.parseMatchExpr
|
||||
}
|
||||
|
||||
case tokenLeftBracket:
|
||||
return p.parseBracketExpr
|
||||
|
||||
case tokenEOF:
|
||||
return nil // allow EOF at this stage
|
||||
}
|
||||
return p.parseError(tok, "expected match expression")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *queryParser) parseBracketExpr() queryParserStateFn {
|
||||
if p.lookahead(tokenInteger, tokenColon) {
|
||||
return p.parseSliceExpr
|
||||
}
|
||||
if p.peek().typ == tokenColon {
|
||||
return p.parseSliceExpr
|
||||
}
|
||||
return p.parseUnionExpr
|
||||
}
|
||||
|
||||
func (p *queryParser) parseUnionExpr() queryParserStateFn {
|
||||
var tok *token
|
||||
|
||||
// this state can be traversed after some sub-expressions
|
||||
// so be careful when setting up state in the parser
|
||||
if p.union == nil {
|
||||
p.union = []pathFn{}
|
||||
}
|
||||
|
||||
loop: // labeled loop for easy breaking
|
||||
for {
|
||||
if len(p.union) > 0 {
|
||||
// parse delimiter or terminator
|
||||
tok = p.getToken()
|
||||
switch tok.typ {
|
||||
case tokenComma:
|
||||
// do nothing
|
||||
case tokenRightBracket:
|
||||
break loop
|
||||
default:
|
||||
return p.parseError(tok, "expected ',' or ']', not '%s'", tok.val)
|
||||
}
|
||||
}
|
||||
|
||||
// parse sub expression
|
||||
tok = p.getToken()
|
||||
switch tok.typ {
|
||||
case tokenInteger:
|
||||
p.union = append(p.union, newMatchIndexFn(tok.Int()))
|
||||
case tokenKey:
|
||||
p.union = append(p.union, newMatchKeyFn(tok.val))
|
||||
case tokenString:
|
||||
p.union = append(p.union, newMatchKeyFn(tok.val))
|
||||
case tokenQuestion:
|
||||
return p.parseFilterExpr
|
||||
default:
|
||||
return p.parseError(tok, "expected union sub expression, not '%s', %d", tok.val, len(p.union))
|
||||
}
|
||||
}
|
||||
|
||||
// if there is only one sub-expression, use that instead
|
||||
if len(p.union) == 1 {
|
||||
p.query.appendPath(p.union[0])
|
||||
} else {
|
||||
p.query.appendPath(&matchUnionFn{p.union})
|
||||
}
|
||||
|
||||
p.union = nil // clear out state
|
||||
return p.parseMatchExpr
|
||||
}
|
||||
|
||||
func (p *queryParser) parseSliceExpr() queryParserStateFn {
|
||||
// init slice to grab all elements
|
||||
start, end, step := 0, math.MaxInt64, 1
|
||||
|
||||
// parse optional start
|
||||
tok := p.getToken()
|
||||
if tok.typ == tokenInteger {
|
||||
start = tok.Int()
|
||||
tok = p.getToken()
|
||||
}
|
||||
if tok.typ != tokenColon {
|
||||
return p.parseError(tok, "expected ':'")
|
||||
}
|
||||
|
||||
// parse optional end
|
||||
tok = p.getToken()
|
||||
if tok.typ == tokenInteger {
|
||||
end = tok.Int()
|
||||
tok = p.getToken()
|
||||
}
|
||||
if tok.typ == tokenRightBracket {
|
||||
p.query.appendPath(newMatchSliceFn(start, end, step))
|
||||
return p.parseMatchExpr
|
||||
}
|
||||
if tok.typ != tokenColon {
|
||||
return p.parseError(tok, "expected ']' or ':'")
|
||||
}
|
||||
|
||||
// parse optional step
|
||||
tok = p.getToken()
|
||||
if tok.typ == tokenInteger {
|
||||
step = tok.Int()
|
||||
if step < 0 {
|
||||
return p.parseError(tok, "step must be a positive value")
|
||||
}
|
||||
tok = p.getToken()
|
||||
}
|
||||
if tok.typ != tokenRightBracket {
|
||||
return p.parseError(tok, "expected ']'")
|
||||
}
|
||||
|
||||
p.query.appendPath(newMatchSliceFn(start, end, step))
|
||||
return p.parseMatchExpr
|
||||
}
|
||||
|
||||
func (p *queryParser) parseFilterExpr() queryParserStateFn {
|
||||
tok := p.getToken()
|
||||
if tok.typ != tokenLeftParen {
|
||||
return p.parseError(tok, "expected left-parenthesis for filter expression")
|
||||
}
|
||||
tok = p.getToken()
|
||||
if tok.typ != tokenKey && tok.typ != tokenString {
|
||||
return p.parseError(tok, "expected key or string for filter funciton name")
|
||||
}
|
||||
name := tok.val
|
||||
tok = p.getToken()
|
||||
if tok.typ != tokenRightParen {
|
||||
return p.parseError(tok, "expected right-parenthesis for filter expression")
|
||||
}
|
||||
p.union = append(p.union, newMatchFilterFn(name, tok.Position))
|
||||
return p.parseUnionExpr
|
||||
}
|
||||
|
||||
func parseQuery(flow chan token) (*Query, error) {
|
||||
parser := &queryParser{
|
||||
flow: flow,
|
||||
tokensBuffer: []token{},
|
||||
query: newQuery(),
|
||||
}
|
||||
parser.run()
|
||||
return parser.query, parser.err
|
||||
}
|
||||
@@ -1,483 +0,0 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type queryTestNode struct {
|
||||
value interface{}
|
||||
position Position
|
||||
}
|
||||
|
||||
func valueString(root interface{}) string {
|
||||
result := "" //fmt.Sprintf("%T:", root)
|
||||
switch node := root.(type) {
|
||||
case *tomlValue:
|
||||
return valueString(node.value)
|
||||
case *QueryResult:
|
||||
items := []string{}
|
||||
for i, v := range node.Values() {
|
||||
items = append(items, fmt.Sprintf("%s:%s",
|
||||
node.Positions()[i].String(), valueString(v)))
|
||||
}
|
||||
sort.Strings(items)
|
||||
result = "[" + strings.Join(items, ", ") + "]"
|
||||
case queryTestNode:
|
||||
result = fmt.Sprintf("%s:%s",
|
||||
node.position.String(), valueString(node.value))
|
||||
case []interface{}:
|
||||
items := []string{}
|
||||
for _, v := range node {
|
||||
items = append(items, valueString(v))
|
||||
}
|
||||
sort.Strings(items)
|
||||
result = "[" + strings.Join(items, ", ") + "]"
|
||||
case *TomlTree:
|
||||
// workaround for unreliable map key ordering
|
||||
items := []string{}
|
||||
for _, k := range node.Keys() {
|
||||
v := node.GetPath([]string{k})
|
||||
items = append(items, k+":"+valueString(v))
|
||||
}
|
||||
sort.Strings(items)
|
||||
result = "{" + strings.Join(items, ", ") + "}"
|
||||
case map[string]interface{}:
|
||||
// workaround for unreliable map key ordering
|
||||
items := []string{}
|
||||
for k, v := range node {
|
||||
items = append(items, k+":"+valueString(v))
|
||||
}
|
||||
sort.Strings(items)
|
||||
result = "{" + strings.Join(items, ", ") + "}"
|
||||
case int64:
|
||||
result += fmt.Sprintf("%d", node)
|
||||
case string:
|
||||
result += "'" + node + "'"
|
||||
case float64:
|
||||
result += fmt.Sprintf("%f", node)
|
||||
case bool:
|
||||
result += fmt.Sprintf("%t", node)
|
||||
case time.Time:
|
||||
result += fmt.Sprintf("'%v'", node)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func assertValue(t *testing.T, result, ref interface{}) {
|
||||
pathStr := valueString(result)
|
||||
refStr := valueString(ref)
|
||||
if pathStr != refStr {
|
||||
t.Errorf("values do not match")
|
||||
t.Log("test:", pathStr)
|
||||
t.Log("ref: ", refStr)
|
||||
}
|
||||
}
|
||||
|
||||
func assertQueryPositions(t *testing.T, toml, query string, ref []interface{}) {
|
||||
tree, err := Load(toml)
|
||||
if err != nil {
|
||||
t.Errorf("Non-nil toml parse error: %v", err)
|
||||
return
|
||||
}
|
||||
q, err := CompileQuery(query)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
results := q.Execute(tree)
|
||||
assertValue(t, results, ref)
|
||||
}
|
||||
|
||||
func TestQueryRoot(t *testing.T) {
|
||||
assertQueryPositions(t,
|
||||
"a = 42",
|
||||
"$",
|
||||
[]interface{}{
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"a": int64(42),
|
||||
}, Position{1, 1},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueryKey(t *testing.T) {
|
||||
assertQueryPositions(t,
|
||||
"[foo]\na = 42",
|
||||
"$.foo.a",
|
||||
[]interface{}{
|
||||
queryTestNode{
|
||||
int64(42), Position{2, 1},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueryKeyString(t *testing.T) {
|
||||
assertQueryPositions(t,
|
||||
"[foo]\na = 42",
|
||||
"$.foo['a']",
|
||||
[]interface{}{
|
||||
queryTestNode{
|
||||
int64(42), Position{2, 1},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueryIndex(t *testing.T) {
|
||||
assertQueryPositions(t,
|
||||
"[foo]\na = [1,2,3,4,5,6,7,8,9,0]",
|
||||
"$.foo.a[5]",
|
||||
[]interface{}{
|
||||
queryTestNode{
|
||||
int64(6), Position{2, 1},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestQuerySliceRange(t *testing.T) {
|
||||
assertQueryPositions(t,
|
||||
"[foo]\na = [1,2,3,4,5,6,7,8,9,0]",
|
||||
"$.foo.a[0:5]",
|
||||
[]interface{}{
|
||||
queryTestNode{
|
||||
int64(1), Position{2, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(2), Position{2, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(3), Position{2, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(4), Position{2, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(5), Position{2, 1},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestQuerySliceStep(t *testing.T) {
|
||||
assertQueryPositions(t,
|
||||
"[foo]\na = [1,2,3,4,5,6,7,8,9,0]",
|
||||
"$.foo.a[0:5:2]",
|
||||
[]interface{}{
|
||||
queryTestNode{
|
||||
int64(1), Position{2, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(3), Position{2, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(5), Position{2, 1},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueryAny(t *testing.T) {
|
||||
assertQueryPositions(t,
|
||||
"[foo.bar]\na=1\nb=2\n[foo.baz]\na=3\nb=4",
|
||||
"$.foo.*",
|
||||
[]interface{}{
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"a": int64(1),
|
||||
"b": int64(2),
|
||||
}, Position{1, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"a": int64(3),
|
||||
"b": int64(4),
|
||||
}, Position{4, 1},
|
||||
},
|
||||
})
|
||||
}
|
||||
func TestQueryUnionSimple(t *testing.T) {
|
||||
assertQueryPositions(t,
|
||||
"[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6",
|
||||
"$.*[bar,foo]",
|
||||
[]interface{}{
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"a": int64(1),
|
||||
"b": int64(2),
|
||||
}, Position{1, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"a": int64(3),
|
||||
"b": int64(4),
|
||||
}, Position{4, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"a": int64(5),
|
||||
"b": int64(6),
|
||||
}, Position{7, 1},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueryRecursionAll(t *testing.T) {
|
||||
assertQueryPositions(t,
|
||||
"[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6",
|
||||
"$..*",
|
||||
[]interface{}{
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"bar": map[string]interface{}{
|
||||
"a": int64(1),
|
||||
"b": int64(2),
|
||||
},
|
||||
},
|
||||
"baz": map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"a": int64(3),
|
||||
"b": int64(4),
|
||||
},
|
||||
},
|
||||
"gorf": map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"a": int64(5),
|
||||
"b": int64(6),
|
||||
},
|
||||
},
|
||||
}, Position{1, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"bar": map[string]interface{}{
|
||||
"a": int64(1),
|
||||
"b": int64(2),
|
||||
},
|
||||
}, Position{1, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"a": int64(1),
|
||||
"b": int64(2),
|
||||
}, Position{1, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(1), Position{2, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(2), Position{3, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"a": int64(3),
|
||||
"b": int64(4),
|
||||
},
|
||||
}, Position{4, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"a": int64(3),
|
||||
"b": int64(4),
|
||||
}, Position{4, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(3), Position{5, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(4), Position{6, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"a": int64(5),
|
||||
"b": int64(6),
|
||||
},
|
||||
}, Position{7, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"a": int64(5),
|
||||
"b": int64(6),
|
||||
}, Position{7, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(5), Position{8, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(6), Position{9, 1},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueryRecursionUnionSimple(t *testing.T) {
|
||||
assertQueryPositions(t,
|
||||
"[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6",
|
||||
"$..['foo','bar']",
|
||||
[]interface{}{
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"bar": map[string]interface{}{
|
||||
"a": int64(1),
|
||||
"b": int64(2),
|
||||
},
|
||||
}, Position{1, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"a": int64(3),
|
||||
"b": int64(4),
|
||||
}, Position{4, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"a": int64(1),
|
||||
"b": int64(2),
|
||||
}, Position{1, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"a": int64(5),
|
||||
"b": int64(6),
|
||||
}, Position{7, 1},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueryFilterFn(t *testing.T) {
|
||||
buff, err := ioutil.ReadFile("example.toml")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
assertQueryPositions(t, string(buff),
|
||||
"$..[?(int)]",
|
||||
[]interface{}{
|
||||
queryTestNode{
|
||||
int64(8001), Position{13, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(8001), Position{13, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(8002), Position{13, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(5000), Position{14, 1},
|
||||
},
|
||||
})
|
||||
|
||||
assertQueryPositions(t, string(buff),
|
||||
"$..[?(string)]",
|
||||
[]interface{}{
|
||||
queryTestNode{
|
||||
"TOML Example", Position{3, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
"Tom Preston-Werner", Position{6, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
"GitHub", Position{7, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
"GitHub Cofounder & CEO\nLikes tater tots and beer.",
|
||||
Position{8, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
"192.168.1.1", Position{12, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
"10.0.0.1", Position{21, 3},
|
||||
},
|
||||
queryTestNode{
|
||||
"eqdc10", Position{22, 3},
|
||||
},
|
||||
queryTestNode{
|
||||
"10.0.0.2", Position{25, 3},
|
||||
},
|
||||
queryTestNode{
|
||||
"eqdc10", Position{26, 3},
|
||||
},
|
||||
})
|
||||
|
||||
assertQueryPositions(t, string(buff),
|
||||
"$..[?(float)]",
|
||||
[]interface{}{
|
||||
// no float values in document
|
||||
})
|
||||
|
||||
tv, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z")
|
||||
assertQueryPositions(t, string(buff),
|
||||
"$..[?(tree)]",
|
||||
[]interface{}{
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"name": "Tom Preston-Werner",
|
||||
"organization": "GitHub",
|
||||
"bio": "GitHub Cofounder & CEO\nLikes tater tots and beer.",
|
||||
"dob": tv,
|
||||
}, Position{5, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"server": "192.168.1.1",
|
||||
"ports": []interface{}{int64(8001), int64(8001), int64(8002)},
|
||||
"connection_max": int64(5000),
|
||||
"enabled": true,
|
||||
}, Position{11, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"alpha": map[string]interface{}{
|
||||
"ip": "10.0.0.1",
|
||||
"dc": "eqdc10",
|
||||
},
|
||||
"beta": map[string]interface{}{
|
||||
"ip": "10.0.0.2",
|
||||
"dc": "eqdc10",
|
||||
},
|
||||
}, Position{17, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"ip": "10.0.0.1",
|
||||
"dc": "eqdc10",
|
||||
}, Position{20, 3},
|
||||
},
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"ip": "10.0.0.2",
|
||||
"dc": "eqdc10",
|
||||
}, Position{24, 3},
|
||||
},
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"data": []interface{}{
|
||||
[]interface{}{"gamma", "delta"},
|
||||
[]interface{}{int64(1), int64(2)},
|
||||
},
|
||||
}, Position{28, 1},
|
||||
},
|
||||
})
|
||||
|
||||
assertQueryPositions(t, string(buff),
|
||||
"$..[?(time)]",
|
||||
[]interface{}{
|
||||
queryTestNode{
|
||||
tv, Position{9, 1},
|
||||
},
|
||||
})
|
||||
|
||||
assertQueryPositions(t, string(buff),
|
||||
"$..[?(bool)]",
|
||||
[]interface{}{
|
||||
queryTestNode{
|
||||
true, Position{15, 1},
|
||||
},
|
||||
})
|
||||
}
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
package toml
|
||||
|
||||
func scanFollows(b []byte, pattern string) bool {
|
||||
n := len(pattern)
|
||||
|
||||
return len(b) >= n && string(b[:n]) == pattern
|
||||
}
|
||||
|
||||
func scanFollowsMultilineBasicStringDelimiter(b []byte) bool {
|
||||
return scanFollows(b, `"""`)
|
||||
}
|
||||
|
||||
func scanFollowsMultilineLiteralStringDelimiter(b []byte) bool {
|
||||
return scanFollows(b, `'''`)
|
||||
}
|
||||
|
||||
func scanFollowsTrue(b []byte) bool {
|
||||
return scanFollows(b, `true`)
|
||||
}
|
||||
|
||||
func scanFollowsFalse(b []byte) bool {
|
||||
return scanFollows(b, `false`)
|
||||
}
|
||||
|
||||
func scanFollowsInf(b []byte) bool {
|
||||
return scanFollows(b, `inf`)
|
||||
}
|
||||
|
||||
func scanFollowsNan(b []byte) bool {
|
||||
return scanFollows(b, `nan`)
|
||||
}
|
||||
|
||||
func scanUnquotedKey(b []byte) ([]byte, []byte) {
|
||||
// unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
|
||||
for i := 0; i < len(b); i++ {
|
||||
if !isUnquotedKeyChar(b[i]) {
|
||||
return b[:i], b[i:]
|
||||
}
|
||||
}
|
||||
|
||||
return b, b[len(b):]
|
||||
}
|
||||
|
||||
func isUnquotedKeyChar(r byte) bool {
|
||||
return (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_'
|
||||
}
|
||||
|
||||
func scanLiteralString(b []byte) ([]byte, []byte, error) {
|
||||
// literal-string = apostrophe *literal-char apostrophe
|
||||
// apostrophe = %x27 ; ' apostrophe
|
||||
// literal-char = %x09 / %x20-26 / %x28-7E / non-ascii
|
||||
for i := 1; i < len(b); {
|
||||
switch b[i] {
|
||||
case '\'':
|
||||
return b[:i+1], b[i+1:], nil
|
||||
case '\n', '\r':
|
||||
return nil, b[i+1:], newDecodeError(b[i:i+1], "literal strings cannot have new lines")
|
||||
}
|
||||
size := utf8ValidNext(b[i:])
|
||||
if size == 0 {
|
||||
return nil, b[i+1:], newDecodeError(b[i:i+1], "invalid character")
|
||||
}
|
||||
i += size
|
||||
}
|
||||
|
||||
return nil, b[len(b):], newDecodeError(b[len(b):], "unterminated literal string")
|
||||
}
|
||||
|
||||
func scanMultilineLiteralString(b []byte) ([]byte, []byte, error) {
|
||||
// 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
|
||||
for i := 3; i < len(b); {
|
||||
switch b[i] {
|
||||
case '\'':
|
||||
if scanFollowsMultilineLiteralStringDelimiter(b[i:]) {
|
||||
i += 3
|
||||
|
||||
// At that point we found 3 apostrophe, and i is the
|
||||
// index of the byte after the third one. The scanner
|
||||
// needs to be eager, because there can be an extra 2
|
||||
// apostrophe that can be accepted at the end of the
|
||||
// string.
|
||||
|
||||
if i >= len(b) || b[i] != '\'' {
|
||||
return b[:i], b[i:], nil
|
||||
}
|
||||
i++
|
||||
|
||||
if i >= len(b) || b[i] != '\'' {
|
||||
return b[:i], b[i:], nil
|
||||
}
|
||||
i++
|
||||
|
||||
if i < len(b) && b[i] == '\'' {
|
||||
return nil, b[i:], newDecodeError(b[i-3:i+1], "''' not allowed in multiline literal string")
|
||||
}
|
||||
|
||||
return b[:i], b[i:], nil
|
||||
}
|
||||
case '\r':
|
||||
if len(b) < i+2 {
|
||||
return nil, b[i:], newDecodeError(b[len(b):], `need a \n after \r`)
|
||||
}
|
||||
if b[i+1] != '\n' {
|
||||
return nil, b[i+2:], newDecodeError(b[i:i+2], `need a \n after \r`)
|
||||
}
|
||||
i += 2 // skip the \n
|
||||
continue
|
||||
}
|
||||
size := utf8ValidNext(b[i:])
|
||||
if size == 0 {
|
||||
return nil, b[i:], newDecodeError(b[i:i+1], "invalid character")
|
||||
}
|
||||
i += size
|
||||
}
|
||||
|
||||
return nil, b[len(b):], newDecodeError(b[len(b):], `multiline literal string not terminated by '''`)
|
||||
}
|
||||
|
||||
func scanWindowsNewline(b []byte) ([]byte, []byte, error) {
|
||||
const lenCRLF = 2
|
||||
if len(b) < lenCRLF {
|
||||
return nil, b, newDecodeError(b, "windows new line expected")
|
||||
}
|
||||
|
||||
if b[1] != '\n' {
|
||||
return nil, b[2:], newDecodeError(b, `windows new line should be \r\n`)
|
||||
}
|
||||
|
||||
return b[:lenCRLF], b[lenCRLF:], nil
|
||||
}
|
||||
|
||||
func scanWhitespace(b []byte) ([]byte, []byte) {
|
||||
for i := 0; i < len(b); i++ {
|
||||
switch b[i] {
|
||||
case ' ', '\t':
|
||||
continue
|
||||
default:
|
||||
return b[:i], b[i:]
|
||||
}
|
||||
}
|
||||
|
||||
return b, b[len(b):]
|
||||
}
|
||||
|
||||
//nolint:unparam
|
||||
func scanComment(b []byte) ([]byte, []byte, error) {
|
||||
// comment-start-symbol = %x23 ; #
|
||||
// non-ascii = %x80-D7FF / %xE000-10FFFF
|
||||
// non-eol = %x09 / %x20-7F / non-ascii
|
||||
//
|
||||
// comment = comment-start-symbol *non-eol
|
||||
|
||||
for i := 1; i < len(b); {
|
||||
if b[i] == '\n' {
|
||||
return b[:i], b[i:], nil
|
||||
}
|
||||
if b[i] == '\r' {
|
||||
if i+1 < len(b) && b[i+1] == '\n' {
|
||||
return b[:i+1], b[i+1:], nil
|
||||
}
|
||||
return nil, nil, newDecodeError(b[i:i+1], "invalid character in comment")
|
||||
}
|
||||
size := utf8ValidNext(b[i:])
|
||||
if size == 0 {
|
||||
return nil, b[i+1:], newDecodeError(b[i:i+1], "invalid character in comment")
|
||||
}
|
||||
|
||||
i += size
|
||||
}
|
||||
|
||||
return b, b[len(b):], nil
|
||||
}
|
||||
|
||||
func scanBasicString(b []byte) ([]byte, bool, []byte, error) {
|
||||
// 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
|
||||
escaped := false
|
||||
i := 1
|
||||
|
||||
for ; i < len(b); i++ {
|
||||
switch b[i] {
|
||||
case '"':
|
||||
return b[:i+1], escaped, b[i+1:], nil
|
||||
case '\n', '\r':
|
||||
return nil, escaped, b[i+1:], newDecodeError(b[i:i+1], "basic strings cannot have new lines")
|
||||
case '\\':
|
||||
if len(b) < i+2 {
|
||||
return nil, escaped, b[i+1:], newDecodeError(b[i:i+1], "need a character after \\")
|
||||
}
|
||||
escaped = true
|
||||
i++ // skip the next character
|
||||
}
|
||||
}
|
||||
|
||||
return nil, escaped, b[len(b):], newDecodeError(b[len(b):], `basic string not terminated by "`)
|
||||
}
|
||||
|
||||
func scanMultilineBasicString(b []byte) ([]byte, bool, []byte, error) {
|
||||
// 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 )
|
||||
|
||||
escaped := false
|
||||
i := 3
|
||||
|
||||
for ; i < len(b); i++ {
|
||||
switch b[i] {
|
||||
case '"':
|
||||
if scanFollowsMultilineBasicStringDelimiter(b[i:]) {
|
||||
i += 3
|
||||
|
||||
// At that point we found 3 apostrophe, and i is the
|
||||
// index of the byte after the third one. The scanner
|
||||
// needs to be eager, because there can be an extra 2
|
||||
// apostrophe that can be accepted at the end of the
|
||||
// string.
|
||||
|
||||
if i >= len(b) || b[i] != '"' {
|
||||
return b[:i], escaped, b[i:], nil
|
||||
}
|
||||
i++
|
||||
|
||||
if i >= len(b) || b[i] != '"' {
|
||||
return b[:i], escaped, b[i:], nil
|
||||
}
|
||||
i++
|
||||
|
||||
if i < len(b) && b[i] == '"' {
|
||||
return nil, escaped, b[i+1:], newDecodeError(b[i-3:i+1], `""" not allowed in multiline basic string`)
|
||||
}
|
||||
|
||||
return b[:i], escaped, b[i:], nil
|
||||
}
|
||||
case '\\':
|
||||
if len(b) < i+2 {
|
||||
return nil, escaped, b[len(b):], newDecodeError(b[len(b):], "need a character after \\")
|
||||
}
|
||||
escaped = true
|
||||
i++ // skip the next character
|
||||
case '\r':
|
||||
if len(b) < i+2 {
|
||||
return nil, escaped, b[len(b):], newDecodeError(b[len(b):], `need a \n after \r`)
|
||||
}
|
||||
if b[i+1] != '\n' {
|
||||
return nil, escaped, b[i+2:], newDecodeError(b[i:i+2], `need a \n after \r`)
|
||||
}
|
||||
i++ // skip the \n
|
||||
}
|
||||
}
|
||||
|
||||
return nil, escaped, b[len(b):], newDecodeError(b[len(b):], `multiline basic string not terminated by """`)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"github.com/pelletier/go-toml/v2/internal/ast"
|
||||
"github.com/pelletier/go-toml/v2/internal/danger"
|
||||
"github.com/pelletier/go-toml/v2/internal/tracker"
|
||||
)
|
||||
|
||||
type strict struct {
|
||||
Enabled bool
|
||||
|
||||
// Tracks the current key being processed.
|
||||
key tracker.KeyTracker
|
||||
|
||||
missing []decodeError
|
||||
}
|
||||
|
||||
func (s *strict) EnterTable(node *ast.Node) {
|
||||
if !s.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
s.key.UpdateTable(node)
|
||||
}
|
||||
|
||||
func (s *strict) EnterArrayTable(node *ast.Node) {
|
||||
if !s.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
s.key.UpdateArrayTable(node)
|
||||
}
|
||||
|
||||
func (s *strict) EnterKeyValue(node *ast.Node) {
|
||||
if !s.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
s.key.Push(node)
|
||||
}
|
||||
|
||||
func (s *strict) ExitKeyValue(node *ast.Node) {
|
||||
if !s.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
s.key.Pop(node)
|
||||
}
|
||||
|
||||
func (s *strict) MissingTable(node *ast.Node) {
|
||||
if !s.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
s.missing = append(s.missing, decodeError{
|
||||
highlight: keyLocation(node),
|
||||
message: "missing table",
|
||||
key: s.key.Key(),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *strict) MissingField(node *ast.Node) {
|
||||
if !s.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
s.missing = append(s.missing, decodeError{
|
||||
highlight: 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 keyLocation(node *ast.Node) []byte {
|
||||
k := node.Key()
|
||||
|
||||
hasOne := k.Next()
|
||||
if !hasOne {
|
||||
panic("should not be called with empty key")
|
||||
}
|
||||
|
||||
start := k.Node().Data
|
||||
end := k.Node().Data
|
||||
|
||||
for k.Next() {
|
||||
end = k.Node().Data
|
||||
}
|
||||
|
||||
return danger.BytesRange(start, end)
|
||||
}
|
||||
@@ -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")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user