Compare commits
1053 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c400ed7908 | |||
| 79dafa38f0 | |||
| 5b1169f9a5 | |||
| 16b5e0767c | |||
| 7739c09163 | |||
| 16afcff09c | |||
| 5aca8a1fe8 | |||
| 5a6928750c | |||
| 15965e7a77 | |||
| 6a7da08002 | |||
| 9049f90cc5 | |||
| 2d86f4f0c0 | |||
| 273dd45128 | |||
| 3d0d7ef180 | |||
| 117f685939 | |||
| d849a87205 | |||
| 6a4162b902 | |||
| f90b4d6498 | |||
| fbadb99634 | |||
| 508e93d11a | |||
| f3fbbac0d9 | |||
| 717f9f2abc | |||
| c3cc251242 | |||
| 2885f7b571 | |||
| a1f25e4138 | |||
| b9ae602399 | |||
| a82279c6a4 | |||
| 895c0e977f | |||
| e6e06d007b | |||
| f08182fb94 | |||
| cc7d4fb972 | |||
| e67c858d41 | |||
| 478a962f34 | |||
| f3e2c6b1bb | |||
| b8b4188af7 | |||
| 5f8d55e2bb | |||
| df46340af2 | |||
| 82fb611c74 | |||
| 891323d041 | |||
| bf61527363 | |||
| e0f5493daa | |||
| 5971037362 | |||
| cde9e354c5 | |||
| 8e8fa92e77 | |||
| 7c0fa4075f | |||
| 9c8a0449ca | |||
| 90c804f6e6 | |||
| 3ae8b9f1b1 | |||
| 1a5b632b90 | |||
| 143311270c | |||
| 03597c43c7 | |||
| 6220564e33 | |||
| 70fdfbd591 | |||
| 5bd978d2a1 | |||
| ca20aff43d | |||
| 870082c12f | |||
| 2c9ff3b014 | |||
| 95af089778 | |||
| 92309041de | |||
| 13d622d57c | |||
| 71676cd715 | |||
| 9c03958791 | |||
| 9be5d6f14c | |||
| 77e035bff0 | |||
| 71b0754679 | |||
| 5e6330c75b | |||
| 00257e2851 | |||
| 90d1948a2b | |||
| 18ba32e8cf | |||
| bcd03b729a | |||
| 1edc3b57dd | |||
| 9671600fff | |||
| 1a7656d076 | |||
| 780586ad68 | |||
| 90ffcffcd7 | |||
| adba2510db | |||
| 81c7ff6737 | |||
| 5b2bb7e681 | |||
| 9528ecd8b0 | |||
| deb42df0e2 | |||
| 1b72c7a302 | |||
| 35f1eeffb2 | |||
| 35487cd36d | |||
| 9b7c3f0dc2 | |||
| ab3359be41 | |||
| 87bec6f73f | |||
| 27180c661f | |||
| 66cd1e0605 | |||
| 1f9b201633 | |||
| 7eb3a334d7 | |||
| 1c24ab8da3 | |||
| 5cfa89c8ab | |||
| d3d9547876 | |||
| 64e99927f5 | |||
| 7eb95a0de1 | |||
| 2132bde2ba | |||
| af49b83fa9 | |||
| c42c5812fc | |||
| 52a28c390c | |||
| b962b71f06 | |||
| 7f5a0cb63c | |||
| 9360447254 | |||
| 66b5d699fa | |||
| 121f9549f2 | |||
| 63a7ac156e | |||
| 5db699469b | |||
| 99ba0b30ee | |||
| 88cea644b6 | |||
| b64b73cb9a | |||
| 2f8fffd237 | |||
| 33ddb49127 | |||
| 5b1f80c350 | |||
| e8f5fd47d5 | |||
| ad1dfac447 | |||
| d157e4526a | |||
| f89540e973 | |||
| 230faf58a8 | |||
| ec865cfe53 | |||
| d242466859 | |||
| d4dee46086 | |||
| 203486a9d3 | |||
| 26f3c95031 | |||
| cc1c3bbc7d | |||
| 6a62840373 | |||
| 89cafb1a21 | |||
| 3750770735 | |||
| 7b70530a8a | |||
| 29a76d5d9a | |||
| 165b67e843 | |||
| 6e2555aee6 | |||
| 7f7d260f83 | |||
| 4caea56663 | |||
| e6e7b804e1 | |||
| 854d4d69ae | |||
| 97ff2dfaa1 | |||
| 949d2709da | |||
| 3da487f8ea | |||
| 2ddba2ce96 | |||
| 50e1a9fb22 | |||
| e8b7d95559 | |||
| d79be8b50a | |||
| b8eba1eb35 | |||
| 3368f085fd | |||
| bf1becbb59 | |||
| 37b1e370b9 | |||
| fcd7ebf736 | |||
| ce2da330f7 | |||
| 13e3de39fe | |||
| 15477e989f | |||
| a6d9623383 | |||
| 55d503317d | |||
| 2cb124b8a7 | |||
| aafcbd9025 | |||
| 498f81075e | |||
| 3c1af91433 | |||
| c783d4af23 | |||
| 642fe3a506 | |||
| 2e56af5d63 | |||
| c320344fac | |||
| 7d5b32fe3f | |||
| 8d309cd827 | |||
| f663e1af1e | |||
| 3744b60f7f | |||
| 687a3d37bf | |||
| 22ca7caa40 | |||
| 3e12849f65 | |||
| dc32e24188 | |||
| b08b3e94d5 | |||
| c597d0a2e0 | |||
| 24a30161f7 | |||
| 65ccc132ae | |||
| 9558472c96 | |||
| fa734fda3c | |||
| 30a4445a47 | |||
| 265277953c | |||
| edec28f58f | |||
| 0243e8d42d | |||
| 18a264db59 | |||
| fd6d80e2c2 | |||
| 3d62683c69 | |||
| f30bdc1380 | |||
| 5d85944359 | |||
| b9d1af6565 | |||
| 51c0bd518e | |||
| 00f31e6f59 | |||
| c9ad86a2dd | |||
| 1c342bc2af | |||
| a318dc7f42 | |||
| 32e01565b4 | |||
| c032cb779c | |||
| b49e0cac6e | |||
| 71b17d104b | |||
| 4f369b961c | |||
| 6d285249e4 | |||
| 825b01408b | |||
| 5945a32bc3 | |||
| ae5a4e5a6c | |||
| 0d7aee50a2 | |||
| 8338f0a5e3 | |||
| 588cb328bf | |||
| d74c0ed7eb | |||
| f81aa1a56e | |||
| 5a010952d6 | |||
| 75c8b74cdc | |||
| 8989cae9d9 | |||
| 2bd04b528a | |||
| 76277ed61a | |||
| 24047b731e | |||
| f19f303275 | |||
| 6a049895a0 | |||
| ebc7a2651f | |||
| c00504af8c | |||
| 4e63044c03 | |||
| 78bbc27fea | |||
| 5c9b85e362 | |||
| dc03021a5d | |||
| 0002efb9d4 | |||
| ee4b40bd25 | |||
| 9286bf82f5 | |||
| a1bfde3e41 | |||
| bad74f95cd | |||
| 221ce82e43 | |||
| 48e2b1ce9f | |||
| 5c0ac105b4 | |||
| 9baa77c752 | |||
| 295347a455 | |||
| 88e9dd9fc9 | |||
| 357a2a276d | |||
| 332b99ad88 | |||
| f964816830 | |||
| 8b5ed36bca | |||
| 68c4313b7a | |||
| 31c2f2651d | |||
| c337dc4436 | |||
| 2c2cbbb633 | |||
| bd01072ad4 | |||
| 71c5039f04 | |||
| 9e392d02fd | |||
| 8a8c915679 | |||
| 2b1f179df6 | |||
| 5ffe44cd26 | |||
| 8f1cd76bf1 | |||
| dd09d7ae82 | |||
| 66509abf1e | |||
| c68f571867 | |||
| d00a2cf2e3 | |||
| 97e72988b1 | |||
| ba2ae41ffe | |||
| 924ba07c1c | |||
| 3dfaa8d0fa | |||
| 24a4b6ed24 | |||
| ff0143df11 | |||
| f036f859d6 | |||
| 1e159fdf3a | |||
| 07a0498c5e | |||
| 825030b80d | |||
| ee236d7955 | |||
| 052a52fc7a | |||
| 5f8981b14f | |||
| 2541b9346a | |||
| 57bd06c16f | |||
| c8193da3d0 | |||
| d8acc9f3f3 | |||
| 49736bd2b6 | |||
| 0b8dea0d04 | |||
| 5bc637f23f | |||
| 5d2d01e235 | |||
| 6b26b18104 | |||
| 1a022dbd79 | |||
| 43c56b20b9 | |||
| 16a77ef0e4 | |||
| 1c358d7b16 | |||
| a167ac7d65 | |||
| 89f0b0d240 | |||
| e63efe327f | |||
| 91b65e3ded | |||
| 4930a2ac8d | |||
| f76008743e | |||
| 27608a5b22 | |||
| 3e932d5e5f | |||
| 75184b44cb | |||
| 0de54c3b15 | |||
| e5babf413d | |||
| 58a7b16a7f | |||
| 83b4b0bdf0 | |||
| b9c1a28a61 | |||
| 2bf043aefc | |||
| b68ec83f29 | |||
| 524d94b5ae | |||
| e256150544 | |||
| 5fe7bce243 | |||
| 01289c99ae | |||
| 0dbf3e545e | |||
| 0e35752ac6 | |||
| f6e2949267 | |||
| ad599675d1 | |||
| c2147d2282 | |||
| 2431edeab0 | |||
| ab8f375ae4 | |||
| ae5b95e88a | |||
| 32ed661a37 | |||
| 822c8c0a5d | |||
| 9aec575dca | |||
| fcec9c4a2a | |||
| 097fae4cd2 | |||
| 63e985a53d | |||
| 6c8ddeb3ac | |||
| 9f96eb5a02 | |||
| 5e9b60a88f | |||
| b71d8a2ee5 | |||
| 0e65a202cd | |||
| 10dbd471fb | |||
| 451242bbf9 | |||
| 562896382d | |||
| a840ab06a4 | |||
| f79e51a435 | |||
| d6725be065 | |||
| 09fcff7910 | |||
| c8927673f4 | |||
| 63bc750355 | |||
| ac6158e841 | |||
| 1419673b96 | |||
| ee25212618 | |||
| a7101394f5 | |||
| f7d9e3fb71 | |||
| f50c52e174 | |||
| f272c5b025 | |||
| ac44a2f89b | |||
| 94e3eabbcf | |||
| 0e3c986801 | |||
| d0886e4795 | |||
| 63223c6d63 | |||
| 98bb73f096 | |||
| dfa7271850 | |||
| e46753aa73 | |||
| 38f6100cc3 | |||
| ee9b6ca971 | |||
| 1a684c072d | |||
| fb3fb16deb | |||
| f07764bce1 | |||
| c814d36985 | |||
| 93c96968dc | |||
| 44b6f1ef89 | |||
| b726edd7cb | |||
| 0293e800ae | |||
| fd7101dfbc | |||
| 3a29bdb1f0 | |||
| 977431b1e3 | |||
| dc9d962690 | |||
| f0f81b066e | |||
| 03e00cc0a2 | |||
| 791d76923c | |||
| ce6337e829 | |||
| bcc6858922 | |||
| 338358c70e | |||
| b0cd3fea16 | |||
| 69703dec91 | |||
| 0993d86f22 | |||
| cd7f0a04dc | |||
| cdc82a99c6 | |||
| 34a0fb3a18 | |||
| 54f68735df | |||
| 8cd602c31a | |||
| 964f04c7c8 | |||
| a9c59ba6b7 | |||
| 36329f1328 | |||
| 29a5314f60 | |||
| 707cd6b203 | |||
| 05d9fcd8c1 | |||
| 880621f6bb | |||
| d7613bd7c7 | |||
| 449e8e3233 | |||
| 15b94b2718 | |||
| 54cb52b483 | |||
| 8ffaa31e39 | |||
| 21a4ed1795 | |||
| f3e71522ae | |||
| bd1dcb7fc8 | |||
| 68c896b916 | |||
| ad28f6621c | |||
| a256645fdb | |||
| 533258e662 | |||
| 7ce34ba1fc | |||
| c55b296385 | |||
| 643efc8650 | |||
| 4f484cd780 | |||
| 679bd43c7c | |||
| a229ae90a2 | |||
| dd2fe61b13 | |||
| 835cdb57d4 | |||
| e8b7357cfc | |||
| 818a9b56c2 | |||
| 691f304096 | |||
| 3bb96ded91 | |||
| 9357abb279 | |||
| 83e4daefdb | |||
| e4d14944a2 | |||
| 7523723933 | |||
| b83f3f864d | |||
| 177dd4b50b | |||
| 199cb577c5 | |||
| 350514b60e | |||
| c97ed3612b | |||
| 6fd7da19e5 | |||
| 3479f9cf98 | |||
| 5e56b44dfa | |||
| c2f2e86735 | |||
| 41292f9d89 | |||
| 46895151c8 | |||
| 376ab29457 | |||
| 1d0fe9f555 | |||
| ddc3193593 | |||
| 2d9c315e75 | |||
| 4a680f03b3 | |||
| b1d26cd999 | |||
| e17660e8ba | |||
| b6f3df2c9f | |||
| e383005f6c | |||
| 2ce8afd0fe | |||
| 87106f450a | |||
| 7a4f95ce15 | |||
| bf81c38315 | |||
| f89ffd1b73 | |||
| dfde34556e | |||
| 8d82759520 | |||
| f4a05fbb36 | |||
| 938bbb5c43 | |||
| 1a15a139c8 | |||
| a45edf8fb5 | |||
| 5a3abc325f | |||
| acd1e3ef5a | |||
| 6aebeebb72 | |||
| 5b23ca7706 | |||
| f87807adf7 | |||
| 972c6deb71 | |||
| 1bd9f58e56 | |||
| 2bdbb22499 | |||
| 03577ebca2 | |||
| 0130a46ced | |||
| dc9776061c | |||
| 426517e66d | |||
| 0743a049d7 | |||
| 94cf741706 | |||
| d1b3b05a0c | |||
| b4052f77f9 | |||
| 3d320e029c | |||
| 19cbbe56f6 | |||
| 60b3cb20ca | |||
| b93d47eeb2 | |||
| 38d791d9b1 | |||
| b5be59f180 | |||
| 1afcbc1a83 | |||
| 06c17411d6 | |||
| b17f29f983 | |||
| 1cff081f51 | |||
| 4b80460515 | |||
| 7fd79bb09c | |||
| f03a0de42c | |||
| ec1dcbf9d3 | |||
| 406232e8f9 | |||
| 1551087ddf | |||
| 1ec2bee098 | |||
| 4b5506d07b | |||
| 8b76d51f2c | |||
| 07c66c57fc | |||
| 5707c697d8 | |||
| bad5beb42b | |||
| e14c9d0f4c | |||
| d160de3ee1 | |||
| f3d0959cae | |||
| b2002bb51d | |||
| 9240d90547 | |||
| eee7572360 | |||
| 90be0ee6d8 | |||
| dc48ae11a5 | |||
| f3fcf569ad | |||
| 32c1fb6c26 | |||
| 4bf1ee292a | |||
| 84763399aa | |||
| ef164354ab | |||
| 4a2e161cfc | |||
| 61e759a489 | |||
| 663b20b77a | |||
| 1af5abf762 | |||
| 965d671707 | |||
| 59e3ebe65d | |||
| 2f712d6ec0 | |||
| 951dd88169 | |||
| 7ac63caeb8 | |||
| 563237a45a | |||
| 7ac8442bcf | |||
| 5bd022fe89 | |||
| bded47846d | |||
| 6eb9880a3f | |||
| 0bedcd47e2 | |||
| 48894a55e8 | |||
| b2406ef4bc | |||
| fd313b76b6 | |||
| 665c532f8c | |||
| fba410c83f | |||
| 9cb0ed32d3 | |||
| fa49e8ad2b | |||
| 1538d1ab2c | |||
| f608040986 | |||
| 53431ab908 | |||
| a9eb337ec7 | |||
| 590681ed24 | |||
| cc8797744b | |||
| 3bb8628e39 | |||
| c3b462ec0a | |||
| 77fb7b69f6 | |||
| 8b3bbc4003 | |||
| 81bb979164 | |||
| 5667cd5dc3 | |||
| 75597664e0 | |||
| cff6533e7b | |||
| 1d6b96143f | |||
| 568c875888 | |||
| c569bf907f | |||
| 3328d56cb2 | |||
| 2bb795e08c | |||
| 8c3ee104f6 | |||
| c955d35bbe | |||
| 62214370ed | |||
| 32124c5b1e | |||
| dbc2b7e7eb | |||
| 0b0370936a | |||
| c5468d613f | |||
| 740f2bc3bd | |||
| 05cf531339 | |||
| b1aed332e7 | |||
| 685e7bb27c | |||
| df63d0f562 | |||
| 4362905b82 | |||
| 30d18bf485 | |||
| d0c477ec78 | |||
| 40e7ab46f6 | |||
| 23e426016d | |||
| ba9477f2e5 | |||
| 30358eb7ff | |||
| b7679a5803 | |||
| c7f9885821 | |||
| d980e84779 | |||
| 0c1e380bed | |||
| 724937e9ae | |||
| 94f1678c3a | |||
| ae70c31d96 | |||
| a260feefe8 | |||
| c3d47e5b21 | |||
| 83c2a4758c | |||
| 02f35e2625 | |||
| fe102d413a | |||
| e8e1a2e2c1 | |||
| f43064ed97 | |||
| 2d5e7da8dc | |||
| c1d367bbc0 | |||
| 5cb919eb3c | |||
| a9448d1a99 | |||
| a24f52a1f0 | |||
| 4fb81a5672 | |||
| 0b5124d4d5 | |||
| 698ac036d5 | |||
| cb7b43609f | |||
| 16d6497396 | |||
| 492772b995 | |||
| e9b8216116 | |||
| 4ed4b10fa2 | |||
| 57f4863107 | |||
| faa4bba898 | |||
| 55b127856b | |||
| 26b35abe70 | |||
| efbad0f325 | |||
| c80d5016f6 | |||
| 71feec7db9 | |||
| 317486df0a | |||
| 4ea7cb48a5 | |||
| 4bf8deddb4 | |||
| 220129584e | |||
| 270ba93fbe | |||
| 96d9e2ab24 | |||
| d5d1e86b1c | |||
| d1722a8706 | |||
| 1397d1438a | |||
| d279408ba5 | |||
| c29885e31f | |||
| 6b9755b3ef | |||
| 0f58784a1b | |||
| a7c99dfb09 | |||
| e55b9cc89b | |||
| 3240f05027 | |||
| 3d1b153a91 | |||
| 54c7d48144 | |||
| e54d1c06d6 | |||
| a898df62c1 | |||
| f5f4812403 | |||
| 49e47d6103 | |||
| b9f7ba04ea | |||
| d34513336d | |||
| 4cba01fc7a | |||
| 2306834995 | |||
| 2b61433258 | |||
| 9e786ccf39 | |||
| 4952ae9442 | |||
| 42c443bd5a | |||
| a222fb985e | |||
| b7bbf40366 | |||
| 78b08b699f | |||
| 8d5c7e0adf | |||
| fc5f1e8685 | |||
| 4cd6e9f767 | |||
| 6fdd8bd09b | |||
| 73fa013011 | |||
| 45bdd10c9b | |||
| 85cc000d23 | |||
| 0e8c054a46 | |||
| dbac4c5ad8 | |||
| 672263421a | |||
| fe40e2ca95 | |||
| 5880487f47 | |||
| c4911e15bc | |||
| 316c69c063 | |||
| 401141bb66 | |||
| e84522f244 | |||
| acbc6943b8 | |||
| dee1e186e3 | |||
| 14952d0c06 | |||
| ff54251e9b | |||
| dd2ba5696e | |||
| 6ee7ee4234 | |||
| 9b050994f5 | |||
| 789c47d0ee | |||
| ba43eb1c91 | |||
| c2ac7bf843 | |||
| 9d9d72c892 | |||
| d943bdca16 | |||
| 82ea599458 | |||
| ba8bf06c00 | |||
| 68dc2e594a | |||
| 5c8e7d66d0 | |||
| 7cb07bfc5d | |||
| 2e57a38e0b | |||
| 487f60c6af | |||
| f1b0cda818 | |||
| b99966fe19 | |||
| 40f1129c23 | |||
| ccc079a9ca | |||
| 8fb3695fa9 | |||
| 4ee0d61a64 | |||
| 9d5b81d2f5 | |||
| 64040c745f | |||
| b3cc37be26 | |||
| 4c42c60651 | |||
| acbb8f0b24 | |||
| 57d9698b0b | |||
| e1769f20a2 | |||
| 94c8bd229d | |||
| 0fde439d8b | |||
| 6cb2d143d6 | |||
| 25857fb660 | |||
| 322c8dead7 | |||
| 8079157329 | |||
| 186443f883 | |||
| 14194a775d | |||
| aa2890b479 | |||
| 8793681d8a | |||
| 4fcf45b5d0 | |||
| d50136223a | |||
| a4547472ec | |||
| b75e517bda | |||
| 6b17205159 | |||
| df4d4001f7 | |||
| bad15a5bdb | |||
| bb70688f1d | |||
| 1b219e333a | |||
| 7baee64cc3 | |||
| 4772fa7df3 | |||
| 79e63829c4 | |||
| 704837bf0b | |||
| 21140042d3 | |||
| eebf2aa62b | |||
| 64705fcf0f | |||
| 6275c6d41e | |||
| a7d77bd3ad | |||
| 30d0f84ea4 | |||
| ade2a7855f | |||
| e8469f1f89 | |||
| c15067b942 | |||
| e494d61647 | |||
| 14ced0e642 | |||
| 16e4f4ebbb | |||
| d01c914e7f | |||
| 64bb5c7004 | |||
| 9ab18c575a | |||
| 2e553d29f7 | |||
| 2499e71809 | |||
| c78f9618cf | |||
| 2f8d431940 | |||
| d8887e0ea1 | |||
| 5b313a44bb | |||
| 83f8c45493 | |||
| 6bd2b155d2 | |||
| 16604de74c | |||
| c422c9e436 | |||
| d519a61f58 | |||
| 3047627715 | |||
| fe2fea5da2 | |||
| f0992e7151 | |||
| 29f3821a21 | |||
| 4d35ead2a4 | |||
| 0dfa615b54 | |||
| be9ab3efcf | |||
| e74457a470 | |||
| c304dae303 | |||
| 526b4c84fd | |||
| 3131ecf2d6 | |||
| 58e26c6d6b | |||
| a9ec8b3632 | |||
| 5ce0ce147e | |||
| e77a9371db | |||
| e9cdfe9edd | |||
| 05bc56fe75 | |||
| 0ad94089e2 | |||
| 38fbc47678 | |||
| 4e4254fa84 | |||
| ecc7833e04 | |||
| f7cfc75394 | |||
| 2d82f34cd4 | |||
| 1cefa18039 | |||
| b41ce7679f | |||
| f523283463 | |||
| 38837b86a8 | |||
| a82e937412 | |||
| 501ea274ad | |||
| 7b01a7fd68 | |||
| a9f35a1ab3 | |||
| 4c5466cad0 | |||
| e88c7e0170 | |||
| 125fb1e9aa | |||
| 514d738b08 | |||
| 3aa747de02 | |||
| 3af4526ef6 | |||
| 5a87dc04d3 | |||
| 77f79de443 | |||
| 64f339b0db | |||
| fd4906846d | |||
| 7a6ca476a8 | |||
| ef6116f795 | |||
| ac0d177939 | |||
| 1542867c71 | |||
| e09a0434a3 | |||
| 010bcd15bc | |||
| 181e0bbb6e | |||
| d19c5d4719 | |||
| 446a3f98d6 | |||
| 62ae658b4d | |||
| 6158eb0f46 | |||
| e257dc0e6e | |||
| 243094e4ab | |||
| 54d933ecc2 | |||
| 194f194f81 | |||
| 88f762077f | |||
| d03c92ddbf | |||
| df230e286f | |||
| 19033bf2e5 | |||
| 0825819e44 | |||
| 9ae65d13a0 | |||
| 3a639c93b5 | |||
| 836ab8ddc4 | |||
| 805a2d11fc | |||
| 7eb1b2473f | |||
| dc0f9fa2d6 | |||
| 21b48c8c41 | |||
| a84997d2f4 | |||
| e4d075af6c | |||
| a1852076fe | |||
| 1b9cf00a26 | |||
| 1d73f1dc90 | |||
| ce469cb3a3 | |||
| 8f86de2187 | |||
| 0225f16924 | |||
| affbb9ac6f | |||
| cb064362d1 | |||
| c6a0ef2e98 | |||
| b2ffac5317 | |||
| 86118e2710 | |||
| 6afe16ba01 | |||
| 2254b33f0e | |||
| 01a60d06f1 | |||
| 224cf12fdd | |||
| cfe653de4d | |||
| 9ea8f84421 | |||
| ecb90f828f | |||
| 6e0f302d20 | |||
| aee932dc93 | |||
| dbddb0845a | |||
| 0dee3e01f8 | |||
| 306b88ef56 | |||
| e11ee9ec0e | |||
| e0969e69e7 | |||
| f6ac63f3ab | |||
| 4a52085e8f | |||
| 2d506e79eb | |||
| e9bb89f044 | |||
| 79cacb2823 | |||
| 060fb42fee | |||
| d95045f4ac | |||
| 1073232ca7 | |||
| e66a5f9b4b | |||
| 68033bd42f | |||
| 917c93564f | |||
| 8c856b6510 | |||
| 8fdd33b3b4 | |||
| 94eb7aa81f | |||
| 9126e0e07d | |||
| 270aa41dfb | |||
| c0754abfca | |||
| 3b77ed6b06 | |||
| 52292d3669 | |||
| 03a48e378c | |||
| 7acb2136ec | |||
| 6603a45dc9 | |||
| 32f8774ddb | |||
| 8c4ee8362e | |||
| e435f81296 | |||
| ae487142ba | |||
| 003ce9deb4 | |||
| 55eacf0319 | |||
| 4ec20e6e3a | |||
| f8e9d9a280 | |||
| 543b21d99f | |||
| e6a1a7aa8b | |||
| 430bdbcaae | |||
| 98b25177e7 | |||
| fb7a30e505 | |||
| 538114a926 | |||
| 9babea96f6 | |||
| 8312b44a43 | |||
| d71edd5ede | |||
| c1b928a3f5 | |||
| 203b235fdd | |||
| 55fb1ec42e | |||
| 9d8a52de68 | |||
| 1a6111d67d | |||
| fc8bb34242 | |||
| e3017801d1 | |||
| f0b92260ed | |||
| bee748a932 | |||
| c0cbb71d26 | |||
| b38974adb2 | |||
| c55c75a29e | |||
| d6ac8ed9fd | |||
| cfe8124cc3 | |||
| 65091233a8 | |||
| 4b087df57e | |||
| 5e84b26a5f | |||
| 44a824ccf5 | |||
| 068e2b73ca | |||
| 005f4d7adc | |||
| 191186ec94 | |||
| 29f1908e5a | |||
| ab0a10115d | |||
| fbc8fe78c9 | |||
| d81cfd5757 | |||
| a048cdd73c | |||
| 57966b59ee | |||
| 3500f8e1c3 | |||
| b0889faa14 | |||
| 4a016c119c | |||
| bf0f4e9d1f | |||
| c63b1c74dd | |||
| eecfb7ab54 | |||
| 8f904ddd74 | |||
| 5953c58224 | |||
| ed909dcf63 | |||
| cc8acbc57b | |||
| bb2bbe9697 | |||
| 2a7499e93c | |||
| 467cb6fb18 | |||
| aa972bbd95 | |||
| 37f34e556f | |||
| 949b2e8db1 | |||
| 2e778f3b31 | |||
| 1bf684aab2 | |||
| 526832fc3f | |||
| d10cb9e9f4 | |||
| 36714250f6 | |||
| 5aae86638b | |||
| 84b3252bfc | |||
| 9f00ad039e | |||
| cbe982f52b | |||
| dc49fecd4b | |||
| a817f21842 | |||
| 14dda205f0 | |||
| 9cbd8841fe | |||
| c77a2e2c93 | |||
| cc54cd6077 | |||
| a0fc1bc4f1 | |||
| bbcb259b02 | |||
| 00e6b7d357 | |||
| 30a39916b6 | |||
| 939f408ef8 | |||
| 6b788e324a | |||
| 85d9d88d0a | |||
| 3927dc9fa5 | |||
| 968d79399d | |||
| 7a772ea313 | |||
| 76e5efc73b | |||
| def2a5f25d | |||
| 021a95c114 | |||
| 7c8d3c2605 | |||
| f8d6109b9d | |||
| f689afee09 | |||
| 98edb1ba01 | |||
| 89e6b65e84 | |||
| 34c069a844 | |||
| 58f42dd8ca | |||
| d0d9df5220 | |||
| 19274ccb2b | |||
| 3e5e01643f | |||
| 41dbbd3fda | |||
| a52adc6b0d | |||
| d128621fc1 | |||
| f54bead1ed | |||
| b6f074a72a | |||
| 106ad5d881 | |||
| f94546edaa | |||
| 11ecc1e365 | |||
| 31d0c4fb45 | |||
| 48495944fa | |||
| 88be57ea46 | |||
| 6700a8ad9f | |||
| e0c82440b3 | |||
| 98fffe31be | |||
| 22723a6df6 | |||
| 35c4dc5150 | |||
| d3f7207e74 | |||
| ad7a62b67d | |||
| 1fdf23ecbd | |||
| f7f4960755 | |||
| 4d45e3ebdc | |||
| 42fd728156 | |||
| a1b11e7a5f | |||
| 8f74968371 | |||
| 67ff2b258e | |||
| a2074a455e | |||
| dda470dc9c | |||
| 311ed40d05 | |||
| 1c753fcac9 | |||
| 1e80fd54f6 | |||
| 84d98011a7 | |||
| 847236314d | |||
| e583e42dfa | |||
| c6b1f3dc77 | |||
| 2a6d1df1bf | |||
| aed74498e6 | |||
| f569580077 | |||
| 0c322f50a5 | |||
| d799bbcb0b | |||
| 24a63f36e7 | |||
| b3869dd472 | |||
| 1b5ed27406 | |||
| ed25368b93 | |||
| fad78a8ac0 | |||
| 12b4a095d3 | |||
| fab6ab4f2e | |||
| 80f9ed8e8b | |||
| 976d2e5ac1 | |||
| 343c9fa98a | |||
| f206a18027 | |||
| 3dc1cf82f1 | |||
| 0d627d2b11 | |||
| ce8ea550a3 | |||
| 1035f8bb2a | |||
| ce59653ccd | |||
| ea9c1ad436 | |||
| e09e92ff03 | |||
| 81cf04ae8b | |||
| 5715e516aa | |||
| 621e6f4548 | |||
| 839b7bd164 | |||
| d081ec85d0 | |||
| 474f73444b | |||
| edce645f34 | |||
| b581daf0ab | |||
| 815a24c186 | |||
| 878cab95e7 | |||
| e7a81459cd | |||
| 15af49e618 | |||
| b3be1ee137 | |||
| 5083b79730 | |||
| b3ead0ca5e | |||
| 78d7e513f9 | |||
| 4472c8d92b | |||
| caec22001d | |||
| e7a906debf | |||
| df8d15aeb6 | |||
| 93d216616b | |||
| c93281b8cc | |||
| f081fa1433 | |||
| baed259802 | |||
| ec6f0e2edb | |||
| d1fbc96a34 | |||
| 2016a7408c | |||
| 9feadf4d15 | |||
| ad57bf0c54 | |||
| ceb7f81a1f | |||
| daabf8c91f | |||
| af20cec9c3 | |||
| 2dbfb0a622 | |||
| 3f5f754189 | |||
| d366a80629 | |||
| d98571dfb3 | |||
| 29e471ef0f | |||
| 710b6b0261 | |||
| 85d4760cde | |||
| 784a2f0c0c | |||
| 2ad00b3569 | |||
| 8a17fb1548 | |||
| 0910ab7d1a | |||
| 604bcb1878 | |||
| 97c4e17671 | |||
| 66d11ce1c7 | |||
| b0db82f2f1 | |||
| e9d8603611 | |||
| 17d1703806 | |||
| ecc0b475ae | |||
| 69d0db9431 | |||
| 9beb42f1ca | |||
| 8432a7c4ae | |||
| 3e1d3914c2 | |||
| cddf81e6a5 | |||
| 35dc998b03 | |||
| 57b2d182c7 | |||
| fb5a5ff38f | |||
| c326683ada | |||
| 9c719c83a6 | |||
| dd913a17e3 | |||
| c8a4ffa764 | |||
| 9db8d35a49 | |||
| 5fb5e5f4c8 | |||
| c232e65001 | |||
| a851b5f5fd | |||
| 9497824350 | |||
| 1a818ff9b4 | |||
| d8bf86f0c8 | |||
| d5d9c2655e | |||
| 2d08bf4b32 | |||
| 6b0a2a60a8 | |||
| 7eb891dd80 | |||
| b6e1927468 | |||
| 93e17c1770 | |||
| 9d63943237 | |||
| bb43345d63 |
@@ -0,0 +1,162 @@
|
|||||||
|
name: Packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
secrets:
|
||||||
|
TAP_GITHUB_TOKEN:
|
||||||
|
required: true
|
||||||
|
PACKAGES_GPG_KEY:
|
||||||
|
required: true
|
||||||
|
PACKAGES_RSA_KEY:
|
||||||
|
required: true
|
||||||
|
DOCKERHUB_TOKEN:
|
||||||
|
required: true
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: The released tag to package, such as v0.1.0
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: packages
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
packages:
|
||||||
|
name: Binaries, packages and image for ${{ inputs.tag }}
|
||||||
|
runs-on: ubuntu-26.04
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
outputs:
|
||||||
|
latest: ${{ steps.latest.outputs.latest }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.tag }}
|
||||||
|
path: src
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
# Check out the packaging config at the workflow's commit, so a tag cut before the
|
||||||
|
# config existed can still be packaged.
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
path: tooling
|
||||||
|
persist-credentials: false
|
||||||
|
- name: The Go version the gate tests
|
||||||
|
id: go
|
||||||
|
run: sed -n 's/^ARG GO_VERSION=/version=/p' tooling/Dockerfile >> "$GITHUB_OUTPUT"
|
||||||
|
- uses: actions/setup-go@v7
|
||||||
|
with:
|
||||||
|
go-version: ${{ steps.go.outputs.version }}
|
||||||
|
cache: false
|
||||||
|
- name: Channels holding one version
|
||||||
|
id: latest
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
TAG: ${{ inputs.tag }}
|
||||||
|
run: |
|
||||||
|
latest="$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName --jq .tagName)"
|
||||||
|
if [ "$latest" = "$TAG" ]; then
|
||||||
|
echo "latest=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "skip=--skip=homebrew,scoop" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "OLDER_TAG=1" >> "$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
- uses: docker/setup-buildx-action@v4
|
||||||
|
- uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- uses: goreleaser/goreleaser-action@v7
|
||||||
|
with:
|
||||||
|
version: v2.18.3
|
||||||
|
workdir: src
|
||||||
|
args: release --clean --config ../tooling/release-tooling/goreleaser.yaml ${{ steps.latest.outputs.skip }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
|
||||||
|
- name: The built binary and the published image print the tag
|
||||||
|
env:
|
||||||
|
TAG: ${{ inputs.tag }}
|
||||||
|
run: |
|
||||||
|
test "$(dist/fejkdata_linux_amd64_v1/fejkdata --version)" = "fejkdata $TAG"
|
||||||
|
test "$(docker run --rm "ghcr.io/larvit/fejkdata:${TAG#v}" --version)" = "fejkdata $TAG"
|
||||||
|
|
||||||
|
dockerhub:
|
||||||
|
name: Docker Hub image for ${{ inputs.tag }}
|
||||||
|
needs: packages
|
||||||
|
runs-on: ubuntu-26.04
|
||||||
|
permissions:
|
||||||
|
packages: read
|
||||||
|
steps:
|
||||||
|
- uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Copy the GHCR image to Docker Hub, and check it prints the tag
|
||||||
|
env:
|
||||||
|
LATEST: ${{ needs.packages.outputs.latest }}
|
||||||
|
TAG: ${{ inputs.tag }}
|
||||||
|
run: |
|
||||||
|
tags=(-t "docker.io/larvit/fejkdata:${TAG#v}")
|
||||||
|
if [ "$LATEST" = true ]; then
|
||||||
|
tags+=(-t docker.io/larvit/fejkdata:latest)
|
||||||
|
fi
|
||||||
|
docker buildx imagetools create "${tags[@]}" "ghcr.io/larvit/fejkdata:${TAG#v}"
|
||||||
|
test "$(docker run --rm "docker.io/larvit/fejkdata:${TAG#v}" --version)" = "fejkdata $TAG"
|
||||||
|
|
||||||
|
repos:
|
||||||
|
name: apt, dnf and apk repositories for ${{ inputs.tag }}
|
||||||
|
needs: packages
|
||||||
|
if: needs.packages.outputs.latest == 'true'
|
||||||
|
runs-on: ubuntu-26.04
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deploy.outputs.page_url }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: The release's Linux packages
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
TAG: ${{ inputs.tag }}
|
||||||
|
run: gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir packages --pattern '*.deb' --pattern '*.rpm' --pattern '*.apk'
|
||||||
|
- uses: docker/setup-buildx-action@v4
|
||||||
|
- name: Install fejkdata from each repository, then export them
|
||||||
|
env:
|
||||||
|
PACKAGES_GPG_KEY: ${{ secrets.PACKAGES_GPG_KEY }}
|
||||||
|
PACKAGES_RSA_KEY: ${{ secrets.PACKAGES_RSA_KEY }}
|
||||||
|
TAG: ${{ inputs.tag }}
|
||||||
|
run: |
|
||||||
|
build() {
|
||||||
|
docker buildx build -f release-tooling/package-repos.Dockerfile \
|
||||||
|
--secret id=gpg,env=PACKAGES_GPG_KEY --secret id=rsa,env=PACKAGES_RSA_KEY \
|
||||||
|
--build-context packages=packages --build-arg "EXPECTED_VERSION=$TAG" "$@" .
|
||||||
|
}
|
||||||
|
build --target checks --output type=local,dest=checks
|
||||||
|
build --target site --output type=local,dest=site
|
||||||
|
- uses: actions/upload-pages-artifact@v5
|
||||||
|
with:
|
||||||
|
path: site
|
||||||
|
- id: deploy
|
||||||
|
uses: actions/deploy-pages@v5
|
||||||
+85
-14
@@ -4,6 +4,9 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '17 5 * * *'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -11,9 +14,10 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: vet + fmt + tests
|
name: vet + fmt + tests
|
||||||
runs-on: ubuntu-24.04
|
if: github.event_name != 'schedule'
|
||||||
|
runs-on: ubuntu-26.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -26,11 +30,16 @@ jobs:
|
|||||||
else
|
else
|
||||||
base='${{ github.event.before }}'
|
base='${{ github.event.before }}'
|
||||||
fi
|
fi
|
||||||
if git diff --quiet "$base"...HEAD -- '*.go' go.mod go.sum Dockerfile .dockerignore .github data testdata README.md; then
|
if git diff --quiet "$base"...HEAD -- '*.go' go.mod go.sum Dockerfile .dockerignore .github data release-tooling testdata vulnerability-filing README.md todo.md; then
|
||||||
echo "code=false" >> "$GITHUB_OUTPUT"
|
echo "code=false" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "code=true" >> "$GITHUB_OUTPUT"
|
echo "code=true" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
|
if git diff --quiet "$base"...HEAD -- '*.go' go.mod go.sum release-tooling Dockerfile .dockerignore LICENSE DATA-LICENSES.md .github/workflows/packages.yml .github/workflows/test.yml; then
|
||||||
|
echo "packaging=false" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "packaging=true" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
echo "base=$base" >> "$GITHUB_OUTPUT"
|
echo "base=$base" >> "$GITHUB_OUTPUT"
|
||||||
- name: CHANGELOG entry for data changes
|
- name: CHANGELOG entry for data changes
|
||||||
run: |
|
run: |
|
||||||
@@ -48,22 +57,84 @@ jobs:
|
|||||||
# failure is version-specific.
|
# failure is version-specific.
|
||||||
- name: Lowest supported Go
|
- name: Lowest supported Go
|
||||||
if: ${{ !cancelled() && steps.changes.outputs.code == 'true' }}
|
if: ${{ !cancelled() && steps.changes.outputs.code == 'true' }}
|
||||||
run: docker build --build-arg GO_VERSION=1.22.12 .
|
run: docker build --build-arg GO_VERSION=1.22.12 --target portable .
|
||||||
|
- name: Package repositories, signed with throwaway keys
|
||||||
|
if: ${{ !cancelled() && steps.changes.outputs.packaging == 'true' }}
|
||||||
|
run: |
|
||||||
|
docker build -f release-tooling/package-repos.Dockerfile --target throwaway-keys --output "$RUNNER_TEMP/keys" .
|
||||||
|
PACKAGES_GPG_KEY="$(cat "$RUNNER_TEMP/keys/gpg.asc")" PACKAGES_RSA_KEY="$(cat "$RUNNER_TEMP/keys/rsa.pem")" \
|
||||||
|
docker build -f release-tooling/package-repos.Dockerfile --target checks \
|
||||||
|
--secret id=gpg,env=PACKAGES_GPG_KEY --secret id=rsa,env=PACKAGES_RSA_KEY .
|
||||||
|
|
||||||
release:
|
vulnerabilities:
|
||||||
name: Gitea release from CHANGELOG.md
|
name: Known vulnerabilities in this Go version and the required modules
|
||||||
needs: test
|
runs-on: ubuntu-26.04
|
||||||
if: github.event_name == 'push'
|
timeout-minutes: 10
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
permissions:
|
||||||
|
actions: write
|
||||||
contents: write
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- run: python3 release-tooling/publish_release.py
|
- name: Fetch govulncheck's report from vuln.go.dev
|
||||||
|
run: docker build --no-cache-filter vulnerabilities --target vulnerability-report --output "$RUNNER_TEMP/vulnerabilities" .
|
||||||
|
- name: File each one in todo.md
|
||||||
|
id: file
|
||||||
|
run: python3 vulnerability-filing/file_vulnerabilities.py "$RUNNER_TEMP/vulnerabilities/report.json" todo.md
|
||||||
|
- name: Propose the filed items
|
||||||
|
if: failure() && steps.file.outcome == 'failure' && github.ref == 'refs/heads/main'
|
||||||
env:
|
env:
|
||||||
GITEA_API_URL: ${{ github.api_url }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
GITEA_REPOSITORY: ${{ github.repository }}
|
run: |
|
||||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
if git diff --quiet todo.md || [ "$(gh pr list --head vulnerabilities --state open --json number --jq length)" != 0 ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git config user.name 'github-actions[bot]'
|
||||||
|
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
|
||||||
|
git switch -C vulnerabilities
|
||||||
|
git commit -qam "File the known vulnerabilities govulncheck reports"
|
||||||
|
git push -qf "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" vulnerabilities
|
||||||
|
gh pr create --head vulnerabilities --title "File the known vulnerabilities govulncheck reports" \
|
||||||
|
--body "govulncheck reports known vulnerabilities in the Go version or a required module, and no release is cut until they are gone. This files them atop the next release in todo.md, scored 0.0 until rank_todo.py scores them; each item names the bump that fixes it."
|
||||||
|
# A pull request the workflow's token opens starts no workflow.
|
||||||
|
gh workflow run test.yml --ref vulnerabilities
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Release from CHANGELOG.md
|
||||||
|
needs: [test, vulnerabilities]
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
runs-on: ubuntu-26.04
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
outputs:
|
||||||
|
tag: ${{ steps.publish.outputs.tag }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- id: publish
|
||||||
|
run: python3 release-tooling/publish_release.py
|
||||||
|
env:
|
||||||
|
FORGE_API_URL: ${{ github.api_url }}
|
||||||
|
FORGE_REPOSITORY: ${{ github.repository }}
|
||||||
|
FORGE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
SHA: ${{ github.sha }}
|
SHA: ${{ github.sha }}
|
||||||
|
|
||||||
|
packages:
|
||||||
|
needs: release
|
||||||
|
if: needs.release.outputs.tag != ''
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
uses: ./.github/workflows/packages.yml
|
||||||
|
with:
|
||||||
|
tag: ${{ needs.release.outputs.tag }}
|
||||||
|
secrets:
|
||||||
|
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
|
||||||
|
PACKAGES_GPG_KEY: ${{ secrets.PACKAGES_GPG_KEY }}
|
||||||
|
PACKAGES_RSA_KEY: ${{ secrets.PACKAGES_RSA_KEY }}
|
||||||
|
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|||||||
@@ -1,11 +1,137 @@
|
|||||||
# Rules
|
# Rules
|
||||||
|
|
||||||
- Go runs only through `docker compose run --rm <test|vet|fmt|build>`; the merge gate is `docker build .` plus CI's changelog check.
|
- Go runs only through `docker compose run --rm <test|vet|fmt|build|cyclo|generate|vulncheck>`; the merge gate is what the README's Development section lists.
|
||||||
- Tests first, in their own commit; the implementation follows in the next. A re-pin of seeded output or of `testdata/shipped_shape.txt` is its own commit.
|
- Tests first, in their own commit; the implementation follows in the next. A re-pin of seeded output or of `testdata/shipped_shape.txt` is its own commit.
|
||||||
- A change under `data/` or to the shape pin adds its `CHANGELOG.md` entry under `Unreleased` in the same PR, and so does a change to a flag, an exit code, an exported name, a fence, a builtin or the lowest Go; what is major is the README's Versioning table.
|
- A change under `data/` or to the shape pin adds its `CHANGELOG.md` entry under `Unreleased` in the same PR, and so does a change to a flag, an exit code, an exported name, a fence, a builtin or the lowest Go; what is major is the README's Versioning table.
|
||||||
- One-line commit messages: no ticket prefix, no repo name, no authorship trailers.
|
- One-line commit messages: no ticket prefix, no repo name, no authorship trailers.
|
||||||
|
- GitHub is where the project lives, so `gh` is the tool: merges are fast-forward
|
||||||
|
only, `gh pr merge --rebase` where the branch is already on top of main.
|
||||||
|
- CodeRabbit skips every pull request until the repository has 10 GitHub stars: check
|
||||||
|
its status once when a PR is marked ready, answer a review if one is there, and never
|
||||||
|
wait for one.
|
||||||
|
- From 0.2.0, README goal 3 is gated at 7.0 on the `comprehension-panel` skill, and below it
|
||||||
|
nothing else merges — no feature, no category, no data — bar a security fix, a
|
||||||
|
dependency bump, a repair of behaviour the README documents, the infrastructure
|
||||||
|
the round itself runs on, and a chunk the comprehension-floor decision lists as
|
||||||
|
shipped under the floor. `todo.md` carries the round: a PR per item it plans, the nine-seat
|
||||||
|
findings at depth 1 or a restructure the maintainer approved, then the nine-seat run
|
||||||
|
again, until the score passes. At or above 7.0
|
||||||
|
every pull request scores with the four-seat run and answers it in one run. No merge
|
||||||
|
lowers the last score its own panel recorded: the nine-seat round and the four-seat
|
||||||
|
run are two ratchets, never compared with each other. A lower score blocks the merge
|
||||||
|
even where it may be panel noise: fix what the seats name until the score rises. A PR
|
||||||
|
that only records a run's scores and files what its seats name merges even when a
|
||||||
|
score fell. Through 0.1.0, a merge is blocked only by a paired ruling that rules worse.
|
||||||
- Hard tabs. No comment by default; delete a restatement, a rationale, history, or a file preamble.
|
- Hard tabs. No comment by default; delete a restatement, a rationale, history, or a file preamble.
|
||||||
- One spelling per result: reject the other at `New`, and let the error name the spelling to use.
|
- Prose naming a source states what that source states: read the register's own field
|
||||||
- A standing choice a reader would relitigate goes under Decisions in the README, not in a comment.
|
before paraphrasing it, and name the register you read before claiming none
|
||||||
|
publishes a spelling.
|
||||||
- A README example is a `json` block that loads and renders as a category; `readme_test.go` runs every one.
|
- A README example is a `json` block that loads and renders as a category; `readme_test.go` runs every one.
|
||||||
- Cyclomatic complexity is gated at 14: the table-shaped dispatches (`eachToken`, `calc.factor`, `walkPath`) sit at 13–14 and stay whole; anything else that reaches 14 is decomposed.
|
- Cyclomatic complexity is gated at 14: split a function that would go over it. A table-shaped dispatch, a switch with one case per kind of its input, may sit at 14 and stays whole.
|
||||||
|
|
||||||
|
# Decisions
|
||||||
|
|
||||||
|
In [docs/decisions.md](docs/decisions.md):
|
||||||
|
|
||||||
|
- GitHub is canonical, and the module path names it
|
||||||
|
- The Gitea copy stays, as a pull mirror of GitHub
|
||||||
|
- The vocabulary sits below `doc.go`'s package clause, not in the package doc
|
||||||
|
- Options and fields share one namespace
|
||||||
|
- `{a|b}` stays beside nested choices
|
||||||
|
- Flags follow getopt_long
|
||||||
|
- A struct tag is a template by its shape
|
||||||
|
- The CLI renders exactly the template on its stdin, and adds nothing
|
||||||
|
- The CLI reads only the library's public API
|
||||||
|
- A template that does not compile is misuse (exit 2), including a reference that resolves to nothing, save a lone reference under the CLI's `--format` until item 129
|
||||||
|
- Whitespace around a JSON object, array, string or null is dropped, and a bare number or boolean is a format string
|
||||||
|
- `FakeTemplate` and `NewTemplate` both stay
|
||||||
|
- The shipped data is embedded, not discovered
|
||||||
|
- The shipped data is Go packages a library imports by choice, and the CLI carries every one, from 0.1.0
|
||||||
|
- A shipped package's folder spells its namespace, its Go name is that namespace in lower case without separators, and its manifest mounts it with `namespace`
|
||||||
|
- A source whose manifest carries an index loads each category on first reach; any other loads in `New`, with what it reads
|
||||||
|
- Every `{…}` draws afresh, and only a name keeps a pick
|
||||||
|
- Reference sigils follow the filesystem
|
||||||
|
- A change to what exists is a major; a minor only adds
|
||||||
|
- An error is a contract by what it names, not its bytes
|
||||||
|
- A load reports every mistake at once
|
||||||
|
- Formats are checked after all data is loaded, so their errors name the category
|
||||||
|
- Raising the lowest supported Go is a major
|
||||||
|
- The format check runs on the latest Go only
|
||||||
|
- The changelog heading is the one spelling of a release; CI cuts the tag
|
||||||
|
- A `--data-path` override rebinds every reference to the category it replaces
|
||||||
|
- The repeat cap bounds renders, not bytes
|
||||||
|
- 64-bit targets only
|
||||||
|
- A constant zero divisor is a load error; in a string column a divisor that is not constant prints `Inf`
|
||||||
|
- Samples say what they emit, transforms what they do
|
||||||
|
- A record is a template seen as columns; a Go struct is the one second schema
|
||||||
|
- A struct's records follow Go's field access, and compile on first use
|
||||||
|
- A category may reference itself, and a record's fences run at load
|
||||||
|
- A record's column set is fixed before the first draw
|
||||||
|
- Null is a `null` item, not a rate
|
||||||
|
- A typed column holds one value, not composed text
|
||||||
|
- A column that only reads one reference or name is the column it reads
|
||||||
|
- A typed column's calc is refused unless proven
|
||||||
|
- `Column` carries text, not a Go value
|
||||||
|
- A unit takes the stem of what it is, and a file the stem of the units it holds
|
||||||
|
- One name, one meaning
|
||||||
|
- The performance gate asserts allocations, not wall-clock time
|
||||||
|
- Rows live in a TSV, the shape in JSON
|
||||||
|
- A selector is bracketed, and a dot inside it is literal
|
||||||
|
- `parent` names the link column and the table alike
|
||||||
|
- After a row, a path names a column or a linked table
|
||||||
|
- A selected row is fixed, not drawn
|
||||||
|
- `..` steps up to the parent row, and a step down after it draws afresh, or once per name's pick
|
||||||
|
- A table selects by one key column, a code, never a free-form name
|
||||||
|
- A parent row with no child row is a load error
|
||||||
|
- A table is a record of string columns
|
||||||
|
- The key map is built at load, the rest on first draw
|
||||||
|
- Two categories may name one TSV
|
||||||
|
- A cell may read a reference, and each row its own
|
||||||
|
- A path is walked once without drawing before it is walked for real
|
||||||
|
- A country's postal codes and streets are siblings under its locality
|
||||||
|
- A locale package imports its country's geo package, and its `Modules()` returns both
|
||||||
|
- The default embed holds every Swedish postort the import can place, with all its streets, and, of the US places that hold most of a postal code's land, every state capital, every place of 100,000 or more and a population-weighted tenth, each with a quarter of its streets, at least ten
|
||||||
|
- A locale's `address` binds its country's record once and prints it whole
|
||||||
|
- A postort's kommun comes from its name, its tätort or its codes, never from distance
|
||||||
|
- A highway designation is not a street, and a US postal code belongs to the place holding most of its land inside places
|
||||||
|
- `--list` stays a plain list of paths
|
||||||
|
- A layout is always quoted
|
||||||
|
- A title is a table under `sex`
|
||||||
|
- A table owns the spelling of a selector on it
|
||||||
|
- No builtin reads the clock, so a date is bounded by days, never by an age
|
||||||
|
- `misc` is what every locale shares
|
||||||
|
- A register's canonical spelling loses to the one its domain writes
|
||||||
|
- `misc.territory` is the spine, and a `misc` table naming a territory links to it
|
||||||
|
- A table whose register publishes no frequency draws evenly
|
||||||
|
- `misc.timezone` weighs a zone by the people living in it
|
||||||
|
- `misc.car` is one flat table, not a make linked to its models
|
||||||
|
- `misc.territory` names its sovereign in a column, and there is no `misc.country` table
|
||||||
|
- `misc.loglevel` is a table keyed by POSIX's keyword, carrying the code
|
||||||
|
- `misc.tld` keys carry the leading dot, where other tables key on a bare code
|
||||||
|
- `misc.tld` is a table of its own, and `misc.territory.tld` stays a column
|
||||||
|
- `misc.territory` carries a currency code, it does not link to `misc.currency`
|
||||||
|
- An extension may name two media types
|
||||||
|
- The Swedish ids draw Skatteverket's test series
|
||||||
|
- The US given names come from a mirror of the SSA file
|
||||||
|
- `List` advertises direct descents only
|
||||||
|
- A path draws through its compiled steps
|
||||||
|
- A name lives in the category binding it, or in the repeat or choice item binding it, and is drawn on its first read
|
||||||
|
- comprehension floor: every dimension and the overall at 7.0 or above, from 0.2.0
|
||||||
|
- The template engine stays the root package until item 107; what reads no engine type, a table's rows included, sits in `internal/`
|
||||||
|
- Goal 5.1's `{…}` covers `{{`, `}}` and a lone `}`, so the goal names no escape
|
||||||
|
- Goal 7.2 holds while a first template can be written with no escape; a literal brace still takes `{{` or `}}`
|
||||||
|
- The locales are Ethnologue 2026's ten most-spoken languages, each in the country with the most speakers of it, the Nordic countries and Germany
|
||||||
|
- A country with no open address register covering it whole ships its places and postal codes without streets
|
||||||
|
- The apt, dnf and apk repositories are static files on GitHub Pages, holding the latest release only
|
||||||
|
- The image is pushed to GHCR and copied to Docker Hub, whose name the README shows
|
||||||
|
- A known vulnerability in the Go version or a required module blocks a release, even in code fejkdata never calls, and blocks no merge
|
||||||
|
|
||||||
|
# Scoring run
|
||||||
|
|
||||||
|
The project values for `~/.claude/skills/comprehension-panel/scoring-run.md`:
|
||||||
|
|
||||||
|
- `{language}`: Go
|
||||||
|
- `{kind}`: a Go library and CLI that renders fake data from JSON templates
|
||||||
|
- `{domain}`: fake test-data generation and template languages
|
||||||
|
- `{domain docs}`: the documentation of any fake-data library, such as Faker
|
||||||
|
- `{3am question}`: a seeded render of `sv_SE.address` prints the postal code of one postort beside a street of another
|
||||||
|
|||||||
+96
-38
@@ -8,41 +8,99 @@ replacement, and each removed path, column or flag.
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- First release: the CLI, the library and the shipped data set.
|
- Prebuilt binaries for Linux, macOS and Windows on amd64 and arm64, packages for apt,
|
||||||
- Table categories: a category JSON naming a `rows` TSV beside it, with the options
|
dnf, apk, Homebrew and Scoop, and the image `larvit/fejkdata` on Docker Hub, also at `ghcr.io/larvit/fejkdata`, so
|
||||||
`key`, `name`, `weight` and `parent`; a path selects a row by key or name,
|
the CLI installs without Go. 0.1.0 ships them too; the README's
|
||||||
`misc.country[SE]`, and descends to a linked table by name; linked tables draw
|
[Install](https://github.com/larvit/fejkdata#install) lists each command.
|
||||||
consistently within one render and draw group. `rows` is an option, so no
|
|
||||||
template may carry a field of that name. A `name` without a `key` resolves
|
## [0.1.0] - 2026-10-13
|
||||||
inside the table's `parent`. Refused at `New`: a `name` without a `key` or a
|
|
||||||
`parent`, a name repeating inside one parent row, a name spelling another row's key, a table named like a column of any
|
The first release: a Go library and a CLI that render locale-aware fake data from JSON
|
||||||
table above it, a table whose format or cell references its own family, and,
|
templates, and the data they ship with. The
|
||||||
within one render and draw group, a path drawing a table another path selects a
|
[README](https://github.com/larvit/fejkdata/blob/v0.1.0/README.md) documents each part.
|
||||||
row of, or two paths pinning different rows of one table.
|
Install the CLI with `go install github.com/larvit/fejkdata/cmd/fejkdata@v0.1.0`, or the
|
||||||
- `New` refuses a root choice of templates sharing one format and one set of string
|
library with `go get github.com/larvit/fejkdata@v0.1.0`. Before 1.0.0 a minor may break,
|
||||||
fields, naming the rows TSV to write instead.
|
and any release may change what a seed draws, so pin one exact version
|
||||||
- `misc.country`, `misc.currency`, `misc.language`, `misc.httpstatus` and
|
([Versioning](https://github.com/larvit/fejkdata/blob/v0.1.0/README.md#versioning)).
|
||||||
`misc.mimetype` are tables. `misc.country` is every ISO 3166 country that has a
|
|
||||||
capital, a currency and a TLD, with the columns `calling-code`, `capital`,
|
### Added
|
||||||
`currency`, `flag`, `languages`, `numeric` and `tld` added; `misc.currency` the
|
|
||||||
current ISO 4217 currencies with a minor unit, with `decimals` and `numeric`
|
- The CLI, `fejkdata`, renders exactly the template on its stdin and adds nothing:
|
||||||
added, and its symbols from CLDR. `DATA-LICENSES.md` lists each source.
|
`echo '{/sv_SE.person} hihi' | fejkdata` prints e.g. `Sara Eriksson hihi`. `--seed`
|
||||||
- `geo.SE` and `geo.US`: five linked tables per country, `region`, `municipality`,
|
repeats a run's output while the version and data stay the same, and without it each
|
||||||
`locality`, `postal-code` and `street`, weighted by population and address counts
|
run takes a new seed; `-n` renders the template that many times, and `--list`
|
||||||
and built from SCB, GeoNames, Trafikverket NVDB and the US Census Bureau, and an
|
prints the paths the data offers.
|
||||||
`address` record over one consistent draw of them. `sv_SE.address` and
|
`--data-path` layers a directory of your own data over the shipped set, and
|
||||||
`en_US.address` read those records, so `en_US.address.street` no longer carries
|
`--no-shipped-data` loads that directory alone. `--format json|ndjson|csv|sql` writes
|
||||||
`name` and `suffix`, and a locale folder loads only beside `geo`.
|
a record's columns, one record per row. The CLI exits `0` on success, `1` on a runtime
|
||||||
- `{date(from,to,'layout')}` and `{time('layout')}`: a second between two days, or
|
error such as data that fails to load, and `2` on misuse.
|
||||||
within one, in a quoted Go layout. `sv_SE.date`, `en_US.date`, `sv_SE.time` and
|
- The library, `github.com/larvit/fejkdata`, for Go 1.22 or later on a 64-bit target.
|
||||||
`en_US.time` render through them, so `date.year`, `date.month`, `date.day` and
|
`New` loads the modules `WithDataFS` names and the folders `WithDataPath` names, drawing
|
||||||
`time.hour` are no longer paths, and `misc.datetime` is an RFC 3339 instant.
|
from the seed `WithSeed` gives. Without a seed, `New` fails and names `WithSeed`;
|
||||||
- `sex`, `first-name` and `last-name` tables in `sv_SE` and `en_US`, weighted by
|
without data, it fails and names `WithDataFS`. The same seed repeats the draws
|
||||||
bearers from SCB, the SSA and the Census Bureau; `first-name` links to `sex`, and a
|
while the version and data stay the same. `Fake` renders a path, and `FakeTemplate`
|
||||||
name both sexes carry is a row under each. `person` reads them, so its columns are
|
and `NewTemplate` render a template. `FakeRecord`, `FakeRecordTemplate` and
|
||||||
`first`, `last`, `prefix` and `sex`, and `person.femalefirst` and
|
`NewRecordTemplate` render a record, whose `JSON`, `CSVHeader`, `CSVLine` and
|
||||||
`person.malefirst` are no longer paths.
|
`SQLInsert` methods write its columns as the CLI does. `FakeStruct` fills a struct's
|
||||||
- `sv_SE.personnummer` and `sv_SE.samordningsnummer`, Skatteverket's test series
|
`fake:"…"` tagged fields. `List` gives the paths `Fake` renders, direct descents only,
|
||||||
from a `sv_SE.birth-number` table under `sex`, in place of `sv_SE.ssn`; `en_US.ssn`
|
and `IsTemplate` tells an inline template from a path. Data that fails to load gives
|
||||||
in the ranges the SSA assigns, and `en_US.itin`. `en_US.person.prefix` no longer
|
an error matching `ErrLoad`, and a record with no fields gives one matching
|
||||||
draws `Mr`, `Mrs`, `Ms` or `Miss`, which could contradict the record's `sex`.
|
`ErrNoColumns`.
|
||||||
|
- Package `github.com/larvit/fejkdata/cli` is the `fejkdata` command:
|
||||||
|
`cli.Main(modules...)` runs it over the modules given, so
|
||||||
|
`cli.Main(append(data.Modules(), mydata)...)` builds a CLI carrying your own data.
|
||||||
|
That CLI names itself by the file it runs from, and its `--version` prints its own
|
||||||
|
module's version before fejkdata's.
|
||||||
|
- The template language
|
||||||
|
([Data format](https://github.com/larvit/fejkdata/blob/v0.1.0/README.md#data-format)).
|
||||||
|
Every value in a data file is a string, a choice, a template, a table, a number or a
|
||||||
|
boolean. A `{…}` token reads a sibling field, a name, or a reference from the data
|
||||||
|
root, the file's folder or the folder above (`{/sv_SE.person.last}`, `{.x}`, `{..x}`),
|
||||||
|
or it calls a builtin. Every `{…}` draws afresh, and a name is the one way to keep a
|
||||||
|
pick: `{/sv_SE.person as p}{p.first} {p.last}` is one person. The builtins draw check
|
||||||
|
digits (`luhn`, `mod11`, `ean`), samples (`digits`, `int`, `float`, `uuid`, `ulid`,
|
||||||
|
`iban`, `date`, `time` and more), a counter (`seq`), arithmetic (`calc`) and
|
||||||
|
transforms (`lowercase`, `uppercase`, `ascii`). `New` refuses data that breaks the
|
||||||
|
grammar, could mean two things or cannot render a valid value.
|
||||||
|
- Records ([Records](https://github.com/larvit/fejkdata/blob/v0.1.0/README.md#records)):
|
||||||
|
a template seen as columns. A column is a string unless it declares a `datatype`,
|
||||||
|
`integer`, `number` or `boolean`, and a `null` item draws it as null.
|
||||||
|
- Tables ([Table](https://github.com/larvit/fejkdata/blob/v0.1.0/README.md#table)): a
|
||||||
|
category naming a `rows` TSV beside it, with the options `key`, `name`, `weight` and
|
||||||
|
`parent`. A path selects a row by key or name, `misc.territory[SE]`, descends to a
|
||||||
|
linked table, `geo.SE.locality[Lund].street`, and steps up to the parent row,
|
||||||
|
`{/misc.timezone[Europe/Oslo]..territory.name}`.
|
||||||
|
- Data sources ([Data](https://github.com/larvit/fejkdata/blob/v0.1.0/README.md#data)):
|
||||||
|
a directory is a namespace, and each JSON file in it a category. A `.fejkdata.json`
|
||||||
|
manifest at a source's root may carry a `namespace`, the dot path the source mounts
|
||||||
|
under, and an `index`, with which each category loads on the first call reaching it.
|
||||||
|
- The shipped data, in Go packages that each export `FS` and `Modules()`:
|
||||||
|
`github.com/larvit/fejkdata/data/en_US` (package `enus`), `…/sv_SE` (`svse`),
|
||||||
|
`…/misc`, `…/geo/SE` (`geose`) and `…/geo/US` (`geous`). `data.Modules()`, from
|
||||||
|
`github.com/larvit/fejkdata/data`, lists every one, and the CLI carries them all.
|
||||||
|
[`DATA-LICENSES.md`](https://github.com/larvit/fejkdata/blob/v0.1.0/DATA-LICENSES.md)
|
||||||
|
names each source.
|
||||||
|
- `sv_SE` and `en_US`: an `address`, a `person` and more per locale. `sex`,
|
||||||
|
`first-name` and `last-name` are tables weighted by bearers, from SCB, the SSA and
|
||||||
|
the Census Bureau. A person's first name and sex agree, and so does an `en_US`
|
||||||
|
person's title; `person.prefix` is null where a person has no title.
|
||||||
|
`sv_SE.personnummer` and `sv_SE.samordningsnummer` draw Skatteverket's test series.
|
||||||
|
`en_US.ssn` and `en_US.itin` draw the ranges the SSA and the IRS assign, with the
|
||||||
|
columns `area`, `group` and `serial`, and `en_US.phone` draws an exchange NANPA may
|
||||||
|
assign.
|
||||||
|
- `misc`: IANA's registries of HTTP methods, protocols, ports and root-zone TLDs, the
|
||||||
|
HTTP status codes, ISO 3166-1 territories, ISO 4217 currencies with their CLDR
|
||||||
|
symbols, ISO 639 languages, the syslog severities, mime-db's media types, tzdb's
|
||||||
|
zones weighted by the people living in each, user agents and cars, all as tables;
|
||||||
|
and coordinates, credit card numbers, RFC 3339 instants, emoji, MAC addresses,
|
||||||
|
ObjectIds and UUIDs.
|
||||||
|
- `geo.SE` and `geo.US`: five linked tables per country, `region`, `municipality`,
|
||||||
|
`locality`, `postal-code` and `street`, weighted by population and address counts
|
||||||
|
and built from SCB, GeoNames, Trafikverket NVDB, the US Census Bureau and Wikidata,
|
||||||
|
and an `address` record that draws a street and a postal code inside one
|
||||||
|
locality; `sv_SE.address` and `en_US.address` print it. Every Swedish postort
|
||||||
|
(postal town) the import can place ships with all its streets. A US place ships
|
||||||
|
only if it is incorporated and holds most of some postal code's land. Of those
|
||||||
|
places, every state capital but Honolulu ships, as does every place of 100,000
|
||||||
|
people or more and a tenth of all of them drawn by population. Each US place
|
||||||
|
carries a quarter of its streets, at least ten.
|
||||||
|
|||||||
+14
-7
@@ -9,16 +9,23 @@ Every shipped dataset, its source, its licence and the attribution it asks for.
|
|||||||
| `geo/SE/locality.tsv`, `postal-code.tsv` | [GeoNames](https://www.geonames.org/) postal codes for SE; populations from SCB tätorter 2023 | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/); CC0 1.0 | "Postal codes from GeoNames, www.geonames.org" | `data-import/geo-se.py` |
|
| `geo/SE/locality.tsv`, `postal-code.tsv` | [GeoNames](https://www.geonames.org/) postal codes for SE; populations from SCB tätorter 2023 | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/); CC0 1.0 | "Postal codes from GeoNames, www.geonames.org" | `data-import/geo-se.py` |
|
||||||
| `geo/SE/region.tsv`, `municipality.tsv` | [SCB](https://www.scb.se/) län and kommun codes 2026 and population 2024 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | none required | `data-import/geo-se.py` |
|
| `geo/SE/region.tsv`, `municipality.tsv` | [SCB](https://www.scb.se/) län and kommun codes 2026 and population 2024 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | none required | `data-import/geo-se.py` |
|
||||||
| `geo/SE/street.tsv` | [Trafikverket NVDB](https://www.trafikverket.se/) Gatunamn, through the open API | CC0 1.0 | none required | `data-import/geo-se.py` |
|
| `geo/SE/street.tsv` | [Trafikverket NVDB](https://www.trafikverket.se/) Gatunamn, through the open API | CC0 1.0 | none required | `data-import/geo-se.py` |
|
||||||
| `geo/US/region.tsv`, `municipality.tsv`, `locality.tsv` | [Census Bureau](https://www.census.gov/) Gazetteer 2026 and population estimates 2025 | [public domain](https://www.usa.gov/government-works) | none required | `data-import/geo-us.py` |
|
| `geo/US/region.tsv`, `municipality.tsv`, `locality.tsv` | [Census Bureau](https://www.census.gov/) Gazetteer 2026 and population estimates 2025; which places are state capitals from [Wikidata](https://www.wikidata.org/) | [public domain](https://www.usa.gov/government-works); CC0 1.0 | none required | `data-import/geo-us.py` |
|
||||||
| `geo/US/postal-code.tsv`, `street.tsv` | Census Bureau ZCTA to place relationships 2020 and TIGER/Line 2025 address ranges and feature names | public domain | none required | `data-import/geo-us.py` |
|
| `geo/US/postal-code.tsv`, `street.tsv` | Census Bureau ZCTA to place relationships 2020 and TIGER/Line 2025 address ranges and feature names | public domain | none required | `data-import/geo-us.py` |
|
||||||
| `sv_SE/first-name.tsv`, `last-name.tsv` | [SCB](https://www.scb.se/) names with at least two bearers, 31 December 2022 | CC0 1.0 | "Källa: SCB" | `data-import/names-se.py` |
|
| `sv_SE/first-name.tsv`, `last-name.tsv` | [SCB](https://www.scb.se/) names with at least two bearers, 31 December 2022 | CC0 1.0 | "Källa: SCB" | `data-import/names-se.py` |
|
||||||
| `en_US/first-name.tsv` | [SSA](https://www.ssa.gov/oact/babynames/) baby names, births 1930 to 2020, through [hackerb9/ssa-baby-names](https://github.com/hackerb9/ssa-baby-names) | public domain | none required | `data-import/names-us.py` |
|
| `en_US/first-name.tsv` | [SSA](https://www.ssa.gov/oact/babynames/) baby names, births 1930 to 2020, through [hackerb9/ssa-baby-names](https://github.com/hackerb9/ssa-baby-names) | public domain | none required | `data-import/names-us.py` |
|
||||||
| `en_US/last-name.tsv` | Census Bureau surnames occurring 100 or more times, 2010 | public domain | none required | `data-import/names-us.py` |
|
| `en_US/last-name.tsv` | Census Bureau surnames occurring 100 or more times, 2010 | public domain | none required | `data-import/names-us.py` |
|
||||||
| `sv_SE/sex.tsv`, `sv_SE/birth-number.tsv`, `en_US/sex.tsv` | curated (Skatteverket's test birth numbers are facts) | — | — | — |
|
| `sv_SE/sex.tsv`, `sv_SE/birth-number.tsv`, `sv_SE/title.tsv`, `en_US/sex.tsv`, `en_US/title.tsv`, `misc/car.tsv` | curated; Skatteverket's test birth numbers are facts, and `car.tsv` waits for an international source (goal 13) | — | — | — |
|
||||||
| `misc/country.tsv` | [datasets/country-codes](https://github.com/datasets/country-codes) | [PDDL 1.0](https://opendatacommons.org/licenses/pddl/1-0/) | none required | `data-import/country.py` |
|
|
||||||
| `misc/currency.tsv` | [datasets/currency-codes](https://github.com/datasets/currency-codes); symbols from [Unicode CLDR](https://github.com/unicode-org/cldr) `en.xml` and `root.xml` | PDDL 1.0; [Unicode License v3](https://www.unicode.org/license.txt) | CLDR: "Copyright © 1991-2025 Unicode, Inc. Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the United States and other countries." | `data-import/currency.py` |
|
| `misc/currency.tsv` | [datasets/currency-codes](https://github.com/datasets/currency-codes); symbols from [Unicode CLDR](https://github.com/unicode-org/cldr) `en.xml` and `root.xml` | PDDL 1.0; [Unicode License v3](https://www.unicode.org/license.txt) | CLDR: "Copyright © 1991-2025 Unicode, Inc. Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the United States and other countries." | `data-import/currency.py` |
|
||||||
| `misc/httpstatus.tsv` | curated (IANA HTTP status codes are facts) | — | — | — |
|
| `misc/httpmethod.tsv` | [IANA HTTP Method Registry](https://www.iana.org/assignments/http-methods/) | [public domain](https://www.iana.org/help/licensing-terms) | none required | `data-import/httpmethod.py` |
|
||||||
| `misc/language.tsv` | curated (ISO 639-1 codes are facts) | — | — | — |
|
| `misc/httpstatus.tsv` | [IANA HTTP Status Code Registry](https://www.iana.org/assignments/http-status-codes/) | [public domain](https://www.iana.org/help/licensing-terms) | none required | `data-import/httpstatus.py` |
|
||||||
| `misc/mimetype.tsv` | curated (IANA media types are facts) | — | — | — |
|
| `misc/language.tsv` | [datasets/language-codes](https://github.com/datasets/language-codes), the [Library of Congress](https://www.loc.gov/standards/iso639-2/) ISO 639-2 register | [PDDL 1.0](https://opendatacommons.org/licenses/pddl/1-0/) | none required | `data-import/language.py` |
|
||||||
|
| `misc/loglevel.tsv` | [RFC 5424](https://www.rfc-editor.org/rfc/rfc5424.txt) Table 2, the syslog message severities; keywords from the [POSIX](https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/syslog.h.html) `<syslog.h>` severity macros | facts | none required | `data-import/loglevel.py` |
|
||||||
|
| `misc/mimetype.tsv` | [mime-db](https://github.com/jshttp/mime-db), the IANA media type registry with filename extensions | [MIT](https://github.com/jshttp/mime-db/blob/master/LICENSE) | "Copyright (c) 2014 Jonathan Ong, Copyright (c) 2015-2022 Douglas Christopher Wilson" | `data-import/mimetype.py` |
|
||||||
|
| `misc/port.tsv` | [IANA Service Name and Transport Protocol Port Number Registry](https://www.iana.org/assignments/service-names-port-numbers/) | [public domain](https://www.iana.org/help/licensing-terms) | none required | `data-import/port.py` |
|
||||||
|
| `misc/protocol.tsv` | [IANA Protocol Numbers](https://www.iana.org/assignments/protocol-numbers/) | [public domain](https://www.iana.org/help/licensing-terms) | none required | `data-import/protocol.py` |
|
||||||
|
| `misc/territory.tsv` | [datasets/country-codes](https://github.com/datasets/country-codes) | [PDDL 1.0](https://opendatacommons.org/licenses/pddl/1-0/) | none required | `data-import/territory.py` |
|
||||||
|
| `misc/timezone.tsv` | [IANA tzdb](https://www.iana.org/time-zones) 2026d `zone.tab` and the standard offset of each zone; weights from [GeoNames](https://www.geonames.org/) `cities15000` populations | [public domain](https://data.iana.org/time-zones/tzdb/LICENSE); [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | "Populations from GeoNames, www.geonames.org" | `data-import/timezone.py` |
|
||||||
|
| `misc/tld.tsv` | [IANA Root Zone Database](https://www.iana.org/domains/root/db), checked against the root zone's [TLD list](https://data.iana.org/TLD/tlds-alpha-by-domain.txt) | [public domain](https://www.iana.org/help/licensing-terms) | none required | `data-import/tld.py` |
|
||||||
|
| `misc/useragent.tsv` | [top-user-agents](https://github.com/microlinkhq/top-user-agents) desktop and mobile lists | [MIT](https://github.com/microlinkhq/top-user-agents/blob/master/LICENSE.md) | "Copyright © 2020 Kiko Beats" | `data-import/useragent.py` |
|
||||||
|
|
||||||
Every other category is hand-written JSON under [`data/`](data), MIT like the code.
|
Every other category is hand-written under [`data/`](data), MIT like the code.
|
||||||
|
|||||||
+33
-9
@@ -1,19 +1,43 @@
|
|||||||
# The merge gate: `docker build .` fails if vet, formatting or tests fail, and CI
|
ARG GO_VERSION=1.27.2
|
||||||
# runs exactly this. Day-to-day, prefer `docker compose run` (bind-mounts source,
|
FROM golang:${GO_VERSION} AS portable
|
||||||
# no rebuilds). GO_VERSION defaults to the latest stable Go; override it to test
|
|
||||||
# the lowest supported version: docker build --build-arg GO_VERSION=1.22.12 .
|
|
||||||
ARG GO_VERSION=1.26.4
|
|
||||||
FROM golang:${GO_VERSION}
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Module layer cached separately from source.
|
|
||||||
COPY go.mod ./
|
COPY go.mod ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN go vet ./... && \
|
RUN go vet ./... && \
|
||||||
go run github.com/fzipp/gocyclo/cmd/gocyclo@v0.6.0 -over 14 -ignore _test . && \
|
go run github.com/fzipp/gocyclo/cmd/gocyclo@v0.6.0 -over 14 -ignore _test . && \
|
||||||
{ unformatted="$(gofmt -l .)"; test -z "$unformatted" || \
|
|
||||||
{ echo "unformatted files:"; echo "$unformatted"; exit 1; }; } && \
|
|
||||||
go test -race ./...
|
go test -race ./...
|
||||||
|
|
||||||
|
FROM golang:${GO_VERSION} AS vulnerabilities
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
RUN go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 -scan module -format json > /report.json
|
||||||
|
|
||||||
|
FROM scratch AS vulnerability-report
|
||||||
|
COPY --from=vulnerabilities /report.json /
|
||||||
|
|
||||||
|
FROM python:3.14.8-slim AS release-tooling
|
||||||
|
COPY release-tooling/ /release-tooling/
|
||||||
|
RUN python3 -m unittest discover -s /release-tooling -p '*_test.py' && touch /tested
|
||||||
|
|
||||||
|
FROM python:3.14.8-slim AS vulnerability-filing
|
||||||
|
COPY vulnerability-filing/ /vulnerability-filing/
|
||||||
|
# The filer's test files an item into the plan itself.
|
||||||
|
COPY todo.md /todo.md
|
||||||
|
RUN python3 -m unittest discover -s /vulnerability-filing -p '*_test.py' && touch /tested
|
||||||
|
|
||||||
|
FROM goreleaser/goreleaser:v2.18.3 AS packaging
|
||||||
|
WORKDIR /check
|
||||||
|
COPY release-tooling/goreleaser.yaml ./
|
||||||
|
RUN git init -q && git remote add origin https://github.com/larvit/fejkdata.git && goreleaser check --config goreleaser.yaml && touch /tested
|
||||||
|
|
||||||
|
FROM portable
|
||||||
|
# BuildKit skips a stage nothing copies from.
|
||||||
|
COPY --from=release-tooling /tested /tmp/release-tooling-tested
|
||||||
|
COPY --from=vulnerability-filing /tested /tmp/vulnerability-filing-tested
|
||||||
|
COPY --from=packaging /tested /tmp/packaging-tested
|
||||||
|
RUN unformatted="$(gofmt -l .)"; test -z "$unformatted" || \
|
||||||
|
{ echo "unformatted files:"; echo "$unformatted"; exit 1; }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2026 Timewave
|
Copyright (c) 2026 Timewave
|
||||||
|
Copyright (c) 2026 Larvit
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package fejkdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/larvit/fejkdata/internal/builtinfunc"
|
||||||
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
|
"github.com/larvit/fejkdata/internal/invariant"
|
||||||
|
)
|
||||||
|
|
||||||
|
// arm is one alternative of a {a|b} token or one operand, split into the head,
|
||||||
|
// whose node `template.startOf` finds, and the tail of a dotted path into it.
|
||||||
|
type arm struct {
|
||||||
|
spelling string // as written, for messages
|
||||||
|
head string
|
||||||
|
tail []string
|
||||||
|
levels []pickKey // the key of each level the steps pass, from the head they start at to the leaf; a step's at indexes it
|
||||||
|
steps []pathStep
|
||||||
|
leaves []node // every node the path may land on, one per variant it passes
|
||||||
|
kind armKind
|
||||||
|
named *nameBinding // namedRead: the binding of the name it reads through
|
||||||
|
}
|
||||||
|
|
||||||
|
// armKind is how expand reads an arm, fixed at compile.
|
||||||
|
type armKind uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
freshRead armKind = iota // drawn afresh at every read
|
||||||
|
namedRead // read through a name, kept in its pick
|
||||||
|
)
|
||||||
|
|
||||||
|
// splitArm splits one name into head and tail. refs maps a reference to what
|
||||||
|
// resolveRefs resolved it to; before that, a reference is whole.
|
||||||
|
func splitArm(name string, refs map[string]resolvedRef) arm {
|
||||||
|
if grammar.IsRef(name) {
|
||||||
|
if r, resolved := refs[name]; resolved {
|
||||||
|
return pathArm(name, r.head, r.tail)
|
||||||
|
}
|
||||||
|
return pathArm(name, name, nil)
|
||||||
|
}
|
||||||
|
segs, err := grammar.SplitPath(name)
|
||||||
|
if err != nil || len(segs) == 1 {
|
||||||
|
return pathArm(name, name, nil)
|
||||||
|
}
|
||||||
|
return pathArm(name, segs[0], segs[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathArm(name, head string, segs []string) arm {
|
||||||
|
return arm{spelling: name, head: head, tail: segs, levels: levelKeys(append([]string{head}, segs...), 1)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// key is the key of the leaf a lands on, the one key every way of writing this read shares.
|
||||||
|
func (a arm) key() pickKey { return a.levels[len(a.levels)-1] }
|
||||||
|
|
||||||
|
func checkSegments(a arm) error {
|
||||||
|
if len(a.tail) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return grammar.CheckSegments(append([]string{a.head}, a.tail...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// op is one compiled unit of a format string: a literal run, a name read,
|
||||||
|
// or a builtin already prepared with its args.
|
||||||
|
type op struct {
|
||||||
|
grammar.Token
|
||||||
|
arms []arm // grammar.PathRead: the '|' alternatives, split into head and tail once
|
||||||
|
call builtinfunc.Call
|
||||||
|
// operands are the fields the builtin reads, in the order its operands func
|
||||||
|
// fixed; expand reads them before the call. nil for a builtin that reads none.
|
||||||
|
operands []arm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *template) compileArms(names []string, targets map[*nameBinding]nameTarget) ([]arm, error) {
|
||||||
|
if len(names) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
arms := make([]arm, len(names))
|
||||||
|
for i, name := range names {
|
||||||
|
a, err := t.compileArm(name, targets)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
arms[i] = a
|
||||||
|
}
|
||||||
|
return arms, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// compileArm compiles one read into a path: from the head it names, or, for a read through a
|
||||||
|
// name, from what that name binds.
|
||||||
|
func (t *template) compileArm(name string, targets map[*nameBinding]nameTarget) (arm, error) {
|
||||||
|
a := splitArm(name, t.refs.byName)
|
||||||
|
if !t.isName(a.head) {
|
||||||
|
start := t.startOf(a.head)
|
||||||
|
if start == nil {
|
||||||
|
panic(invariant.Broken("{%s} reads a head nothing resolved", a.spelling))
|
||||||
|
}
|
||||||
|
w := compilePath(start, a.tail)
|
||||||
|
a.steps, a.leaves = w.steps, w.leaves
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
b := t.nameScope.lookup(a.head)
|
||||||
|
if grammar.HasSelector(a.tail) {
|
||||||
|
if b.bindsField() {
|
||||||
|
return a, fmt.Errorf("a path through name %q may not select a row; bind the row to a name of its own", a.head)
|
||||||
|
}
|
||||||
|
return a, fmt.Errorf("a path through name %q may not select a row; read the row without the name, {%s}, or bind it to a name of its own", a.head, grammar.JoinSegments(append([]string{b.ref}, a.tail...)))
|
||||||
|
}
|
||||||
|
target := targets[b]
|
||||||
|
full := append(target.tail[:len(target.tail):len(target.tail)], a.tail...)
|
||||||
|
if err := provePath(target.start, full, a.head, len(target.tail)); err != nil {
|
||||||
|
return a, err
|
||||||
|
}
|
||||||
|
w := compilePath(target.start, full)
|
||||||
|
a.kind, a.named, a.steps, a.leaves = namedRead, b, w.steps, w.leaves
|
||||||
|
a.levels = levelKeys(full, 0)
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
+44
-32
@@ -6,17 +6,17 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func benchGenerator(b *testing.B, dir string) *Generator {
|
func benchGenerator(b *testing.B, data Option) *Generator {
|
||||||
b.Helper()
|
b.Helper()
|
||||||
f, err := New(WithoutShippedData(), WithDataPath(dir), WithSeed(1))
|
f, err := New(data, WithSeed(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
func benchPath(b *testing.B, dir, path string) {
|
func benchPath(b *testing.B, data Option, path string) {
|
||||||
f := benchGenerator(b, dir)
|
f := benchGenerator(b, data)
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
@@ -26,12 +26,12 @@ func benchPath(b *testing.B, dir, path string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkPerson(b *testing.B) { benchPath(b, "data", "sv_SE.person") }
|
func BenchmarkPerson(b *testing.B) { benchPath(b, withShipped(), "sv_SE.person") }
|
||||||
func BenchmarkAddress(b *testing.B) { benchPath(b, "data", "sv_SE.address") }
|
func BenchmarkAddress(b *testing.B) { benchPath(b, withShipped(), "sv_SE.address") }
|
||||||
func BenchmarkWord(b *testing.B) { benchPath(b, "data", "sv_SE.word") }
|
func BenchmarkWord(b *testing.B) { benchPath(b, withShipped(), "sv_SE.word") }
|
||||||
func BenchmarkCreditcard(b *testing.B) { benchPath(b, "data", "misc.creditcard") }
|
func BenchmarkCreditcard(b *testing.B) { benchPath(b, withShipped(), "misc.creditcard") }
|
||||||
func BenchmarkPersonnummer(b *testing.B) { benchPath(b, "data", "sv_SE.personnummer") }
|
func BenchmarkPersonnummer(b *testing.B) { benchPath(b, withShipped(), "sv_SE.personnummer") }
|
||||||
func BenchmarkUUIDv7(b *testing.B) { benchPath(b, "data", "misc.uuid") }
|
func BenchmarkUUIDv7(b *testing.B) { benchPath(b, withShipped(), "misc.uuid") }
|
||||||
|
|
||||||
func tmpData(b *testing.B, name, body string) string {
|
func tmpData(b *testing.B, name, body string) string {
|
||||||
b.Helper()
|
b.Helper()
|
||||||
@@ -44,52 +44,64 @@ func tmpData(b *testing.B, name, body string) string {
|
|||||||
|
|
||||||
func BenchmarkCalc(b *testing.B) {
|
func BenchmarkCalc(b *testing.B) {
|
||||||
dir := tmpData(b, "inv", `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":"19.99","qty":"3"}`)
|
dir := tmpData(b, "inv", `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":"19.99","qty":"3"}`)
|
||||||
benchPath(b, dir, "inv")
|
benchPath(b, WithDataPath(dir), "inv")
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkLongLiteral(b *testing.B) {
|
func BenchmarkLongLiteral(b *testing.B) {
|
||||||
dir := tmpData(b, "sql", `{"format":"INSERT INTO customers (id, name, city) VALUES (123, '{word}', '{word}');","word":["alpha","beta","gamma","delta"]}`)
|
dir := tmpData(b, "sql", `{"format":"INSERT INTO customers (id, name, city) VALUES (123, '{word}', '{word}');","word":["alpha","beta","gamma","delta"]}`)
|
||||||
benchPath(b, dir, "sql")
|
benchPath(b, WithDataPath(dir), "sql")
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkRepeat(b *testing.B) {
|
func BenchmarkRepeat(b *testing.B) {
|
||||||
dir := tmpData(b, "many", `{"format":"{word}","repeat":20,"separator":", ","word":["alpha","beta","gamma","delta"]}`)
|
dir := tmpData(b, "many", `{"format":"{word}","repeat":20,"separator":", ","word":["alpha","beta","gamma","delta"]}`)
|
||||||
benchPath(b, dir, "many")
|
benchPath(b, WithDataPath(dir), "many")
|
||||||
}
|
}
|
||||||
|
|
||||||
// BenchmarkBound measures a format that binds a head and reads two paths from it —
|
// BenchmarkNamed measures a format that binds a field to a name and reads two paths through
|
||||||
// what a correlated pair costs against BenchmarkUnbound, the same output drawn from
|
// it — what a correlated pair costs against BenchmarkUnnamed, the same output drawn from two
|
||||||
// two independent fields.
|
// independent fields.
|
||||||
func BenchmarkBound(b *testing.B) {
|
func BenchmarkNamed(b *testing.B) {
|
||||||
dir := tmpData(b, "addr", `{"format":"{place.postal-code} {place.locality}","place":[{"format":"{locality}","locality":"Stockholm","postal-code":"1{digits(2)} {digits(2)}"},{"format":"{locality}","locality":"Tranås","postal-code":"573 {digits(2)}"}]}`)
|
dir := tmpData(b, "addr", `{"format":"{place as p}{p.postal-code} {p.locality}","place":[{"format":"{locality}","locality":"Stockholm","postal-code":"1{digits(2)} {digits(2)}"},{"format":"{locality}","locality":"Tranås","postal-code":"573 {digits(2)}"}]}`)
|
||||||
benchPath(b, dir, "addr")
|
benchPath(b, WithDataPath(dir), "addr")
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkUnbound(b *testing.B) {
|
func BenchmarkUnnamed(b *testing.B) {
|
||||||
dir := tmpData(b, "addr", `{"format":"{postal-code} {locality}","postal-code":["1{digits(2)} {digits(2)}","573 {digits(2)}"],"locality":["Stockholm","Tranås"]}`)
|
dir := tmpData(b, "addr", `{"format":"{postal-code} {locality}","postal-code":["1{digits(2)} {digits(2)}","573 {digits(2)}"],"locality":["Stockholm","Tranås"]}`)
|
||||||
benchPath(b, dir, "addr")
|
benchPath(b, WithDataPath(dir), "addr")
|
||||||
}
|
}
|
||||||
|
|
||||||
// BenchmarkBoundDeep reads two paths through an intermediate level, the shape the
|
// BenchmarkNamedDeep reads two paths through a name and an intermediate level.
|
||||||
// depth question is about.
|
func BenchmarkNamedDeep(b *testing.B) {
|
||||||
func BenchmarkBoundDeep(b *testing.B) {
|
dir := tmpData(b, "addr", `{"format":"{p as q}{q.addr.city} {q.addr.zip}","p":{"format":"{addr}","addr":{"format":"{city}","city":["Stockholm","Tranås"],"zip":"1{digits(2)} {digits(2)}"}}}`)
|
||||||
dir := tmpData(b, "addr", `{"format":"{p.addr.city} {p.addr.zip}","p":{"format":"{addr}","addr":{"format":"{city}","city":["Stockholm","Tranås"],"zip":"1{digits(2)} {digits(2)}"}}}`)
|
benchPath(b, WithDataPath(dir), "addr")
|
||||||
benchPath(b, dir, "addr")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BenchmarkBoundWide reads ten paths from one row: the point where the maps an
|
// BenchmarkNamedWide reads ten paths through one name: the point where the maps a pick keeps
|
||||||
// expansion holds outgrow a single bucket.
|
// outgrow a single bucket.
|
||||||
func BenchmarkBoundWide(b *testing.B) {
|
func BenchmarkNamedWide(b *testing.B) {
|
||||||
dir := tmpData(b, "row", `{"format":"{r.a}{r.b}{r.c}{r.d}{r.e}{r.f}{r.g}{r.h}{r.i}{r.j}","r":[
|
dir := tmpData(b, "row", `{"format":"{r as s}{s.a}{s.b}{s.c}{s.d}{s.e}{s.f}{s.g}{s.h}{s.i}{s.j}","r":[
|
||||||
{"format":"x","a":"1","b":"2","c":"3","d":"4","e":"5","f":"6","g":"7","h":"8","i":"9","j":"0"},
|
{"format":"x","a":"1","b":"2","c":"3","d":"4","e":"5","f":"6","g":"7","h":"8","i":"9","j":"0"},
|
||||||
{"format":"y","a":"A","b":"B","c":"C","d":"D","e":"E","f":"F","g":"G","h":"H","i":"I","j":"J"}]}`)
|
{"format":"y","a":"A","b":"B","c":"C","d":"D","e":"E","f":"F","g":"G","h":"H","i":"I","j":"J"}]}`)
|
||||||
benchPath(b, dir, "row")
|
benchPath(b, WithDataPath(dir), "row")
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkFirstFake(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
f, err := New(withShipped(), WithSeed(1))
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := f.Fake("sv_SE.address"); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkNew(b *testing.B) {
|
func BenchmarkNew(b *testing.B) {
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
if _, err := New(WithSeed(1)); err != nil {
|
if _, err := New(withShipped(), WithSeed(1)); err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-577
@@ -1,577 +0,0 @@
|
|||||||
package fejkdata
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"unicode"
|
|
||||||
)
|
|
||||||
|
|
||||||
// maxLen caps sample output lengths (hex, nanoid, base64, digits, upper, lower)
|
|
||||||
// and maxDecimals float/calc decimal places, so a fat-fingered or overflowing
|
|
||||||
// argument fails at New instead of trying to allocate gigabytes — or panicking —
|
|
||||||
// at render.
|
|
||||||
const (
|
|
||||||
maxLen = 1 << 20
|
|
||||||
maxDecimals = 1024
|
|
||||||
)
|
|
||||||
|
|
||||||
// builtins is the registry of {name(args)} functions. Derivations read the digits
|
|
||||||
// emitted so far in the current expansion (place them after their payload);
|
|
||||||
// samples read only the rng. A time-based id (uuid v7, ulid) draws its timestamp
|
|
||||||
// from the rng, not the wall clock, so seeded output stays reproducible.
|
|
||||||
var builtins = map[string]builtin{
|
|
||||||
"luhn": {arity: 0, prep: derive(func(e string) string { return string(rune('0' + luhnCheck(e))) })},
|
|
||||||
"mod11": {arity: 0, prep: derive(mod11Check)},
|
|
||||||
"ean": {arity: 0, prep: derive(eanCheck)},
|
|
||||||
"uuid": {arity: 0, prep: sample(uuidV7)},
|
|
||||||
"ulid": {arity: 0, prep: sample(ulid)},
|
|
||||||
"nanoid": {arity: 1, check: posIntArg, prep: chars(nanoidAlphabet)},
|
|
||||||
"hex": {arity: 1, check: posIntArg, prep: chars(hexDigits)},
|
|
||||||
"digits": {arity: 1, check: posIntArg, prep: chars("0123456789"), number: func(token string, a []string) proven {
|
|
||||||
return printing(token, DataTypeString, bounded(0, math.Pow(10, float64(atoi(a[0])))-1, true))
|
|
||||||
}},
|
|
||||||
"upper": {arity: 1, check: posIntArg, prep: chars("ABCDEFGHIJKLMNOPQRSTUVWXYZ")},
|
|
||||||
"lower": {arity: 1, check: posIntArg, prep: chars("abcdefghijklmnopqrstuvwxyz")},
|
|
||||||
"base64": {arity: 1, check: posIntArg, prep: func(a []string) callFn {
|
|
||||||
n := atoi(a[0])
|
|
||||||
return func(s *session, _ string, _ []string) string {
|
|
||||||
return base64.StdEncoding.EncodeToString(randBytes(s, n))
|
|
||||||
}
|
|
||||||
}},
|
|
||||||
"int": {arity: 2, check: intRangeArgs, prep: func(a []string) callFn {
|
|
||||||
lo, span := atoi(a[0]), atoi(a[1])-atoi(a[0])+1
|
|
||||||
return func(s *session, _ string, _ []string) string { return strconv.Itoa(lo + s.IntN(span)) }
|
|
||||||
}, number: func(token string, a []string) proven {
|
|
||||||
return printing(token, DataTypeInteger, bounded(float64(atoi(a[0])), float64(atoi(a[1])), true))
|
|
||||||
}},
|
|
||||||
"float": {arity: 3, check: floatArgs, prep: func(a []string) callFn {
|
|
||||||
lo, hi, dp := atof(a[0]), atof(a[1]), atoi(a[2])
|
|
||||||
return func(s *session, _ string, _ []string) string {
|
|
||||||
return formatFloat(lo+s.Float64()*(hi-lo), dp)
|
|
||||||
}
|
|
||||||
}, number: func(token string, a []string) proven {
|
|
||||||
return printedNumber(token, bounded(atof(a[0]), atof(a[1]), false), atoi(a[2]))
|
|
||||||
}},
|
|
||||||
"iban": {arity: 1, check: ibanArg, prep: func(a []string) callFn {
|
|
||||||
cc := a[0]
|
|
||||||
return func(s *session, _ string, _ []string) string { return iban(s, cc) }
|
|
||||||
}},
|
|
||||||
"date": {arity: -1, check: dateArgs, prep: datePrep},
|
|
||||||
"time": {arity: -1, check: timeArg, prep: timePrep},
|
|
||||||
"calc": {arity: -1, check: checkCalc, prep: calcPrep, operands: calcOperands},
|
|
||||||
"lowercase": {arity: 1, check: transformArg, prep: transformPrep(strings.ToLower), operands: transformOperand},
|
|
||||||
"uppercase": {arity: 1, check: transformArg, prep: transformPrep(strings.ToUpper), operands: transformOperand},
|
|
||||||
"ascii": {arity: 1, check: transformArg, prep: transformPrep(asciiFold), operands: transformOperand},
|
|
||||||
// seq is the one stateful builtin: a per-session counter from 1, advancing on
|
|
||||||
// each call. An optional name selects an independent counter; no name uses the
|
|
||||||
// default one. Deterministic by construction, so seeded output stays stable.
|
|
||||||
"seq": {arity: -1, check: seqArg, prep: func(a []string) callFn {
|
|
||||||
key := ""
|
|
||||||
if len(a) == 1 {
|
|
||||||
key = a[0]
|
|
||||||
}
|
|
||||||
return func(s *session, _ string, _ []string) string {
|
|
||||||
return strconv.FormatUint(s.next(key), 10)
|
|
||||||
}
|
|
||||||
}, number: func(token string, _ []string) proven {
|
|
||||||
return printing(token, DataTypeInteger, bounded(1, math.MaxInt64, true))
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
|
|
||||||
// derive and sample are the two argument-free builtin shapes: a derivation reads
|
|
||||||
// the output emitted so far, a sample reads only the rng. chars is the shape of a
|
|
||||||
// sample of n characters drawn from an alphabet.
|
|
||||||
func derive(f func(emitted string) string) func([]string) callFn {
|
|
||||||
return func([]string) callFn {
|
|
||||||
return func(_ *session, emitted string, _ []string) string { return f(emitted) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sample(f func(rng) string) func([]string) callFn {
|
|
||||||
return func([]string) callFn {
|
|
||||||
return func(s *session, _ string, _ []string) string { return f(s) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func chars(alphabet string) func([]string) callFn {
|
|
||||||
return func(a []string) callFn {
|
|
||||||
n := atoi(a[0])
|
|
||||||
return func(s *session, _ string, _ []string) string { return randChars(s, n, alphabet) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hexDigits = "0123456789abcdef"
|
|
||||||
|
|
||||||
// formatFloat prints v to dp decimals, -1 for the shortest form, and a zero unsigned.
|
|
||||||
func formatFloat(v float64, dp int) string {
|
|
||||||
s := strconv.FormatFloat(v, 'f', dp, 64)
|
|
||||||
if strings.HasPrefix(s, "-") && strings.Trim(s, "-0.") == "" {
|
|
||||||
return s[1:]
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// transforms are the builtins that rewrite one operand's value; they nest, so
|
|
||||||
// {lowercase(ascii(x))} folds then lowers.
|
|
||||||
var transforms = map[string]func(string) string{
|
|
||||||
"ascii": asciiFold,
|
|
||||||
"lowercase": strings.ToLower,
|
|
||||||
"uppercase": strings.ToUpper,
|
|
||||||
}
|
|
||||||
|
|
||||||
// unwrapTransform peels nested transform calls off an operand arg, returning the
|
|
||||||
// field it finally names and the transforms to apply, innermost last.
|
|
||||||
func unwrapTransform(arg string) (leaf string, chain []func(string) string, err error) {
|
|
||||||
for {
|
|
||||||
name, args, isCall := funcCall(arg)
|
|
||||||
if !isCall {
|
|
||||||
return arg, chain, nil
|
|
||||||
}
|
|
||||||
fn, isTransform := transforms[name]
|
|
||||||
if !isTransform {
|
|
||||||
return "", nil, fmt.Errorf("%s(%s) is not a transform, so it cannot be an operand", name, strings.Join(args, ","))
|
|
||||||
}
|
|
||||||
if len(args) != 1 {
|
|
||||||
return "", nil, fmt.Errorf("%s takes 1 arg, got %d", name, len(args))
|
|
||||||
}
|
|
||||||
chain = append(chain, fn)
|
|
||||||
arg = args[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func transformArg(fields map[string]node, a []string) error {
|
|
||||||
leaf, _, err := unwrapTransform(a[0])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if isRef(leaf) {
|
|
||||||
_, _, err := refShape(leaf)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return checkArm(leaf, fields, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func transformOperand(a []string) []string {
|
|
||||||
leaf, _, err := unwrapTransform(a[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return []string{leaf}
|
|
||||||
}
|
|
||||||
|
|
||||||
func transformPrep(outer func(string) string) func([]string) callFn {
|
|
||||||
return func(a []string) callFn {
|
|
||||||
_, chain, err := unwrapTransform(a[0])
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("fejkdata: transform arg %q reached prep unvalidated: %v", a[0], err))
|
|
||||||
}
|
|
||||||
return func(_ *session, _ string, operands []string) string {
|
|
||||||
v := operands[0]
|
|
||||||
for i := len(chain) - 1; i >= 0; i-- {
|
|
||||||
v = chain[i](v)
|
|
||||||
}
|
|
||||||
return outer(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// asciiFolds maps the Latin letters with diacritics or ligatures to ASCII.
|
|
||||||
var asciiFolds = map[rune]string{
|
|
||||||
'À': "A", 'Á': "A", 'Â': "A", 'Ã': "A", 'Ä': "A", 'Å': "A", 'Æ': "AE", 'Ç': "C",
|
|
||||||
'È': "E", 'É': "E", 'Ê': "E", 'Ë': "E", 'Ì': "I", 'Í': "I", 'Î': "I", 'Ï': "I",
|
|
||||||
'Ð': "D", 'Ñ': "N", 'Ò': "O", 'Ó': "O", 'Ô': "O", 'Õ': "O", 'Ö': "O", 'Ø': "O",
|
|
||||||
'Ù': "U", 'Ú': "U", 'Û': "U", 'Ü': "U", 'Ý': "Y", 'Þ': "Th", 'ß': "ss", 'Œ': "OE",
|
|
||||||
'à': "a", 'á': "a", 'â': "a", 'ã': "a", 'ä': "a", 'å': "a", 'æ': "ae", 'ç': "c",
|
|
||||||
'è': "e", 'é': "e", 'ê': "e", 'ë': "e", 'ì': "i", 'í': "i", 'î': "i", 'ï': "i",
|
|
||||||
'ð': "d", 'ñ': "n", 'ò': "o", 'ó': "o", 'ô': "o", 'õ': "o", 'ö': "o", 'ø': "o",
|
|
||||||
'ù': "u", 'ú': "u", 'û': "u", 'ü': "u", 'ý': "y", 'þ': "th", 'ÿ': "y", 'œ': "oe",
|
|
||||||
}
|
|
||||||
|
|
||||||
// asciiFold rewrites s to ASCII: folded Latin letters stay, any other non-ASCII
|
|
||||||
// rune is dropped.
|
|
||||||
func asciiFold(s string) string {
|
|
||||||
var b strings.Builder
|
|
||||||
for _, r := range s {
|
|
||||||
if r <= unicode.MaxASCII {
|
|
||||||
b.WriteRune(r)
|
|
||||||
} else {
|
|
||||||
b.WriteString(asciiFolds[r])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// atoi parses an arg a builtin's check already validated. It panics rather than
|
|
||||||
// returning zero, so a check that stops covering its own args is a stack trace and
|
|
||||||
// not a silently wrong length, range or decimal count.
|
|
||||||
func atoi(s string) int {
|
|
||||||
n, err := strconv.Atoi(s)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("fejkdata: builtin arg %q reached prep unvalidated: %v", s, err))
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
// atof is atoi for a float arg, and reports an unvalidated one the same way.
|
|
||||||
func atof(s string) float64 {
|
|
||||||
f, err := strconv.ParseFloat(s, 64)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("fejkdata: builtin arg %q reached prep unvalidated: %v", s, err))
|
|
||||||
}
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
|
||||||
func randBytes(r rng, n int) []byte {
|
|
||||||
b := make([]byte, n)
|
|
||||||
for i := range b {
|
|
||||||
b[i] = byte(r.IntN(256))
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func randChars(r rng, n int, alphabet string) string {
|
|
||||||
b := make([]byte, n)
|
|
||||||
for i := range b {
|
|
||||||
b[i] = alphabet[r.IntN(len(alphabet))]
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// plainInt parses an integer arg written the one way: no sign, no leading zero.
|
|
||||||
func plainInt(s string) (int, error) {
|
|
||||||
n, err := strconv.Atoi(s)
|
|
||||||
if errors.Is(err, strconv.ErrRange) {
|
|
||||||
return 0, fmt.Errorf("%q is past the integer range: %w", s, err)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("%q is not an integer", s)
|
|
||||||
}
|
|
||||||
if strconv.Itoa(n) != s {
|
|
||||||
return 0, fmt.Errorf("%q is not a plain integer; write %d", s, n)
|
|
||||||
}
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func posIntArg(_ map[string]node, a []string) error {
|
|
||||||
n, err := plainInt(a[0])
|
|
||||||
if errors.Is(err, strconv.ErrRange) {
|
|
||||||
return fmt.Errorf("count %q exceeds the maximum %d", a[0], maxLen)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("count %w", err)
|
|
||||||
}
|
|
||||||
if n < 1 {
|
|
||||||
return fmt.Errorf("count %q must be positive", a[0])
|
|
||||||
}
|
|
||||||
if n > maxLen {
|
|
||||||
return fmt.Errorf("count %d exceeds the maximum %d", n, maxLen)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func intRangeArgs(_ map[string]node, a []string) error {
|
|
||||||
lo, err := plainInt(a[0])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("int(min,max): min %w", err)
|
|
||||||
}
|
|
||||||
hi, err := plainInt(a[1])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("int(min,max): max %w", err)
|
|
||||||
}
|
|
||||||
if lo > hi {
|
|
||||||
return fmt.Errorf("int(min,max): min %d > max %d", lo, hi)
|
|
||||||
}
|
|
||||||
if lo == hi {
|
|
||||||
return fmt.Errorf("int(%d,%d) is the constant %d; write it as text", lo, hi, lo)
|
|
||||||
}
|
|
||||||
if uint64(hi)-uint64(lo) >= uint64(math.MaxInt64) { // span hi-lo+1 would overflow int -> IntN panic
|
|
||||||
return fmt.Errorf("int(min,max): range %d..%d is too wide", lo, hi)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func floatArgs(_ map[string]node, a []string) error {
|
|
||||||
lo, e1 := strconv.ParseFloat(a[0], 64)
|
|
||||||
hi, e2 := strconv.ParseFloat(a[1], 64)
|
|
||||||
if e1 != nil || e2 != nil {
|
|
||||||
return fmt.Errorf("float(min,max,dp) needs numeric bounds, got %q,%q", a[0], a[1])
|
|
||||||
}
|
|
||||||
dp, err := plainInt(a[2])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("float(min,max,dp): decimals %w", err)
|
|
||||||
}
|
|
||||||
if math.IsNaN(lo) || math.IsNaN(hi) || math.IsInf(lo, 0) || math.IsInf(hi, 0) {
|
|
||||||
return fmt.Errorf("float(min,max,dp) needs finite bounds, got %q,%q", a[0], a[1])
|
|
||||||
}
|
|
||||||
if lo > hi {
|
|
||||||
return fmt.Errorf("float(min,max,dp): min %v > max %v", lo, hi)
|
|
||||||
}
|
|
||||||
if dp < 0 || dp > maxDecimals {
|
|
||||||
return fmt.Errorf("float(min,max,dp): decimals %d out of range 0..%d", dp, maxDecimals)
|
|
||||||
}
|
|
||||||
if lo == hi {
|
|
||||||
return fmt.Errorf("float(%s,%s,%d) is the constant %q; write it as text", a[0], a[1], dp, strconv.FormatFloat(lo, 'f', dp, 64))
|
|
||||||
}
|
|
||||||
if math.IsInf(hi-lo, 0) { // an overflowing span would render as "+Inf"
|
|
||||||
return fmt.Errorf("float(min,max,dp): range %v..%v is too wide", lo, hi)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func seqArg(_ map[string]node, a []string) error {
|
|
||||||
if len(a) > 1 {
|
|
||||||
return fmt.Errorf("seq takes at most one name, got %d args", len(a))
|
|
||||||
}
|
|
||||||
if len(a) == 1 && a[0] == "" {
|
|
||||||
return fmt.Errorf("seq name must not be empty")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// nanoidAlphabet is the 64-char URL-safe set Nano IDs use.
|
|
||||||
const nanoidAlphabet = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
||||||
|
|
||||||
// uuidV7 builds an RFC 9562 v7 UUID. The 48-bit timestamp field is drawn from
|
|
||||||
// the rng (not the clock) to stay reproducible, then the version (7) and variant
|
|
||||||
// (10) bits are forced; the rest is random.
|
|
||||||
func uuidV7(r rng) string {
|
|
||||||
b := randBytes(r, 16)
|
|
||||||
b[6] = b[6]&0x0f | 0x70
|
|
||||||
b[8] = b[8]&0x3f | 0x80
|
|
||||||
var sb strings.Builder
|
|
||||||
for i, x := range b {
|
|
||||||
if i == 4 || i == 6 || i == 8 || i == 10 {
|
|
||||||
sb.WriteByte('-')
|
|
||||||
}
|
|
||||||
sb.WriteByte(hexDigits[x>>4])
|
|
||||||
sb.WriteByte(hexDigits[x&0x0f])
|
|
||||||
}
|
|
||||||
return sb.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// crockford is the ULID/Crockford base32 alphabet (no I, L, O, U).
|
|
||||||
const crockford = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
||||||
|
|
||||||
// ulid builds a 26-char ULID: 128 random bits (timestamp from the rng) encoded
|
|
||||||
// big-endian into base32, the 130-bit stream left-padded with two zero bits.
|
|
||||||
func ulid(r rng) string {
|
|
||||||
b := randBytes(r, 16)
|
|
||||||
out := make([]byte, 26)
|
|
||||||
for i := range out {
|
|
||||||
v := 0
|
|
||||||
for j := 0; j < 5; j++ { // 5 bits per char; the two leading pad bits are zero
|
|
||||||
real := 5*i + j - 2
|
|
||||||
bit := 0
|
|
||||||
if real >= 0 {
|
|
||||||
bit = int(b[real/8]>>(7-uint(real%8))) & 1
|
|
||||||
}
|
|
||||||
v = v<<1 | bit
|
|
||||||
}
|
|
||||||
out[i] = crockford[v]
|
|
||||||
}
|
|
||||||
return string(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
// luhnCheck returns the Luhn check digit (0-9) over the digits of s; non-digit
|
|
||||||
// runes are skipped. Doubling runs from the rightmost digit, so the result is
|
|
||||||
// correct whatever the payload length.
|
|
||||||
func luhnCheck(s string) int {
|
|
||||||
sum, double := 0, true
|
|
||||||
for i := len(s) - 1; i >= 0; i-- {
|
|
||||||
c := s[i]
|
|
||||||
if c < '0' || c > '9' {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
d := int(c - '0')
|
|
||||||
if double {
|
|
||||||
if d *= 2; d > 9 {
|
|
||||||
d -= 9
|
|
||||||
}
|
|
||||||
}
|
|
||||||
double = !double
|
|
||||||
sum += d
|
|
||||||
}
|
|
||||||
return (10 - sum%10) % 10
|
|
||||||
}
|
|
||||||
|
|
||||||
// mod11Check returns the weighted mod-11 check character over the digits of s
|
|
||||||
// (weights 2..7 cycling from the right). A would-be value of 10 emits 'X', as in
|
|
||||||
// ISBN-10 / ISO 7064; non-digits are skipped.
|
|
||||||
func mod11Check(s string) string {
|
|
||||||
sum, w := 0, 2
|
|
||||||
for i := len(s) - 1; i >= 0; i-- {
|
|
||||||
c := s[i]
|
|
||||||
if c < '0' || c > '9' {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sum += int(c-'0') * w
|
|
||||||
if w++; w > 7 {
|
|
||||||
w = 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if chk := (11 - sum%11) % 11; chk != 10 {
|
|
||||||
return string(rune('0' + chk))
|
|
||||||
}
|
|
||||||
return "X"
|
|
||||||
}
|
|
||||||
|
|
||||||
// eanCheck returns the EAN-13 / UPC-A / ISBN-13 / GTIN check digit over the
|
|
||||||
// digits of s: weights 3 and 1 alternating from the rightmost digit, mod 10.
|
|
||||||
func eanCheck(s string) string {
|
|
||||||
sum, w := 0, 3
|
|
||||||
for i := len(s) - 1; i >= 0; i-- {
|
|
||||||
c := s[i]
|
|
||||||
if c < '0' || c > '9' {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sum += int(c-'0') * w
|
|
||||||
w = 4 - w // 3 <-> 1
|
|
||||||
}
|
|
||||||
return string(rune('0' + (10-sum%10)%10))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ibanLen maps a supported country code to the full IBAN length. The check digits
|
|
||||||
// sit between the country code and the BBAN, so — unlike luhn/ean — iban can't be
|
|
||||||
// a left-to-right derivation; it generates the whole value instead.
|
|
||||||
var ibanLen = map[string]int{"BE": 16, "DE": 22, "DK": 18, "ES": 24, "FI": 18, "NO": 15, "SE": 24}
|
|
||||||
|
|
||||||
func ibanArg(_ map[string]node, a []string) error {
|
|
||||||
if _, ok := ibanLen[a[0]]; !ok {
|
|
||||||
return fmt.Errorf("iban(%q): unsupported country code", a[0])
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// iban generates a structurally valid IBAN for cc: a numeric BBAN of the right
|
|
||||||
// length, then mod-97 check digits. Real bank/branch structure isn't modelled —
|
|
||||||
// the result passes length and checksum validation, which is what fake data needs.
|
|
||||||
func iban(r rng, cc string) string {
|
|
||||||
bban := make([]byte, ibanLen[cc]-4)
|
|
||||||
for i := range bban {
|
|
||||||
bban[i] = byte('0' + r.IntN(10))
|
|
||||||
}
|
|
||||||
rem := 0
|
|
||||||
feed := func(d int) { rem = (rem*10 + d) % 97 }
|
|
||||||
for _, c := range bban {
|
|
||||||
feed(int(c - '0'))
|
|
||||||
}
|
|
||||||
for i := 0; i < len(cc); i++ { // letters A-Z -> 10..35, fed as two digits
|
|
||||||
v := int(cc[i]-'A') + 10
|
|
||||||
feed(v / 10)
|
|
||||||
feed(v % 10)
|
|
||||||
}
|
|
||||||
feed(0)
|
|
||||||
feed(0)
|
|
||||||
return fmt.Sprintf("%s%02d%s", cc, 98-rem, bban)
|
|
||||||
}
|
|
||||||
|
|
||||||
const dayLayout = "2006-01-02"
|
|
||||||
|
|
||||||
// Alike in no field; layoutDay differs from layoutProbe in its date fields only.
|
|
||||||
var (
|
|
||||||
layoutProbe = time.Date(2001, 2, 3, 4, 5, 6, 0, time.UTC)
|
|
||||||
layoutProbe2 = time.Date(2010, 11, 12, 13, 14, 15, 0, time.UTC)
|
|
||||||
layoutDay = time.Date(2010, 11, 12, 4, 5, 6, 0, time.UTC)
|
|
||||||
)
|
|
||||||
|
|
||||||
// layoutArg is the Go layout a quoted arg holds, refused when unquoted or constant.
|
|
||||||
func layoutArg(a string) (string, error) {
|
|
||||||
if len(a) < 2 || a[0] != '\'' || a[len(a)-1] != '\'' {
|
|
||||||
return "", fmt.Errorf("layout %s is not quoted; write '%s'", a, strings.Trim(a, "'"))
|
|
||||||
}
|
|
||||||
layout := a[1 : len(a)-1]
|
|
||||||
if layoutProbe.Format(layout) == layoutProbe2.Format(layout) {
|
|
||||||
return "", fmt.Errorf("layout '%s' names no field, so it is the constant %q; write it as text", layout, layout)
|
|
||||||
}
|
|
||||||
return layout, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// layoutOf is layoutArg for an arg a check already validated.
|
|
||||||
func layoutOf(a string) string {
|
|
||||||
layout, err := layoutArg(a)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("fejkdata: builtin arg %q reached prep unvalidated: %v", a, err))
|
|
||||||
}
|
|
||||||
return layout
|
|
||||||
}
|
|
||||||
|
|
||||||
// layoutArity checks a call ending in a layout takes n args, naming the quoted
|
|
||||||
// layout when an unquoted one split into more.
|
|
||||||
func layoutArity(name string, n int, a []string) error {
|
|
||||||
if len(a) == n {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
hint := ""
|
|
||||||
if len(a) > n {
|
|
||||||
hint = fmt.Sprintf("; a layout holding a comma is quoted: '%s'", strings.Join(a[n-1:], ", "))
|
|
||||||
}
|
|
||||||
return fmt.Errorf("%s takes %d args, got %d%s", name, n, len(a), hint)
|
|
||||||
}
|
|
||||||
|
|
||||||
func dateArgs(_ map[string]node, a []string) error {
|
|
||||||
if err := layoutArity("date", 3, a); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
from, err := time.Parse(dayLayout, a[0])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("date(from,to,layout): from %q is not a YYYY-MM-DD date", a[0])
|
|
||||||
}
|
|
||||||
to, err := time.Parse(dayLayout, a[1])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("date(from,to,layout): to %q is not a YYYY-MM-DD date", a[1])
|
|
||||||
}
|
|
||||||
if !from.Before(to) {
|
|
||||||
return fmt.Errorf("date(from,to,layout): from %s is not before to %s", a[0], a[1])
|
|
||||||
}
|
|
||||||
layout, err := layoutArg(a[2])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if layoutProbe.Format(layout) == layoutDay.Format(layout) {
|
|
||||||
return fmt.Errorf("date(from,to,layout): '%s' names no date field; write time('%s')", layout, layout)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func timeArg(_ map[string]node, a []string) error {
|
|
||||||
if err := layoutArity("time", 1, a); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
layout, err := layoutArg(a[0])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if layoutProbe.Format(layout) != layoutDay.Format(layout) {
|
|
||||||
return fmt.Errorf("time(layout): '%s' names a date field; write date(from,to,layout)", layout)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// datePrep draws a second in [from 00:00:00, to 23:59:59] UTC; the span is counted
|
|
||||||
// in seconds, since a Duration overflows past 292 years.
|
|
||||||
func datePrep(a []string) callFn {
|
|
||||||
from, err := time.Parse(dayLayout, a[0])
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("fejkdata: builtin arg %q reached prep unvalidated: %v", a[0], err))
|
|
||||||
}
|
|
||||||
to, err := time.Parse(dayLayout, a[1])
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("fejkdata: builtin arg %q reached prep unvalidated: %v", a[1], err))
|
|
||||||
}
|
|
||||||
layout, start, span := layoutOf(a[2]), from.Unix(), int(to.Unix()-from.Unix())+86400
|
|
||||||
return func(s *session, _ string, _ []string) string {
|
|
||||||
return time.Unix(start+int64(s.IntN(span)), 0).UTC().Format(layout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func timePrep(a []string) callFn {
|
|
||||||
layout := layoutOf(a[0])
|
|
||||||
return func(s *session, _ string, _ []string) string {
|
|
||||||
return time.Unix(int64(s.IntN(86400)), 0).UTC().Format(layout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+153
-55
@@ -2,11 +2,14 @@ package fejkdata
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/larvit/fejkdata/internal/invariant"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBuiltinIDGenerators(t *testing.T) {
|
func TestBuiltinIDGenerators(t *testing.T) {
|
||||||
@@ -101,10 +104,10 @@ func TestBuiltinChecksums(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestBuiltinSeqPerSession pins seq's contract: a counter from 1, advancing on
|
// TestBuiltinSeqPerGenerator pins seq's contract: a counter from 1, advancing on
|
||||||
// each call, named counters independent, and the whole thing scoped to one
|
// each call, named counters independent, and the whole thing scoped to one
|
||||||
// session so a fresh Generator restarts at 1.
|
// draw state so a fresh Generator restarts at 1.
|
||||||
func TestBuiltinSeqPerSession(t *testing.T) {
|
func TestBuiltinSeqPerGenerator(t *testing.T) {
|
||||||
f := engine(1)
|
f := engine(1)
|
||||||
for i := 1; i <= 5; i++ {
|
for i := 1; i <= 5; i++ {
|
||||||
if got := mustRender(t, f, `"{seq()}"`); got != strconv.Itoa(i) {
|
if got := mustRender(t, f, `"{seq()}"`); got != strconv.Itoa(i) {
|
||||||
@@ -121,9 +124,9 @@ func TestBuiltinSeqPerSession(t *testing.T) {
|
|||||||
if got := mustRender(t, engine(2), `{"format":"{seq()}","repeat":3,"separator":","}`); got != "1,2,3" {
|
if got := mustRender(t, engine(2), `{"format":"{seq()}","repeat":3,"separator":","}`); got != "1,2,3" {
|
||||||
t.Fatalf("repeat seq = %q, want 1,2,3", got)
|
t.Fatalf("repeat seq = %q, want 1,2,3", got)
|
||||||
}
|
}
|
||||||
// ...and a fresh session restarts from 1.
|
// ...and a fresh generator restarts from 1.
|
||||||
if got := mustRender(t, engine(2), `"{seq()}"`); got != "1" {
|
if got := mustRender(t, engine(2), `"{seq()}"`); got != "1" {
|
||||||
t.Fatalf("new session seq = %q, want 1", got)
|
t.Fatalf("new generator seq = %q, want 1", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +145,7 @@ func TestBuiltinArgLimits(t *testing.T) {
|
|||||||
`{"format":"{x}","x":"1","repeat":2000000000}`, // repeat -> multi-GB join
|
`{"format":"{x}","x":"1","repeat":2000000000}`, // repeat -> multi-GB join
|
||||||
}
|
}
|
||||||
for _, tmpl := range bad {
|
for _, tmpl := range bad {
|
||||||
if _, err := compile(parse(t, tmpl)); err == nil {
|
if _, err := resolved(t, tmpl); err == nil {
|
||||||
t.Errorf("compile(%s) = nil error, want a limit rejection", tmpl)
|
t.Errorf("compile(%s) = nil error, want a limit rejection", tmpl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,51 +190,26 @@ func ibanValid(s string) bool {
|
|||||||
return rem == 1
|
return rem == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRegistryShapes pins the builtin contract compileOps relies on: every entry
|
// mustPanic fails unless call panics with invariant.Broken's phrase, which is what
|
||||||
// supplies prep, and args parsed at compile only behind a check.
|
// separates a reported invariant break from a silently wrong value.
|
||||||
func TestRegistryShapes(t *testing.T) {
|
|
||||||
for name, b := range builtins {
|
|
||||||
if b.prep == nil {
|
|
||||||
t.Errorf("builtin %q has no prep: compileOps would call a nil func", name)
|
|
||||||
}
|
|
||||||
if b.arity != 0 && b.check == nil {
|
|
||||||
t.Errorf("builtin %q parses args in prep with no check", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestArgGuardsPanic pins the guards that report a builtin arg its check should
|
|
||||||
// have rejected. No data reaches them — checkFunc runs a builtin's check before
|
|
||||||
// compileOps ever calls prep — so they are exercised directly.
|
|
||||||
func TestArgGuardsPanic(t *testing.T) {
|
|
||||||
for name, call := range map[string]func(){
|
|
||||||
"atoi on an unvalidated arg": func() { atoi("nope") },
|
|
||||||
"atof on an unvalidated arg": func() { atof("nope") },
|
|
||||||
} {
|
|
||||||
mustPanic(t, name, call)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// mustPanic fails unless call panics, which is what separates a reported
|
|
||||||
// invariant break from a silently wrong value.
|
|
||||||
func mustPanic(t *testing.T, name string, call func()) {
|
func mustPanic(t *testing.T, name string, call func()) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
defer func() {
|
defer func() {
|
||||||
if recover() == nil {
|
if r := recover(); r == nil || !strings.HasPrefix(fmt.Sprint(r), invariant.Broken("")) {
|
||||||
t.Errorf("%s: no panic, want the invariant reported", name)
|
t.Errorf("%s: recovered %v, want the invariant reported as an internal error", name, r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
call()
|
call()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClassBuiltinArgs(t *testing.T) {
|
func TestClassBuiltinArgs(t *testing.T) {
|
||||||
for _, bad := range []string{`"{digits(0)}"`, `"{digits(2000000000)}"`, `"{upper(-1)}"`, `"{lower(x)}"`, `"{digits()}"`, `"{upper(1,2)}"`} {
|
for _, bad := range []string{`"{digits(2000000000)}"`, `"{upper(-1)}"`, `"{lower(x)}"`, `"{digits()}"`, `"{upper(1,2)}"`} {
|
||||||
if _, err := compile(parse(t, bad)); err == nil {
|
if _, err := resolved(t, bad); err == nil {
|
||||||
t.Errorf("compile(%s) = nil error, want the arg rejected", bad)
|
t.Errorf("compile(%s) = nil error, want the arg rejected", bad)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, ok := range []string{`"{digits(1048576)}"`, `"{upper(1)}"`, `"{lower(26)}"`} {
|
for _, ok := range []string{`"{digits(1048576)}"`, `"{upper(1)}"`, `"{lower(26)}"`} {
|
||||||
if _, err := compile(parse(t, ok)); err != nil {
|
if _, err := resolved(t, ok); err != nil {
|
||||||
t.Errorf("compile(%s) = %v", ok, err)
|
t.Errorf("compile(%s) = %v", ok, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -293,28 +271,148 @@ func TestBuiltinDateAndTime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestBuiltinDateArgs pins the New-time checks: bounds are calendar dates in order,
|
// TestBuiltinDateArgs pins the New-time checks: bounds are calendar dates in order, and the
|
||||||
// the layout is quoted, names a field, and for time names no date field.
|
// layout is quoted.
|
||||||
func TestBuiltinDateArgs(t *testing.T) {
|
func TestBuiltinDateArgs(t *testing.T) {
|
||||||
for tmpl, want := range map[string]string{
|
for tmpl, want := range map[string]string{
|
||||||
`"{date(1990-13-01,1990-12-31,'2006-01-02')}"`: "1990-13-01",
|
`"{date(1990-13-01,1990-12-31,'2006-01-02')}"`: "1990-13-01",
|
||||||
`"{date(1990-12-31,1990-01-01,'2006-01-02')}"`: "before",
|
`"{time('2006-01-02 15:04')}"`: "write date(from,to,layout)",
|
||||||
`"{date(1990-01-01,1990-01-01,'2006-01-02')}"`: "before",
|
`"{date(1990-12-31,1990-01-01,'2006-01-02')}"`: "is after",
|
||||||
`"{date(1990-01-01,1990-12-31,2006-01-02)}"`: "'2006-01-02'",
|
`"{date(1990-01-01,1990-12-31,2006-01-02)}"`: "'2006-01-02'",
|
||||||
`"{date(1990-01-01,1990-12-31,'January 2, 2006)}"`: "'",
|
`"{date(1990-01-01,1990-12-31,'January 2, 2006)}"`: "'",
|
||||||
`"{date(1990-01-01,1990-12-31,'x')}"`: "text",
|
`"{date(1990-01-01,1990-12-31,\"January 2, 2006\")}"`: "quoted: 'January 2, 2006'",
|
||||||
`"{date(1990-01-01,1990-12-31,'')}"`: "text",
|
`"{date(1990-01-01,1990-12-31)}"`: "3 arguments",
|
||||||
`"{date(1990-01-01,1990-12-31)}"`: "3 args",
|
`"{date(1990-01-01,1990-12-31,January 2, 2006)}"`: "'January 2, 2006'",
|
||||||
`"{date(1990-01-01,1990-12-31,January 2, 2006)}"`: "'January 2, 2006'",
|
`"{time(3:04 PM, Mon)}"`: "'3:04 PM, Mon'",
|
||||||
`"{time(3:04 PM, Mon)}"`: "'3:04 PM, Mon'",
|
`"{time(15:04)}"`: "'15:04'",
|
||||||
`"{time(15:04)}"`: "'15:04'",
|
|
||||||
`"{time('2006-01-02 15:04')}"`: "date(",
|
|
||||||
`"{time('x')}"`: "text",
|
|
||||||
`"{date(1990-01-01,1990-12-31,'15:04')}"`: "time('15:04')",
|
|
||||||
} {
|
} {
|
||||||
_, err := compile(parse(t, tmpl))
|
_, err := resolved(t, tmpl)
|
||||||
if err == nil || !strings.Contains(err.Error(), want) {
|
if err == nil || !strings.Contains(err.Error(), want) {
|
||||||
t.Errorf("compile(%s) = %v, want an error mentioning %q", tmpl, err, want)
|
t.Errorf("compile(%s) = %v, want an error mentioning %q", tmpl, err, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBuiltinLayoutErrorsNameARunnableSpelling pins the two layout errors a user
|
||||||
|
// can follow: a double-quoted layout is named single-quoted, without its own
|
||||||
|
// quotes carried into the suggestion, whether it split on a comma or not.
|
||||||
|
func TestBuiltinLayoutErrorsNameARunnableSpelling(t *testing.T) {
|
||||||
|
_, err := compile(parse(t, `"{date(1990-01-01,1990-12-31,\"2006-01-02\")}"`))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "write '2006-01-02'") {
|
||||||
|
t.Fatalf("a double-quoted layout = %v, want it named single-quoted", err)
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), `'"`) {
|
||||||
|
t.Errorf("%v names a layout that renders its own quotes", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "is double-quoted") {
|
||||||
|
t.Errorf("%v does not say which quotes were wrong", err)
|
||||||
|
}
|
||||||
|
_, err = compile(parse(t, `"{time(15:04)}"`))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "write '15:04'") {
|
||||||
|
t.Fatalf("a bare layout = %v, want it named quoted", err)
|
||||||
|
}
|
||||||
|
// The comma hint belongs to a layout that split, not to a call given extra args.
|
||||||
|
_, err = compile(parse(t, `"{time(0,12,'15:04')}"`))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "takes 1 argument, got 3") {
|
||||||
|
t.Fatalf("time with three args = %v, want the count named in the singular", err)
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "holding a comma") {
|
||||||
|
t.Errorf("%v offers the comma hint though the layout is already quoted", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBuiltinDateSpansOneDay pins that from == to is a day: with a clock layout it
|
||||||
|
// draws every second of it, and without one it prints that day.
|
||||||
|
func TestBuiltinDateSpansOneDay(t *testing.T) {
|
||||||
|
f := engine(1)
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for i := 0; i < 500; i++ {
|
||||||
|
got := mustRender(t, f, `"{date(2026-01-01,2026-01-01,'2006-01-02 15:04:05')}"`)
|
||||||
|
if !strings.HasPrefix(got, "2026-01-01 ") {
|
||||||
|
t.Fatalf("date over one day = %q, out of range", got)
|
||||||
|
}
|
||||||
|
seen[got] = true
|
||||||
|
}
|
||||||
|
if len(seen) < 400 {
|
||||||
|
t.Fatalf("date over one day drew %d distinct seconds in 500, want the whole day", len(seen))
|
||||||
|
}
|
||||||
|
if got := mustRender(t, f, `"{date(2026-01-01,2026-01-01,'2006-01-02')}"`); got != "2026-01-01" {
|
||||||
|
t.Fatalf("one day in a date-only layout = %q, want that day", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClassBuiltins(t *testing.T) {
|
||||||
|
cases := map[string]*regexp.Regexp{
|
||||||
|
`"{digits(1)}"`: regexp.MustCompile(`^[0-9]$`),
|
||||||
|
`"{digits(3)}"`: regexp.MustCompile(`^[0-9]{3}$`),
|
||||||
|
`"{int(1,9)}"`: regexp.MustCompile(`^[1-9]$`),
|
||||||
|
`"{upper(1)}"`: regexp.MustCompile(`^[A-Z]$`),
|
||||||
|
`"{lower(1)}"`: regexp.MustCompile(`^[a-z]$`),
|
||||||
|
`"{upper(2)}{lower(2)}"`: regexp.MustCompile(`^[A-Z]{2}[a-z]{2}$`),
|
||||||
|
}
|
||||||
|
f := engine(7)
|
||||||
|
for tmpl, re := range cases {
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
if got := mustRender(t, f, tmpl); !re.MatchString(got) {
|
||||||
|
t.Fatalf("%s produced %q, want %s", tmpl, got, re)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFunctionTokenLuhn(t *testing.T) {
|
||||||
|
// {luhn()} appends a Luhn check digit over the digits emitted so far in the
|
||||||
|
// current expansion (non-digits skipped but kept). It reads the output
|
||||||
|
// buffer, so a value is never re-rendered. Bodies are escaped to fix input.
|
||||||
|
f := engine(1)
|
||||||
|
cases := map[string]string{
|
||||||
|
`"811218987{luhn()}"`: "8112189876", // personnummer body
|
||||||
|
`"7992739871{luhn()}"`: "79927398713", // classic Luhn vector
|
||||||
|
`"811218-987{luhn()}"`: "811218-9876", // '-' skipped, kept
|
||||||
|
`{"format":"{n}{luhn()}","n":"811218987"}`: "8112189876", // over a rendered token
|
||||||
|
}
|
||||||
|
for tmpl, want := range cases {
|
||||||
|
if got := mustRender(t, f, tmpl); got != want {
|
||||||
|
t.Fatalf("%s = %q, want %q", tmpl, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuiltinCompileErrors(t *testing.T) {
|
||||||
|
for _, bad := range []string{
|
||||||
|
`"{upper(x)}"`, // count must be an integer
|
||||||
|
`"{int(a,b)}"`, // non-integer args
|
||||||
|
`"{int(5,1)}"`, // min > max
|
||||||
|
`"{nanoid(-1)}"`, // negative count
|
||||||
|
`"{float(1,2,-1)}"`, // negative decimals
|
||||||
|
`"{float(NaN,NaN,2)}"`, // bounds must be finite
|
||||||
|
`"{float(Inf,Inf,2)}"`, // same-sign infinities
|
||||||
|
`"{float(1,NaN,2)}"`, // one NaN bound
|
||||||
|
`"{float(-Inf,1,2)}"`, // one infinite bound
|
||||||
|
`"{iban(US)}"`, // unsupported country
|
||||||
|
`"{seq(a,b)}"`, // seq takes at most one name
|
||||||
|
} {
|
||||||
|
if _, err := resolved(t, bad); err == nil {
|
||||||
|
t.Errorf("compile(%s) = nil error, want error", bad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArgErrorsNameTheSpelling(t *testing.T) {
|
||||||
|
if _, err := resolved(t, `"{hex(99999999999999999999)}"`); err == nil || !strings.Contains(err.Error(), "exceeds the maximum") {
|
||||||
|
t.Errorf("compile(hex past the range) = %v, want the maximum named", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestASignOrLeadingZeroIsTheInteger(t *testing.T) {
|
||||||
|
for long, short := range map[string]string{
|
||||||
|
`"{float(1,2,02)}"`: `"{float(1,2,2)}"`,
|
||||||
|
`{"format":"{calc(a,02)}","a":"1"}`: `{"format":"{calc(a,2)}","a":"1"}`,
|
||||||
|
`"{digits(+5)}"`: `"{digits(5)}"`,
|
||||||
|
`"{digits(05)}"`: `"{digits(5)}"`,
|
||||||
|
`"{int(007,9)}"`: `"{int(7,9)}"`,
|
||||||
|
`"{int(+1,5)}"`: `"{int(1,5)}"`,
|
||||||
|
} {
|
||||||
|
sameRenders(t, long, short)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,392 +2,149 @@ package fejkdata
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
|
||||||
|
"github.com/larvit/fejkdata/internal/builtinfunc"
|
||||||
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
|
"github.com/larvit/fejkdata/internal/invariant"
|
||||||
)
|
)
|
||||||
|
|
||||||
// calcNode is a parsed expression node. It evaluates over the operand values expand
|
// checkCalcFields runs checkOperands over each calc operand naming a field; an operand naming no
|
||||||
// read, so the evaluator touches neither the rng nor the node tree.
|
// field is a name, which checkCalcNames checks once names resolve.
|
||||||
type calcNode interface {
|
func checkCalcFields(args []string, fields map[string]node) error {
|
||||||
eval(operands []string) float64
|
return checkOperands(args[0], builtinfunc.ParsedCalc(args[0]), func(name string) []node {
|
||||||
}
|
if n, ok := fields[name]; ok {
|
||||||
|
return []node{n}
|
||||||
type calcNum float64 // a number literal
|
|
||||||
type calcVar string // a sibling-field name, before indexVars places it
|
|
||||||
type calcIdx int // an operand, by its position in the values expand read
|
|
||||||
type calcNeg struct{ x calcNode }
|
|
||||||
type calcBin struct { // a + - * / b
|
|
||||||
op byte
|
|
||||||
l, r calcNode
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n calcNum) eval([]string) float64 { return float64(n) }
|
|
||||||
|
|
||||||
func (n calcIdx) eval(operands []string) float64 {
|
|
||||||
v, err := strconv.ParseFloat(strings.TrimSpace(operands[n]), 64)
|
|
||||||
if err != nil {
|
|
||||||
return math.NaN() // a non-numeric operand stays visible, never an error
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// eval on an unplaced name cannot happen: calcPrep runs indexVars over every
|
|
||||||
// expression it compiles, so only a calcIdx reaches a render. It panics rather
|
|
||||||
// than returning NaN, so a node kind indexVars forgets is a stack trace and not a
|
|
||||||
// silently wrong number.
|
|
||||||
func (n calcVar) eval([]string) float64 {
|
|
||||||
panic(fmt.Sprintf("fejkdata: calc operand %q was never placed", string(n)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n calcNeg) eval(operands []string) float64 { return -n.x.eval(operands) }
|
|
||||||
|
|
||||||
func (n calcBin) eval(operands []string) float64 {
|
|
||||||
l, r := n.l.eval(operands), n.r.eval(operands)
|
|
||||||
switch n.op {
|
|
||||||
case '+':
|
|
||||||
return l + r
|
|
||||||
case '-':
|
|
||||||
return l - r
|
|
||||||
case '*':
|
|
||||||
return l * r
|
|
||||||
default: // '/'
|
|
||||||
return l / r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkCalc validates a calc token at compile time: a parseable expression whose
|
|
||||||
// operands all name existing fields, and an optional non-negative integer dp. It
|
|
||||||
// is a builtin check (fields first), so calc dispatches through the registry like
|
|
||||||
// every other {name(args)} function.
|
|
||||||
func checkCalc(fields map[string]node, args []string) error {
|
|
||||||
if len(args) < 1 || len(args) > 2 {
|
|
||||||
return fmt.Errorf("calc takes an expression and an optional decimals count, got %d args", len(args))
|
|
||||||
}
|
|
||||||
expr, err := parseCalc(args[0])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("calc(%q): %w", args[0], err)
|
|
||||||
}
|
|
||||||
for _, name := range calcVars(expr) {
|
|
||||||
operand, ok := fields[name]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("calc(%q): no field %q", args[0], name)
|
|
||||||
}
|
}
|
||||||
if text, never := neverNumeric(operand); never {
|
return nil
|
||||||
return fmt.Errorf("calc(%q): operand %q is never a number: it renders %q", args[0], name, text)
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkCalcNames holds each calc of t reading a name to the checks checkCalcFields makes of a field.
|
||||||
|
func checkCalcNames(label string, t *template) error {
|
||||||
|
for _, o := range t.compiled.ops {
|
||||||
|
if o.Fn != builtinfunc.CalcName || !slices.ContainsFunc(o.operands, func(a arm) bool { return a.kind == namedRead }) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := checkOperands(o.Args[0], builtinfunc.ParsedCalc(o.Args[0]), operandNodes(o)); err != nil {
|
||||||
|
return fmt.Errorf("%s: token {%s}: %w", label, o.Body, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if divisor, zero := constantZeroDivisor(expr, fields); zero {
|
return nil
|
||||||
return fmt.Errorf("calc(%q) divides by %s, which is always zero", args[0], divisor)
|
}
|
||||||
|
|
||||||
|
// operandNodes lists every node each operand of o may render, once its template and names resolve.
|
||||||
|
func operandNodes(o op) func(name string) []node {
|
||||||
|
return func(name string) []node {
|
||||||
|
a := o.operands[slices.IndexFunc(o.operands, func(a arm) bool { return a.head == name })]
|
||||||
|
if len(a.leaves) == 0 {
|
||||||
|
panic(invariant.Broken("calc operand %q was read before it was compiled", name))
|
||||||
|
}
|
||||||
|
return a.leaves
|
||||||
}
|
}
|
||||||
if len(args) == 2 {
|
}
|
||||||
dp, err := plainInt(args[1])
|
|
||||||
if err != nil {
|
// checkOperands refuses an operand that is never a number and a division by a constant zero.
|
||||||
return fmt.Errorf("calc decimals %w", err)
|
// operand lists every node an operand may render, nil while that is unknown.
|
||||||
}
|
func checkOperands(text string, c grammar.Calc, operand func(name string) []node) error {
|
||||||
if dp < 0 || dp > maxDecimals {
|
for _, name := range c.Operands {
|
||||||
return fmt.Errorf("calc decimals %d must be in 0..%d", dp, maxDecimals)
|
if rendered, never := allNeverNumeric(operand(name)); never {
|
||||||
|
return fmt.Errorf("calc(%q): operand %q is never a number: it renders %q", text, name, rendered)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if divisor, zero := constantZeroDivisor(c.Expr, operand); zero {
|
||||||
|
return fmt.Errorf("calc(%q) divides by %s, which is always zero", text, divisor)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// constantZeroDivisor finds a division whose right side is a constant zero: number
|
// constantZeroDivisor finds a division whose right side is a constant zero: number
|
||||||
// literals and fixed operands folded, anything that varies left unknown.
|
// literals and fixed operands folded, anything that varies left unknown.
|
||||||
func constantZeroDivisor(n calcNode, fields map[string]node) (string, bool) {
|
func constantZeroDivisor(n grammar.CalcNode, operand func(string) []node) (string, bool) {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case calcNeg:
|
case grammar.CalcNeg:
|
||||||
return constantZeroDivisor(n.x, fields)
|
return constantZeroDivisor(n.X, operand)
|
||||||
case calcBin:
|
case grammar.CalcBin:
|
||||||
if n.op == '/' {
|
if n.Operator == '/' {
|
||||||
if v, known := constantValue(n.r, fields); known && v == 0 {
|
if v, known := constantValue(n.R, operand); known && v == 0 {
|
||||||
return calcText(n.r), true
|
return grammar.CalcText(n.R), true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if d, zero := constantZeroDivisor(n.l, fields); zero {
|
if d, zero := constantZeroDivisor(n.L, operand); zero {
|
||||||
return d, true
|
return d, true
|
||||||
}
|
}
|
||||||
return constantZeroDivisor(n.r, fields)
|
return constantZeroDivisor(n.R, operand)
|
||||||
}
|
}
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
// constantValue evaluates an expression whose every operand is fixed.
|
// constantValue evaluates an expression whose every operand is fixed.
|
||||||
func constantValue(n calcNode, fields map[string]node) (float64, bool) {
|
func constantValue(n grammar.CalcNode, operand func(string) []node) (float64, bool) {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case calcNum:
|
case grammar.CalcNum:
|
||||||
return float64(n), true
|
return float64(n), true
|
||||||
case calcVar:
|
case grammar.CalcVar:
|
||||||
t, ok := fields[string(n)].(*template)
|
nodes := operand(n.Name)
|
||||||
if !ok || !t.fixed || t.repeat > 1 {
|
if len(nodes) != 1 {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
v, err := strconv.ParseFloat(strings.TrimSpace(t.lit), 64)
|
t, ok := nodes[0].(*template)
|
||||||
|
if !ok || t.repeat != 1 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
lit, fixed := t.fixedText()
|
||||||
|
if !fixed {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
v, err := strconv.ParseFloat(strings.TrimSpace(lit), 64)
|
||||||
return v, err == nil
|
return v, err == nil
|
||||||
case calcNeg:
|
case grammar.CalcNeg:
|
||||||
v, ok := constantValue(n.x, fields)
|
v, ok := constantValue(n.X, operand)
|
||||||
return -v, ok
|
return -v, ok
|
||||||
case calcBin:
|
case grammar.CalcBin:
|
||||||
l, lok := constantValue(n.l, fields)
|
l, lok := constantValue(n.L, operand)
|
||||||
r, rok := constantValue(n.r, fields)
|
r, rok := constantValue(n.R, operand)
|
||||||
if !lok || !rok {
|
if !lok || !rok {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
return calcBin{n.op, calcNum(l), calcNum(r)}.eval(nil), true
|
return builtinfunc.Arith(n.Operator, l, r), true
|
||||||
}
|
}
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// calcText spells an expression node the way an author would read it.
|
// neverNumeric reports a node no render of which is a number: a null, a repeat of 0, fixed
|
||||||
func calcText(n calcNode) string {
|
// text that does not parse, or a choice of only such items. text is one such render.
|
||||||
switch n := n.(type) {
|
|
||||||
case calcNum:
|
|
||||||
return strconv.FormatFloat(float64(n), 'f', -1, 64)
|
|
||||||
case calcVar:
|
|
||||||
return string(n)
|
|
||||||
case calcNeg:
|
|
||||||
return "-" + calcText(n.x)
|
|
||||||
case calcBin:
|
|
||||||
return "(" + calcText(n.l) + " " + string(n.op) + " " + calcText(n.r) + ")"
|
|
||||||
}
|
|
||||||
return "?"
|
|
||||||
}
|
|
||||||
|
|
||||||
// neverNumeric reports a node no render of which is a number: a null, fixed text that
|
|
||||||
// does not parse, or a choice of only such items. text is one such render.
|
|
||||||
func neverNumeric(n node) (text string, never bool) {
|
func neverNumeric(n node) (text string, never bool) {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case *null:
|
case *nullItem:
|
||||||
return "", true
|
return "", true
|
||||||
case *template:
|
case *template:
|
||||||
if !n.fixed || n.repeat > 1 {
|
lit, fixed := n.fixedText()
|
||||||
|
if n.repeat == 0 {
|
||||||
|
return "", true
|
||||||
|
}
|
||||||
|
if !fixed || n.repeat != 1 {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
if _, err := strconv.ParseFloat(strings.TrimSpace(n.lit), 64); err != nil {
|
if _, err := strconv.ParseFloat(strings.TrimSpace(lit), 64); err != nil {
|
||||||
return n.lit, true
|
return lit, true
|
||||||
}
|
}
|
||||||
case *choice:
|
case *choice:
|
||||||
for _, it := range n.items {
|
return allNeverNumeric(n.items)
|
||||||
t, itemNever := neverNumeric(it)
|
|
||||||
if !itemNever {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
text = t
|
|
||||||
}
|
|
||||||
return text, true
|
|
||||||
}
|
}
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
// calcPrep parses the expression and decimals once, at compile time, and places each
|
// allNeverNumeric reports whether no render of any of nodes is a number; false for no nodes.
|
||||||
// operand name at the position expand will read it into. checkCalc proved both args
|
func allNeverNumeric(nodes []node) (text string, never bool) {
|
||||||
// valid, so no step here can fail; dp -1 prints the minimal form.
|
for _, n := range nodes {
|
||||||
func calcPrep(args []string) callFn {
|
t, nodeNever := neverNumeric(n)
|
||||||
expr, err := parseCalc(args[0])
|
if !nodeNever {
|
||||||
if err != nil { // a nil AST would be a nil dereference per render, with no message
|
return "", false
|
||||||
panic(fmt.Sprintf("fejkdata: calc(%q) reached prep unparsed: %v", args[0], err))
|
|
||||||
}
|
|
||||||
at := make(map[string]int)
|
|
||||||
for i, name := range calcVars(expr) {
|
|
||||||
at[name] = i
|
|
||||||
}
|
|
||||||
placed := indexVars(expr, at)
|
|
||||||
dp := calcDecimals(args)
|
|
||||||
return func(_ *session, _ string, operands []string) string {
|
|
||||||
return formatFloat(placed.eval(operands), dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// calcDecimals is a calc's decimals count, or -1 for the shortest form.
|
|
||||||
func calcDecimals(args []string) int {
|
|
||||||
if len(args) == 2 {
|
|
||||||
return atoi(args[1])
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
// indexVars replaces each operand name with its position in the values expand reads.
|
|
||||||
// Both sides take that order from calcVars, so they cannot drift.
|
|
||||||
func indexVars(n calcNode, at map[string]int) calcNode {
|
|
||||||
switch n := n.(type) {
|
|
||||||
case calcVar:
|
|
||||||
i, placed := at[string(n)]
|
|
||||||
if !placed { // calcVars named every operand, so a miss means the two disagree
|
|
||||||
panic(fmt.Sprintf("fejkdata: calc operand %q is not among the names read for it", string(n)))
|
|
||||||
}
|
}
|
||||||
return calcIdx(i)
|
text = t
|
||||||
case calcNeg:
|
|
||||||
return calcNeg{indexVars(n.x, at)}
|
|
||||||
case calcBin:
|
|
||||||
return calcBin{n.op, indexVars(n.l, at), indexVars(n.r, at)}
|
|
||||||
}
|
}
|
||||||
return n
|
return text, len(nodes) > 0
|
||||||
}
|
|
||||||
|
|
||||||
// calcOperands lists the sibling-field names a calc's args read. checkCalc reports
|
|
||||||
// an expression that does not parse, so one that does not simply names nothing.
|
|
||||||
func calcOperands(args []string) []string {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
expr, err := parseCalc(args[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return calcVars(expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// calcVars lists the distinct field names an expression reads, in the order it first
|
|
||||||
// names each. That order is the contract between expand, which reads the operands
|
|
||||||
// into a slice, and indexVars, which places each name at its position in it.
|
|
||||||
func calcVars(n calcNode) []string {
|
|
||||||
var out []string
|
|
||||||
seen := map[string]bool{}
|
|
||||||
var walk func(calcNode)
|
|
||||||
walk = func(n calcNode) {
|
|
||||||
switch n := n.(type) {
|
|
||||||
case calcVar:
|
|
||||||
if name := string(n); !seen[name] {
|
|
||||||
seen[name] = true
|
|
||||||
out = append(out, name)
|
|
||||||
}
|
|
||||||
case calcNeg:
|
|
||||||
walk(n.x)
|
|
||||||
case calcBin:
|
|
||||||
walk(n.l)
|
|
||||||
walk(n.r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
walk(n)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// calcParser is a recursive-descent parser over the expression runes, threading
|
|
||||||
// expr -> term -> factor for the standard * / before + - precedence.
|
|
||||||
type calcParser struct {
|
|
||||||
rs []rune
|
|
||||||
pos int
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseCalc parses a whole expression, requiring it to consume all input.
|
|
||||||
func parseCalc(expr string) (calcNode, error) {
|
|
||||||
p := &calcParser{rs: []rune(expr)}
|
|
||||||
if p.space(); p.pos >= len(p.rs) {
|
|
||||||
return nil, fmt.Errorf("empty expression")
|
|
||||||
}
|
|
||||||
n, err := p.expr()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if p.space(); p.pos != len(p.rs) {
|
|
||||||
return nil, fmt.Errorf("unexpected %q", string(p.rs[p.pos:]))
|
|
||||||
}
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *calcParser) space() {
|
|
||||||
for p.pos < len(p.rs) && unicode.IsSpace(p.rs[p.pos]) {
|
|
||||||
p.pos++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *calcParser) expr() (calcNode, error) { return p.binary(p.term, '+', '-') }
|
|
||||||
func (p *calcParser) term() (calcNode, error) { return p.binary(p.factor, '*', '/') }
|
|
||||||
|
|
||||||
// binary parses a left-associative run of next() operands joined by the given
|
|
||||||
// operators, the one shape expr and term share.
|
|
||||||
func (p *calcParser) binary(next func() (calcNode, error), ops ...byte) (calcNode, error) {
|
|
||||||
n, err := next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
p.space()
|
|
||||||
if p.pos >= len(p.rs) || !contains(ops, byte(p.rs[p.pos])) {
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
op := byte(p.rs[p.pos])
|
|
||||||
p.pos++
|
|
||||||
r, err := next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
n = calcBin{op, n, r}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// factor is a table-shaped scanner, one case per token kind, kept whole on purpose.
|
|
||||||
func (p *calcParser) factor() (calcNode, error) {
|
|
||||||
p.space()
|
|
||||||
if p.pos >= len(p.rs) {
|
|
||||||
return nil, fmt.Errorf("unexpected end of expression")
|
|
||||||
}
|
|
||||||
switch c := p.rs[p.pos]; {
|
|
||||||
case c == '-':
|
|
||||||
p.pos++
|
|
||||||
x, err := p.factor()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return calcNeg{x}, nil
|
|
||||||
case c == '(':
|
|
||||||
p.pos++
|
|
||||||
n, err := p.expr()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if p.space(); p.pos >= len(p.rs) || p.rs[p.pos] != ')' {
|
|
||||||
return nil, fmt.Errorf("missing ')'")
|
|
||||||
}
|
|
||||||
p.pos++
|
|
||||||
return n, nil
|
|
||||||
case c == '.' || c >= '0' && c <= '9':
|
|
||||||
return p.number()
|
|
||||||
case c == '_' || unicode.IsLetter(c):
|
|
||||||
return p.ident()
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unexpected %q", string(c))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *calcParser) number() (calcNode, error) {
|
|
||||||
start, dot := p.pos, false
|
|
||||||
for p.pos < len(p.rs) {
|
|
||||||
if c := p.rs[p.pos]; c >= '0' && c <= '9' {
|
|
||||||
p.pos++
|
|
||||||
} else if c == '.' && !dot {
|
|
||||||
dot, p.pos = true, p.pos+1
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
v, err := strconv.ParseFloat(string(p.rs[start:p.pos]), 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("bad number %q", string(p.rs[start:p.pos]))
|
|
||||||
}
|
|
||||||
return calcNum(v), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ident reads a field name: a letter or '_', then letters, digits or '_'. A '-'
|
|
||||||
// is always the minus operator, so a hyphenated field name can't be an operand.
|
|
||||||
func (p *calcParser) ident() (calcNode, error) {
|
|
||||||
start := p.pos
|
|
||||||
for p.pos < len(p.rs) {
|
|
||||||
if c := p.rs[p.pos]; c == '_' || unicode.IsLetter(c) || unicode.IsDigit(c) {
|
|
||||||
p.pos++
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return calcVar(string(p.rs[start:p.pos])), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func contains(bs []byte, b byte) bool {
|
|
||||||
for _, x := range bs {
|
|
||||||
if x == b {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|||||||
+140
-64
@@ -3,8 +3,11 @@ package fejkdata
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestCalcArithmetic pins the operators, precedence, parentheses and unary minus
|
// TestCalcArithmetic pins the operators, precedence, parentheses and unary minus
|
||||||
@@ -72,6 +75,9 @@ func TestCalcFields(t *testing.T) {
|
|||||||
if got := mustRender(t, engine(1), `{"format":"{calc(a - b)}","a":{"format":"{n}","n":"10"},"b":"3"}`); got != "7" {
|
if got := mustRender(t, engine(1), `{"format":"{calc(a - b)}","a":{"format":"{n}","n":"10"},"b":"3"}`); got != "7" {
|
||||||
t.Fatalf("calc(a - b) = %q, want 7", got)
|
t.Fatalf("calc(a - b) = %q, want 7", got)
|
||||||
}
|
}
|
||||||
|
if got := mustRender(t, engine(1), `{"format":"{calc(b * (a - b))}","a":"10","b":"3"}`); got != "21" {
|
||||||
|
t.Fatalf("calc(b * (a - b)) = %q, want 21", got)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCalcNonNumericIsNaN pins the never-fail rule: a field that sometimes does
|
// TestCalcNonNumericIsNaN pins the never-fail rule: a field that sometimes does
|
||||||
@@ -94,43 +100,26 @@ func TestCalcReproducible(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestTokenOperandsReadsOnlyAnOperandBuiltin pins what the helper answers for a body that
|
// TestParsedCallReadsOnlyAnOperandBuiltin pins the operands a parsed call carries for a
|
||||||
// is not a calc, and for one whose expression does not parse: nothing, either way.
|
// call that is not a calc, and for one whose expression does not parse: none, either way.
|
||||||
// checkCalc is what reports a bad expression, so the callers that run after it
|
func TestParsedCallReadsOnlyAnOperandBuiltin(t *testing.T) {
|
||||||
// never meet one — but they must not have to depend on that order to be safe.
|
for _, format := range []string{"{luhn()}", "{calc()}", "{calc(1 +)}", "{calc(()}"} {
|
||||||
func TestTokenOperandsReadsOnlyAnOperandBuiltin(t *testing.T) {
|
if toks, err := grammar.ParseFormat(format); err != nil || len(toks) != 1 || tokenReads(toks[0]) != nil {
|
||||||
for _, body := range []string{"plain", "luhn()", "calc()", "calc(1 +)", "calc(()"} {
|
t.Errorf("ParseFormat(%q) = %+v, %v, want one call reading no operand", format, toks, err)
|
||||||
if got := tokenOperands(body); got != nil {
|
|
||||||
t.Errorf("tokenOperands(%q) = %v, want none", body, got)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if got := tokenOperands("calc(net * qty)"); len(got) != 2 {
|
if toks, err := grammar.ParseFormat("{calc(net * qty)}"); err != nil || len(toks) != 1 || len(tokenReads(toks[0])) != 2 {
|
||||||
t.Errorf("tokenOperands(calc(net * qty)) = %v, want both operands", got)
|
t.Errorf("ParseFormat({calc(net * qty)}) = %+v, %v, want both operands", toks, err)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestCalcGuardsPanic pins the guards on calc's own invariants: checkCalc parsed
|
|
||||||
// the expression before prep sees it, and indexVars places every name calcVars
|
|
||||||
// read, so a break in either is reported rather than computed around.
|
|
||||||
func TestCalcGuardsPanic(t *testing.T) {
|
|
||||||
for name, call := range map[string]func(){
|
|
||||||
"prep on an expression that does not parse": func() { calcPrep([]string{"1 +"}) },
|
|
||||||
"an operand name never placed": func() { calcVar("n").eval(nil) },
|
|
||||||
"indexVars on a name it did not read": func() { indexVars(calcVar("n"), map[string]int{}) },
|
|
||||||
} {
|
|
||||||
mustPanic(t, name, call)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- one draw, one value: a calc operand reads the expansion's draw ---
|
// --- one draw, one value: a calc operand reads the expansion's draw ---
|
||||||
|
|
||||||
// TestCalcOperandReadsTheExpansionsDraw pins the one-draw rule for a calc operand.
|
// TestANamedCalcOperandIsTheOneShown pins how a calc computes from the value its format
|
||||||
// A field the format renders and a calc reads is drawn once per expansion, so the
|
// shows: the format and the calc read one name.
|
||||||
// operand shown is the operand computed — the correlation the dotted-path rule
|
func TestANamedCalcOperandIsTheOneShown(t *testing.T) {
|
||||||
// already gives a level (see bound_test.go), applied to a plain sibling.
|
|
||||||
func TestCalcOperandReadsTheExpansionsDraw(t *testing.T) {
|
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"inv": `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":["19.99","5.00","100.00"],"qty":["2","3","7"]}`,
|
"inv": `{"format":"{net as n}{qty as q}{n} x {q} = {calc(n * q, 2)}","net":["19.99","5.00","100.00"],"qty":["2","3","7"]}`,
|
||||||
})
|
})
|
||||||
f := newGenerator(t, dir, WithSeed(3))
|
f := newGenerator(t, dir, WithSeed(3))
|
||||||
for i := 0; i < 300; i++ {
|
for i := 0; i < 300; i++ {
|
||||||
@@ -145,39 +134,23 @@ func TestCalcOperandReadsTheExpansionsDraw(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCalcOperandSharesOneDraw pins the reach of that hold: the draw belongs to the
|
// TestACalcOperandDrawsAfresh pins that a calc reading a field draws it as any {…} does.
|
||||||
// expansion, not to the calc, so the bare token rendering the same name reads it too.
|
func TestACalcOperandDrawsAfresh(t *testing.T) {
|
||||||
func TestCalcOperandSharesOneDraw(t *testing.T) {
|
dir := writeData(t, map[string]string{"same": `{"format":"{w} {calc(w)}","w":["1","2","3","4","5"]}`})
|
||||||
dir := writeData(t, map[string]string{
|
|
||||||
"same": `{"format":"{w} {calc(w)}","w":["1","2","3","4","5"]}`,
|
|
||||||
})
|
|
||||||
f := newGenerator(t, dir, WithSeed(5))
|
f := newGenerator(t, dir, WithSeed(5))
|
||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
got := fake(t, f, "same")
|
if p := strings.Fields(fake(t, f, "same")); p[0] != p[1] {
|
||||||
if p := strings.Fields(got); len(p) != 2 || p[0] != p[1] {
|
|
||||||
t.Fatalf("same = %q, want one value twice", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFieldNoCalcReadsDrawsEachTime guards the boundary: only a name a calc reads is
|
|
||||||
// held, so an ordinary {w} {w} still draws twice.
|
|
||||||
func TestFieldNoCalcReadsDrawsEachTime(t *testing.T) {
|
|
||||||
dir := writeData(t, map[string]string{"two": `{"format":"{w} {w}","w":["1","2","3","4","5"]}`})
|
|
||||||
f := newGenerator(t, dir, WithSeed(5))
|
|
||||||
for i := 0; i < 200; i++ {
|
|
||||||
if p := strings.Fields(fake(t, f, "two")); p[0] != p[1] {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.Fatal("{w} {w} never differed in 200 draws, want two independent draws")
|
t.Fatal("{w} {calc(w)} never differed in 200 draws, want two independent draws")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCalcHoldIsPerExpansion pins the scope of the hold: each repeat iteration is
|
// TestANamedCalcOperandPicksEachIteration pins that a name a repeat binds picks again on each
|
||||||
// its own expansion, so it draws again while staying self-consistent.
|
// iteration, staying one value within it.
|
||||||
func TestCalcHoldIsPerExpansion(t *testing.T) {
|
func TestANamedCalcOperandPicksEachIteration(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"rep": `{"format":"{n}={calc(n * 1)}","repeat":8,"separator":" ","n":["2","3","4","5","6","7","8","9"]}`,
|
"rep": `{"format":"{n as m}{m}={calc(m * 1)}","repeat":8,"separator":" ","n":["2","3","4","5","6","7","8","9"]}`,
|
||||||
})
|
})
|
||||||
f := newGenerator(t, dir, WithSeed(11))
|
f := newGenerator(t, dir, WithSeed(11))
|
||||||
varied := false
|
varied := false
|
||||||
@@ -198,12 +171,12 @@ func TestCalcHoldIsPerExpansion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCalcHoldIsPerTemplate pins that a nested template holds its own: the inner
|
// TestTwoNamesPickApart pins that a nested template's name is a pick of its own: the inner
|
||||||
// {v} and its calc agree with each other, not with the outer pair.
|
// pair agrees with itself, not with the outer pair.
|
||||||
func TestCalcHoldIsPerTemplate(t *testing.T) {
|
func TestTwoNamesPickApart(t *testing.T) {
|
||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"nest": `{"format":"{v}={calc(v * 1)} {inner}","v":["2","3","4","5","6","7","8","9"],
|
"nest": `{"format":"{v as a}{a}={calc(a * 1)} {inner}","v":["2","3","4","5","6","7","8","9"],
|
||||||
"inner":{"format":"{v}={calc(v * 1)}","v":["2","3","4","5","6","7","8","9"]}}`,
|
"inner":{"format":"{v as b}{b}={calc(b * 1)}","v":["2","3","4","5","6","7","8","9"]}}`,
|
||||||
})
|
})
|
||||||
f := newGenerator(t, dir, WithSeed(13))
|
f := newGenerator(t, dir, WithSeed(13))
|
||||||
differed := false
|
differed := false
|
||||||
@@ -231,7 +204,7 @@ func TestCalcOverANeverNumericOperandIsRejected(t *testing.T) {
|
|||||||
`{"format":"{calc(x * 2)}","x":["a","b"]}`: `"x"`,
|
`{"format":"{calc(x * 2)}","x":["a","b"]}`: `"x"`,
|
||||||
`{"format":"{calc(x + y)}","x":"1","y":"{{2}}"}`: `"{2}"`,
|
`{"format":"{calc(x + y)}","x":"1","y":"{{2}}"}`: `"{2}"`,
|
||||||
} {
|
} {
|
||||||
_, err := compile(parse(t, src))
|
_, err := resolved(t, src)
|
||||||
if err == nil || !strings.Contains(err.Error(), want) || !strings.Contains(err.Error(), "never a number") {
|
if err == nil || !strings.Contains(err.Error(), want) || !strings.Contains(err.Error(), "never a number") {
|
||||||
t.Errorf("compile(%s) = %v, want the operand rejected naming %s", src, err, want)
|
t.Errorf("compile(%s) = %v, want the operand rejected naming %s", src, err, want)
|
||||||
}
|
}
|
||||||
@@ -242,7 +215,7 @@ func TestCalcOverANeverNumericOperandIsRejected(t *testing.T) {
|
|||||||
`{"format":"{calc(x * 2)}","x":["1","abc"]}`,
|
`{"format":"{calc(x * 2)}","x":["1","abc"]}`,
|
||||||
`{"format":"{calc(x * 2)}","x":"{digits(2)}"}`,
|
`{"format":"{calc(x * 2)}","x":"{digits(2)}"}`,
|
||||||
} {
|
} {
|
||||||
if _, err := compile(parse(t, ok)); err != nil {
|
if _, err := resolved(t, ok); err != nil {
|
||||||
t.Errorf("compile(%s) = %v, want it accepted", ok, err)
|
t.Errorf("compile(%s) = %v, want it accepted", ok, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,14 +223,14 @@ func TestCalcOverANeverNumericOperandIsRejected(t *testing.T) {
|
|||||||
|
|
||||||
func TestCalcConstantZeroDivisorIsRejected(t *testing.T) {
|
func TestCalcConstantZeroDivisorIsRejected(t *testing.T) {
|
||||||
for src, want := range map[string]string{
|
for src, want := range map[string]string{
|
||||||
`"{calc(1/0)}"`: "divides by 0",
|
`"{calc(1/0)}"`: "divides by 0",
|
||||||
`"{calc(2/(1-1))}"`: "divides by (1 - 1)",
|
`"{calc(2/(1-1))}"`: "divides by (1 - 1)",
|
||||||
`{"format":"{calc(x/y)}","x":"1","y":"0"}`: "divides by y",
|
`{"format":"{calc(x/y)}","x":"1","y":"0"}`: "divides by y",
|
||||||
`{"format":"{calc(x/(y*2))}","x":"1","y":" 0 "}`: "divides by (y * 2)",
|
`{"format":"{calc(x/(y*2))}","x":"1","y":" 0 "}`: "divides by (y * 2)",
|
||||||
`{"format":"{calc((a/0)+b)}","a":"1","b":"2"}`: "divides by 0",
|
`{"format":"{calc((a/0)+b)}","a":"1","b":"2"}`: "divides by 0",
|
||||||
`{"format":"{calc(a/-0)}","a":"1"}`: "divides by -0",
|
`{"format":"{calc(a/-0)}","a":"1"}`: "divides by -0",
|
||||||
} {
|
} {
|
||||||
if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), want) {
|
if _, err := resolved(t, src); err == nil || !strings.Contains(err.Error(), want) {
|
||||||
t.Errorf("compile(%s) = %v, want the constant zero divisor rejected naming %q", src, err, want)
|
t.Errorf("compile(%s) = %v, want the constant zero divisor rejected naming %q", src, err, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,3 +245,106 @@ func TestCalcConstantZeroDivisorIsRejected(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Fatal("a sometimes-zero divisor never printed +Inf in 50 draws")
|
t.Fatal("a sometimes-zero divisor never printed +Inf in 50 draws")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCalcCompileErrors(t *testing.T) {
|
||||||
|
for _, bad := range []string{
|
||||||
|
`"{calc(1/0)}"`, // a constant zero divisor
|
||||||
|
`{"format":"{calc(x/y)}","x":"1","y":"0"}`, // a fixed zero divisor
|
||||||
|
`"{calc()}"`, // calc needs an expression
|
||||||
|
`"{calc(1 +)}"`, // dangling operator
|
||||||
|
`"{calc((1 + 2)}"`, // unbalanced parenthesis
|
||||||
|
`"{calc(1 2)}"`, // two operands, no operator
|
||||||
|
`"{calc(price)}"`, // operand names no field
|
||||||
|
`"{calc(1, 2, 3)}"`, // too many args
|
||||||
|
`"{calc(1, x)}"`, // decimals arg not an integer
|
||||||
|
`"{calc(1, -1)}"`, // decimals negative
|
||||||
|
} {
|
||||||
|
if _, err := resolved(t, bad); err == nil {
|
||||||
|
t.Errorf("compile(%s) = nil error, want error", bad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalcReadsAName(t *testing.T) {
|
||||||
|
dir := writeData(t, map[string]string{
|
||||||
|
"n": `["2","3","5","7"]`,
|
||||||
|
"card": `"{/n as k}{k} x 2 = {calc(k * 2)}"`,
|
||||||
|
})
|
||||||
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
var k, double int
|
||||||
|
got := fake(t, f, "card")
|
||||||
|
if _, err := fmt.Sscanf(got, "%d x 2 = %d", &k, &double); err != nil || double != k*2 {
|
||||||
|
t.Fatalf("card = %q, want the calc to compute from the pick {k} prints", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalcReadsANameItReadsNowhereElse(t *testing.T) {
|
||||||
|
dir := writeData(t, map[string]string{
|
||||||
|
"n": `["2","3"]`,
|
||||||
|
"card": `"{/n as k}{calc(k * 2)}"`,
|
||||||
|
})
|
||||||
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
|
if got := fake(t, f, "card"); got != "4" && got != "6" {
|
||||||
|
t.Fatalf("card = %q, want 4 or 6", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalcRefusesABadOperand(t *testing.T) {
|
||||||
|
for card, want := range map[string]string{
|
||||||
|
`"{/word as w}{w}{calc(w * 2)}"`: `operand "w" is never a number: it renders "def"`,
|
||||||
|
`"{/zero as z}{z}{calc(1 / z)}"`: `divides by z, which is always zero`,
|
||||||
|
`"{calc(nope * 2)}"`: `no field "nope"`,
|
||||||
|
`"{/word as w}{w}{calc(nope * 2)}"`: `no field or name "nope"; the names bound here are "w"`,
|
||||||
|
} {
|
||||||
|
dir := writeData(t, map[string]string{
|
||||||
|
"word": `["abc","def"]`,
|
||||||
|
"zero": `"0"`,
|
||||||
|
"card": card,
|
||||||
|
})
|
||||||
|
if _, err := New(withShipped(), WithDataPath(dir), WithSeed(1)); err == nil || !strings.Contains(err.Error(), want) {
|
||||||
|
t.Errorf("New with card %s = %v, want an error containing %q", card, err, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestATypedColumnRefusesACalcOverNoDigits(t *testing.T) {
|
||||||
|
_, err := New(WithSeed(1), WithDataPath(writeData(t, map[string]string{
|
||||||
|
"row": `{"format":"","x":{"format":"{calc(d + 1)}","d":"{digits(0)}","datatype":"integer"}}`,
|
||||||
|
})))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "{digits(0)} prints nothing, which is no number") {
|
||||||
|
t.Fatalf("New = %v, want a calc over {digits(0)} refused in a typed column", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestATypedColumnProvesACalcOverAName(t *testing.T) {
|
||||||
|
dir := writeData(t, map[string]string{
|
||||||
|
"n": `"{int(1,9)}"`,
|
||||||
|
"order": `{"format":"{/n as k}{k}","double":{"format":"{calc(k * 2)}","datatype":"integer"}}`,
|
||||||
|
})
|
||||||
|
f := newGenerator(t, dir, WithSeed(1))
|
||||||
|
r, err := f.FakeRecord("order")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if v, err := strconv.Atoi(r.Columns()[0].Value); err != nil || v < 2 || v > 18 || v%2 != 0 {
|
||||||
|
t.Fatalf("double = %q, want an even integer in 2..18", r.Columns()[0].Value)
|
||||||
|
}
|
||||||
|
if !strings.Contains(r.JSON(), `"double":`+r.Columns()[0].Value) {
|
||||||
|
t.Fatalf("JSON = %s, want double written as a number", r.JSON())
|
||||||
|
}
|
||||||
|
bad := writeData(t, map[string]string{
|
||||||
|
"word": `["1","x"]`,
|
||||||
|
"order": `{"format":"{/word as w}{w}","double":{"format":"{calc(w * 2)}","datatype":"integer"}}`,
|
||||||
|
})
|
||||||
|
if _, err := New(withShipped(), WithDataPath(bad), WithSeed(1)); err == nil || !strings.Contains(err.Error(), `operand "w": "x" is not a number`) {
|
||||||
|
t.Fatalf("New = %v, want the typed column refused over an operand that is not always a number", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestARepeatOfZeroIsNeverANumber(t *testing.T) {
|
||||||
|
if _, err := resolved(t, `{"format":"{calc(x / y)}","x":"1","y":{"format":"0","repeat":0}}`); err == nil || !strings.Contains(err.Error(), `operand "y" is never a number: it renders ""`) {
|
||||||
|
t.Errorf("a calc over a repeat of 0 = %v, want the operand refused as never a number", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+706
@@ -0,0 +1,706 @@
|
|||||||
|
// Package cli is the fejkdata command: it renders the template on its stdin with fake
|
||||||
|
// values from the modules it is given and any directories layered over them. A program
|
||||||
|
// passing its own modules to Main is a fejkdata CLI carrying them, for example
|
||||||
|
// os.Exit(cli.Main(append(data.Modules(), mydata)...)).
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"math/rand/v2"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"runtime/debug"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/larvit/fejkdata"
|
||||||
|
)
|
||||||
|
|
||||||
|
const usage = `Usage: <template> | COMMAND [flags]
|
||||||
|
COMMAND --list | --help | --version
|
||||||
|
|
||||||
|
COMMAND reads one template from stdin and renders it:
|
||||||
|
|
||||||
|
echo 'id: {int(1,999)}' | COMMAND
|
||||||
|
echo '{"format":"{x}","x":["bosse","lina"]}' | COMMAND
|
||||||
|
COMMAND < template.txt
|
||||||
|
|
||||||
|
COMMAND <<'EOF'
|
||||||
|
born {date(1990-01-01,2010-12-31,'2006-01-02')}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
A template is a format string, or a JSON object, array, string or null. Its {…} tokens
|
||||||
|
reach the data by reference from the root: {/path}, for any path --list prints, shipped
|
||||||
|
or layered with --data-path. A reference names a category or a dotted path into one, a
|
||||||
|
table's row by key or name ({/table[KEY]}), and .. steps up from a row to the row it
|
||||||
|
links to.
|
||||||
|
|
||||||
|
Under --format text, the default, what you send is what renders, and COMMAND adds
|
||||||
|
nothing: a format string keeps every byte, so echo's newline ends each render, and
|
||||||
|
printf '%s' sends none. -n joins renders with --separator, empty by default. A JSON
|
||||||
|
object, array or string drops the whitespace around it, and null alone renders nothing;
|
||||||
|
42 or true alone is a format string, printed as written. An empty or blank stdin is a
|
||||||
|
format string like any other, and renders as sent. A quoted heredoc, <<'EOF', passes $,
|
||||||
|
a backslash or a quote as written.
|
||||||
|
|
||||||
|
With --format json, ndjson, csv or sql the template must be a record — a JSON
|
||||||
|
template whose fields are its columns, or a lone reference such as {/path}:
|
||||||
|
one reference and nothing else, with one newline after it allowed — and the rows are
|
||||||
|
written as one JSON array, one JSON object per line, one CSV row (after a header), or
|
||||||
|
one INSERT.
|
||||||
|
|
||||||
|
-d, --data-path D a data directory to layer over the shipped data (repeatable; last wins on a clash)
|
||||||
|
--format F output form: text (default), json, ndjson, csv or sql
|
||||||
|
-h, --help print this help, then exit
|
||||||
|
--list list the paths the data offers, each written after {/ in a template, then exit;
|
||||||
|
--repeat, --separator, --format and --table do nothing here
|
||||||
|
--no-shipped-data load only the --data-path directories
|
||||||
|
-n, --repeat N render the template N times, 0..1048576 (default 1); 0 prints nothing,
|
||||||
|
[] under --format json, or the header under --format csv
|
||||||
|
-s, --seed N same seed, version and data: identical output (default: a random seed)
|
||||||
|
--separator S string between repeated renders (default empty); refused with a record
|
||||||
|
--format, which has its own separators
|
||||||
|
--table T the INSERT target for --format sql, ignored under any other
|
||||||
|
(default: a lone reference's last segment, else records)
|
||||||
|
--version print the version, then exit
|
||||||
|
|
||||||
|
A short flag's value attaches or follows (-n3, -n 3); short flags bundle (-hn 3);
|
||||||
|
-- ends the flags.
|
||||||
|
|
||||||
|
Exit codes: 0 success, 1 runtime error, 2 misuse.
|
||||||
|
`
|
||||||
|
|
||||||
|
type invocation struct {
|
||||||
|
args []string
|
||||||
|
dirs []string
|
||||||
|
format string
|
||||||
|
help bool
|
||||||
|
list bool
|
||||||
|
noShipped bool
|
||||||
|
repeat int
|
||||||
|
seed uint64
|
||||||
|
seeded bool
|
||||||
|
separator string
|
||||||
|
separatorSet bool
|
||||||
|
table string
|
||||||
|
tableSet bool
|
||||||
|
version bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type flagDef struct {
|
||||||
|
long string
|
||||||
|
short string
|
||||||
|
value bool
|
||||||
|
set func(*invocation, string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var flagDefs = []flagDef{
|
||||||
|
{"data-path", "d", true, func(in *invocation, v string) error { in.dirs = append(in.dirs, v); return nil }},
|
||||||
|
{"format", "", true, func(in *invocation, v string) error { in.format = v; return nil }},
|
||||||
|
{"help", "h", false, func(in *invocation, _ string) error { in.help = true; return nil }},
|
||||||
|
{"list", "", false, func(in *invocation, _ string) error { in.list = true; return nil }},
|
||||||
|
{"no-shipped-data", "", false, func(in *invocation, _ string) error { in.noShipped = true; return nil }},
|
||||||
|
{"repeat", "n", true, func(in *invocation, v string) error {
|
||||||
|
n, err := strconv.Atoi(v)
|
||||||
|
if err != nil || n < 0 || n > fejkdata.MaxRepeat {
|
||||||
|
return fmt.Errorf("--repeat needs an integer in 0..%d, got %q", fejkdata.MaxRepeat, v)
|
||||||
|
}
|
||||||
|
in.repeat = n
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
{"seed", "s", true, func(in *invocation, v string) error {
|
||||||
|
n, err := strconv.ParseUint(v, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("--seed needs an unsigned integer, got %q", v)
|
||||||
|
}
|
||||||
|
in.seed, in.seeded = n, true
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
{"separator", "", true, func(in *invocation, v string) error { in.separator, in.separatorSet = v, true; return nil }},
|
||||||
|
{"table", "", true, func(in *invocation, v string) error { in.table, in.tableSet = v, true; return nil }},
|
||||||
|
{"version", "", false, func(in *invocation, _ string) error { in.version = true; return nil }},
|
||||||
|
}
|
||||||
|
|
||||||
|
func flagByLong(name string) *flagDef {
|
||||||
|
for i := range flagDefs {
|
||||||
|
if flagDefs[i].long == name {
|
||||||
|
return &flagDefs[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func flagByShort(name string) *flagDef {
|
||||||
|
for i := range flagDefs {
|
||||||
|
if flagDefs[i].short == name {
|
||||||
|
return &flagDefs[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// flagArg is one flag as written: its definition and the value attached to it,
|
||||||
|
// if any.
|
||||||
|
type flagArg struct {
|
||||||
|
def *flagDef
|
||||||
|
value string
|
||||||
|
inline bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitFlags reads one argv element as flags: --name or --name=value, or -abc
|
||||||
|
// where each letter is a short flag and the first that takes a value takes the
|
||||||
|
// rest of the element.
|
||||||
|
func splitFlags(arg string) ([]flagArg, error) {
|
||||||
|
if strings.HasPrefix(arg, "--") {
|
||||||
|
name, value, inline := strings.Cut(arg[2:], "=")
|
||||||
|
def := flagByLong(name)
|
||||||
|
if def == nil {
|
||||||
|
return nil, fmt.Errorf("unknown flag --%s", name)
|
||||||
|
}
|
||||||
|
return []flagArg{{def, value, inline}}, nil
|
||||||
|
}
|
||||||
|
if name, _, _ := strings.Cut(arg[1:], "="); len(name) > 1 && flagByLong(name) != nil {
|
||||||
|
return nil, fmt.Errorf("unknown flag %s; use --%s", arg, name)
|
||||||
|
}
|
||||||
|
var flags []flagArg
|
||||||
|
letters := arg[1:]
|
||||||
|
for i, r := range letters {
|
||||||
|
letter := string(r)
|
||||||
|
def := flagByShort(letter)
|
||||||
|
if def == nil {
|
||||||
|
return nil, fmt.Errorf("unknown flag -%s", letter)
|
||||||
|
}
|
||||||
|
if !def.value {
|
||||||
|
flags = append(flags, flagArg{def: def})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rest := letters[i+len(letter):]
|
||||||
|
if strings.HasPrefix(rest, "=") {
|
||||||
|
return nil, fmt.Errorf("-%s takes its value attached (-%s%s) or next (-%s %s); = belongs to --%s=%s",
|
||||||
|
letter, letter, rest[1:], letter, rest[1:], def.long, rest[1:])
|
||||||
|
}
|
||||||
|
return append(flags, flagArg{def, rest, rest != ""}), nil
|
||||||
|
}
|
||||||
|
return flags, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseArgs(argv []string) (invocation, error) {
|
||||||
|
in := invocation{repeat: 1, format: "text"}
|
||||||
|
for i := 0; i < len(argv); i++ {
|
||||||
|
arg := argv[i]
|
||||||
|
if arg == "--" {
|
||||||
|
in.args = append(in.args, argv[i+1:]...)
|
||||||
|
return in, nil
|
||||||
|
}
|
||||||
|
if len(arg) < 2 || arg[0] != '-' {
|
||||||
|
in.args = append(in.args, arg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
flags, err := splitFlags(arg)
|
||||||
|
if err != nil {
|
||||||
|
return in, err
|
||||||
|
}
|
||||||
|
for _, fl := range flags {
|
||||||
|
if !fl.def.value {
|
||||||
|
if fl.inline {
|
||||||
|
return in, fmt.Errorf("--%s takes no value", fl.def.long)
|
||||||
|
}
|
||||||
|
_ = fl.def.set(&in, "")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value := fl.value
|
||||||
|
if !fl.inline {
|
||||||
|
if i++; i >= len(argv) {
|
||||||
|
return in, fmt.Errorf("--%s needs a value", fl.def.long)
|
||||||
|
}
|
||||||
|
value = argv[i]
|
||||||
|
}
|
||||||
|
if err := fl.def.set(&in, value); err != nil {
|
||||||
|
return in, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return in, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordFormat is one way to write a record out: the line each record renders,
|
||||||
|
// the header that precedes the first one, and the open/close frame plus the
|
||||||
|
// between-record separator a document form needs.
|
||||||
|
type recordFormat struct {
|
||||||
|
header func(*fejkdata.Record) string
|
||||||
|
line func(r *fejkdata.Record, table string) string
|
||||||
|
open string
|
||||||
|
close string
|
||||||
|
sep string
|
||||||
|
writes string // what it writes between values, which --separator would seem to set
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordFormats is every --format that writes records. json frames the records
|
||||||
|
// as one array document; ndjson writes one object per line.
|
||||||
|
var recordFormats = map[string]recordFormat{
|
||||||
|
"csv": {header: (*fejkdata.Record).CSVHeader, line: func(r *fejkdata.Record, _ string) string { return r.CSVLine() }, sep: "\n", writes: `always separates fields with ","`},
|
||||||
|
"json": {line: jsonLine, open: "[", close: "]", sep: ",\n", writes: "writes the records as one JSON array"},
|
||||||
|
"ndjson": {line: jsonLine, sep: "\n", writes: "writes one record per line"},
|
||||||
|
"sql": {line: func(r *fejkdata.Record, table string) string { return r.SQLInsert(table) }, sep: "\n", writes: "writes one INSERT per line"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonLine(r *fejkdata.Record, _ string) string { return r.JSON() }
|
||||||
|
|
||||||
|
func (in invocation) writesRecords() bool {
|
||||||
|
return in.format != "text"
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatNames lists the --format values, text included, for the misuse error.
|
||||||
|
func formatNames() string {
|
||||||
|
names := []string{"text"}
|
||||||
|
for name := range recordFormats {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
return strings.Join(names[:len(names)-1], ", ") + " or " + names[len(names)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkFlags rejects a flag value or combination that cannot run.
|
||||||
|
func (in invocation) checkFlags() error {
|
||||||
|
if _, ok := recordFormats[in.format]; !ok && in.format != "text" {
|
||||||
|
return fmt.Errorf("--format takes %s, got %q", formatNames(), in.format)
|
||||||
|
}
|
||||||
|
if in.tableSet && in.table == "" && in.format == "sql" {
|
||||||
|
return errors.New("--table names the INSERT target, so it cannot be empty")
|
||||||
|
}
|
||||||
|
if f, records := recordFormats[in.format]; records && in.separatorSet && !in.list {
|
||||||
|
return fmt.Errorf("--separator joins text renders; --format %s %s", in.format, f.writes)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pipeHint(name string) string { return "echo 'id: {int(1,999)}' | " + name }
|
||||||
|
|
||||||
|
// operandError refuses an argument that is no flag, naming the spelling that reads what
|
||||||
|
// it most likely means: dropping it for -, a file for a file's name, else a template
|
||||||
|
// piped in.
|
||||||
|
func operandError(arg, name string) error {
|
||||||
|
unexpected := "unexpected argument '" + arg + "'"
|
||||||
|
if arg == "-" {
|
||||||
|
return fmt.Errorf("%s; stdin is read already, so drop it", unexpected)
|
||||||
|
}
|
||||||
|
if fi, err := os.Stat(arg); err == nil && fi.Mode().IsRegular() {
|
||||||
|
return fmt.Errorf("%s; read a file from stdin: %s < %s", unexpected, name, shellQuoted(arg))
|
||||||
|
}
|
||||||
|
ref := "{/" + strings.TrimPrefix(arg, "/") + "}"
|
||||||
|
_, argLone := loneReference(arg)
|
||||||
|
inline, err := fejkdata.IsTemplate(arg)
|
||||||
|
_, refLone := loneReference(ref)
|
||||||
|
example := pipeHint(name)
|
||||||
|
switch {
|
||||||
|
case argLone || inline:
|
||||||
|
example = piped(arg, name)
|
||||||
|
case err == nil && refLone:
|
||||||
|
example = piped(ref, name)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s; the template is read from stdin: %s", unexpected, example)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check rejects a flag combination or an operand that cannot run, before any data is
|
||||||
|
// read.
|
||||||
|
func (in invocation) check(name string) error {
|
||||||
|
if err := in.checkFlags(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(in.args) > 0 {
|
||||||
|
return operandError(in.args[0], name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (in invocation) options(modules []fs.FS, systemSeed uint64) []fejkdata.Option {
|
||||||
|
if in.noShipped {
|
||||||
|
modules = nil
|
||||||
|
}
|
||||||
|
opts := []fejkdata.Option{fejkdata.WithDataFS(modules...)}
|
||||||
|
for _, dir := range in.dirs {
|
||||||
|
opts = append(opts, fejkdata.WithDataPath(dir))
|
||||||
|
}
|
||||||
|
seed := in.seed
|
||||||
|
if !in.seeded {
|
||||||
|
seed = systemSeed
|
||||||
|
}
|
||||||
|
return append(opts, fejkdata.WithSeed(seed))
|
||||||
|
}
|
||||||
|
|
||||||
|
// write streams the input's renders to w, repeat of them joined by the separator; a write
|
||||||
|
// failure surfaces from Flush, bufio keeping the first one.
|
||||||
|
func (in invocation) write(f *fejkdata.Generator, src input, w io.Writer) error {
|
||||||
|
if in.writesRecords() {
|
||||||
|
return in.writeRecords(f, src, w)
|
||||||
|
}
|
||||||
|
t, err := f.NewTemplate(src.template)
|
||||||
|
if err != nil {
|
||||||
|
return templateError(err)
|
||||||
|
}
|
||||||
|
out := bufio.NewWriter(w)
|
||||||
|
for i := 0; i < in.repeat; i++ {
|
||||||
|
if i > 0 {
|
||||||
|
out.WriteString(in.separator)
|
||||||
|
}
|
||||||
|
out.WriteString(t.Fake())
|
||||||
|
}
|
||||||
|
return out.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordStream builds the record drawer for the input, plus the INSERT table
|
||||||
|
// a sql format names.
|
||||||
|
func (in invocation) recordStream(f *fejkdata.Generator, src input) (func() (*fejkdata.Record, error), string, error) {
|
||||||
|
record := func() (*fejkdata.Record, error) { return f.FakeRecord(src.record) }
|
||||||
|
if src.record == "" {
|
||||||
|
t, err := f.NewRecordTemplate(src.template)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", templateError(spacedReference(src.template, err))
|
||||||
|
}
|
||||||
|
record = func() (*fejkdata.Record, error) { return t.Fake(), nil }
|
||||||
|
}
|
||||||
|
table := in.table
|
||||||
|
if table == "" {
|
||||||
|
table = defaultTable(src.record)
|
||||||
|
}
|
||||||
|
return record, table, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeRecords streams a record per line in the chosen format, framing a document
|
||||||
|
// form with its open/close brackets and a header preceding the first record.
|
||||||
|
func (in invocation) writeRecords(f *fejkdata.Generator, src input, w io.Writer) error {
|
||||||
|
record, table, err := in.recordStream(f, src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
format := recordFormats[in.format]
|
||||||
|
if in.repeat == 0 {
|
||||||
|
return writeNoRecord(record, format, w)
|
||||||
|
}
|
||||||
|
out := bufio.NewWriter(w)
|
||||||
|
if format.open != "" {
|
||||||
|
out.WriteString(format.open)
|
||||||
|
out.WriteByte('\n')
|
||||||
|
}
|
||||||
|
for i := 0; i < in.repeat; i++ {
|
||||||
|
r, err := record()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if i == 0 && format.header != nil {
|
||||||
|
out.WriteString(format.header(r))
|
||||||
|
out.WriteByte('\n')
|
||||||
|
}
|
||||||
|
if i > 0 {
|
||||||
|
out.WriteString(format.sep)
|
||||||
|
}
|
||||||
|
out.WriteString(format.line(r, table))
|
||||||
|
}
|
||||||
|
if format.close != "" {
|
||||||
|
out.WriteByte('\n')
|
||||||
|
out.WriteString(format.close)
|
||||||
|
}
|
||||||
|
out.WriteByte('\n')
|
||||||
|
return out.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeNoRecord writes zero records: the header where the format writes one, an empty document
|
||||||
|
// where it frames one, else nothing. It draws one record, which names the header's columns, so a
|
||||||
|
// lone reference to nothing fails as it would with records.
|
||||||
|
func writeNoRecord(record func() (*fejkdata.Record, error), format recordFormat, w io.Writer) error {
|
||||||
|
r, err := record()
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
return err
|
||||||
|
case format.header != nil:
|
||||||
|
_, err = io.WriteString(w, format.header(r)+"\n")
|
||||||
|
case format.open != "":
|
||||||
|
_, err = io.WriteString(w, format.open+format.close+"\n")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultTable names the INSERT target when --table is absent: the path's last
|
||||||
|
// segment, or "records" when stdin is not a lone reference.
|
||||||
|
func defaultTable(path string) string {
|
||||||
|
if path == "" {
|
||||||
|
return "records"
|
||||||
|
}
|
||||||
|
segments := strings.Split(withoutSelectors(path), ".")
|
||||||
|
return segments[len(segments)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// withoutSelectors is path with its [selectors] cut out.
|
||||||
|
func withoutSelectors(path string) string {
|
||||||
|
var names strings.Builder
|
||||||
|
for depth, i := 0, 0; i < len(path); i++ {
|
||||||
|
switch {
|
||||||
|
case path[i] == '[':
|
||||||
|
depth++
|
||||||
|
case path[i] == ']':
|
||||||
|
depth--
|
||||||
|
case depth == 0:
|
||||||
|
names.WriteByte(path[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// inputError marks a failure that is the input's own fault — nothing piped in, a template
|
||||||
|
// that does not compile, or a stdin that cannot be read. run reports it as misuse (exit 2, with a
|
||||||
|
// pointer to --help), unlike a lone reference to nothing under --format, which is a runtime
|
||||||
|
// error (exit 1).
|
||||||
|
type inputError struct{ error }
|
||||||
|
|
||||||
|
func (e inputError) Unwrap() error { return e.error }
|
||||||
|
|
||||||
|
// jsonSpace is the whitespace JSON allows around a value.
|
||||||
|
const jsonSpace = " \t\r\n"
|
||||||
|
|
||||||
|
// input is what stdin holds: the template, and the path a lone reference names as a
|
||||||
|
// record.
|
||||||
|
type input struct {
|
||||||
|
template string
|
||||||
|
record string
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseInput keeps stdin whole as the template, and names a record where stdin is a lone
|
||||||
|
// reference once JSON's surrounding whitespace, or one final newline, is cut.
|
||||||
|
func parseInput(raw string) input {
|
||||||
|
trimmed := strings.Trim(raw, jsonSpace)
|
||||||
|
text := raw
|
||||||
|
if json.Valid([]byte(trimmed)) {
|
||||||
|
text = trimmed
|
||||||
|
}
|
||||||
|
text, found := strings.CutSuffix(text, "\n")
|
||||||
|
if found {
|
||||||
|
text = strings.TrimSuffix(text, "\r")
|
||||||
|
}
|
||||||
|
record, _ := loneReference(text)
|
||||||
|
return input{template: raw, record: record}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loneReference returns the path that text reads when it is one reference from the root
|
||||||
|
// and nothing else, bare or as a JSON string.
|
||||||
|
func loneReference(text string) (string, bool) {
|
||||||
|
var unquoted string
|
||||||
|
if strings.HasPrefix(text, `"`) && strings.HasSuffix(text, `"`) && json.Unmarshal([]byte(text), &unquoted) == nil {
|
||||||
|
text = unquoted
|
||||||
|
}
|
||||||
|
path, opens := strings.CutPrefix(text, "{/")
|
||||||
|
path, closes := strings.CutSuffix(path, "}")
|
||||||
|
if !opens || !closes || !isPath(path) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return path, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// isPath reports whether a reference's body after its / is a path alone: no second /, no
|
||||||
|
// folder sigil, no empty segment, and none of the |, ( or " as " a token reads outside a selector.
|
||||||
|
// It copies the grammar, since the CLI reads only the library's public API.
|
||||||
|
func isPath(body string) bool {
|
||||||
|
if body == "" || strings.ContainsAny(body[:1], "/.") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
names := withoutSelectors(body)
|
||||||
|
if strings.ContainsAny(names, "|(") || strings.Contains(names, " as ") || hasEmptySegment(names) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
inline, err := fejkdata.IsTemplate(body)
|
||||||
|
return !inline && err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasEmptySegment reports whether a dotted path holds a segment with no name: two dots are a
|
||||||
|
// step up, and only a step up may end the path.
|
||||||
|
func hasEmptySegment(path string) bool {
|
||||||
|
segments := strings.Split(path, ".")
|
||||||
|
stepUp := false
|
||||||
|
for i, seg := range segments[1:] {
|
||||||
|
last := i == len(segments)-2
|
||||||
|
switch {
|
||||||
|
case seg != "":
|
||||||
|
stepUp = false
|
||||||
|
case stepUp != last:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
stepUp = !stepUp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// spacedReference adds to a record refusal that whitespace around a lone reference made
|
||||||
|
// the template text.
|
||||||
|
func spacedReference(template string, err error) error {
|
||||||
|
if trimmed := strings.TrimSpace(template); trimmed != template && errors.Is(err, fejkdata.ErrNoColumns) {
|
||||||
|
if path, lone := loneReference(trimmed); lone {
|
||||||
|
return fmt.Errorf("%w; the whitespace around {/%s} makes it text, so remove it to write the record %s", err, path, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// piped is the command piping template into the command called name; some shells' echo
|
||||||
|
// reads a backslash as an escape, so a template holding one goes through printf.
|
||||||
|
func piped(template, name string) string {
|
||||||
|
if strings.ContainsRune(template, '\\') {
|
||||||
|
return "printf '%s' " + shellQuoted(template) + " | " + name
|
||||||
|
}
|
||||||
|
return "echo " + shellQuoted(template) + " | " + name
|
||||||
|
}
|
||||||
|
|
||||||
|
// shellQuoted is s as a shell reads it back: bare where it holds nothing a shell expands.
|
||||||
|
func shellQuoted(s string) string {
|
||||||
|
if s != "" && !strings.ContainsAny(s, " \t\r\n'\"$`\\*?[]{}()<>|&;#~!") {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main runs the command on the process's arguments and standard streams over modules, each
|
||||||
|
// passed to [fejkdata.WithDataFS], and returns the exit code: 0 ok, 1 runtime error,
|
||||||
|
// 2 misuse. --no-shipped-data leaves modules out.
|
||||||
|
func Main(modules ...fs.FS) int {
|
||||||
|
var stdin io.Reader = os.Stdin
|
||||||
|
if fi, err := os.Stdin.Stat(); err == nil && fi.Mode()&os.ModeCharDevice != 0 {
|
||||||
|
stdin = nil
|
||||||
|
}
|
||||||
|
return run(commandName(os.Args[0]), modules, os.Args[1:], rand.Uint64(), stdin, os.Stdout, os.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// commandName is the name the command runs under: the file arg0 names, less a Windows
|
||||||
|
// .exe, or fejkdata where arg0 names none.
|
||||||
|
func commandName(arg0 string) string {
|
||||||
|
name := filepath.Base(arg0)
|
||||||
|
if ext := filepath.Ext(name); strings.EqualFold(ext, ".exe") {
|
||||||
|
name = strings.TrimSuffix(name, ext)
|
||||||
|
}
|
||||||
|
if name == "." || name == "" {
|
||||||
|
return "fejkdata"
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// run is Main over the given streams, run as the command name, seeded by systemSeed where
|
||||||
|
// no --seed is given. A nil stdin is a terminal, which holds no template.
|
||||||
|
func run(name string, modules []fs.FS, args []string, systemSeed uint64, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||||
|
in, err := parseArgs(args)
|
||||||
|
if err != nil {
|
||||||
|
return misuse(stderr, name, err)
|
||||||
|
}
|
||||||
|
if in.help {
|
||||||
|
io.WriteString(stdout, strings.ReplaceAll(usage, "COMMAND", name))
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if in.version {
|
||||||
|
info, _ := debug.ReadBuildInfo()
|
||||||
|
fmt.Fprintln(stdout, versionLine(name, info))
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if err := in.check(name); err != nil {
|
||||||
|
return misuse(stderr, name, err)
|
||||||
|
}
|
||||||
|
var src input
|
||||||
|
if !in.list {
|
||||||
|
if src, err = readStdin(stdin, name); err != nil {
|
||||||
|
return fail(stderr, name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f, err := fejkdata.New(in.options(modules, systemSeed)...)
|
||||||
|
if err != nil {
|
||||||
|
return fail(stderr, name, err)
|
||||||
|
}
|
||||||
|
if in.list {
|
||||||
|
for _, p := range f.List() {
|
||||||
|
fmt.Fprintln(stdout, p)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if err := in.write(f, src, stdout); err != nil {
|
||||||
|
return fail(stderr, name, err)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func readStdin(stdin io.Reader, name string) (input, error) {
|
||||||
|
if stdin == nil {
|
||||||
|
return input{}, inputError{fmt.Errorf("the template comes from stdin, and nothing is piped in: %s; %s --list prints the paths a {/path} reference reads", pipeHint(name), name)}
|
||||||
|
}
|
||||||
|
raw, err := io.ReadAll(stdin)
|
||||||
|
if err != nil {
|
||||||
|
return input{}, inputError{fmt.Errorf("stdin cannot be read: %w", err)}
|
||||||
|
}
|
||||||
|
return parseInput(string(raw)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// templateError is misuse unless the data the template reads failed to load.
|
||||||
|
func templateError(err error) error {
|
||||||
|
if errors.Is(err, fejkdata.ErrLoad) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return inputError{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fail(stderr io.Writer, name string, err error) int {
|
||||||
|
var te inputError
|
||||||
|
if errors.As(err, &te) {
|
||||||
|
return misuse(stderr, name, te.error)
|
||||||
|
}
|
||||||
|
report(stderr, name, err)
|
||||||
|
if errors.Is(err, fejkdata.ErrNoColumns) {
|
||||||
|
fmt.Fprintln(stderr, "wrap that JSON in single quotes, which keep a shell from expanding its braces")
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func misuse(stderr io.Writer, name string, err error) int {
|
||||||
|
report(stderr, name, err)
|
||||||
|
fmt.Fprintf(stderr, "try '%s --help'\n", name)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// report writes err under the command name, in place of the library's own on each line.
|
||||||
|
func report(stderr io.Writer, name string, err error) {
|
||||||
|
msg := strings.ReplaceAll(strings.TrimPrefix(err.Error(), "fejkdata: "), "\nfejkdata: ", "\n"+name+": ")
|
||||||
|
fmt.Fprintf(stderr, "%s: %s\n", name, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fejkdataModule is the path of the module holding the library; the root package's path is
|
||||||
|
// the module's.
|
||||||
|
var fejkdataModule = reflect.TypeOf((*fejkdata.Generator)(nil)).Elem().PkgPath()
|
||||||
|
|
||||||
|
// versionLine names the fejkdata version the binary is built with. Where another module's
|
||||||
|
// main package carries the CLI, the main module's version comes first, under the command name.
|
||||||
|
func versionLine(name string, info *debug.BuildInfo) string {
|
||||||
|
if info == nil {
|
||||||
|
return "fejkdata devel"
|
||||||
|
}
|
||||||
|
if info.Main.Path == fejkdataModule {
|
||||||
|
return "fejkdata " + moduleVersion(info.Main)
|
||||||
|
}
|
||||||
|
for _, m := range info.Deps {
|
||||||
|
if m.Path == fejkdataModule {
|
||||||
|
return name + " " + moduleVersion(info.Main) + ", fejkdata " + moduleVersion(*m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name + " " + moduleVersion(info.Main) + ", fejkdata devel"
|
||||||
|
}
|
||||||
|
|
||||||
|
// moduleVersion is the version m is built at: the replacement's where a replace directive
|
||||||
|
// names one, and "devel" where it carries none, as a local directory does.
|
||||||
|
func moduleVersion(m debug.Module) string {
|
||||||
|
if m.Replace != nil {
|
||||||
|
m = *m.Replace
|
||||||
|
}
|
||||||
|
if m.Version == "" || m.Version == "(devel)" {
|
||||||
|
return "devel"
|
||||||
|
}
|
||||||
|
return m.Version
|
||||||
|
}
|
||||||
+961
@@ -0,0 +1,961 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/csv"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"testing/fstest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// shipped holds the shipped modules as read from the checkout, each folder under ../data
|
||||||
|
// holding a manifest, so these tests link no data package.
|
||||||
|
var shipped = func() []fs.FS {
|
||||||
|
var modules []fs.FS
|
||||||
|
err := filepath.WalkDir(filepath.Join("..", "data"), func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err == nil && d.Name() == ".fejkdata.json" {
|
||||||
|
modules = append(modules, os.DirFS(filepath.Dir(path)))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil || len(modules) == 0 {
|
||||||
|
panic(fmt.Sprintf("no shipped module found under ../data: %v", err))
|
||||||
|
}
|
||||||
|
return modules
|
||||||
|
}()
|
||||||
|
|
||||||
|
// svSE, enUS and misc are copies of a shipped module's files without its manifest, so a test
|
||||||
|
// layers its categories unprefixed with -d.
|
||||||
|
var svSE, enUS, misc string
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
tmp, err := os.MkdirTemp("", "fejkdata-cli-test")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
svSE, enUS, misc = bareCopy(tmp, "sv_SE"), bareCopy(tmp, "en_US"), bareCopy(tmp, "misc")
|
||||||
|
code := m.Run()
|
||||||
|
os.RemoveAll(tmp)
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bareCopy copies the category and rows files of the shipped module in data/<module> into a
|
||||||
|
// folder under tmp, and returns that folder.
|
||||||
|
func bareCopy(tmp, module string) string {
|
||||||
|
src, dst := filepath.Join("..", "data", module), filepath.Join(tmp, module)
|
||||||
|
if err := os.Mkdir(dst, 0o755); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
for _, pattern := range []string{"*.json", "*.tsv"} {
|
||||||
|
files, err := filepath.Glob(filepath.Join(src, pattern))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
for _, f := range files {
|
||||||
|
if strings.HasPrefix(filepath.Base(f), ".") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b, err := os.ReadFile(f)
|
||||||
|
if err == nil {
|
||||||
|
err = os.WriteFile(filepath.Join(dst, filepath.Base(f)), b, 0o644)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
func runOut(stdin string, args ...string) (int, string, string) {
|
||||||
|
var out, errb bytes.Buffer
|
||||||
|
code := run("fejkdata", shipped, args, 1, strings.NewReader(stdin), &out, &errb)
|
||||||
|
return code, out.String(), errb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// runTerminal runs as from a shell with nothing piped in.
|
||||||
|
func runTerminal(args ...string) (int, string, string) {
|
||||||
|
var out, errb bytes.Buffer
|
||||||
|
code := run("fejkdata", shipped, args, 1, nil, &out, &errb)
|
||||||
|
return code, out.String(), errb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunLoadsTheModulesItIsGiven(t *testing.T) {
|
||||||
|
module := fstest.MapFS{"pet.json": {Data: []byte(`{"format":"{name}","name":["Fido"]}`)}}
|
||||||
|
var out, errb bytes.Buffer
|
||||||
|
if code := run("fejkdata", []fs.FS{module}, nil, 1, strings.NewReader("{/pet}"), &out, &errb); code != 0 || out.String() != "Fido" {
|
||||||
|
t.Errorf("run over a module holding pet = %d, %q, %q, want Fido", code, out.String(), errb.String())
|
||||||
|
}
|
||||||
|
out.Reset()
|
||||||
|
if code := run("fejkdata", []fs.FS{module}, []string{"--list"}, 1, nil, &out, &errb); code != 0 || out.String() != "pet\npet.name\n" {
|
||||||
|
t.Errorf("--list over a module holding pet = %d, %q, want pet and pet.name only", code, out.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunOutputsValue(t *testing.T) {
|
||||||
|
code, out, errb := runOut("{/person}", "--data-path", svSE)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||||
|
}
|
||||||
|
if out == "" || strings.Contains(out, "\n") {
|
||||||
|
t.Fatalf("output = %q, stderr=%q; want one value and nothing added", out, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunDotPath(t *testing.T) {
|
||||||
|
code, full, _ := runOut("{/person}", "--seed", "7", "--data-path", svSE)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("person run = %d", code)
|
||||||
|
}
|
||||||
|
code, last, errb := runOut("{/person.last}", "--seed", "7", "--data-path", svSE)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("person.last run = %d, stderr=%q", code, errb)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(last) == "" {
|
||||||
|
t.Fatal("empty person.last output")
|
||||||
|
}
|
||||||
|
if last == full {
|
||||||
|
t.Errorf("person.last %q should differ from person %q", last, full)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunWithoutASeedTakesTheSystemSeed(t *testing.T) {
|
||||||
|
var drawn, seeded, errb bytes.Buffer
|
||||||
|
if code := run("fejkdata", shipped, nil, 42, strings.NewReader("{uuid()}"), &drawn, &errb); code != 0 {
|
||||||
|
t.Fatalf("run with system seed 42 = %d, stderr=%q", code, errb.String())
|
||||||
|
}
|
||||||
|
if code := run("fejkdata", shipped, []string{"--seed", "42"}, 7, strings.NewReader("{uuid()}"), &seeded, &errb); code != 0 {
|
||||||
|
t.Fatalf("run --seed 42 = %d, stderr=%q", code, errb.String())
|
||||||
|
}
|
||||||
|
if drawn.String() != seeded.String() {
|
||||||
|
t.Fatalf("run with system seed 42 = %q, run --seed 42 = %q; want the same draw", drawn.String(), seeded.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunSeedSpellings(t *testing.T) {
|
||||||
|
_, want, _ := runOut("{/address}", "--seed", "42", "--data-path", svSE)
|
||||||
|
for _, args := range [][]string{
|
||||||
|
{"--seed=42", "--data-path", svSE},
|
||||||
|
{"-s", "42", "-d", svSE},
|
||||||
|
{"--data-path", svSE, "--seed", "42"},
|
||||||
|
} {
|
||||||
|
code, got, errb := runOut("{/address}", args...)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("run(%v) = %d, stderr=%q", args, code, errb)
|
||||||
|
}
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("run(%v) = %q, want %q", args, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunShortFlagValues(t *testing.T) {
|
||||||
|
_, want, _ := runOut("{/address}", "--seed", "42", "--data-path", svSE)
|
||||||
|
for _, args := range [][]string{
|
||||||
|
{"-s42", "-d", svSE},
|
||||||
|
{"-s", "42", "-d" + svSE},
|
||||||
|
{"-d", svSE, "-s42"},
|
||||||
|
} {
|
||||||
|
code, got, errb := runOut("{/address}", args...)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("run(%v) = %d, stderr=%q", args, code, errb)
|
||||||
|
}
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("run(%v) = %q, want %q", args, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, three, _ := runOut("{/word}\n", "-s", "1", "-n3", "-d", svSE)
|
||||||
|
if lines := strings.Split(strings.TrimRight(three, "\n"), "\n"); len(lines) != 3 {
|
||||||
|
t.Errorf("-n3 gave %d lines: %q", len(lines), three)
|
||||||
|
}
|
||||||
|
for _, c := range []struct {
|
||||||
|
args []string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{[]string{"-s=42", "-d", svSE}, "-s42"},
|
||||||
|
{[]string{"-s=42", "-d", svSE}, "--seed=42"},
|
||||||
|
{[]string{"-d=" + svSE}, "--data-path="},
|
||||||
|
{[]string{"-nd", "3", "-d", svSE}, `--repeat needs an integer in 0..1048576, got "d"`},
|
||||||
|
{[]string{"--seed=", "-d", svSE}, `--seed needs an unsigned integer, got ""`},
|
||||||
|
} {
|
||||||
|
code, out, errb := runOut("{/word}", c.args...)
|
||||||
|
if code != 2 || out != "" {
|
||||||
|
t.Errorf("run(%v) = %d, stdout %q, want misuse", c.args, code, out)
|
||||||
|
}
|
||||||
|
if !strings.Contains(errb, c.want) {
|
||||||
|
t.Errorf("run(%v) stderr = %q, want %q", c.args, errb, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
code, out, _ := runOut("", "-hd", svSE)
|
||||||
|
if code != 0 || !strings.Contains(out, "Usage") {
|
||||||
|
t.Errorf("-hd (bundled help) = %d, %q, want usage", code, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunDoubleDashEndsFlags(t *testing.T) {
|
||||||
|
code, out, errb := runOut("{/person}", "--data-path", svSE, "--")
|
||||||
|
if code != 0 || strings.TrimSpace(out) == "" {
|
||||||
|
t.Fatalf("run = %d, out=%q, stderr=%q", code, out, errb)
|
||||||
|
}
|
||||||
|
code, _, errb = runOut("{/person}", "--data-path", svSE, "--", "--list")
|
||||||
|
if code != 2 || !strings.Contains(errb, "stdin") {
|
||||||
|
t.Errorf("after --, --list should be an argument: code %d, stderr %q", code, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunSingleDashLongIsRejected(t *testing.T) {
|
||||||
|
for _, c := range []struct {
|
||||||
|
args []string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{[]string{"-seed", "42", "-d", svSE}, "use --seed"},
|
||||||
|
{[]string{"-seed=42", "-d", svSE}, "use --seed"},
|
||||||
|
{[]string{"-data-path", svSE}, "use --data-path"},
|
||||||
|
{[]string{"-list", "-d", svSE}, "use --list"},
|
||||||
|
{[]string{"--nope", "-d", svSE}, "unknown flag --nope"},
|
||||||
|
{[]string{"-x", "-d", svSE}, "unknown flag -x"},
|
||||||
|
} {
|
||||||
|
code, _, errb := runOut("{/person}", c.args...)
|
||||||
|
if code != 2 {
|
||||||
|
t.Errorf("run(%v) = %d, want 2", c.args, code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(errb, c.want) {
|
||||||
|
t.Errorf("run(%v) stderr = %q, want %q", c.args, errb, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunFlagValues(t *testing.T) {
|
||||||
|
for _, c := range []struct {
|
||||||
|
args []string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{[]string{"--seed", "-d", svSE}, `--seed needs an unsigned integer, got "-d"`},
|
||||||
|
{[]string{"-d", svSE, "--seed"}, "--seed needs a value"},
|
||||||
|
{[]string{"--seed", "x", "-d", svSE}, "--seed"},
|
||||||
|
{[]string{"--list=1", "-d", svSE}, "--list takes no value"},
|
||||||
|
{[]string{"--repeat", "-1", "-d", svSE}, "--repeat"},
|
||||||
|
{[]string{"-n", "-1", "-d", svSE}, "--repeat"},
|
||||||
|
} {
|
||||||
|
code, _, errb := runOut("{/person}", c.args...)
|
||||||
|
if code != 2 {
|
||||||
|
t.Errorf("run(%v) = %d, want 2", c.args, code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(errb, c.want) {
|
||||||
|
t.Errorf("run(%v) stderr = %q, want %q", c.args, errb, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRepeat(t *testing.T) {
|
||||||
|
code, out, errb := runOut("{/word}\n", "--seed", "1", "--repeat", "3", "--data-path", svSE)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||||
|
}
|
||||||
|
lines := strings.Split(strings.TrimRight(out, "\n"), "\n")
|
||||||
|
if len(lines) != 3 {
|
||||||
|
t.Errorf("repeat=3 gave %d lines: %q", len(lines), out)
|
||||||
|
}
|
||||||
|
_, short, _ := runOut("{/word}\n", "--seed", "1", "-n", "3", "-d", svSE)
|
||||||
|
if short != out {
|
||||||
|
t.Errorf("-n 3 = %q, want the same as --repeat 3 %q", short, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunSeparator(t *testing.T) {
|
||||||
|
code, out, errb := runOut("{/word}", "--repeat", "3", "--separator", ",", "--data-path", svSE)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||||
|
}
|
||||||
|
if n := strings.Count(out, "\n"); n != 0 {
|
||||||
|
t.Errorf("want no newline, got %d: %q", n, out)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, ",") {
|
||||||
|
t.Errorf("values should be comma-joined: %q", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRepeatAdvancesRNG(t *testing.T) {
|
||||||
|
code, out, errb := runOut("{/person}\n", "--seed", "1", "--repeat", "5", "--data-path", svSE)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||||
|
}
|
||||||
|
uniq := map[string]bool{}
|
||||||
|
for _, l := range strings.Split(strings.TrimRight(out, "\n"), "\n") {
|
||||||
|
uniq[l] = true
|
||||||
|
}
|
||||||
|
if len(uniq) < 2 {
|
||||||
|
t.Errorf("repeat should vary output, all identical: %q", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunMisuse(t *testing.T) {
|
||||||
|
for _, c := range []struct {
|
||||||
|
stdin string
|
||||||
|
args []string
|
||||||
|
}{
|
||||||
|
{"{/person}", []string{"-d", svSE, "word"}},
|
||||||
|
{"", []string{"-d", svSE, "--list", "person"}},
|
||||||
|
{"{/sv_SE.person}", []string{"--no-shipped-data"}},
|
||||||
|
} {
|
||||||
|
code, out, errb := runOut(c.stdin, c.args...)
|
||||||
|
if code != 2 {
|
||||||
|
t.Errorf("run(%q, %v) = %d, want 2", c.stdin, c.args, code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(errb, "try 'fejkdata --help'") {
|
||||||
|
t.Errorf("run(%q, %v) stderr = %q, want a pointer to --help", c.stdin, c.args, errb)
|
||||||
|
}
|
||||||
|
if out != "" {
|
||||||
|
t.Errorf("run(%q, %v) stdout = %q, want nothing", c.stdin, c.args, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunTakesNoOperand(t *testing.T) {
|
||||||
|
file := filepath.Join(t.TempDir(), "users.tmpl")
|
||||||
|
if err := os.WriteFile(file, []byte("{/sv_SE.person}"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, c := range []struct {
|
||||||
|
args []string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{[]string{"sv_SE.person"}, "echo '{/sv_SE.person}' | fejkdata"},
|
||||||
|
{[]string{"/en_US.address"}, "echo '{/en_US.address}' | fejkdata"},
|
||||||
|
{[]string{"{/en_US.person}"}, "echo '{/en_US.person}' | fejkdata"},
|
||||||
|
{[]string{"name: {/sv_SE.person.last}"}, "echo 'name: {/sv_SE.person.last}' | fejkdata"},
|
||||||
|
{[]string{"geo.US.locality[O'Fallon].name"}, `echo '{/geo.US.locality[O'\''Fallon].name}' | fejkdata`},
|
||||||
|
{[]string{"en_US.address"}, "echo '{/en_US.address}' | fejkdata"},
|
||||||
|
{[]string{`C:\temp {/sv_SE.person}`}, `printf '%s' 'C:\temp {/sv_SE.person}' | fejkdata`},
|
||||||
|
{[]string{"-"}, "drop it"},
|
||||||
|
{[]string{file}, "fejkdata < " + file},
|
||||||
|
} {
|
||||||
|
code, out, errb := runOut("", c.args...)
|
||||||
|
if code != 2 || out != "" || !strings.Contains(errb, "unexpected argument") || !strings.Contains(errb, c.want) {
|
||||||
|
t.Errorf("run(%v) = %d, %q, %q; want misuse naming %q", c.args, code, out, errb, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type failingReader struct{}
|
||||||
|
|
||||||
|
func (failingReader) Read([]byte) (int, error) { return 0, errors.New("bad file descriptor") }
|
||||||
|
|
||||||
|
func TestRunUnreadableStdinIsMisuse(t *testing.T) {
|
||||||
|
var out, errb bytes.Buffer
|
||||||
|
if code := run("fejkdata", shipped, nil, 1, failingReader{}, &out, &errb); code != 2 || !strings.Contains(errb.String(), "cannot be read") {
|
||||||
|
t.Errorf("run with an unreadable stdin = %d, %q; want misuse naming it", code, errb.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunTerminalStdinIsMisuse(t *testing.T) {
|
||||||
|
code, out, errb := runTerminal("--seed", "1")
|
||||||
|
if code != 2 || out != "" || !strings.Contains(errb, "nothing is piped in: echo 'id: {int(1,999)}' | fejkdata; fejkdata --list prints the paths a {/path} reference reads\n") {
|
||||||
|
t.Errorf("run with nothing piped = %d, %q, %q; want misuse showing a template piped to fejkdata", code, out, errb)
|
||||||
|
}
|
||||||
|
for _, args := range [][]string{{"--list"}, {"--help"}, {"--version"}} {
|
||||||
|
if code, out, errb := runTerminal(args...); code != 0 || out == "" {
|
||||||
|
t.Errorf("run(%v) with nothing piped = %d, %q, %q; want it to run, reading no stdin", args, code, out, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRendersExactlyWhatIsSent(t *testing.T) {
|
||||||
|
for stdin, want := range map[string]string{
|
||||||
|
"hihi": "hihi",
|
||||||
|
"hihi\n": "hihi\n",
|
||||||
|
"hihi\r\n": "hihi\r\n",
|
||||||
|
"hihi\n\n": "hihi\n\n",
|
||||||
|
"{{x}}\n": "{x}\n",
|
||||||
|
"\"a\"\n": "a",
|
||||||
|
" \t\"a\"\r\n\n": "a",
|
||||||
|
"{/misc.territory[SE].alpha2}": "SE",
|
||||||
|
"{/misc.territory[SE].alpha2}\n": "SE\n",
|
||||||
|
} {
|
||||||
|
code, out, errb := runOut(stdin, "--seed", "1")
|
||||||
|
if code != 0 || out != want {
|
||||||
|
t.Errorf("run(%q) = %d, %q, %q; want %q", stdin, code, out, errb, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if code, out, errb := runOut("{\"format\":\"{x}\\n\",\"x\":[\"a\",\"b\"]}\n"); code != 0 || (out != "a\n" && out != "b\n") {
|
||||||
|
t.Errorf("a JSON template's format = %d, %q, %q; want its own newline only", code, out, errb)
|
||||||
|
}
|
||||||
|
if code, out, errb := runOut("{/sv_SE.word}\n", "--seed", "1", "-n", "3", "--separator", ","); code != 0 || strings.Count(out, "\n,") != 2 || !strings.HasSuffix(out, "\n") {
|
||||||
|
t.Errorf("echo's newline and a separator = %d, %q, %q; want each render's newline, then the separator", code, out, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRendersBlankStdin(t *testing.T) {
|
||||||
|
for _, stdin := range []string{"", "\n", " \r\n\t"} {
|
||||||
|
if code, out, errb := runOut(stdin); code != 0 || out != stdin {
|
||||||
|
t.Errorf("run(%q) = %d, %q, %q; want the blank template rendered as it is", stdin, code, out, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunTextPrintsAsWritten(t *testing.T) {
|
||||||
|
for _, stdin := range []string{"sv_SE.person", "[Skåne län]", "x[1]y", `say "hi"`} {
|
||||||
|
code, out, errb := runOut(stdin)
|
||||||
|
if code != 0 || out != stdin {
|
||||||
|
t.Errorf("run(%q) = %d, %q, %q; want the text as written", stdin, code, out, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunMultipleDataPaths(t *testing.T) {
|
||||||
|
code, out, errb := runOut("{/person}", "-d", enUS, "--data-path", svSE)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("run(multi-dir) = %d, stderr=%q", code, errb)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(out) == "" {
|
||||||
|
t.Fatal("empty output for multi-dir run")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunUnknownCategoryFails(t *testing.T) {
|
||||||
|
code, _, errb := runOut("{/nope}", "--data-path", svSE, "--format", "csv")
|
||||||
|
if code != 1 {
|
||||||
|
t.Fatalf("run = %d, want 1", code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(errb, "nope") {
|
||||||
|
t.Errorf("stderr %q should name the unknown category", errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunList(t *testing.T) {
|
||||||
|
code, out, errb := runOut("", "--data-path", svSE, "--list")
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||||
|
}
|
||||||
|
for _, want := range []string{"person", "person.last", "address"} {
|
||||||
|
if !strings.Contains(out, want) {
|
||||||
|
t.Errorf("list output missing %q:\n%s", want, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunHelpOnStdout(t *testing.T) {
|
||||||
|
for _, h := range []string{"-h", "--help"} {
|
||||||
|
code, out, errb := runOut("", h)
|
||||||
|
if code != 0 {
|
||||||
|
t.Errorf("%s = %d, want 0", h, code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "Usage") || errb != "" {
|
||||||
|
t.Errorf("%s: stdout = %q, stderr = %q, want usage on stdout only", h, out, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVersionLineNamesFejkdatasVersion(t *testing.T) {
|
||||||
|
fejkdata := func(version string, replace *debug.Module) *debug.Module {
|
||||||
|
return &debug.Module{Path: "github.com/larvit/fejkdata", Version: version, Replace: replace}
|
||||||
|
}
|
||||||
|
mycli := debug.Module{Path: "example.com/mycli", Version: "v9.0.0"}
|
||||||
|
for _, c := range []struct {
|
||||||
|
info *debug.BuildInfo
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{&debug.BuildInfo{Main: *fejkdata("v0.1.0", nil)}, "fejkdata v0.1.0"},
|
||||||
|
{&debug.BuildInfo{Main: *fejkdata("(devel)", nil)}, "fejkdata devel"},
|
||||||
|
{&debug.BuildInfo{Main: mycli, Deps: []*debug.Module{fejkdata("v0.1.0", nil)}}, "mycli v9.0.0, fejkdata v0.1.0"},
|
||||||
|
{&debug.BuildInfo{Main: mycli, Deps: []*debug.Module{fejkdata("v0.1.0", &debug.Module{Path: "../fejkdata"})}}, "mycli v9.0.0, fejkdata devel"},
|
||||||
|
{&debug.BuildInfo{Main: mycli, Deps: []*debug.Module{fejkdata("v0.1.0", &debug.Module{Path: "example.com/fork", Version: "v0.1.1"})}}, "mycli v9.0.0, fejkdata v0.1.1"},
|
||||||
|
{&debug.BuildInfo{Main: mycli}, "mycli v9.0.0, fejkdata devel"},
|
||||||
|
{nil, "fejkdata devel"},
|
||||||
|
} {
|
||||||
|
if got := versionLine("mycli", c.info); got != c.want {
|
||||||
|
t.Errorf("versionLine(mycli, %+v) = %q, want %q", c.info, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommandNameIsTheFileItRunsFrom(t *testing.T) {
|
||||||
|
for arg0, want := range map[string]string{
|
||||||
|
"/usr/local/bin/mycli": "mycli",
|
||||||
|
"mycli.EXE": "mycli",
|
||||||
|
"mycli.exe": "mycli",
|
||||||
|
"my.cli": "my.cli",
|
||||||
|
"": "fejkdata",
|
||||||
|
} {
|
||||||
|
if got := commandName(arg0); got != want {
|
||||||
|
t.Errorf("commandName(%q) = %q, want %q", arg0, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunNamesItselfByTheCommandName(t *testing.T) {
|
||||||
|
var out, errb bytes.Buffer
|
||||||
|
if code := run("mycli", shipped, []string{"--nope"}, 1, nil, &out, &errb); code != 2 || errb.String() != "mycli: unknown flag --nope\ntry 'mycli --help'\n" {
|
||||||
|
t.Errorf("mycli --nope = %d, %q", code, errb.String())
|
||||||
|
}
|
||||||
|
errb.Reset()
|
||||||
|
if code := run("mycli", shipped, nil, 1, nil, &out, &errb); code != 2 || !strings.Contains(errb.String(), "| mycli; mycli --list prints") {
|
||||||
|
t.Errorf("mycli with nothing piped = %d, %q, want a pipe into mycli", code, errb.String())
|
||||||
|
}
|
||||||
|
errb.Reset()
|
||||||
|
if code := run("mycli", []fs.FS{nil, nil}, nil, 1, strings.NewReader("x"), &out, &errb); code != 1 || strings.Count(errb.String(), "mycli: ") != 2 || strings.Contains(errb.String(), "fejkdata") {
|
||||||
|
t.Errorf("mycli over two nil modules = %d, %q, want each line under mycli", code, errb.String())
|
||||||
|
}
|
||||||
|
errb.Reset()
|
||||||
|
if code := run("mycli", shipped, []string{"-d", "/nope"}, 1, strings.NewReader("{/sv_SE.person}"), &out, &errb); code != 1 || !strings.HasPrefix(errb.String(), "mycli: ") || strings.Contains(errb.String(), "fejkdata") {
|
||||||
|
t.Errorf("mycli -d /nope = %d, %q, want a load error under mycli", code, errb.String())
|
||||||
|
}
|
||||||
|
if code := run("mycli", shipped, []string{"--help"}, 1, nil, &out, &errb); code != 0 || !strings.HasPrefix(out.String(), "Usage: <template> | mycli [flags]\n") || strings.Contains(out.String(), "fejkdata") {
|
||||||
|
t.Errorf("mycli --help = %d, %q, want mycli named throughout", code, out.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunVersion(t *testing.T) {
|
||||||
|
code, out, errb := runOut("", "--version")
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("--version = %d, stderr=%q", code, errb)
|
||||||
|
}
|
||||||
|
version, ok := strings.CutPrefix(out, "fejkdata ")
|
||||||
|
if !ok || strings.TrimSpace(version) == "" {
|
||||||
|
t.Errorf("--version = %q, want the command name and a version on stdout", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunMissingDirFails(t *testing.T) {
|
||||||
|
code, _, errb := runOut("{/person}", "--data-path", "../data/nope")
|
||||||
|
if code != 1 {
|
||||||
|
t.Fatalf("run = %d, want 1", code)
|
||||||
|
}
|
||||||
|
if errb == "" {
|
||||||
|
t.Error("want an error message on stderr")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunShippedDataByDefault(t *testing.T) {
|
||||||
|
code, out, errb := runOut("{/sv_SE.person}", "--seed", "1")
|
||||||
|
if code != 0 || strings.TrimSpace(out) == "" {
|
||||||
|
t.Fatalf("run = %d, out=%q, stderr=%q", code, out, errb)
|
||||||
|
}
|
||||||
|
code, list, _ := runOut("", "--list")
|
||||||
|
if code != 0 || !strings.Contains(list, "en_US.person\n") || !strings.Contains(list, "misc.uuid\n") {
|
||||||
|
t.Errorf("--list without --data-path = %d, %q", code, list)
|
||||||
|
}
|
||||||
|
code, _, errb = runOut("{/person}")
|
||||||
|
if code != 2 || !strings.Contains(errb, "person") {
|
||||||
|
t.Errorf("a category outside the shipped tree: code %d, stderr %q", code, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunSelectsATableRow(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
for name, content := range map[string]string{
|
||||||
|
"region.json": `{"format":"{name}","rows":"region.tsv","key":"code","name":"name"}`,
|
||||||
|
"region.tsv": "code\tname\n01\tStockholms län\n12\tSkåne län\n",
|
||||||
|
} {
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if code, out, errb := runOut("{/region[12]}", "--no-shipped-data", "-d", dir); code != 0 || out != "Skåne län" {
|
||||||
|
t.Fatalf("{/region[12]} = %d, %q, stderr=%q", code, out, errb)
|
||||||
|
}
|
||||||
|
if code, out, _ := runOut("{/region[Skåne län].code}", "--no-shipped-data", "-d", dir); code != 0 || out != "12" {
|
||||||
|
t.Fatalf("{/region[Skåne län].code} = %d, %q", code, out)
|
||||||
|
}
|
||||||
|
if code, out, _ := runOut("{/region[12]}", "--no-shipped-data", "-d", dir, "--format", "sql"); code != 0 || out != `INSERT INTO "region" ("code", "name") VALUES ('12', 'Skåne län');`+"\n" {
|
||||||
|
t.Fatalf("--format sql {/region[12]} = %d, %q, want the table named without its selector", code, out)
|
||||||
|
}
|
||||||
|
if code, _, errb := runOut("{/region[99]}", "--no-shipped-data", "-d", dir); code != 2 || !strings.Contains(errb, `"99"`) {
|
||||||
|
t.Fatalf("{/region[99]} = %d, stderr=%q, want misuse naming the row", code, errb)
|
||||||
|
}
|
||||||
|
if code, out, _ := runOut("{/misc.territory[SE]}", "--seed", "1", "--format", "csv"); code != 0 || !strings.HasPrefix(out, "alpha2,") || !strings.Contains(out, "\nSE,SWE,") {
|
||||||
|
t.Fatalf("--format csv {/misc.territory[SE]} = %d, %q", code, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsageExamplesRenderWithNoData(t *testing.T) {
|
||||||
|
examples := regexp.MustCompile(`(?m)^ echo '([^']*)' \| COMMAND$`).FindAllStringSubmatch(usage+"\n "+pipeHint("COMMAND")+"\n", -1)
|
||||||
|
if len(examples) < 3 {
|
||||||
|
t.Fatalf("usage and pipe hint hold %d echo examples, want the help's two and the hint's", len(examples))
|
||||||
|
}
|
||||||
|
for _, e := range examples {
|
||||||
|
if code, out, errb := runOut(e[1], "--no-shipped-data"); code != 0 || strings.TrimSpace(out) == "" {
|
||||||
|
t.Errorf("echo %q with no data: run = %d, %q, stderr %q", e[1], code, out, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, token := range regexp.MustCompile(`\{/[^}]*\}`).FindAllString(usage, -1) {
|
||||||
|
if token != "{/path}" && token != "{/table[KEY]}" {
|
||||||
|
t.Errorf("usage names %s, a path a custom CLI may not carry", token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.Contains(usage, "\nExit codes: 0 success, 1 runtime error, 2 misuse.\n") {
|
||||||
|
t.Errorf("usage does not list the exit codes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunOperandMisuseBeforeLoad(t *testing.T) {
|
||||||
|
code, _, errb := runOut("", "--no-shipped-data", "x")
|
||||||
|
if code != 2 || !strings.Contains(errb, "unexpected argument") || strings.Contains(errb, "--data-path") {
|
||||||
|
t.Fatalf("an operand with no data = %d, %q; want the operand refused before any load", code, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunInlineTemplate(t *testing.T) {
|
||||||
|
code, out, errb := runOut("name: {/sv_SE.person.last}", "--seed", "1")
|
||||||
|
if code != 0 || !strings.HasPrefix(out, "name: ") || strings.Contains(out, "{") {
|
||||||
|
t.Fatalf("inline format string = %d, %q, stderr %q", code, out, errb)
|
||||||
|
}
|
||||||
|
code, out, errb = runOut(`{"format":"name: {x}","x":["bosse","lina"]}`, "--seed", "1")
|
||||||
|
if code != 0 || (out != "name: bosse" && out != "name: lina") {
|
||||||
|
t.Fatalf("inline JSON template = %d, %q, want one name, stderr %q", code, out, errb)
|
||||||
|
}
|
||||||
|
code, out, errb = runOut(`"name: {/sv_SE.person.last}"`, "--seed", "1")
|
||||||
|
if code != 0 || !strings.HasPrefix(out, "name: ") || strings.Contains(out, "{") {
|
||||||
|
t.Fatalf("inline JSON string = %d, %q, stderr %q", code, out, errb)
|
||||||
|
}
|
||||||
|
code, out, errb = runOut("{digits(1)}\n", "--seed", "1", "-n", "2")
|
||||||
|
if code != 0 || len(strings.Split(strings.TrimRight(out, "\n"), "\n")) != 2 {
|
||||||
|
t.Fatalf("inline template with --repeat = %d, %q, stderr %q", code, out, errb)
|
||||||
|
}
|
||||||
|
code, out, errb = runOut("{/sv_SE.person} hihi", "--seed", "1")
|
||||||
|
if code != 0 || !strings.HasSuffix(out, " hihi") || strings.Contains(out, "{") {
|
||||||
|
t.Fatalf("a reference and text = %d, %q, stderr %q", code, out, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunFormatTakesOnlyALoneReferenceAsARecord(t *testing.T) {
|
||||||
|
for _, stdin := range []string{"{//sv_SE.person}", "{/sv_SE.person|/misc.uuid}", "{/sv_SE.person as p}", "{/}", "{/.sv_SE.person}"} {
|
||||||
|
if code, out, errb := runOut(stdin, "--format", "json"); code != 2 || out != "" {
|
||||||
|
t.Errorf("run(%q, --format json) = %d, %q, %q; want misuse", stdin, code, out, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if code, out, errb := runOut(`{"format":"x"}`); code != 0 || out != "x" {
|
||||||
|
t.Errorf(`run({"format":"x"}) = %d, %q, %q; want x`, code, out, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunTemplateMisuse(t *testing.T) {
|
||||||
|
for stdin, want := range map[string]string{
|
||||||
|
"{bad": "unterminated",
|
||||||
|
"name: {/no.such.path}": "no entry",
|
||||||
|
"a } b": "}}",
|
||||||
|
"{//sv_SE.person}": "write {/sv_SE.person}",
|
||||||
|
} {
|
||||||
|
code, out, errb := runOut(stdin, "--seed", "1")
|
||||||
|
if code != 2 || out != "" || !strings.Contains(errb, "try 'fejkdata --help'") || !strings.Contains(errb, want) {
|
||||||
|
t.Errorf("run(%q) = %d, %q, %q; want misuse naming %q and --help", stdin, code, out, errb, want)
|
||||||
|
}
|
||||||
|
if strings.Contains(errb, "fejkdata: fejkdata:") {
|
||||||
|
t.Errorf("run(%q) doubled the program prefix: %q", stdin, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunNoShippedData(t *testing.T) {
|
||||||
|
code, list, errb := runOut("", "--no-shipped-data", "-d", misc, "--list")
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||||
|
}
|
||||||
|
if strings.Contains(list, "sv_SE") || !strings.Contains(list, "uuid\n") {
|
||||||
|
t.Errorf("--no-shipped-data --list = %q, want only the given dir", list)
|
||||||
|
}
|
||||||
|
code, out, _ := runOut("{/uuid}", "--no-shipped-data", "-d", misc, "-s", "3")
|
||||||
|
if code != 0 || strings.TrimSpace(out) == "" {
|
||||||
|
t.Errorf("run = %d, out=%q", code, out)
|
||||||
|
}
|
||||||
|
if code, out, errb := runOut("{digits(3)}", "--no-shipped-data"); code != 0 || len(out) != 3 {
|
||||||
|
t.Errorf("--no-shipped-data alone = %d, %q, %q, want a template reading no data rendered", code, out, errb)
|
||||||
|
}
|
||||||
|
if code, _, errb := runOut("{/sv_SE.person}", "--no-shipped-data"); code != 2 || !strings.HasSuffix(errb, "no data is loaded\ntry 'fejkdata --help'\n") {
|
||||||
|
t.Errorf("--no-shipped-data alone = %d, %q, want a reference to nothing refused", code, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRepeatOfZeroWritesNoRecord(t *testing.T) {
|
||||||
|
dir := recordDir(t)
|
||||||
|
for _, c := range []struct {
|
||||||
|
args []string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{nil, ""},
|
||||||
|
{[]string{"--format", "json"}, "[]\n"},
|
||||||
|
{[]string{"--format", "ndjson"}, ""},
|
||||||
|
{[]string{"--format", "csv"}, "first,last\n"},
|
||||||
|
{[]string{"--format", "sql"}, ""},
|
||||||
|
} {
|
||||||
|
if code, out, errb := runOut("{/users}", append(c.args, "-n", "0", "-d", dir)...); code != 0 || out != c.want {
|
||||||
|
t.Errorf("run(-n 0 %v) = %d, %q, %q, want %q", c.args, code, out, errb, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if code, _, _ := runOut("{/nope}", "-n", "0", "--format", "json", "-d", dir); code != 1 {
|
||||||
|
t.Errorf("run(-n 0) of a reference to nothing = %d, want 1", code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRepeatIsBounded(t *testing.T) {
|
||||||
|
for _, args := range [][]string{
|
||||||
|
{"--repeat", "9223372036854775807"},
|
||||||
|
{"--repeat", "1048577"},
|
||||||
|
{"-n", "99999999999"},
|
||||||
|
} {
|
||||||
|
code, out, errb := runOut("{/sv_SE.word}", args...)
|
||||||
|
if code != 2 || out != "" || !strings.Contains(errb, "0..1048576") {
|
||||||
|
t.Errorf("run(%v) = %d, %q, %q, want misuse naming the range", args, code, out, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dir := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "x.json"), []byte(`"x"`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
code, out, errb := runOut("{/x}", "--no-shipped-data", "-d", dir, "--repeat", "1048576", "--separator", "")
|
||||||
|
if code != 0 || len(out) != 1048576 {
|
||||||
|
t.Errorf("repeat at the cap = %d, %d bytes, stderr %q; want every render streamed", code, len(out), errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunIgnoresAFlagWithNoEffect(t *testing.T) {
|
||||||
|
dir := recordDir(t)
|
||||||
|
for _, c := range []struct {
|
||||||
|
stdin string
|
||||||
|
base, ignored []string
|
||||||
|
}{
|
||||||
|
{"", []string{"--list"}, []string{"-n", "3"}},
|
||||||
|
{"", []string{"--list"}, []string{"--separator", ","}},
|
||||||
|
{"", []string{"--list"}, []string{"--format", "json"}},
|
||||||
|
{"", []string{"--list"}, []string{"--table", "t"}},
|
||||||
|
{"{/users}", nil, []string{"--table", "t"}},
|
||||||
|
{"{/users}", []string{"--format", "json"}, []string{"--table", "t"}},
|
||||||
|
} {
|
||||||
|
args := append([]string{"--seed", "1", "--data-path", dir}, c.base...)
|
||||||
|
_, want, _ := runOut(c.stdin, args...)
|
||||||
|
code, got, errb := runOut(c.stdin, append(args, c.ignored...)...)
|
||||||
|
if code != 0 || want == "" || got != want {
|
||||||
|
t.Errorf("run(%v + %v) = %d, %q, %q; want what %v alone prints, %q", c.base, c.ignored, code, got, errb, c.base, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunReadsABareNumberAsAFormatString(t *testing.T) {
|
||||||
|
if code, out, errb := runOut("42\n", "-n", "3"); code != 0 || out != "42\n42\n42\n" {
|
||||||
|
t.Errorf("echo 42 | fejkdata -n 3 = %d, %q, %q, want 42 on three lines", code, out, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRendersAnyJSONValue(t *testing.T) {
|
||||||
|
for stdin, want := range map[string]string{"42": "42", "42\n": "42\n", "true\n": "true\n", " 7 ": " 7 ", "null\n": "", " null ": ""} {
|
||||||
|
if code, out, errb := runOut(stdin); code != 0 || out != want {
|
||||||
|
t.Errorf("run(%q) = %d, %q, %q, want %q", stdin, code, out, errb, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunUnknownFlagIsNamedByRune(t *testing.T) {
|
||||||
|
code, _, errb := runOut("{/sv_SE.word}", "-ä")
|
||||||
|
if code != 2 || !strings.Contains(errb, "unknown flag -ä") {
|
||||||
|
t.Errorf("run(-ä) = %d, %q, want the flag named whole", code, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunEmptyDataPathIsNamed(t *testing.T) {
|
||||||
|
code, _, errb := runOut("{/sv_SE.word}", "--data-path=")
|
||||||
|
if code != 1 || !strings.Contains(errb, "empty") {
|
||||||
|
t.Errorf("run(--data-path=) = %d, %q, want the empty path named", code, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordDir(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
dir := t.TempDir()
|
||||||
|
content := `{"format":"{first} {last}","first":["Ada","Bo"],"last":["Lovelace","Ek"]}`
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "users.json"), []byte(content), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRecordJSON(t *testing.T) {
|
||||||
|
code, out, errb := runOut("{/users}", "--seed", "1", "--format", "json", "--repeat", "2", "--data-path", recordDir(t))
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||||
|
}
|
||||||
|
var arr []map[string]string
|
||||||
|
if err := json.Unmarshal([]byte(out), &arr); err != nil {
|
||||||
|
t.Fatalf("json output is not one JSON array: %v\n%q", err, out)
|
||||||
|
}
|
||||||
|
if len(arr) != 2 || len(arr[0]) != 2 || arr[0]["first"] == "" || arr[0]["last"] == "" {
|
||||||
|
t.Fatalf("json output = %q, want an array of two records with first and last columns", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRecordNDJSON(t *testing.T) {
|
||||||
|
code, out, errb := runOut("{/users}", "--seed", "1", "--format", "ndjson", "--repeat", "2", "--data-path", recordDir(t))
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||||
|
}
|
||||||
|
lines := strings.Split(strings.TrimRight(out, "\n"), "\n")
|
||||||
|
if len(lines) != 2 {
|
||||||
|
t.Fatalf("ndjson output = %d lines %q, want one object per line", len(lines), out)
|
||||||
|
}
|
||||||
|
for _, line := range lines {
|
||||||
|
var m map[string]string
|
||||||
|
if err := json.Unmarshal([]byte(line), &m); err != nil || len(m) != 2 {
|
||||||
|
t.Fatalf("ndjson line %q is not one object: %v", line, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRecordCSVRoundTrips(t *testing.T) {
|
||||||
|
code, out, errb := runOut("{/users}\r\n", "--seed", "1", "--format", "csv", "--repeat", "3", "--data-path", recordDir(t))
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||||
|
}
|
||||||
|
lines := strings.Split(strings.TrimRight(out, "\n"), "\n")
|
||||||
|
if len(lines) != 4 {
|
||||||
|
t.Fatalf("csv output = %d lines %q, want a header and 3 rows", len(lines), out)
|
||||||
|
}
|
||||||
|
if lines[0] != "first,last" {
|
||||||
|
t.Fatalf("csv header = %q, want first,last", lines[0])
|
||||||
|
}
|
||||||
|
r, err := csv.NewReader(strings.NewReader(out)).ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("csv output is not valid CSV: %v", err)
|
||||||
|
}
|
||||||
|
if len(r) != 4 || len(r[0]) != 2 {
|
||||||
|
t.Fatalf("csv parsed to %v, want 4 records of 2 fields", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRecordSQL(t *testing.T) {
|
||||||
|
code, out, errb := runOut("{/users}", "--seed", "1", "--format", "sql", "--repeat", "2", "--table", "people", "--data-path", recordDir(t))
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||||
|
}
|
||||||
|
lines := strings.Split(strings.TrimRight(out, "\n"), "\n")
|
||||||
|
if len(lines) != 2 || !strings.HasPrefix(lines[0], `INSERT INTO "people" ("first", "last") VALUES (`) {
|
||||||
|
t.Fatalf("sql output = %q, want two INSERT INTO people statements", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRecordDefaultTable(t *testing.T) {
|
||||||
|
code, out, errb := runOut("{/users}", "--seed", "1", "--format", "sql", "--data-path", recordDir(t))
|
||||||
|
if code != 0 || !strings.HasPrefix(out, `INSERT INTO "users" (`) {
|
||||||
|
t.Fatalf("default table = %d, %q, want the reference's last segment; stderr %q", code, out, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRecordOfAQuotedLoneReference(t *testing.T) {
|
||||||
|
code, out, errb := runOut(`"{/users}"`, "--seed", "1", "--format", "sql", "--data-path", recordDir(t))
|
||||||
|
if code != 0 || !strings.HasPrefix(out, `INSERT INTO "users" (`) {
|
||||||
|
t.Fatalf("a JSON string holding {/users} = %d, %q, %q; want the record users", code, out, errb)
|
||||||
|
}
|
||||||
|
code, out, errb = runOut("{/users} ", "--format", "csv", "--data-path", recordDir(t))
|
||||||
|
if code != 2 || out != "" || !strings.Contains(errb, "whitespace around {/users}") {
|
||||||
|
t.Errorf("{/users} and a space = %d, %q, %q; want misuse naming the whitespace", code, out, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRecordInlineTemplate(t *testing.T) {
|
||||||
|
code, out, errb := runOut(`{"format":"{x}","x":["a","b"]}`, "--seed", "1", "--format", "json")
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("inline record = %d, stderr=%q", code, errb)
|
||||||
|
}
|
||||||
|
var arr []map[string]string
|
||||||
|
if err := json.Unmarshal([]byte(out), &arr); err != nil || len(arr) != 1 || (arr[0]["x"] != "a" && arr[0]["x"] != "b") {
|
||||||
|
t.Fatalf("inline record json = %q, want one record with a column x (err %v)", out, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRecordMisuse(t *testing.T) {
|
||||||
|
for _, c := range []struct {
|
||||||
|
args []string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{[]string{"--format", "yaml"}, "--format takes csv, json, ndjson, sql or text"},
|
||||||
|
{[]string{"--format", "sql", "--table", ""}, "--table names the INSERT target"},
|
||||||
|
{[]string{"--format", "csv", "--separator", ";"}, `--separator joins text renders; --format csv always separates fields with ","`},
|
||||||
|
{[]string{"--format", "json", "--separator", ","}, "--separator joins text renders"},
|
||||||
|
{[]string{"--format", "ndjson", "--separator", ","}, "--separator joins text renders"},
|
||||||
|
{[]string{"--format", "sql", "--separator", ","}, "--separator joins text renders"},
|
||||||
|
} {
|
||||||
|
code, out, errb := runOut("{/users}", c.args...)
|
||||||
|
if code != 2 || out != "" || !strings.Contains(errb, c.want) {
|
||||||
|
t.Errorf("run(%v) = %d, %q, %q; want misuse naming %q", c.args, code, out, errb, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRecordOnABareValueIsRuntimeError(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "word.json"), []byte(`["a","b"]`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
code, _, errb := runOut("{/word}", "--format", "json", "--data-path", dir)
|
||||||
|
if code != 1 {
|
||||||
|
t.Fatalf("record on a choice = %d, want exit 1 (runtime error), stderr %q", code, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRecordRejectsATopLevelRepeat(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
content := `{"format":"{a}-","repeat":3,"separator":"|","a":["x","y"]}`
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "rep.json"), []byte(content), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
code, _, errb := runOut("{/rep}", "--format", "json", "--data-path", dir)
|
||||||
|
if code != 1 || !strings.Contains(errb, "carries repeat 3") {
|
||||||
|
t.Errorf("record on a repeating path = %d, %q; want exit 1 naming the repeat", code, errb)
|
||||||
|
}
|
||||||
|
code, _, errb = runOut(content, "--format", "json")
|
||||||
|
if code != 2 || !strings.Contains(errb, "carries repeat 3") {
|
||||||
|
t.Errorf("record on a repeating inline template = %d, %q; want exit 2 naming the repeat", code, errb)
|
||||||
|
}
|
||||||
|
if code, out, _ := runOut("{/rep}", "--seed", "1", "--data-path", dir); code != 0 || strings.TrimSpace(out) != "x-|x-|x-" {
|
||||||
|
t.Errorf("text view = %d, %q; want the repeat still composed", code, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunNumbersFollowTheShell(t *testing.T) {
|
||||||
|
_, want, _ := runOut("{/sv_SE.word}", "--seed", "7", "--repeat", "3")
|
||||||
|
code, got, errb := runOut("{/sv_SE.word}", "--seed", "007", "--repeat", "+3")
|
||||||
|
if code != 0 || got != want {
|
||||||
|
t.Errorf("run(--seed 007 --repeat +3) = %d, %q, stderr %q; want the same as --seed 7 --repeat 3 %q", code, got, errb, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRunRecordOfAFieldlessPath pins the way out of asking a category of one value
|
||||||
|
// for columns: the record to write, and the quoting a shell needs to take it. The
|
||||||
|
// template is well-formed and only the data cannot serve it, so the code is 1.
|
||||||
|
func TestRunRecordOfAFieldlessPath(t *testing.T) {
|
||||||
|
code, out, errb := runOut("{/sv_SE.personnummer}", "--format", "csv")
|
||||||
|
if code != 1 {
|
||||||
|
t.Fatalf("run(--format csv) of {/sv_SE.personnummer} = %d, want 1", code)
|
||||||
|
}
|
||||||
|
for _, want := range []string{`{"format":"","personnummer":"{/sv_SE.personnummer}"}`, "single quotes"} {
|
||||||
|
if !strings.Contains(errb, want) {
|
||||||
|
t.Errorf("stderr = %q, want it to name %s", errb, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if out != "" {
|
||||||
|
t.Errorf("stdout = %q, want nothing", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestADataPathCategoryFailingAtFirstReachExits1(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
for name, data := range map[string]string{
|
||||||
|
".fejkdata.json": `{"index": {"bad": {"paths": [""]}}}`,
|
||||||
|
"bad.json": `"{/nope}"`,
|
||||||
|
} {
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, name), []byte(data), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, args := range [][]string{{"-d", dir}, {"-d", dir, "--format", "json"}} {
|
||||||
|
if code, _, errb := runOut("{/bad}", args...); code != 1 || !strings.Contains(errb, "{/nope}") {
|
||||||
|
t.Errorf("%v on a category failing to load = %d, %q; want exit 1 naming the reference", args, code, errb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
|
)
|
||||||
|
|
||||||
|
// grammarLoneReference is the path a format reads when the grammar parses it as one path
|
||||||
|
// read of one reference from the root, and nothing else.
|
||||||
|
func grammarLoneReference(text string) (string, bool) {
|
||||||
|
toks, err := grammar.ParseFormat(text)
|
||||||
|
if err != nil || len(toks) != 1 || toks[0].Kind != grammar.PathRead || len(toks[0].Arms) != 1 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
sigil, path, err := grammar.RefShape(toks[0].Arms[0])
|
||||||
|
if err != nil || sigil != "/" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return path, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoneReferenceAgreesWithTheGrammar(t *testing.T) {
|
||||||
|
for _, text := range []string{
|
||||||
|
" {/x}",
|
||||||
|
"x",
|
||||||
|
"{.x}",
|
||||||
|
"{/.x}",
|
||||||
|
"{//x}",
|
||||||
|
"{/a...b}",
|
||||||
|
"{/a..b}",
|
||||||
|
"{/a..}",
|
||||||
|
"{/a.[x]}",
|
||||||
|
"{/a.}",
|
||||||
|
"{/a|/b}",
|
||||||
|
"{/a}{/b}",
|
||||||
|
"{/misc.currency[US Dollar (Next day)]}",
|
||||||
|
"{/sv_SE.person}",
|
||||||
|
"{/t[a as b].c}",
|
||||||
|
"{/t[f(x)]}",
|
||||||
|
"{/x as p}",
|
||||||
|
"{/x(1)}",
|
||||||
|
"{/x}}",
|
||||||
|
"{/}",
|
||||||
|
} {
|
||||||
|
path, lone := loneReference(text)
|
||||||
|
want, wantLone := grammarLoneReference(text)
|
||||||
|
if lone != wantLone || path != want {
|
||||||
|
t.Errorf("loneReference(%q) = %q, %v; the grammar reads %q, %v", text, path, lone, want, wantLone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-488
@@ -1,495 +1,12 @@
|
|||||||
// Command fejkdata prints fake values from the shipped data and any directories
|
// Command fejkdata renders the template on its stdin with fake values from every shipped
|
||||||
// layered over it.
|
// module; package cli documents it.
|
||||||
//
|
|
||||||
// fejkdata sv_SE.person # a full person
|
|
||||||
// fejkdata sv_SE.person.last # just the surname
|
|
||||||
// fejkdata --data-path ./mydata sv_SE.person # layer custom data; the last dir wins
|
|
||||||
// fejkdata --seed 42 sv_SE.address
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"runtime/debug"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitea.larvit.se/larvit/fejkdata"
|
"github.com/larvit/fejkdata/cli"
|
||||||
|
"github.com/larvit/fejkdata/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
const usage = `Usage: fejkdata [flags] <path|template>
|
func main() { os.Exit(cli.Main(data.Modules()...)) }
|
||||||
|
|
||||||
<path> a category, or a dotted path into one (person, person.last);
|
|
||||||
a table's row by key or name: 'misc.country[SE]', 'misc.country[Sweden].tld'
|
|
||||||
<template> a format string or JSON value to render inline, e.g.
|
|
||||||
'name: {/sv_SE.person.last}' or '{"format":"{x}","x":["bosse","lina"]}'
|
|
||||||
|
|
||||||
An argument containing a { token, or a JSON object, array or string, is a
|
|
||||||
template; any other argument is a path (a path never contains a brace or a quote,
|
|
||||||
and a bracket only as a [selector] after a table's name). Templates reach the
|
|
||||||
data by reference from the root — {/sv_SE.person.last} — whether the data is
|
|
||||||
shipped or layered with --data-path. An argument carrying a closing brace or a
|
|
||||||
quote but no valid JSON names neither.
|
|
||||||
|
|
||||||
With --format json, ndjson, csv or sql the argument must name a record — a
|
|
||||||
template whose fields are its columns — and the rows are written as one JSON
|
|
||||||
array, one JSON object per line, one CSV row (after a header), or one INSERT.
|
|
||||||
|
|
||||||
-d, --data-path D a data directory to layer over the shipped data (repeatable; last wins on a clash)
|
|
||||||
--format F output form: text (default), json, ndjson, csv or sql
|
|
||||||
-h, --help print this help, then exit
|
|
||||||
--list list the paths the data offers, then exit
|
|
||||||
--no-shipped-data load only the --data-path directories
|
|
||||||
-n, --repeat N render the value N times, 1..1048576 (default 1)
|
|
||||||
-s, --seed N seed for reproducible output
|
|
||||||
--separator S string between repeated values (default newline)
|
|
||||||
--table T the INSERT target for --format sql (default: the path's last segment, or records for an inline template)
|
|
||||||
--version print the version, then exit
|
|
||||||
|
|
||||||
Flags may come before or after <path|template>; -- ends the flags. A short flag's
|
|
||||||
value attaches or follows (-n3, -n 3); short flags bundle (-hn 3).
|
|
||||||
`
|
|
||||||
|
|
||||||
type invocation struct {
|
|
||||||
dirs []string
|
|
||||||
format string
|
|
||||||
formatSet bool
|
|
||||||
help bool
|
|
||||||
list bool
|
|
||||||
noShipped bool
|
|
||||||
paths []string
|
|
||||||
repeat int
|
|
||||||
repeatSet bool
|
|
||||||
seed uint64
|
|
||||||
seeded bool
|
|
||||||
separator string
|
|
||||||
separatorSet bool
|
|
||||||
table string
|
|
||||||
tableSet bool
|
|
||||||
version bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type flagDef struct {
|
|
||||||
long string
|
|
||||||
short string
|
|
||||||
value bool
|
|
||||||
set func(*invocation, string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
var flagDefs = []flagDef{
|
|
||||||
{"data-path", "d", true, func(in *invocation, v string) error { in.dirs = append(in.dirs, v); return nil }},
|
|
||||||
{"format", "", true, func(in *invocation, v string) error { in.format, in.formatSet = v, true; return nil }},
|
|
||||||
{"help", "h", false, func(in *invocation, _ string) error { in.help = true; return nil }},
|
|
||||||
{"list", "", false, func(in *invocation, _ string) error { in.list = true; return nil }},
|
|
||||||
{"no-shipped-data", "", false, func(in *invocation, _ string) error { in.noShipped = true; return nil }},
|
|
||||||
{"repeat", "n", true, func(in *invocation, v string) error {
|
|
||||||
n, err := strconv.Atoi(v)
|
|
||||||
if err != nil || n < 1 || n > fejkdata.MaxRepeat {
|
|
||||||
return fmt.Errorf("--repeat needs an integer in 1..%d, got %q", fejkdata.MaxRepeat, v)
|
|
||||||
}
|
|
||||||
in.repeat, in.repeatSet = n, true
|
|
||||||
return nil
|
|
||||||
}},
|
|
||||||
{"seed", "s", true, func(in *invocation, v string) error {
|
|
||||||
n, err := strconv.ParseUint(v, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("--seed needs an unsigned integer, got %q", v)
|
|
||||||
}
|
|
||||||
in.seed, in.seeded = n, true
|
|
||||||
return nil
|
|
||||||
}},
|
|
||||||
{"separator", "", true, func(in *invocation, v string) error { in.separator, in.separatorSet = v, true; return nil }},
|
|
||||||
{"table", "", true, func(in *invocation, v string) error { in.table, in.tableSet = v, true; return nil }},
|
|
||||||
{"version", "", false, func(in *invocation, _ string) error { in.version = true; return nil }},
|
|
||||||
}
|
|
||||||
|
|
||||||
func flagByLong(name string) *flagDef {
|
|
||||||
for i := range flagDefs {
|
|
||||||
if flagDefs[i].long == name {
|
|
||||||
return &flagDefs[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func flagByShort(name string) *flagDef {
|
|
||||||
for i := range flagDefs {
|
|
||||||
if flagDefs[i].short == name {
|
|
||||||
return &flagDefs[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// flagArg is one flag as written: its definition and the value attached to it,
|
|
||||||
// if any.
|
|
||||||
type flagArg struct {
|
|
||||||
def *flagDef
|
|
||||||
value string
|
|
||||||
inline bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// splitFlags reads one argv element as flags: --name or --name=value, or -abc
|
|
||||||
// where each letter is a short flag and the first that takes a value takes the
|
|
||||||
// rest of the element.
|
|
||||||
func splitFlags(arg string) ([]flagArg, error) {
|
|
||||||
if strings.HasPrefix(arg, "--") {
|
|
||||||
name, value, inline := strings.Cut(arg[2:], "=")
|
|
||||||
def := flagByLong(name)
|
|
||||||
if def == nil {
|
|
||||||
return nil, fmt.Errorf("unknown flag --%s", name)
|
|
||||||
}
|
|
||||||
return []flagArg{{def, value, inline}}, nil
|
|
||||||
}
|
|
||||||
if name, _, _ := strings.Cut(arg[1:], "="); len(name) > 1 && flagByLong(name) != nil {
|
|
||||||
return nil, fmt.Errorf("unknown flag %s; use --%s", arg, name)
|
|
||||||
}
|
|
||||||
var flags []flagArg
|
|
||||||
letters := arg[1:]
|
|
||||||
for i, r := range letters {
|
|
||||||
letter := string(r)
|
|
||||||
def := flagByShort(letter)
|
|
||||||
if def == nil {
|
|
||||||
return nil, fmt.Errorf("unknown flag -%s", letter)
|
|
||||||
}
|
|
||||||
if !def.value {
|
|
||||||
flags = append(flags, flagArg{def: def})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rest := letters[i+len(letter):]
|
|
||||||
if strings.HasPrefix(rest, "=") {
|
|
||||||
return nil, fmt.Errorf("-%s takes its value attached (-%s%s) or next (-%s %s); = belongs to --%s=%s",
|
|
||||||
letter, letter, rest[1:], letter, rest[1:], def.long, rest[1:])
|
|
||||||
}
|
|
||||||
return append(flags, flagArg{def, rest, rest != ""}), nil
|
|
||||||
}
|
|
||||||
return flags, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseArgs(argv []string) (invocation, error) {
|
|
||||||
in := invocation{repeat: 1, separator: "\n", format: "text"}
|
|
||||||
for i := 0; i < len(argv); i++ {
|
|
||||||
arg := argv[i]
|
|
||||||
if arg == "--" {
|
|
||||||
in.paths = append(in.paths, argv[i+1:]...)
|
|
||||||
return in, nil
|
|
||||||
}
|
|
||||||
if len(arg) < 2 || arg[0] != '-' {
|
|
||||||
in.paths = append(in.paths, arg)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
flags, err := splitFlags(arg)
|
|
||||||
if err != nil {
|
|
||||||
return in, err
|
|
||||||
}
|
|
||||||
for _, fl := range flags {
|
|
||||||
if !fl.def.value {
|
|
||||||
if fl.inline {
|
|
||||||
return in, fmt.Errorf("--%s takes no value", fl.def.long)
|
|
||||||
}
|
|
||||||
_ = fl.def.set(&in, "")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
value := fl.value
|
|
||||||
if !fl.inline {
|
|
||||||
if i++; i >= len(argv) {
|
|
||||||
return in, fmt.Errorf("--%s needs a value", fl.def.long)
|
|
||||||
}
|
|
||||||
value = argv[i]
|
|
||||||
}
|
|
||||||
if err := fl.def.set(&in, value); err != nil {
|
|
||||||
return in, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return in, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// recordFormat is one way to write a record out: the line each record renders,
|
|
||||||
// the header that precedes the first one, and the open/close frame plus the
|
|
||||||
// between-record separator a document form needs.
|
|
||||||
type recordFormat struct {
|
|
||||||
header func(*fejkdata.Record) string
|
|
||||||
line func(r *fejkdata.Record, table string) string
|
|
||||||
open string
|
|
||||||
close string
|
|
||||||
sep string
|
|
||||||
}
|
|
||||||
|
|
||||||
// recordFormats is every --format that writes records. json frames the records
|
|
||||||
// as one array document; ndjson is the same column, one object per line.
|
|
||||||
var recordFormats = map[string]recordFormat{
|
|
||||||
"csv": {header: (*fejkdata.Record).CSVHeader, line: func(r *fejkdata.Record, _ string) string { return r.CSVLine() }, sep: "\n"},
|
|
||||||
"json": {line: jsonLine, open: "[", close: "]", sep: ",\n"},
|
|
||||||
"ndjson": {line: jsonLine, sep: "\n"},
|
|
||||||
"sql": {line: func(r *fejkdata.Record, table string) string { return r.SQLInsert(table) }, sep: "\n"},
|
|
||||||
}
|
|
||||||
|
|
||||||
func jsonLine(r *fejkdata.Record, _ string) string { return r.JSON() }
|
|
||||||
|
|
||||||
// writesRecords reports whether the format writes records rather than plain text.
|
|
||||||
func (in invocation) writesRecords() bool {
|
|
||||||
return in.format != "text"
|
|
||||||
}
|
|
||||||
|
|
||||||
// formatNames lists the --format values, text included, for the misuse error.
|
|
||||||
func formatNames() string {
|
|
||||||
names := []string{"text"}
|
|
||||||
for name := range recordFormats {
|
|
||||||
names = append(names, name)
|
|
||||||
}
|
|
||||||
sort.Strings(names)
|
|
||||||
return strings.Join(names[:len(names)-1], ", ") + " or " + names[len(names)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkFlags rejects a flag value or combination that cannot run.
|
|
||||||
func (in invocation) checkFlags() error {
|
|
||||||
if _, ok := recordFormats[in.format]; !ok && in.format != "text" {
|
|
||||||
return fmt.Errorf("--format takes %s, got %q", formatNames(), in.format)
|
|
||||||
}
|
|
||||||
if in.tableSet && in.table == "" {
|
|
||||||
return errors.New("--table names the INSERT target, so it cannot be empty")
|
|
||||||
}
|
|
||||||
if in.tableSet && in.format != "sql" {
|
|
||||||
return errors.New("--table names the INSERT target, so it needs --format sql")
|
|
||||||
}
|
|
||||||
if in.writesRecords() && in.separatorSet {
|
|
||||||
return errors.New("--separator joins text values, so it has no effect with --format " + in.format)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// check rejects a flag combination or an argument that cannot run, and reports
|
|
||||||
// what the argument names, so its shape is settled before any data is read.
|
|
||||||
func (in invocation) check() (argKind, error) {
|
|
||||||
if err := in.checkFlags(); err != nil {
|
|
||||||
return argPath, err
|
|
||||||
}
|
|
||||||
if in.list && len(in.paths) > 0 {
|
|
||||||
return argPath, errors.New("--list takes no path")
|
|
||||||
}
|
|
||||||
if in.list && (in.repeatSet || in.separatorSet || in.formatSet || in.tableSet) {
|
|
||||||
return argPath, errors.New("--list takes no --repeat, --separator, --format or --table")
|
|
||||||
}
|
|
||||||
if in.list {
|
|
||||||
return argPath, nil
|
|
||||||
}
|
|
||||||
if len(in.paths) != 1 {
|
|
||||||
return argPath, fmt.Errorf("expected one path or template, got %d", len(in.paths))
|
|
||||||
}
|
|
||||||
return classify(in.paths[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (in invocation) options() []fejkdata.Option {
|
|
||||||
var opts []fejkdata.Option
|
|
||||||
if in.noShipped {
|
|
||||||
opts = append(opts, fejkdata.WithoutShippedData())
|
|
||||||
}
|
|
||||||
for _, dir := range in.dirs {
|
|
||||||
opts = append(opts, fejkdata.WithDataPath(dir))
|
|
||||||
}
|
|
||||||
if in.seeded {
|
|
||||||
opts = append(opts, fejkdata.WithSeed(in.seed))
|
|
||||||
}
|
|
||||||
return opts
|
|
||||||
}
|
|
||||||
|
|
||||||
// write streams the argument's renders to w, repeat of them joined by the
|
|
||||||
// separator and ended by a newline. A value that renders once renders every time,
|
|
||||||
// so a render failure comes before anything is written; a write failure surfaces
|
|
||||||
// from Flush, bufio keeping the first one.
|
|
||||||
func (in invocation) write(f *fejkdata.Generator, kind argKind, w io.Writer) error {
|
|
||||||
if in.writesRecords() {
|
|
||||||
return in.writeRecords(f, kind, w)
|
|
||||||
}
|
|
||||||
draw, err := in.textDraw(f, kind, in.paths[0])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
out := bufio.NewWriter(w)
|
|
||||||
for i := 0; i < in.repeat; i++ {
|
|
||||||
v, err := draw()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if i > 0 {
|
|
||||||
out.WriteString(in.separator)
|
|
||||||
}
|
|
||||||
out.WriteString(v)
|
|
||||||
}
|
|
||||||
out.WriteString("\n")
|
|
||||||
return out.Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
// textDraw builds what one text render yields: the value, from a path or an
|
|
||||||
// inline template.
|
|
||||||
func (in invocation) textDraw(f *fejkdata.Generator, kind argKind, arg string) (func() (string, error), error) {
|
|
||||||
if kind != argTemplate {
|
|
||||||
return func() (string, error) { return f.Fake(arg) }, nil
|
|
||||||
}
|
|
||||||
t, err := f.NewTemplate(arg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, templateError{err}
|
|
||||||
}
|
|
||||||
return func() (string, error) { return t.Fake(), nil }, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// recordStream builds the record drawer for the argument, plus the INSERT table
|
|
||||||
// a sql format names.
|
|
||||||
func (in invocation) recordStream(f *fejkdata.Generator, kind argKind, arg string) (func() (*fejkdata.Record, error), string, error) {
|
|
||||||
record := func() (*fejkdata.Record, error) { return f.FakeRecord(arg) }
|
|
||||||
if kind == argTemplate {
|
|
||||||
t, err := f.NewRecordTemplate(arg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", templateError{err}
|
|
||||||
}
|
|
||||||
record = func() (*fejkdata.Record, error) { return t.Fake(), nil }
|
|
||||||
}
|
|
||||||
table := in.table
|
|
||||||
if table == "" {
|
|
||||||
table = defaultTable(arg, kind)
|
|
||||||
}
|
|
||||||
return record, table, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeRecords streams a record per line in the chosen format, framing a document
|
|
||||||
// form with its open/close brackets and a header preceding the first record.
|
|
||||||
func (in invocation) writeRecords(f *fejkdata.Generator, kind argKind, w io.Writer) error {
|
|
||||||
record, table, err := in.recordStream(f, kind, in.paths[0])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
format := recordFormats[in.format]
|
|
||||||
out := bufio.NewWriter(w)
|
|
||||||
if format.open != "" {
|
|
||||||
out.WriteString(format.open)
|
|
||||||
out.WriteByte('\n')
|
|
||||||
}
|
|
||||||
for i := 0; i < in.repeat; i++ {
|
|
||||||
r, err := record()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if i == 0 && format.header != nil {
|
|
||||||
out.WriteString(format.header(r))
|
|
||||||
out.WriteByte('\n')
|
|
||||||
}
|
|
||||||
if i > 0 {
|
|
||||||
out.WriteString(format.sep)
|
|
||||||
}
|
|
||||||
out.WriteString(format.line(r, table))
|
|
||||||
}
|
|
||||||
if format.close != "" {
|
|
||||||
out.WriteByte('\n')
|
|
||||||
out.WriteString(format.close)
|
|
||||||
}
|
|
||||||
out.WriteByte('\n')
|
|
||||||
return out.Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
// defaultTable names the INSERT target when --table is absent: the path's last
|
|
||||||
// segment, or "records" for an inline template that sits in no folder.
|
|
||||||
func defaultTable(arg string, kind argKind) string {
|
|
||||||
if kind == argTemplate {
|
|
||||||
return "records"
|
|
||||||
}
|
|
||||||
var names strings.Builder // the path with its [selectors] cut out
|
|
||||||
for depth, i := 0, 0; i < len(arg); i++ {
|
|
||||||
switch {
|
|
||||||
case arg[i] == '[':
|
|
||||||
depth++
|
|
||||||
case arg[i] == ']':
|
|
||||||
depth--
|
|
||||||
case depth == 0:
|
|
||||||
names.WriteByte(arg[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
segments := strings.Split(names.String(), ".")
|
|
||||||
return segments[len(segments)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// templateError marks a render failure that is the argument's own fault — an
|
|
||||||
// inline template that does not compile. run reports it as misuse (exit 2, with a
|
|
||||||
// pointer to --help), unlike an unknown path, which is a runtime error (exit 1).
|
|
||||||
type templateError struct{ error }
|
|
||||||
|
|
||||||
func (e templateError) Unwrap() error { return e.error }
|
|
||||||
|
|
||||||
type argKind int
|
|
||||||
|
|
||||||
const (
|
|
||||||
argPath argKind = iota
|
|
||||||
argTemplate
|
|
||||||
)
|
|
||||||
|
|
||||||
// classify reads what a positional argument names by its shape (see fejkdata.IsTemplate).
|
|
||||||
func classify(arg string) (argKind, error) {
|
|
||||||
inline, err := fejkdata.IsTemplate(arg)
|
|
||||||
if inline {
|
|
||||||
return argTemplate, nil
|
|
||||||
}
|
|
||||||
return argPath, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() { os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) }
|
|
||||||
|
|
||||||
// run returns the exit code: 0 ok, 1 runtime error, 2 misuse.
|
|
||||||
func run(args []string, stdout, stderr io.Writer) int {
|
|
||||||
in, err := parseArgs(args)
|
|
||||||
if err != nil {
|
|
||||||
return misuse(stderr, err)
|
|
||||||
}
|
|
||||||
if in.help {
|
|
||||||
fmt.Fprint(stdout, usage)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if in.version {
|
|
||||||
fmt.Fprintln(stdout, "fejkdata "+buildVersion())
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
kind, err := in.check()
|
|
||||||
if err != nil {
|
|
||||||
return misuse(stderr, err)
|
|
||||||
}
|
|
||||||
f, err := fejkdata.New(in.options()...)
|
|
||||||
if errors.Is(err, fejkdata.ErrNoData) {
|
|
||||||
return misuse(stderr, errors.New("--no-shipped-data needs at least one --data-path"))
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(stderr, err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if in.list {
|
|
||||||
for _, p := range f.List() {
|
|
||||||
fmt.Fprintln(stdout, p)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if err := in.write(f, kind, stdout); err != nil {
|
|
||||||
var te templateError
|
|
||||||
if errors.As(err, &te) {
|
|
||||||
return misuse(stderr, te.error)
|
|
||||||
}
|
|
||||||
fmt.Fprintln(stderr, err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func misuse(stderr io.Writer, err error) int {
|
|
||||||
// A library error already names the program, so the prefix is not doubled.
|
|
||||||
fmt.Fprintf(stderr, "fejkdata: %s\ntry 'fejkdata --help'\n", strings.TrimPrefix(err.Error(), "fejkdata: "))
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildVersion is the module version go install stamps into the binary, or "devel".
|
|
||||||
func buildVersion() string {
|
|
||||||
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "" {
|
|
||||||
return info.Main.Version
|
|
||||||
}
|
|
||||||
return "devel"
|
|
||||||
}
|
|
||||||
|
|||||||
+30
-598
@@ -2,614 +2,46 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/csv"
|
|
||||||
"encoding/json"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"os/exec"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
func TestMain(m *testing.M) {
|
||||||
svSE = "../../data/sv_SE"
|
if os.Getenv("FEJKDATA_RUN_MAIN") == "1" {
|
||||||
enUS = "../../data/en_US"
|
main()
|
||||||
misc = "../../data/misc"
|
}
|
||||||
)
|
os.Exit(m.Run())
|
||||||
|
|
||||||
func runOut(args ...string) (int, string, string) {
|
|
||||||
var out, errb bytes.Buffer
|
|
||||||
code := run(args, &out, &errb)
|
|
||||||
return code, out.String(), errb.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunOutputsValue(t *testing.T) {
|
func TestMainRendersTheShippedData(t *testing.T) {
|
||||||
code, out, errb := runOut("--data-path", svSE, "person")
|
cmd := exec.Command(os.Args[0], "--seed", "1")
|
||||||
if code != 0 {
|
cmd.Env = append(os.Environ(), "FEJKDATA_RUN_MAIN=1")
|
||||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
cmd.Stdin = strings.NewReader("{/misc.territory[SE].alpha2} {/en_US.person.last} {/sv_SE.address.locality}")
|
||||||
}
|
var stderr bytes.Buffer
|
||||||
if strings.TrimSpace(out) == "" {
|
cmd.Stderr = &stderr
|
||||||
t.Fatalf("empty output, stderr=%q", errb)
|
out, err := cmd.Output()
|
||||||
}
|
if fields := strings.Fields(string(out)); err != nil || len(fields) < 3 || fields[0] != "SE" {
|
||||||
if !strings.HasSuffix(out, "\n") {
|
t.Errorf("fejkdata = %v, %q, %q, want SE followed by a last name and a locality", err, out, stderr.String())
|
||||||
t.Errorf("output should end with newline, got %q", out)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunDotPath(t *testing.T) {
|
func TestMainWithoutASeedDrawsANewSeedEachRun(t *testing.T) {
|
||||||
code, full, _ := runOut("--seed", "7", "--data-path", svSE, "person")
|
var outs [2]string
|
||||||
if code != 0 {
|
for i := range outs {
|
||||||
t.Fatalf("person run = %d", code)
|
cmd := exec.Command(os.Args[0])
|
||||||
}
|
cmd.Env = append(os.Environ(), "FEJKDATA_RUN_MAIN=1")
|
||||||
code, last, errb := runOut("--seed", "7", "--data-path", svSE, "person.last")
|
cmd.Stdin = strings.NewReader("{uuid()}")
|
||||||
if code != 0 {
|
var stderr bytes.Buffer
|
||||||
t.Fatalf("person.last run = %d, stderr=%q", code, errb)
|
cmd.Stderr = &stderr
|
||||||
}
|
out, err := cmd.Output()
|
||||||
if strings.TrimSpace(last) == "" {
|
if err != nil || len(out) == 0 {
|
||||||
t.Fatal("empty person.last output")
|
t.Fatalf("fejkdata without --seed = %v, %q, %q", err, out, stderr.String())
|
||||||
}
|
|
||||||
if last == full {
|
|
||||||
t.Errorf("person.last %q should differ from person %q", last, full)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunSeedSpellings(t *testing.T) {
|
|
||||||
_, want, _ := runOut("--seed", "42", "--data-path", svSE, "address")
|
|
||||||
for _, args := range [][]string{
|
|
||||||
{"--seed=42", "--data-path", svSE, "address"},
|
|
||||||
{"-s", "42", "-d", svSE, "address"},
|
|
||||||
{"--data-path", svSE, "address", "--seed", "42"},
|
|
||||||
} {
|
|
||||||
code, got, errb := runOut(args...)
|
|
||||||
if code != 0 {
|
|
||||||
t.Fatalf("run(%v) = %d, stderr=%q", args, code, errb)
|
|
||||||
}
|
|
||||||
if got != want {
|
|
||||||
t.Errorf("run(%v) = %q, want %q", args, got, want)
|
|
||||||
}
|
}
|
||||||
}
|
outs[i] = string(out)
|
||||||
}
|
}
|
||||||
|
if outs[0] == outs[1] {
|
||||||
func TestRunShortFlagValues(t *testing.T) {
|
t.Errorf("two runs without --seed both printed %q, want a new seed each run", outs[0])
|
||||||
_, want, _ := runOut("--seed", "42", "--data-path", svSE, "address")
|
|
||||||
for _, args := range [][]string{
|
|
||||||
{"-s42", "-d", svSE, "address"},
|
|
||||||
{"-s", "42", "-d" + svSE, "address"},
|
|
||||||
{"-d", svSE, "address", "-s42"},
|
|
||||||
} {
|
|
||||||
code, got, errb := runOut(args...)
|
|
||||||
if code != 0 {
|
|
||||||
t.Fatalf("run(%v) = %d, stderr=%q", args, code, errb)
|
|
||||||
}
|
|
||||||
if got != want {
|
|
||||||
t.Errorf("run(%v) = %q, want %q", args, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, three, _ := runOut("-s", "1", "-n3", "-d", svSE, "word")
|
|
||||||
if lines := strings.Split(strings.TrimRight(three, "\n"), "\n"); len(lines) != 3 {
|
|
||||||
t.Errorf("-n3 gave %d lines: %q", len(lines), three)
|
|
||||||
}
|
|
||||||
for _, c := range []struct {
|
|
||||||
args []string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{[]string{"-s=42", "-d", svSE, "address"}, "-s42"},
|
|
||||||
{[]string{"-s=42", "-d", svSE, "address"}, "--seed=42"},
|
|
||||||
{[]string{"-d=" + svSE, "address"}, "--data-path="},
|
|
||||||
{[]string{"-nd", "3", "-d", svSE, "word"}, `--repeat needs an integer in 1..1048576, got "d"`},
|
|
||||||
{[]string{"--seed=", "-d", svSE, "word"}, `--seed needs an unsigned integer, got ""`},
|
|
||||||
} {
|
|
||||||
code, out, errb := runOut(c.args...)
|
|
||||||
if code != 2 || out != "" {
|
|
||||||
t.Errorf("run(%v) = %d, stdout %q, want misuse", c.args, code, out)
|
|
||||||
}
|
|
||||||
if !strings.Contains(errb, c.want) {
|
|
||||||
t.Errorf("run(%v) stderr = %q, want %q", c.args, errb, c.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
code, out, _ := runOut("-hd", svSE)
|
|
||||||
if code != 0 || !strings.Contains(out, "Usage") {
|
|
||||||
t.Errorf("-hd (bundled help) = %d, %q, want usage", code, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunDoubleDashEndsFlags(t *testing.T) {
|
|
||||||
code, out, errb := runOut("--data-path", svSE, "--", "person")
|
|
||||||
if code != 0 || strings.TrimSpace(out) == "" {
|
|
||||||
t.Fatalf("run = %d, out=%q, stderr=%q", code, out, errb)
|
|
||||||
}
|
|
||||||
code, _, errb = runOut("--data-path", svSE, "--", "--list")
|
|
||||||
if code != 1 || !strings.Contains(errb, "--list") {
|
|
||||||
t.Errorf("after --, --list should be a path: code %d, stderr %q", code, errb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunSingleDashLongIsRejected(t *testing.T) {
|
|
||||||
for _, c := range []struct {
|
|
||||||
args []string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{[]string{"-seed", "42", "-d", svSE, "person"}, "use --seed"},
|
|
||||||
{[]string{"-seed=42", "-d", svSE, "person"}, "use --seed"},
|
|
||||||
{[]string{"-data-path", svSE, "person"}, "use --data-path"},
|
|
||||||
{[]string{"-list", "-d", svSE}, "use --list"},
|
|
||||||
{[]string{"--nope", "-d", svSE, "person"}, "unknown flag --nope"},
|
|
||||||
{[]string{"-x", "-d", svSE, "person"}, "unknown flag -x"},
|
|
||||||
} {
|
|
||||||
code, _, errb := runOut(c.args...)
|
|
||||||
if code != 2 {
|
|
||||||
t.Errorf("run(%v) = %d, want 2", c.args, code)
|
|
||||||
}
|
|
||||||
if !strings.Contains(errb, c.want) {
|
|
||||||
t.Errorf("run(%v) stderr = %q, want %q", c.args, errb, c.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunFlagValues(t *testing.T) {
|
|
||||||
for _, c := range []struct {
|
|
||||||
args []string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{[]string{"--seed", "-d", svSE, "person"}, `--seed needs an unsigned integer, got "-d"`},
|
|
||||||
{[]string{"-d", svSE, "person", "--seed"}, "--seed needs a value"},
|
|
||||||
{[]string{"--seed", "x", "-d", svSE, "person"}, "--seed"},
|
|
||||||
{[]string{"--list=1", "-d", svSE}, "--list takes no value"},
|
|
||||||
{[]string{"--repeat", "0", "-d", svSE, "person"}, "--repeat"},
|
|
||||||
{[]string{"-n", "-1", "-d", svSE, "person"}, "--repeat"},
|
|
||||||
} {
|
|
||||||
code, _, errb := runOut(c.args...)
|
|
||||||
if code != 2 {
|
|
||||||
t.Errorf("run(%v) = %d, want 2", c.args, code)
|
|
||||||
}
|
|
||||||
if !strings.Contains(errb, c.want) {
|
|
||||||
t.Errorf("run(%v) stderr = %q, want %q", c.args, errb, c.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunRepeat(t *testing.T) {
|
|
||||||
code, out, errb := runOut("--seed", "1", "--repeat", "3", "--data-path", svSE, "word")
|
|
||||||
if code != 0 {
|
|
||||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
|
||||||
}
|
|
||||||
lines := strings.Split(strings.TrimRight(out, "\n"), "\n")
|
|
||||||
if len(lines) != 3 {
|
|
||||||
t.Errorf("repeat=3 gave %d lines: %q", len(lines), out)
|
|
||||||
}
|
|
||||||
_, short, _ := runOut("--seed", "1", "-n", "3", "-d", svSE, "word")
|
|
||||||
if short != out {
|
|
||||||
t.Errorf("-n 3 = %q, want the same as --repeat 3 %q", short, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunSeparator(t *testing.T) {
|
|
||||||
code, out, errb := runOut("--repeat", "3", "--separator", ",", "--data-path", svSE, "word")
|
|
||||||
if code != 0 {
|
|
||||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
|
||||||
}
|
|
||||||
if n := strings.Count(out, "\n"); n != 1 {
|
|
||||||
t.Errorf("want one trailing newline, got %d: %q", n, out)
|
|
||||||
}
|
|
||||||
if !strings.Contains(out, ",") {
|
|
||||||
t.Errorf("values should be comma-joined: %q", out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunRepeatAdvancesRNG(t *testing.T) {
|
|
||||||
code, out, errb := runOut("--seed", "1", "--repeat", "5", "--data-path", svSE, "person")
|
|
||||||
if code != 0 {
|
|
||||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
|
||||||
}
|
|
||||||
uniq := map[string]bool{}
|
|
||||||
for _, l := range strings.Split(strings.TrimRight(out, "\n"), "\n") {
|
|
||||||
uniq[l] = true
|
|
||||||
}
|
|
||||||
if len(uniq) < 2 {
|
|
||||||
t.Errorf("repeat should vary output, all identical: %q", out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunMisuse(t *testing.T) {
|
|
||||||
for _, args := range [][]string{{}, {"--data-path", svSE}, {"-d", svSE, "person", "word"}, {"-d", svSE, "--list", "person"}, {"--no-shipped-data", "sv_SE.person"}} {
|
|
||||||
code, out, errb := runOut(args...)
|
|
||||||
if code != 2 {
|
|
||||||
t.Errorf("run(%v) = %d, want 2", args, code)
|
|
||||||
}
|
|
||||||
if !strings.Contains(errb, "try 'fejkdata --help'") {
|
|
||||||
t.Errorf("run(%v) stderr = %q, want a pointer to --help", args, errb)
|
|
||||||
}
|
|
||||||
if out != "" {
|
|
||||||
t.Errorf("run(%v) stdout = %q, want nothing", args, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunMultipleDataPaths(t *testing.T) {
|
|
||||||
code, out, errb := runOut("-d", enUS, "--data-path", svSE, "person")
|
|
||||||
if code != 0 {
|
|
||||||
t.Fatalf("run(multi-dir) = %d, stderr=%q", code, errb)
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(out) == "" {
|
|
||||||
t.Fatal("empty output for multi-dir run")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunUnknownCategoryFails(t *testing.T) {
|
|
||||||
code, _, errb := runOut("--data-path", svSE, "nope")
|
|
||||||
if code != 1 {
|
|
||||||
t.Fatalf("run = %d, want 1", code)
|
|
||||||
}
|
|
||||||
if !strings.Contains(errb, "nope") {
|
|
||||||
t.Errorf("stderr %q should name the unknown category", errb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunList(t *testing.T) {
|
|
||||||
code, out, errb := runOut("--data-path", svSE, "--list")
|
|
||||||
if code != 0 {
|
|
||||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
|
||||||
}
|
|
||||||
for _, want := range []string{"person", "person.last", "address"} {
|
|
||||||
if !strings.Contains(out, want) {
|
|
||||||
t.Errorf("list output missing %q:\n%s", want, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunHelpOnStdout(t *testing.T) {
|
|
||||||
for _, h := range []string{"-h", "--help"} {
|
|
||||||
code, out, errb := runOut(h)
|
|
||||||
if code != 0 {
|
|
||||||
t.Errorf("%s = %d, want 0", h, code)
|
|
||||||
}
|
|
||||||
if !strings.Contains(out, "Usage") || errb != "" {
|
|
||||||
t.Errorf("%s: stdout = %q, stderr = %q, want usage on stdout only", h, out, errb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunVersion(t *testing.T) {
|
|
||||||
code, out, errb := runOut("--version")
|
|
||||||
if code != 0 {
|
|
||||||
t.Fatalf("--version = %d, stderr=%q", code, errb)
|
|
||||||
}
|
|
||||||
version, ok := strings.CutPrefix(out, "fejkdata ")
|
|
||||||
if !ok || strings.TrimSpace(version) == "" {
|
|
||||||
t.Errorf("--version = %q, want the command name and a version on stdout", out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunMissingDirFails(t *testing.T) {
|
|
||||||
code, _, errb := runOut("--data-path", "../../data/nope", "person")
|
|
||||||
if code != 1 {
|
|
||||||
t.Fatalf("run = %d, want 1", code)
|
|
||||||
}
|
|
||||||
if errb == "" {
|
|
||||||
t.Error("want an error message on stderr")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunShippedDataByDefault(t *testing.T) {
|
|
||||||
code, out, errb := runOut("--seed", "1", "sv_SE.person")
|
|
||||||
if code != 0 || strings.TrimSpace(out) == "" {
|
|
||||||
t.Fatalf("run = %d, out=%q, stderr=%q", code, out, errb)
|
|
||||||
}
|
|
||||||
code, list, _ := runOut("--list")
|
|
||||||
if code != 0 || !strings.Contains(list, "en_US.person\n") || !strings.Contains(list, "misc.uuid\n") {
|
|
||||||
t.Errorf("--list without --data-path = %d, %q", code, list)
|
|
||||||
}
|
|
||||||
code, _, errb = runOut("person")
|
|
||||||
if code != 1 || !strings.Contains(errb, "person") {
|
|
||||||
t.Errorf("a category outside the shipped tree: code %d, stderr %q", code, errb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClassify(t *testing.T) {
|
|
||||||
for arg, want := range map[string]argKind{"sv_SE.person": argPath, "name: {x}": argTemplate} {
|
|
||||||
if got, err := classify(arg); err != nil || got != want {
|
|
||||||
t.Errorf("classify(%q) = %v, %v; want %v", arg, got, err, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if _, err := classify("[abc]"); err == nil || !strings.Contains(err.Error(), `"["`) || !strings.Contains(err.Error(), "JSON") {
|
|
||||||
t.Errorf("classify([abc]) = %v; want it rejected naming the leading bracket", err)
|
|
||||||
}
|
|
||||||
if got, err := classify("geo.SE.municipality[St. Louis].name"); err != nil || got != argPath {
|
|
||||||
t.Errorf("classify(a selecting path) = %v, %v; want a path", got, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunSelectsATableRow(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
for name, content := range map[string]string{
|
|
||||||
"region.json": `{"format":"{name}","rows":"region.tsv","key":"code","name":"name"}`,
|
|
||||||
"region.tsv": "code\tname\n01\tStockholms län\n12\tSkåne län\n",
|
|
||||||
} {
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if code, out, errb := runOut("--no-shipped-data", "-d", dir, "region[12]"); code != 0 || out != "Skåne län\n" {
|
|
||||||
t.Fatalf("region[12] = %d, %q, stderr=%q", code, out, errb)
|
|
||||||
}
|
|
||||||
if code, out, _ := runOut("--no-shipped-data", "-d", dir, "region[Skåne län].code"); code != 0 || out != "12\n" {
|
|
||||||
t.Fatalf("region[Skåne län].code = %d, %q", code, out)
|
|
||||||
}
|
|
||||||
if code, out, _ := runOut("--no-shipped-data", "-d", dir, "--format", "sql", "region[12]"); code != 0 || out != `INSERT INTO "region" ("code", "name") VALUES ('12', 'Skåne län');`+"\n" {
|
|
||||||
t.Fatalf("--format sql region[12] = %d, %q, want the table named without its selector", code, out)
|
|
||||||
}
|
|
||||||
if code, _, errb := runOut("--no-shipped-data", "-d", dir, "region[99]"); code != 1 || !strings.Contains(errb, `"99"`) {
|
|
||||||
t.Fatalf("region[99] = %d, stderr=%q, want a runtime error naming the row", code, errb)
|
|
||||||
}
|
|
||||||
if code, _, errb := runOut("--no-shipped-data", "-d", dir, "[Skåne län]"); code != 2 || !strings.Contains(errb, "JSON") {
|
|
||||||
t.Fatalf("[Skåne län] = %d, stderr=%q, want misuse: a leading bracket that is no JSON names nothing", code, errb)
|
|
||||||
}
|
|
||||||
if code, out, _ := runOut("--seed", "1", "--format", "csv", "misc.country[SE]"); code != 0 || !strings.HasPrefix(out, "alpha2,") || !strings.Contains(out, "\nSE,SWE,") {
|
|
||||||
t.Fatalf("--format csv misc.country[SE] = %d, %q", code, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUsageReferencesResolve(t *testing.T) {
|
|
||||||
for _, token := range regexp.MustCompile(`\{/[^}]+\}`).FindAllString(usage, -1) {
|
|
||||||
path := token[2 : len(token)-1]
|
|
||||||
code, out, errb := runOut("--seed", "1", path)
|
|
||||||
if code != 0 || strings.TrimSpace(out) == "" {
|
|
||||||
t.Errorf("usage advertises %s: run %s = %d, %q, stderr %q", token, path, code, out, errb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunShapeMisuseBeforeLoad(t *testing.T) {
|
|
||||||
code, _, errb := runOut("--no-shipped-data", "[abc]")
|
|
||||||
if code != 2 || !strings.Contains(errb, "[abc]") || strings.Contains(errb, "--data-path") {
|
|
||||||
t.Fatalf("shape misuse with no data = %d, %q; want the shape error before any load", code, errb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunInlineTemplate(t *testing.T) {
|
|
||||||
code, out, errb := runOut("--seed", "1", "name: {/sv_SE.person.last}")
|
|
||||||
if code != 0 || !strings.HasPrefix(out, "name: ") || strings.Contains(out, "{") {
|
|
||||||
t.Fatalf("inline format string = %d, %q, stderr %q", code, out, errb)
|
|
||||||
}
|
|
||||||
code, out, errb = runOut("--seed", "1", `{"format":"name: {x}","x":["bosse","lina"]}`)
|
|
||||||
if code != 0 || (out != "name: bosse\n" && out != "name: lina\n") {
|
|
||||||
t.Fatalf("inline JSON template = %d, %q, want one name, stderr %q", code, out, errb)
|
|
||||||
}
|
|
||||||
code, out, errb = runOut("--seed", "1", `"name: {/sv_SE.person.last}"`)
|
|
||||||
if code != 0 || !strings.HasPrefix(out, "name: ") || strings.Contains(out, "{") {
|
|
||||||
t.Fatalf("inline JSON string = %d, %q, stderr %q", code, out, errb)
|
|
||||||
}
|
|
||||||
code, out, errb = runOut("--seed", "1", "-n", "2", `{digits(1)}`)
|
|
||||||
if code != 0 || len(strings.Split(strings.TrimRight(out, "\n"), "\n")) != 2 {
|
|
||||||
t.Fatalf("inline template with --repeat = %d, %q, stderr %q", code, out, errb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunTemplateMisuse(t *testing.T) {
|
|
||||||
for arg, want := range map[string]string{
|
|
||||||
"{bad": "unterminated",
|
|
||||||
"[red,green]": `starts with "["`,
|
|
||||||
`{"format":"x"}`: "is a string",
|
|
||||||
"name: {/no.such.path}": "no entry",
|
|
||||||
"{/sv_SE.person}": "write sv_SE.person",
|
|
||||||
"x[1]y": `"]"`,
|
|
||||||
` ["a","b"] `: "may not be padded",
|
|
||||||
} {
|
|
||||||
code, out, errb := runOut("--seed", "1", arg)
|
|
||||||
if code != 2 || out != "" || !strings.Contains(errb, "try 'fejkdata --help'") || !strings.Contains(errb, want) {
|
|
||||||
t.Errorf("run(%q) = %d, %q, %q; want misuse naming %q and --help", arg, code, out, errb, want)
|
|
||||||
}
|
|
||||||
if strings.Contains(errb, "fejkdata: fejkdata:") {
|
|
||||||
t.Errorf("run(%q) doubled the program prefix: %q", arg, errb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunNoShippedData(t *testing.T) {
|
|
||||||
code, list, errb := runOut("--no-shipped-data", "-d", misc, "--list")
|
|
||||||
if code != 0 {
|
|
||||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
|
||||||
}
|
|
||||||
if strings.Contains(list, "sv_SE") || !strings.Contains(list, "uuid\n") {
|
|
||||||
t.Errorf("--no-shipped-data --list = %q, want only the given dir", list)
|
|
||||||
}
|
|
||||||
code, out, _ := runOut("--no-shipped-data", "-d", misc, "-s", "3", "uuid")
|
|
||||||
if code != 0 || strings.TrimSpace(out) == "" {
|
|
||||||
t.Errorf("run = %d, out=%q", code, out)
|
|
||||||
}
|
|
||||||
code, _, errb = runOut("--no-shipped-data", "sv_SE.person")
|
|
||||||
if code != 2 || !strings.Contains(errb, "--no-shipped-data needs at least one --data-path") {
|
|
||||||
t.Errorf("--no-shipped-data alone = %d, %q, want misuse naming --data-path", code, errb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunRepeatIsBounded(t *testing.T) {
|
|
||||||
for _, args := range [][]string{
|
|
||||||
{"--repeat", "9223372036854775807", "sv_SE.word"},
|
|
||||||
{"--repeat", "1048577", "sv_SE.word"},
|
|
||||||
{"-n", "99999999999", "sv_SE.word"},
|
|
||||||
} {
|
|
||||||
code, out, errb := runOut(args...)
|
|
||||||
if code != 2 || out != "" || !strings.Contains(errb, "1..1048576") {
|
|
||||||
t.Errorf("run(%v) = %d, %q, %q, want misuse naming the range", args, code, out, errb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dir := t.TempDir()
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "x.json"), []byte(`"x"`), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
code, out, errb := runOut("--no-shipped-data", "-d", dir, "--repeat", "1048576", "--separator", "", "x")
|
|
||||||
if code != 0 || len(out) != 1048576+1 {
|
|
||||||
t.Errorf("repeat at the cap = %d, %d bytes, stderr %q; want every render streamed", code, len(out), errb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunListTakesNoRepeatOrSeparator(t *testing.T) {
|
|
||||||
for _, args := range [][]string{{"--list", "-n", "3"}, {"--list", "--separator", ","}} {
|
|
||||||
code, _, errb := runOut(args...)
|
|
||||||
if code != 2 || !strings.Contains(errb, "--list takes no") {
|
|
||||||
t.Errorf("run(%v) = %d, %q, want misuse", args, code, errb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunUnknownFlagIsNamedByRune(t *testing.T) {
|
|
||||||
code, _, errb := runOut("-ä", "sv_SE.word")
|
|
||||||
if code != 2 || !strings.Contains(errb, "unknown flag -ä") {
|
|
||||||
t.Errorf("run(-ä) = %d, %q, want the flag named whole", code, errb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunEmptyDataPathIsNamed(t *testing.T) {
|
|
||||||
code, _, errb := runOut("--data-path=", "sv_SE.word")
|
|
||||||
if code != 1 || !strings.Contains(errb, "empty") {
|
|
||||||
t.Errorf("run(--data-path=) = %d, %q, want the empty path named", code, errb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func recordDir(t *testing.T) string {
|
|
||||||
t.Helper()
|
|
||||||
dir := t.TempDir()
|
|
||||||
content := `{"format":"{first} {last}","first":["Ada","Bo"],"last":["Lovelace","Ek"]}`
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "users.json"), []byte(content), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
return dir
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunRecordJSON(t *testing.T) {
|
|
||||||
code, out, errb := runOut("--seed", "1", "--format", "json", "--repeat", "2", "--data-path", recordDir(t), "users")
|
|
||||||
if code != 0 {
|
|
||||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
|
||||||
}
|
|
||||||
var arr []map[string]string
|
|
||||||
if err := json.Unmarshal([]byte(out), &arr); err != nil {
|
|
||||||
t.Fatalf("json output is not one JSON array: %v\n%q", err, out)
|
|
||||||
}
|
|
||||||
if len(arr) != 2 || len(arr[0]) != 2 || arr[0]["first"] == "" || arr[0]["last"] == "" {
|
|
||||||
t.Fatalf("json output = %q, want an array of two records with first and last columns", out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunRecordNDJSON(t *testing.T) {
|
|
||||||
code, out, errb := runOut("--seed", "1", "--format", "ndjson", "--repeat", "2", "--data-path", recordDir(t), "users")
|
|
||||||
if code != 0 {
|
|
||||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
|
||||||
}
|
|
||||||
lines := strings.Split(strings.TrimRight(out, "\n"), "\n")
|
|
||||||
if len(lines) != 2 {
|
|
||||||
t.Fatalf("ndjson output = %d lines %q, want one object per line", len(lines), out)
|
|
||||||
}
|
|
||||||
for _, line := range lines {
|
|
||||||
var m map[string]string
|
|
||||||
if err := json.Unmarshal([]byte(line), &m); err != nil || len(m) != 2 {
|
|
||||||
t.Fatalf("ndjson line %q is not one object: %v", line, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunRecordCSVRoundTrips(t *testing.T) {
|
|
||||||
code, out, errb := runOut("--seed", "1", "--format", "csv", "--repeat", "3", "--data-path", recordDir(t), "users")
|
|
||||||
if code != 0 {
|
|
||||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
|
||||||
}
|
|
||||||
lines := strings.Split(strings.TrimRight(out, "\n"), "\n")
|
|
||||||
if len(lines) != 4 {
|
|
||||||
t.Fatalf("csv output = %d lines %q, want a header and 3 rows", len(lines), out)
|
|
||||||
}
|
|
||||||
if lines[0] != "first,last" {
|
|
||||||
t.Fatalf("csv header = %q, want first,last", lines[0])
|
|
||||||
}
|
|
||||||
r, err := csv.NewReader(strings.NewReader(out)).ReadAll()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("csv output is not valid CSV: %v", err)
|
|
||||||
}
|
|
||||||
if len(r) != 4 || len(r[0]) != 2 {
|
|
||||||
t.Fatalf("csv parsed to %v, want 4 records of 2 fields", r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunRecordSQL(t *testing.T) {
|
|
||||||
code, out, errb := runOut("--seed", "1", "--format", "sql", "--repeat", "2", "--table", "people", "--data-path", recordDir(t), "users")
|
|
||||||
if code != 0 {
|
|
||||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
|
||||||
}
|
|
||||||
lines := strings.Split(strings.TrimRight(out, "\n"), "\n")
|
|
||||||
if len(lines) != 2 || !strings.HasPrefix(lines[0], `INSERT INTO "people" ("first", "last") VALUES (`) {
|
|
||||||
t.Fatalf("sql output = %q, want two INSERT INTO people statements", out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunRecordDefaultTable(t *testing.T) {
|
|
||||||
code, out, errb := runOut("--seed", "1", "--format", "sql", "--data-path", recordDir(t), "users")
|
|
||||||
if code != 0 || !strings.HasPrefix(out, `INSERT INTO "users" (`) {
|
|
||||||
t.Fatalf("default table = %d, %q, want the path's last segment; stderr %q", code, out, errb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunRecordInlineTemplate(t *testing.T) {
|
|
||||||
code, out, errb := runOut("--seed", "1", "--format", "json", `{"format":"{x}","x":["a","b"]}`)
|
|
||||||
if code != 0 {
|
|
||||||
t.Fatalf("inline record = %d, stderr=%q", code, errb)
|
|
||||||
}
|
|
||||||
var arr []map[string]string
|
|
||||||
if err := json.Unmarshal([]byte(out), &arr); err != nil || len(arr) != 1 || (arr[0]["x"] != "a" && arr[0]["x"] != "b") {
|
|
||||||
t.Fatalf("inline record json = %q, want one record with a column x (err %v)", out, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunRecordMisuse(t *testing.T) {
|
|
||||||
for _, c := range []struct {
|
|
||||||
args []string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{[]string{"--format", "yaml", "users"}, "--format takes csv, json, ndjson, sql or text"},
|
|
||||||
{[]string{"--format", "json", "--separator", ",", "users"}, "--separator joins text values"},
|
|
||||||
{[]string{"--table", "t", "users"}, "--table names the INSERT target"},
|
|
||||||
{[]string{"--format", "json", "--table", "t", "users"}, "--table names the INSERT target"},
|
|
||||||
{[]string{"--format", "sql", "--table", "", "users"}, "--table names the INSERT target"},
|
|
||||||
} {
|
|
||||||
code, out, errb := runOut(c.args...)
|
|
||||||
if code != 2 || out != "" || !strings.Contains(errb, c.want) {
|
|
||||||
t.Errorf("run(%v) = %d, %q, %q; want misuse naming %q", c.args, code, out, errb, c.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunRecordOnABareValueIsRuntimeError(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "word.json"), []byte(`["a","b"]`), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
code, _, errb := runOut("--format", "json", "--data-path", dir, "word")
|
|
||||||
if code != 1 {
|
|
||||||
t.Fatalf("record on a choice = %d, want exit 1 (runtime error), stderr %q", code, errb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunRecordRejectsATopLevelRepeat(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
content := `{"format":"{a}-","repeat":3,"separator":"|","a":["x","y"]}`
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "rep.json"), []byte(content), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
code, _, errb := runOut("--format", "json", "--data-path", dir, "rep")
|
|
||||||
if code != 1 || !strings.Contains(errb, "carries repeat 3") {
|
|
||||||
t.Errorf("record on a repeating path = %d, %q; want exit 1 naming the repeat", code, errb)
|
|
||||||
}
|
|
||||||
code, _, errb = runOut("--format", "json", content)
|
|
||||||
if code != 2 || !strings.Contains(errb, "carries repeat 3") {
|
|
||||||
t.Errorf("record on a repeating inline template = %d, %q; want exit 2 naming the repeat", code, errb)
|
|
||||||
}
|
|
||||||
if code, out, _ := runOut("--seed", "1", "--data-path", dir, "rep"); code != 0 || strings.TrimSpace(out) != "x-|x-|x-" {
|
|
||||||
t.Errorf("text view = %d, %q; want the repeat still composed", code, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunNumbersFollowTheShell(t *testing.T) {
|
|
||||||
_, want, _ := runOut("--seed", "7", "--repeat", "3", "sv_SE.word")
|
|
||||||
code, got, errb := runOut("--seed", "007", "--repeat", "+3", "sv_SE.word")
|
|
||||||
if code != 0 || got != want {
|
|
||||||
t.Errorf("run(--seed 007 --repeat +3) = %d, %q, stderr %q; want the same as --seed 7 --repeat 3 %q", code, got, errb, want)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-20
@@ -1,21 +1,5 @@
|
|||||||
# Docker-based dev workflow — no local tooling beyond Docker is required.
|
|
||||||
#
|
|
||||||
# docker compose run --rm test
|
|
||||||
# docker compose run --rm vet
|
|
||||||
# docker compose run --rm build
|
|
||||||
#
|
|
||||||
# The full gate — vet, formatting and tests — is `docker build .`, which is what
|
|
||||||
# CI runs.
|
|
||||||
#
|
|
||||||
# Build caches persist in the `gocache` volume, so re-runs are fast and the
|
|
||||||
# host tree stays clean. Commands that rewrite source (fmt, tidy) keep your
|
|
||||||
# ownership when run with `--user "$(id -u):$(id -g)"`.
|
|
||||||
#
|
|
||||||
# GO_VERSION picks the toolchain image (default: latest stable). Test the lowest
|
|
||||||
# supported Go with: GO_VERSION=1.22 docker compose run --rm test
|
|
||||||
|
|
||||||
x-go: &go
|
x-go: &go
|
||||||
image: golang:${GO_VERSION:-1.26.4}
|
image: golang:${GO_VERSION:-1.27.2}
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
@@ -33,12 +17,16 @@ services:
|
|||||||
|
|
||||||
cover:
|
cover:
|
||||||
<<: *go
|
<<: *go
|
||||||
command: go test -cover ./...
|
command: sh -c "go test -coverpkg=./... -coverprofile=/tmp/cover.out ./... && go tool cover -func=/tmp/cover.out | tail -1"
|
||||||
|
|
||||||
bench:
|
bench:
|
||||||
<<: *go
|
<<: *go
|
||||||
command: go test -run XXX -bench . -benchmem ./...
|
command: go test -run XXX -bench . -benchmem ./...
|
||||||
|
|
||||||
|
generate:
|
||||||
|
<<: *go
|
||||||
|
command: go generate ./...
|
||||||
|
|
||||||
vet:
|
vet:
|
||||||
<<: *go
|
<<: *go
|
||||||
command: go vet ./...
|
command: go vet ./...
|
||||||
@@ -47,6 +35,10 @@ services:
|
|||||||
<<: *go
|
<<: *go
|
||||||
command: go run github.com/fzipp/gocyclo/cmd/gocyclo@v0.6.0 -over 14 -ignore _test .
|
command: go run github.com/fzipp/gocyclo/cmd/gocyclo@v0.6.0 -over 14 -ignore _test .
|
||||||
|
|
||||||
|
vulncheck:
|
||||||
|
<<: *go
|
||||||
|
command: go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 -scan module
|
||||||
|
|
||||||
build:
|
build:
|
||||||
<<: *go
|
<<: *go
|
||||||
command: go build ./...
|
command: go build ./...
|
||||||
@@ -59,13 +51,12 @@ services:
|
|||||||
<<: *go
|
<<: *go
|
||||||
command: go mod tidy
|
command: go mod tidy
|
||||||
|
|
||||||
# Interactive shell for ad-hoc work: docker compose run --rm dev
|
|
||||||
dev:
|
dev:
|
||||||
<<: *go
|
<<: *go
|
||||||
command: sh
|
command: sh
|
||||||
|
|
||||||
data-import:
|
data-import:
|
||||||
image: python:3.14.7-slim
|
image: python:3.14.8-slim
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
|
|||||||
+46
-66
@@ -1,14 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Rebuild data/geo/SE/*.tsv from SCB (CC0), GeoNames (CC BY 4.0) and Trafikverket NVDB (CC0).
|
"""Rebuild data/geo/SE/*.tsv from SCB (CC0), GeoNames (CC BY 4.0) and Trafikverket NVDB (CC0).
|
||||||
|
|
||||||
TRAFIKVERKET_API_KEY=… data-import/geo-se.py [--key-file FILE] [--cache DIR] [--streets-per-locality N] [--out DIR]
|
TRAFIKVERKET_API_KEY=… data-import/geo-se.py [--key-file FILE] [--cache DIR] [--min-streets N] [--street-share F] [--out DIR]
|
||||||
"""
|
"""
|
||||||
import argparse
|
|
||||||
import collections
|
import collections
|
||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import math
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
@@ -16,9 +14,10 @@ import urllib.request
|
|||||||
import zipfile
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import geo
|
||||||
import source
|
import source
|
||||||
import xlsx
|
|
||||||
import tsv
|
import tsv
|
||||||
|
import xlsx
|
||||||
|
|
||||||
CODES = "https://www.scb.se/contentassets/7a89e48960f741e08918e489ea36354a/kommunlankod-2026.xlsx"
|
CODES = "https://www.scb.se/contentassets/7a89e48960f741e08918e489ea36354a/kommunlankod-2026.xlsx"
|
||||||
POPULATION = "https://api.scb.se/OV0104/v1/doris/sv/ssd/START/BE/BE0101/BE0101A/BefolkningNy"
|
POPULATION = "https://api.scb.se/OV0104/v1/doris/sv/ssd/START/BE/BE0101/BE0101A/BefolkningNy"
|
||||||
@@ -34,11 +33,17 @@ TATORTER = "https://geodata.scb.se/geoserver/stat/wfs?service=WFS&version=2.0.0&
|
|||||||
POSTAL_CODES = "https://download.geonames.org/export/zip/SE.zip"
|
POSTAL_CODES = "https://download.geonames.org/export/zip/SE.zip"
|
||||||
NVDB = "https://api.trafikinfo.trafikverket.se/v2/data.json"
|
NVDB = "https://api.trafikinfo.trafikverket.se/v2/data.json"
|
||||||
NVDB_PAGE = 50000
|
NVDB_PAGE = 50000
|
||||||
OUT = Path(__file__).resolve().parent.parent / "data" / "geo" / "SE"
|
|
||||||
CACHE = Path(__file__).resolve().parent / "cache"
|
|
||||||
TIMEZONE = "Europe/Stockholm"
|
TIMEZONE = "Europe/Stockholm"
|
||||||
|
# This script's reading of the "exceptions in big cities" in docs/research/research-geo-se.md, Postal codes → Structure.
|
||||||
ONE_POSITION = {"Stockholm", "Göteborg", "Malmö"}
|
ONE_POSITION = {"Stockholm", "Göteborg", "Malmö"}
|
||||||
|
# A tätort's floor: "minst 200 invånare", docs/research/research-geo-se.md, Lantmäteriet → Ortnamn.
|
||||||
UNMATCHED_POPULATION = 200
|
UNMATCHED_POPULATION = 200
|
||||||
|
# Not a street: a road number, a ferry route, a cycle path's or road association's label, a placeholder, or two names
|
||||||
|
# or a description in one.
|
||||||
|
NOT_A_STREET = re.compile(r"^(Europaväg|Länsväg|Riksväg) \d|^Färjeled |\b(GC|SMF|VSF)\b|^NN$|,")
|
||||||
|
PARTICLES = {"af", "de", "von"}
|
||||||
|
|
||||||
|
|
||||||
def scb_codes(cache):
|
def scb_codes(cache):
|
||||||
regions, municipalities = {}, {}
|
regions, municipalities = {}, {}
|
||||||
for cells in xlsx.rows(source.fetch(CODES, cache, "kommunlankod.xlsx", magic=b"PK")):
|
for cells in xlsx.rows(source.fetch(CODES, cache, "kommunlankod.xlsx", magic=b"PK")):
|
||||||
@@ -102,36 +107,11 @@ def nvdb_segments(cache, key):
|
|||||||
yield name, float(lat), float(lon)
|
yield name, float(lat), float(lon)
|
||||||
|
|
||||||
|
|
||||||
class Nearest:
|
|
||||||
"""Nearest point by an equirectangular distance, over a degree grid."""
|
|
||||||
|
|
||||||
def __init__(self, points, cell=0.05):
|
|
||||||
self.cell = cell
|
|
||||||
self.grid = collections.defaultdict(list)
|
|
||||||
for lat, lon, value in points:
|
|
||||||
self.grid[(int(lat // cell), int(lon // cell))].append((lat, lon, value))
|
|
||||||
|
|
||||||
def find(self, lat, lon):
|
|
||||||
ci, cj = int(lat // self.cell), int(lon // self.cell)
|
|
||||||
best, best_d = None, math.inf
|
|
||||||
ring = 0
|
|
||||||
while ring < 400:
|
|
||||||
for i in range(ci - ring, ci + ring + 1):
|
|
||||||
for j in range(cj - ring, cj + ring + 1):
|
|
||||||
if max(abs(i - ci), abs(j - cj)) != ring:
|
|
||||||
continue
|
|
||||||
for plat, plon, value in self.grid.get((i, j), ()):
|
|
||||||
d = (plat - lat) ** 2 + ((plon - lon) * math.cos(math.radians(lat))) ** 2
|
|
||||||
if d < best_d:
|
|
||||||
best, best_d = value, d
|
|
||||||
if best is not None and math.sqrt(best_d) < ring * self.cell * math.cos(math.radians(lat)):
|
|
||||||
return best
|
|
||||||
ring += 1
|
|
||||||
return best
|
|
||||||
|
|
||||||
|
|
||||||
def street_delivery(name, codes):
|
def street_delivery(name, codes):
|
||||||
"""The codes delivered to a street: the digit after the postort's own prefix says box, company or reply."""
|
"""The codes delivered to a street. A postort's own prefix is three digits where one three-digit prefix starts at
|
||||||
|
least half its codes, else two. Outside ONE_POSITION's cities, the digit after that prefix is 0 or 1 for boxes and 8
|
||||||
|
for reply mail; inside them, a second digit 0 marks boxes. The box and reply-mail digits are per
|
||||||
|
docs/research/research-geo-se.md, Postal codes → Structure."""
|
||||||
if name in ONE_POSITION:
|
if name in ONE_POSITION:
|
||||||
return [c for c in codes if c[1] != "0"]
|
return [c for c in codes if c[1] != "0"]
|
||||||
largest = collections.Counter(c[:3] for c in codes).most_common(1)[0][1]
|
largest = collections.Counter(c[:3] for c in codes).most_common(1)[0][1]
|
||||||
@@ -161,6 +141,8 @@ def population_of(name, municipality, tatorter, municipalities, population):
|
|||||||
|
|
||||||
|
|
||||||
def well_cased(name):
|
def well_cased(name):
|
||||||
|
"""Whether each word of name is capitalised: GeoNames also spells some postorter, such as STOCKHOLM and örebro, in
|
||||||
|
capitals or lower case."""
|
||||||
return all(part[:1].isupper() and (len(part) == 1 or not part.isupper()) for part in re.split(r"[ -]", name))
|
return all(part[:1].isupper() and (len(part) == 1 or not part.isupper()) for part in re.split(r"[ -]", name))
|
||||||
|
|
||||||
|
|
||||||
@@ -174,58 +156,56 @@ def localities(codes, tatorter, municipalities, population):
|
|||||||
municipality, method = municipality_of(name, rows, tatorter, municipalities)
|
municipality, method = municipality_of(name, rows, tatorter, municipalities)
|
||||||
how[method] += 1
|
how[method] += 1
|
||||||
kept = street_delivery(name, [r["code"].replace(" ", "") for r in rows])
|
kept = street_delivery(name, [r["code"].replace(" ", "") for r in rows])
|
||||||
with_point = [r for r in rows if r["lat"] is not None]
|
point = geo.centroid(rows)
|
||||||
if municipality is None or not kept or not with_point or not well_cased(name):
|
if municipality is None or not kept or point is None or not well_cased(name):
|
||||||
continue
|
continue
|
||||||
lat = sum(r["lat"] for r in with_point) / len(with_point)
|
lat, lon = point
|
||||||
lon = sum(r["lon"] for r in with_point) / len(with_point)
|
|
||||||
out[name] = {"name": name, "municipality": municipality, "population": population_of(name, municipality, tatorter, municipalities, population), "lat": f"{lat:.4f}", "lon": f"{lon:.4f}", "codes": kept}
|
out[name] = {"name": name, "municipality": municipality, "population": population_of(name, municipality, tatorter, municipalities, population), "lat": f"{lat:.4f}", "lon": f"{lon:.4f}", "codes": kept}
|
||||||
print(f"municipality by {dict(how)}; {len(by_locality) - len(out)} postorter dropped", file=sys.stderr)
|
print(f"municipality by {dict(how)}; {len(by_locality) - len(out)} postorter dropped before streets", file=sys.stderr)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def streets(segments, codes, localities, per_locality):
|
def street_name(name):
|
||||||
|
# A word of four capitals or fewer is an acronym or a Roman numeral: IOGT-vägen, Karl XII gatan.
|
||||||
|
opening = name[0].isupper() or name.split()[0] in PARTICLES
|
||||||
|
return opening and not NOT_A_STREET.search(name) and not any(len(w) >= 5 and w.isupper() for w in name.split())
|
||||||
|
|
||||||
|
|
||||||
|
def streets(segments, codes, localities, share, minimum):
|
||||||
"""The names with most segments per locality, each segment at its nearest code centroid."""
|
"""The names with most segments per locality, each segment at its nearest code centroid."""
|
||||||
nearest = Nearest((r["lat"], r["lon"], r["locality"]) for r in codes if r["lat"] is not None and r["locality"] in localities)
|
nearest = geo.Nearest((r["lat"], r["lon"], r["locality"]) for r in codes if r["lat"] is not None and r["locality"] in localities)
|
||||||
count = collections.Counter()
|
count = collections.Counter()
|
||||||
for name, lat, lon in segments:
|
for name, lat, lon in segments:
|
||||||
if name[0].isalpha():
|
if street_name(name):
|
||||||
count[(nearest.find(lat, lon), name)] += 1
|
count[(nearest.find(lat, lon), name)] += 1
|
||||||
of = collections.defaultdict(list)
|
return geo.top_streets(count, share, minimum, "segments")
|
||||||
for (locality, name), n in count.items():
|
|
||||||
of[locality].append((n, name))
|
|
||||||
return {locality: [{"name": name, "locality": locality, "segments": n} for n, name in sorted(named, key=lambda s: (-s[0], s[1]))[:per_locality]] for locality, named in of.items()}
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
a = geo.parser(__doc__, "SE", 1.0, key_file={"help": "file holding the Trafikverket API key; TRAFIKVERKET_API_KEY otherwise"}).parse_args()
|
||||||
p.add_argument("--cache", default=str(CACHE))
|
|
||||||
p.add_argument("--key-file", help="file holding the Trafikverket API key; TRAFIKVERKET_API_KEY otherwise")
|
|
||||||
p.add_argument("--out", default=str(OUT))
|
|
||||||
p.add_argument("--streets-per-locality", type=int, default=10)
|
|
||||||
a = p.parse_args()
|
|
||||||
key = Path(a.key_file).read_text().strip() if a.key_file else os.environ.get("TRAFIKVERKET_API_KEY")
|
key = Path(a.key_file).read_text().strip() if a.key_file else os.environ.get("TRAFIKVERKET_API_KEY")
|
||||||
if not key:
|
if not key:
|
||||||
sys.exit("set TRAFIKVERKET_API_KEY or pass --key-file")
|
sys.exit("set TRAFIKVERKET_API_KEY or pass --key-file")
|
||||||
cache, out = Path(a.cache), Path(a.out)
|
cache, out = geo.directories(a)
|
||||||
cache.mkdir(parents=True, exist_ok=True)
|
|
||||||
out.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
regions, municipalities = scb_codes(cache)
|
region_names, municipality_names = scb_codes(cache)
|
||||||
population = scb_population(cache)
|
population = scb_population(cache)
|
||||||
codes = geonames(cache)
|
codes = geonames(cache)
|
||||||
places = localities(codes, scb_tatorter(cache), municipalities, population)
|
places = localities(codes, scb_tatorter(cache), municipality_names, population)
|
||||||
named = streets(nvdb_segments(cache, key), codes, places, a.streets_per_locality)
|
named = streets(nvdb_segments(cache, key), codes, places, a.street_share, a.min_streets)
|
||||||
places = {name: l for name, l in places.items() if name in named}
|
regions = {c: {"code": c, "name": n, "population": population[c], "timezone": TIMEZONE} for c, n in region_names.items()}
|
||||||
empty = sorted(m for m in municipalities if not any(l["municipality"] == m for l in places.values()))
|
municipalities = {c: {"code": c, "name": n, "region": c[:2], "population": population[c]} for c, n in municipality_names.items()}
|
||||||
if empty:
|
kept, dropped = geo.fence(regions, municipalities, places, named, {k: l["codes"] for k, l in places.items()})
|
||||||
sys.exit(f"municipalities without a locality: {empty}")
|
geo.log_dropped_localities(dropped["locality"])
|
||||||
|
if dropped["municipality"] or dropped["region"]:
|
||||||
|
sys.exit(f"municipalities without a locality: {sorted(dropped['municipality'])}; regions without a municipality: {sorted(dropped['region'])}")
|
||||||
|
places = kept["locality"]
|
||||||
|
|
||||||
tsv.write(out / "region.tsv", ["code", "name", "population", "timezone"], [{"code": c, "name": n, "population": population[c], "timezone": TIMEZONE} for c, n in sorted(regions.items())])
|
tsv.write(out / "region.tsv", ["code", "name", "population", "timezone"], [r for _, r in sorted(kept["region"].items())])
|
||||||
tsv.write(out / "municipality.tsv", ["code", "name", "region", "population"], [{"code": c, "name": n, "region": c[:2], "population": population[c]} for c, n in sorted(municipalities.items())])
|
tsv.write(out / "municipality.tsv", ["code", "name", "region", "population"], [m for _, m in sorted(kept["municipality"].items())])
|
||||||
tsv.write(out / "locality.tsv", ["name", "municipality", "population", "lat", "lon"], [l for _, l in sorted(places.items())])
|
tsv.write(out / "locality.tsv", ["name", "municipality", "population", "lat", "lon"], [l for _, l in sorted(places.items())])
|
||||||
tsv.write(out / "postal-code.tsv", ["code", "locality"], sorted(({"code": f"{c[:3]} {c[3:]}", "locality": l["name"]} for l in places.values() for c in l["codes"]), key=lambda r: r["code"]))
|
tsv.write(out / "postal-code.tsv", ["code", "locality"], sorted(({"code": f"{c[:3]} {c[3:]}", "locality": l["name"]} for l in places.values() for c in l["codes"]), key=lambda r: r["code"]))
|
||||||
tsv.write(out / "street.tsv", ["name", "locality", "segments"], [s for locality in sorted(named) for s in named[locality]])
|
tsv.write(out / "street.tsv", ["name", "locality", "segments"], [s for locality in sorted(places) for s in named[locality]])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+76
-41
@@ -1,19 +1,21 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Rebuild data/geo/US/*.tsv from the Census Bureau's Gazetteer, population estimates, ZCTA relationships and TIGER/Line files (public domain).
|
"""Rebuild data/geo/US/*.tsv from the Census Bureau's Gazetteer, population estimates, ZCTA relationships and TIGER/Line files (public domain), and Wikidata's state capitals (CC0).
|
||||||
|
|
||||||
data-import/geo-us.py [--cache DIR] [--min-population N] [--streets-per-locality N] [--out DIR]
|
data-import/geo-us.py [--cache DIR] [--min-streets N] [--place-share F] [--street-share F] [--out DIR]
|
||||||
"""
|
"""
|
||||||
import argparse
|
|
||||||
import collections
|
import collections
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
|
import math
|
||||||
|
import random
|
||||||
import re
|
import re
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
|
import urllib.parse
|
||||||
import zipfile
|
import zipfile
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
import geo
|
||||||
import source
|
import source
|
||||||
import tsv
|
import tsv
|
||||||
|
|
||||||
@@ -24,14 +26,20 @@ COUNTIES = POPULATION.format("counties/totals/co-est2025-alldata.csv")
|
|||||||
PLACES = POPULATION.format("cities/totals/sub-est2025.csv")
|
PLACES = POPULATION.format("cities/totals/sub-est2025.csv")
|
||||||
ZCTA_PLACE = "https://www2.census.gov/geo/docs/maps-data/data/rel2020/zcta520/tab20_zcta520_place20_natl.txt"
|
ZCTA_PLACE = "https://www2.census.gov/geo/docs/maps-data/data/rel2020/zcta520/tab20_zcta520_place20_natl.txt"
|
||||||
TIGER = "https://www2.census.gov/geo/tiger/TIGER2025/{0}/tl_2025_{1}_{2}.zip"
|
TIGER = "https://www2.census.gov/geo/tiger/TIGER2025/{0}/tl_2025_{1}_{2}.zip"
|
||||||
OUT = Path(__file__).resolve().parent.parent / "data" / "geo" / "US"
|
|
||||||
CACHE = Path(__file__).resolve().parent / "cache"
|
|
||||||
ESTIMATE = "POPESTIMATE2025"
|
ESTIMATE = "POPESTIMATE2025"
|
||||||
CDP = "57"
|
CDP = "57"
|
||||||
|
# Each state's capital from Wikidata, with the capital's GNIS ids, which the Gazetteer carries as ANSICODE.
|
||||||
|
CAPITALS = "https://query.wikidata.org/sparql?" + urllib.parse.urlencode({"query": """
|
||||||
|
SELECT ?iso ?capitalLabel ?gnis WHERE {
|
||||||
|
?state wdt:P31 wd:Q35657; wdt:P300 ?iso; wdt:P36 ?capital.
|
||||||
|
OPTIONAL { ?capital wdt:P590 ?gnis. }
|
||||||
|
SERVICE wikibase:label { bd:serviceParam wikibase:language "en". }
|
||||||
|
}"""})
|
||||||
|
SHIPPED_POPULATION = 100000
|
||||||
HIGHWAY = re.compile(r"\b(I- |Hwy |Highway |Loop |Rte |Route |Rd )\d")
|
HIGHWAY = re.compile(r"\b(I- |Hwy |Highway |Loop |Rte |Route |Rd )\d")
|
||||||
SUFFIX = re.compile(r" (city and borough|city|town|village|borough|municipality|comunidad|zona urbana|metropolitan government|metro government|consolidated government|unified government|urban county|corporation|plantation)( \(balance\))?$")
|
SUFFIX = re.compile(r" (city and borough|city|town|village|borough|municipality|comunidad|zona urbana|metropolitan government|metro government|consolidated government|unified government|urban county|corporation|plantation)( \(balance\))?$")
|
||||||
# Places whose Census name is a merged government's; the postal city is what an address carries.
|
# Places whose Census name is not the postal city an address carries.
|
||||||
NAMES = {"1303440": "Athens", "1304204": "Augusta", "1349008": "Macon", "2148006": "Louisville", "3011397": "Butte", "4732742": "Hartsville", "4752006": "Nashville"}
|
NAMES = {"0665042": "Ventura", "1303440": "Athens", "1304204": "Augusta", "1349008": "Macon", "1608830": "Boise", "2146027": "Lexington", "2148006": "Louisville", "3011397": "Butte", "4732742": "Hartsville", "4744382": "Lynchburg", "4752006": "Nashville"}
|
||||||
TIMEZONES = {
|
TIMEZONES = {
|
||||||
"AK": "America/Anchorage", "AL": "America/Chicago", "AR": "America/Chicago", "AZ": "America/Phoenix",
|
"AK": "America/Anchorage", "AL": "America/Chicago", "AR": "America/Chicago", "AZ": "America/Phoenix",
|
||||||
"CA": "America/Los_Angeles", "CO": "America/Denver", "CT": "America/New_York", "DC": "America/New_York",
|
"CA": "America/Los_Angeles", "CO": "America/Denver", "CT": "America/New_York", "DC": "America/New_York",
|
||||||
@@ -107,8 +115,8 @@ def place_name(geoid, name):
|
|||||||
return stripped
|
return stripped
|
||||||
|
|
||||||
|
|
||||||
def localities(cache, min_population, counties):
|
def localities(cache, counties):
|
||||||
"""Each shipped place with its county, population and centroid."""
|
"""Each incorporated place with its county, population and centroid."""
|
||||||
place_population, county_part = {}, collections.defaultdict(list)
|
place_population, county_part = {}, collections.defaultdict(list)
|
||||||
for r in csv_rows(cache, PLACES, "sub-est2025.csv"):
|
for r in csv_rows(cache, PLACES, "sub-est2025.csv"):
|
||||||
if r["SUMLEV"] == "162":
|
if r["SUMLEV"] == "162":
|
||||||
@@ -118,18 +126,47 @@ def localities(cache, min_population, counties):
|
|||||||
out = {}
|
out = {}
|
||||||
for r in gazetteer(cache, "place"):
|
for r in gazetteer(cache, "place"):
|
||||||
geoid, population = r["GEOID"], place_population.get(r["GEOID"], 0)
|
geoid, population = r["GEOID"], place_population.get(r["GEOID"], 0)
|
||||||
if r["FUNCSTAT"] not in ("A", "F", "N") or r["LSAD"] == CDP or population < min_population or not county_part.get(geoid):
|
if r["FUNCSTAT"] not in ("A", "B", "F", "N") or r["LSAD"] == CDP or not county_part.get(geoid):
|
||||||
continue
|
continue
|
||||||
county = max(county_part[geoid])[1]
|
county = max(county_part[geoid])[1]
|
||||||
if county not in counties:
|
if county not in counties:
|
||||||
print(f"{geoid} {r['NAME']}: county {county} unknown, dropped", file=sys.stderr)
|
print(f"{geoid} {r['NAME']}: county {county} unknown, dropped", file=sys.stderr)
|
||||||
continue
|
continue
|
||||||
out[geoid] = {"code": geoid, "name": place_name(geoid, r["NAME"]), "municipality": county, "population": population, "lat": r["INTPTLAT"], "lon": r["INTPTLONG"]}
|
out[geoid] = {"code": geoid, "gnis": int(r["ANSICODE"]), "name": place_name(geoid, r["NAME"]), "municipality": county, "population": population, "lat": r["INTPTLAT"], "lon": r["INTPTLONG"]}
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def postal_codes(cache, localities):
|
def capitals(cache, places, counties):
|
||||||
"""Each ZCTA whose largest part inside an incorporated place lies in a shipped place."""
|
"""The codes of the places that are a state's capital, matched by any of its GNIS ids, else by a name no other place in the state carries."""
|
||||||
|
by_gnis = {p["gnis"]: code for code, p in places.items()}
|
||||||
|
named = collections.defaultdict(list)
|
||||||
|
for code, p in places.items():
|
||||||
|
named[(counties[p["municipality"]]["region"], p["name"])].append(code)
|
||||||
|
by_name = {key: codes[0] for key, codes in named.items() if len(codes) == 1}
|
||||||
|
gnis_of = collections.defaultdict(set)
|
||||||
|
for r in csv.DictReader(io.StringIO(text(source.fetch(CAPITALS, cache, "capitals.csv", magic=b"iso,", headers={"Accept": "text/csv"})))):
|
||||||
|
gnis_of[(r["iso"].removeprefix("US-"), r["capitalLabel"])].add(int(r["gnis"] or 0))
|
||||||
|
out = set()
|
||||||
|
for (state, name), ids in sorted(gnis_of.items()):
|
||||||
|
code = next((by_gnis[i] for i in sorted(ids) if i in by_gnis), None) or by_name.get((state, name))
|
||||||
|
if code:
|
||||||
|
out.add(code)
|
||||||
|
else:
|
||||||
|
print(f"{state} {name}: capital matches no incorporated place by GNIS id or by a name unique in its state", file=sys.stderr)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def drawn(places, share):
|
||||||
|
"""The share of the places with people, each drawn with odds by its population: Efraimidis and Spirakis's weighted
|
||||||
|
sampling without replacement. Each place's random number is seeded by its code, so a place added to or dropped from
|
||||||
|
the sources moves only the places near the cut."""
|
||||||
|
peopled = [code for code, p in places.items() if p["population"] > 0]
|
||||||
|
key = {code: math.log(1 - random.Random(code).random()) / places[code]["population"] for code in peopled}
|
||||||
|
return {code: places[code] for code in sorted(peopled, key=lambda c: (key[c], c), reverse=True)[:round(len(peopled) * share)]}
|
||||||
|
|
||||||
|
|
||||||
|
def zcta_localities(cache, localities):
|
||||||
|
"""Each ZCTA mapped to the incorporated place holding most of its land inside incorporated places, where localities holds that place."""
|
||||||
parts = {}
|
parts = {}
|
||||||
for r in csv.DictReader(io.StringIO(text(source.fetch(ZCTA_PLACE, cache, "zcta-place.txt"))), delimiter="|"):
|
for r in csv.DictReader(io.StringIO(text(source.fetch(ZCTA_PLACE, cache, "zcta-place.txt"))), delimiter="|"):
|
||||||
if r["GEOID_ZCTA5_20"] and r["GEOID_PLACE_20"] and not r["NAMELSAD_PLACE_20"].endswith(" CDP"):
|
if r["GEOID_ZCTA5_20"] and r["GEOID_PLACE_20"] and not r["NAMELSAD_PLACE_20"].endswith(" CDP"):
|
||||||
@@ -138,7 +175,7 @@ def postal_codes(cache, localities):
|
|||||||
return {zcta: place for zcta, place in largest.items() if place in localities}
|
return {zcta: place for zcta, place in largest.items() if place in localities}
|
||||||
|
|
||||||
|
|
||||||
def streets(cache, counties, locality_of_zcta, per_locality):
|
def streets(cache, counties, locality_of_zcta, share, minimum):
|
||||||
"""The names with most TIGER address ranges per place, and the address ranges per ZCTA."""
|
"""The names with most TIGER address ranges per place, and the address ranges per ZCTA."""
|
||||||
with concurrent.futures.ThreadPoolExecutor(3) as pool:
|
with concurrent.futures.ThreadPoolExecutor(3) as pool:
|
||||||
list(pool.map(lambda c: (tiger_zip(cache, "addr", c), tiger_zip(cache, "featnames", c)), counties))
|
list(pool.map(lambda c: (tiger_zip(cache, "addr", c), tiger_zip(cache, "featnames", c)), counties))
|
||||||
@@ -153,23 +190,12 @@ def streets(cache, counties, locality_of_zcta, per_locality):
|
|||||||
if r["PAFLAG"] == "P" and r["FULLNAME"] and not HIGHWAY.search(r["FULLNAME"]):
|
if r["PAFLAG"] == "P" and r["FULLNAME"] and not HIGHWAY.search(r["FULLNAME"]):
|
||||||
for z in zips.get(r["TLID"], ()):
|
for z in zips.get(r["TLID"], ()):
|
||||||
count[(locality_of_zcta[z], r["FULLNAME"])] += 1
|
count[(locality_of_zcta[z], r["FULLNAME"])] += 1
|
||||||
of = collections.defaultdict(list)
|
return geo.top_streets(count, share, minimum, "addresses"), addresses
|
||||||
for (locality, name), n in count.items():
|
|
||||||
of[locality].append((n, name))
|
|
||||||
named = {locality: [{"name": name, "locality": locality, "addresses": n} for n, name in sorted(ranked, key=lambda s: (-s[0], s[1]))[:per_locality]] for locality, ranked in of.items()}
|
|
||||||
return named, addresses
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
a = geo.parser(__doc__, "US", 0.25, place_share={"type": float, "default": 0.1}).parse_args()
|
||||||
p.add_argument("--cache", default=str(CACHE))
|
cache, out = geo.directories(a)
|
||||||
p.add_argument("--min-population", type=int, default=25000)
|
|
||||||
p.add_argument("--out", default=str(OUT))
|
|
||||||
p.add_argument("--streets-per-locality", type=int, default=10)
|
|
||||||
a = p.parse_args()
|
|
||||||
cache, out = Path(a.cache), Path(a.out)
|
|
||||||
cache.mkdir(parents=True, exist_ok=True)
|
|
||||||
out.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
state_population = {r["STATE"]: r[ESTIMATE] for r in csv_rows(cache, STATES, "nst-est2025.csv") if r["SUMLEV"] == "040"}
|
state_population = {r["STATE"]: r[ESTIMATE] for r in csv_rows(cache, STATES, "nst-est2025.csv") if r["SUMLEV"] == "040"}
|
||||||
regions = {r["USPS"]: {"abbr": r["USPS"], "code": r["GEOID"], "name": r["NAME"], "population": state_population[r["GEOID"]], "timezone": TIMEZONES[r["USPS"]]} for r in gazetteer(cache, "state")}
|
regions = {r["USPS"]: {"abbr": r["USPS"], "code": r["GEOID"], "name": r["NAME"], "population": state_population[r["GEOID"]], "timezone": TIMEZONES[r["USPS"]]} for r in gazetteer(cache, "state")}
|
||||||
@@ -182,20 +208,29 @@ def main():
|
|||||||
counties[r["GEOID"]] = {"code": r["GEOID"], "name": r["NAME"], "region": r["USPS"], "population": county_population[r["GEOID"]]}
|
counties[r["GEOID"]] = {"code": r["GEOID"], "name": r["NAME"], "region": r["USPS"], "population": county_population[r["GEOID"]]}
|
||||||
print(f"counties without a population estimate, dropped: {dict(unestimated)}", file=sys.stderr)
|
print(f"counties without a population estimate, dropped: {dict(unestimated)}", file=sys.stderr)
|
||||||
|
|
||||||
places = localities(cache, a.min_population, counties)
|
places = localities(cache, counties)
|
||||||
locality_of_zcta = postal_codes(cache, places)
|
owner_of_zcta = zcta_localities(cache, places)
|
||||||
named, addresses = streets(cache, sorted({l["municipality"] for l in places.values()}), locality_of_zcta, a.streets_per_locality)
|
capital_codes = capitals(cache, places, counties)
|
||||||
for geoid in [l for l in places if l not in named]:
|
owners = {p: places[p] for p in set(owner_of_zcta.values())}
|
||||||
print(f"{geoid} {places[geoid]['name']}: no streets, dropped", file=sys.stderr)
|
for code in sorted(capital_codes - set(owners)):
|
||||||
del places[geoid]
|
print(f"{code} {places[code]['name']}: capital owns no ZCTA, dropped", file=sys.stderr)
|
||||||
kept_counties = {l["municipality"] for l in places.values()}
|
places = drawn(owners, a.place_share) | {code: p for code, p in owners.items() if code in capital_codes or p["population"] >= SHIPPED_POPULATION}
|
||||||
kept_regions = {counties[c]["region"] for c in kept_counties}
|
locality_of_zcta = {zcta: p for zcta, p in owner_of_zcta.items() if p in places}
|
||||||
|
named, addresses = streets(cache, sorted({l["municipality"] for l in places.values()}), locality_of_zcta, a.street_share, a.min_streets)
|
||||||
|
zctas = collections.defaultdict(list)
|
||||||
|
for zcta, locality in sorted(locality_of_zcta.items()):
|
||||||
|
if addresses[zcta]:
|
||||||
|
zctas[locality].append(zcta)
|
||||||
|
kept, dropped = geo.fence(regions, counties, places, named, zctas)
|
||||||
|
geo.log_dropped_localities(dropped["locality"])
|
||||||
|
print(f"counties without a place, dropped: {len(dropped['municipality'])}; states without a county, dropped: {sorted(dropped['region'])}", file=sys.stderr)
|
||||||
|
places = kept["locality"]
|
||||||
|
|
||||||
tsv.write(out / "region.tsv", ["abbr", "code", "name", "population", "timezone"], [r for _, r in sorted(regions.items()) if r["abbr"] in kept_regions])
|
tsv.write(out / "region.tsv", ["abbr", "code", "name", "population", "timezone"], [r for _, r in sorted(kept["region"].items())])
|
||||||
tsv.write(out / "municipality.tsv", ["code", "name", "region", "population"], [c for _, c in sorted(counties.items()) if c["code"] in kept_counties])
|
tsv.write(out / "municipality.tsv", ["code", "name", "region", "population"], [c for _, c in sorted(kept["municipality"].items())])
|
||||||
tsv.write(out / "locality.tsv", ["code", "name", "municipality", "population", "lat", "lon"], [l for _, l in sorted(places.items())])
|
tsv.write(out / "locality.tsv", ["code", "name", "municipality", "population", "lat", "lon"], [l for _, l in sorted(places.items())])
|
||||||
tsv.write(out / "postal-code.tsv", ["code", "locality", "addresses"], [{"code": z, "locality": l, "addresses": addresses[z]} for z, l in sorted(locality_of_zcta.items()) if addresses[z] and l in places])
|
tsv.write(out / "postal-code.tsv", ["code", "locality", "addresses"], sorted(({"code": z, "locality": l, "addresses": addresses[z]} for l in places for z in zctas[l]), key=lambda r: r["code"]))
|
||||||
tsv.write(out / "street.tsv", ["name", "locality", "addresses"], [s for locality in sorted(named) for s in named[locality]])
|
tsv.write(out / "street.tsv", ["name", "locality", "addresses"], [s for locality in sorted(places) for s in named[locality]])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"""The steps a country's geo import may share with another's."""
|
||||||
|
import argparse
|
||||||
|
import collections
|
||||||
|
import math
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DATA = Path(__file__).resolve().parent.parent / "data" / "geo"
|
||||||
|
CACHE = Path(__file__).resolve().parent / "cache"
|
||||||
|
|
||||||
|
|
||||||
|
def parser(doc, country, street_share, **own):
|
||||||
|
"""A parser for the options every country's import takes, plus the script's own: each keyword names one option, with
|
||||||
|
"_" standing for "-"."""
|
||||||
|
options = {"cache": {"default": str(CACHE)}, "out": {"default": str(DATA / country)}, "min_streets": {"type": int, "default": 10}, "street_share": {"type": float, "default": street_share}, **own}
|
||||||
|
p = argparse.ArgumentParser(description=doc.splitlines()[0])
|
||||||
|
for name in sorted(options):
|
||||||
|
p.add_argument("--" + name.replace("_", "-"), **options[name])
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def directories(args):
|
||||||
|
"""The cache and out directories of the parsed options, both created."""
|
||||||
|
cache, out = Path(args.cache), Path(args.out)
|
||||||
|
cache.mkdir(parents=True, exist_ok=True)
|
||||||
|
out.mkdir(parents=True, exist_ok=True)
|
||||||
|
return cache, out
|
||||||
|
|
||||||
|
|
||||||
|
def centroid(rows):
|
||||||
|
"""The mean lat and lon of the rows with a point, or None where no row has one."""
|
||||||
|
points = [r for r in rows if r["lat"] is not None]
|
||||||
|
if not points:
|
||||||
|
return None
|
||||||
|
return sum(r["lat"] for r in points) / len(points), sum(r["lon"] for r in points) / len(points)
|
||||||
|
|
||||||
|
|
||||||
|
class Nearest:
|
||||||
|
"""Nearest point by an equirectangular distance, over a degree grid."""
|
||||||
|
|
||||||
|
def __init__(self, points, cell=0.05):
|
||||||
|
self.cell = cell
|
||||||
|
self.grid = collections.defaultdict(list)
|
||||||
|
for lat, lon, value in points:
|
||||||
|
self.grid[(int(lat // cell), int(lon // cell))].append((lat, lon, value))
|
||||||
|
|
||||||
|
def find(self, lat, lon):
|
||||||
|
ci, cj = int(lat // self.cell), int(lon // self.cell)
|
||||||
|
best, best_d = None, math.inf
|
||||||
|
ring = 0
|
||||||
|
while ring < 400:
|
||||||
|
for i in range(ci - ring, ci + ring + 1):
|
||||||
|
for j in range(cj - ring, cj + ring + 1):
|
||||||
|
if max(abs(i - ci), abs(j - cj)) != ring:
|
||||||
|
continue
|
||||||
|
for plat, plon, value in self.grid.get((i, j), ()):
|
||||||
|
d = (plat - lat) ** 2 + ((plon - lon) * math.cos(math.radians(lat))) ** 2
|
||||||
|
if d < best_d:
|
||||||
|
best, best_d = value, d
|
||||||
|
if best is not None and math.sqrt(best_d) < ring * self.cell * math.cos(math.radians(lat)):
|
||||||
|
return best
|
||||||
|
ring += 1
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
def top_streets(count, share, minimum, column):
|
||||||
|
"""count maps (locality, street name) to a count. Returns, per locality, rows for its most-counted names, share of
|
||||||
|
them rounded up and at least minimum, ties broken by name; each row holds its count under column."""
|
||||||
|
of = collections.defaultdict(list)
|
||||||
|
for (locality, name), n in count.items():
|
||||||
|
of[locality].append((n, name))
|
||||||
|
return {locality: [{"name": name, "locality": locality, column: n} for n, name in sorted(named, key=lambda s: (-s[0], s[1]))[:max(minimum, math.ceil(len(named) * share))]] for locality, named in of.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def fence(regions, municipalities, localities, streets, postal_codes):
|
||||||
|
"""Meet the loader's fence that every parent row has a child row. Keeps each locality with entries in both streets
|
||||||
|
and postal_codes, then each municipality and region left with a child.
|
||||||
|
|
||||||
|
regions and municipalities map a code to a row; a municipality row names its region under "region". localities,
|
||||||
|
streets and postal_codes map a locality's key to its row, its streets and its postal codes. A locality row holds
|
||||||
|
name, municipality, population, lat and lon, plus its country's own keys.
|
||||||
|
|
||||||
|
Returns kept, then dropped: each maps "locality", "municipality" and "region" to that table's rows."""
|
||||||
|
kept = {"locality": {k: l for k, l in localities.items() if streets.get(k) and postal_codes.get(k)}}
|
||||||
|
municipality_codes = {l["municipality"] for l in kept["locality"].values()}
|
||||||
|
kept["municipality"] = {c: m for c, m in municipalities.items() if c in municipality_codes}
|
||||||
|
region_codes = {m["region"] for m in kept["municipality"].values()}
|
||||||
|
kept["region"] = {c: r for c, r in regions.items() if c in region_codes}
|
||||||
|
every = {"locality": localities, "municipality": municipalities, "region": regions}
|
||||||
|
dropped = {table: {k: row for k, row in rows.items() if k not in kept[table]} for table, rows in every.items()}
|
||||||
|
return kept, dropped
|
||||||
|
|
||||||
|
|
||||||
|
def log_dropped_localities(rows):
|
||||||
|
"""Logs each locality row as dropped, by its key, plus its name where that differs."""
|
||||||
|
for key, row in rows.items():
|
||||||
|
label = key if key == row["name"] else f"{key} {row['name']}"
|
||||||
|
print(f"{label}: no street or no postal code, dropped", file=sys.stderr)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rebuild data/misc/httpmethod.tsv from the IANA HTTP Method Registry.
|
||||||
|
|
||||||
|
data-import/httpmethod.py [--source URL_OR_FILE] [--cache DIR] [--out FILE]
|
||||||
|
|
||||||
|
A method ships when the register cites RFC 9110 section 9.3 or RFC 5789 for it, which
|
||||||
|
is the eight HTTP defines and PATCH; the register's other entries do not. The
|
||||||
|
register's `yes` and `no` ship as `true` and `false`, which a consumer's boolean reads.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import source
|
||||||
|
import tsv
|
||||||
|
|
||||||
|
SOURCE = "https://www.iana.org/assignments/http-methods/methods.csv"
|
||||||
|
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "httpmethod.tsv"
|
||||||
|
CACHE = Path(__file__).resolve().parent / "cache"
|
||||||
|
COLUMNS = ["idempotent", "method", "safe"]
|
||||||
|
CORE = re.compile(r"\[RFC9110, Section 9\.3\.|\[RFC5789, Section 2\]")
|
||||||
|
BOOLEAN = {"yes": "true", "no": "false"}
|
||||||
|
SELECTOR = '[]{}"|' # mirrors inSelector in internal/grammar/path.go
|
||||||
|
EXPECTED = 9
|
||||||
|
|
||||||
|
|
||||||
|
def rows(text):
|
||||||
|
for r in csv.DictReader(io.StringIO(text)):
|
||||||
|
method = (r["Method Name"] or "").strip()
|
||||||
|
if not CORE.search(r["Reference"] or ""):
|
||||||
|
continue
|
||||||
|
if set(method) & set(SELECTOR):
|
||||||
|
sys.exit(f"{method}: a method holding one of {SELECTOR} is one a selector cannot spell")
|
||||||
|
answers = {}
|
||||||
|
for column in ("Safe", "Idempotent"):
|
||||||
|
answer = (r[column] or "").strip()
|
||||||
|
if answer not in BOOLEAN:
|
||||||
|
sys.exit(f"{method}: {column} {answer!r} is neither yes nor no")
|
||||||
|
answers[column] = BOOLEAN[answer]
|
||||||
|
yield {"idempotent": answers["Idempotent"], "method": method, "safe": answers["Safe"]}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
|
p.add_argument("--cache", default=str(CACHE))
|
||||||
|
p.add_argument("--source", default=SOURCE)
|
||||||
|
p.add_argument("--out", default=str(OUT))
|
||||||
|
a = p.parse_args()
|
||||||
|
table = sorted(rows(source.fetch(a.source, a.cache, "http-methods.csv").decode("utf-8")), key=lambda r: r["method"])
|
||||||
|
if len(table) != EXPECTED:
|
||||||
|
sys.exit(f"{len(table)} methods cite RFC 9110 section 9.3 or RFC 5789, not {EXPECTED}; the register's Reference column has moved")
|
||||||
|
tsv.write(a.out, COLUMNS, table)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rebuild data/misc/httpstatus.tsv from the IANA HTTP Status Code Registry.
|
||||||
|
|
||||||
|
data-import/httpstatus.py [--source URL_OR_FILE] [--cache DIR] [--out FILE]
|
||||||
|
|
||||||
|
What ships is the codes in use: a range, an unassigned code and a qualified reason are all skipped.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import source
|
||||||
|
import tsv
|
||||||
|
|
||||||
|
SOURCE = "https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv"
|
||||||
|
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "httpstatus.tsv"
|
||||||
|
CACHE = Path(__file__).resolve().parent / "cache"
|
||||||
|
COLUMNS = ["code", "reason"]
|
||||||
|
|
||||||
|
|
||||||
|
def rows(text):
|
||||||
|
for r in csv.DictReader(io.StringIO(text)):
|
||||||
|
code, reason = r["Value"].strip(), r["Description"].strip()
|
||||||
|
if code.isdigit() and reason and "(" not in reason and reason != "Unassigned":
|
||||||
|
yield {"code": code, "reason": reason}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
|
p.add_argument("--cache", default=str(CACHE))
|
||||||
|
p.add_argument("--source", default=SOURCE)
|
||||||
|
p.add_argument("--out", default=str(OUT))
|
||||||
|
a = p.parse_args()
|
||||||
|
table = rows(source.fetch(a.source, a.cache, "http-status-codes-1.csv").decode("utf-8"))
|
||||||
|
tsv.write(a.out, COLUMNS, sorted(table, key=lambda r: int(r["code"])))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rebuild data/misc/language.tsv from datasets/language-codes (PDDL), the LoC ISO 639-2 register.
|
||||||
|
|
||||||
|
data-import/language.py [--source URL_OR_FILE] [--cache DIR] [--out FILE]
|
||||||
|
|
||||||
|
Only the 639-2 entries carrying a 639-1 code ship, since the alpha-2 code is the key.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import source
|
||||||
|
import tsv
|
||||||
|
|
||||||
|
SOURCE = "https://raw.githubusercontent.com/datasets/language-codes/main/data/language-codes-full.csv"
|
||||||
|
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "language.tsv"
|
||||||
|
CACHE = Path(__file__).resolve().parent / "cache"
|
||||||
|
COLUMNS = ["code", "code3", "name"]
|
||||||
|
|
||||||
|
|
||||||
|
def name(english):
|
||||||
|
"""The register lists every synonym; the first, without its qualifier, is the one a selector spells."""
|
||||||
|
return re.sub(r"\s*\([^)]*\)", "", english.split(";")[0]).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def rows(text):
|
||||||
|
for r in csv.DictReader(io.StringIO(text)):
|
||||||
|
if r["alpha2"]:
|
||||||
|
yield {"code": r["alpha2"], "code3": r["alpha3-t"] or r["alpha3-b"], "name": name(r["English"])}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
|
p.add_argument("--cache", default=str(CACHE))
|
||||||
|
p.add_argument("--source", default=SOURCE)
|
||||||
|
p.add_argument("--out", default=str(OUT))
|
||||||
|
a = p.parse_args()
|
||||||
|
table = rows(source.fetch(a.source, a.cache, "language-codes-full.csv").decode("utf-8"))
|
||||||
|
tsv.write(a.out, COLUMNS, sorted(table, key=lambda r: r["code"]))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rebuild data/misc/loglevel.tsv from RFC 5424 and POSIX <syslog.h>.
|
||||||
|
|
||||||
|
data-import/loglevel.py [--source URL_OR_FILE] [--posix URL_OR_FILE] [--cache DIR] [--out FILE]
|
||||||
|
|
||||||
|
Every severity in Table 2 ships, paired to POSIX's keywords by the order each source
|
||||||
|
lists them, which holds only because a keyword prefixes the severity RFC 5424 numbers
|
||||||
|
alike.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import html
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import source
|
||||||
|
import tsv
|
||||||
|
|
||||||
|
SOURCE = "https://www.rfc-editor.org/rfc/rfc5424.txt"
|
||||||
|
POSIX = "https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/syslog.h.html"
|
||||||
|
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "loglevel.tsv"
|
||||||
|
CACHE = Path(__file__).resolve().parent / "cache"
|
||||||
|
COLUMNS = ["code", "keyword", "severity"]
|
||||||
|
TABLE = re.compile(r"Table 1\.\s+Syslog Message Facilities(.*?)Table 2\.\s+Syslog Message Severities", re.S)
|
||||||
|
ROW = re.compile(r"^ +(\d) +([A-Z][a-z]+): ", re.M)
|
||||||
|
SECTION = re.compile(r"severity level portion of the.*?macros:(.*?)The following shall be declared as functions", re.S)
|
||||||
|
MACRO = re.compile(r"\bLOG_([A-Z]+)\b")
|
||||||
|
EXPECTED = 8
|
||||||
|
|
||||||
|
|
||||||
|
def severities(text):
|
||||||
|
table = TABLE.search(text)
|
||||||
|
if not table:
|
||||||
|
sys.exit("the RFC holds no text between the facility and the severity caption; its tables have moved")
|
||||||
|
return ROW.findall(table.group(1))
|
||||||
|
|
||||||
|
|
||||||
|
def keywords(page):
|
||||||
|
section = SECTION.search(html.unescape(re.sub(r"<[^>]+>", "", page)))
|
||||||
|
if not section:
|
||||||
|
sys.exit("POSIX spells no severity list between its own caption and the function declarations; the page has moved")
|
||||||
|
return [m.lower() for m in MACRO.findall(section.group(1))]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
|
p.add_argument("--cache", default=str(CACHE))
|
||||||
|
p.add_argument("--source", default=SOURCE)
|
||||||
|
p.add_argument("--posix", default=POSIX)
|
||||||
|
p.add_argument("--out", default=str(OUT))
|
||||||
|
a = p.parse_args()
|
||||||
|
table = sorted(severities(source.fetch(a.source, a.cache, "rfc5424.txt").decode("utf-8")))
|
||||||
|
codes = [code for code, _ in table]
|
||||||
|
if codes != [str(i) for i in range(EXPECTED)]:
|
||||||
|
sys.exit(f"the severity table numbers {codes}, not 0 through {EXPECTED - 1}")
|
||||||
|
words = keywords(source.fetch(a.posix, a.cache, "posix-syslog.h.html").decode("utf-8"))
|
||||||
|
if len(words) != EXPECTED:
|
||||||
|
sys.exit(f"POSIX lists {len(words)} severity macros, not {EXPECTED}: {words}")
|
||||||
|
rows = []
|
||||||
|
for (code, severity), keyword in zip(table, words):
|
||||||
|
if not severity.lower().startswith(keyword):
|
||||||
|
sys.exit(f"severity {code} is {severity} in the RFC and {keyword} in POSIX, which is no prefix of it")
|
||||||
|
rows.append({"code": code, "keyword": keyword, "severity": severity})
|
||||||
|
tsv.write(a.out, COLUMNS, rows)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rebuild data/misc/mimetype.tsv from mime-db (MIT), the IANA media type registry with extensions.
|
||||||
|
|
||||||
|
data-import/mimetype.py [--source URL_OR_FILE] [--cache DIR] [--out FILE]
|
||||||
|
|
||||||
|
Only types mime-db records as IANA-registered, with a filename extension, ship.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import source
|
||||||
|
import tsv
|
||||||
|
|
||||||
|
SOURCE = "https://raw.githubusercontent.com/jshttp/mime-db/master/db.json"
|
||||||
|
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "mimetype.tsv"
|
||||||
|
CACHE = Path(__file__).resolve().parent / "cache"
|
||||||
|
COLUMNS = ["ext", "type"]
|
||||||
|
FIXUPS = {"application/mp4": "mp4s", "audio/mpeg": "mp3", "audio/ogg": "ogg", "video/quicktime": "mov"}
|
||||||
|
|
||||||
|
|
||||||
|
def rows(db):
|
||||||
|
for mimetype, entry in db.items():
|
||||||
|
if entry.get("source") == "iana" and entry.get("extensions"):
|
||||||
|
yield {"ext": "." + FIXUPS.get(mimetype, entry["extensions"][0]), "type": mimetype}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
|
p.add_argument("--cache", default=str(CACHE))
|
||||||
|
p.add_argument("--source", default=SOURCE)
|
||||||
|
p.add_argument("--out", default=str(OUT))
|
||||||
|
a = p.parse_args()
|
||||||
|
table = rows(json.loads(source.fetch(a.source, a.cache, "mime-db.json").decode("utf-8")))
|
||||||
|
tsv.write(a.out, COLUMNS, sorted(table, key=lambda r: r["type"]))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -10,8 +10,8 @@ import re
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import source
|
import source
|
||||||
import xlsx
|
|
||||||
import tsv
|
import tsv
|
||||||
|
import xlsx
|
||||||
|
|
||||||
SOURCE = "https://www.scb.se/contentassets/9fe7dbb460994c72b835163dbc491ef9/namn-med-minst-tva-barare-31-december-2022.xlsx"
|
SOURCE = "https://www.scb.se/contentassets/9fe7dbb460994c72b835163dbc491ef9/namn-med-minst-tva-barare-31-december-2022.xlsx"
|
||||||
OUT = Path(__file__).resolve().parent.parent / "data" / "sv_SE"
|
OUT = Path(__file__).resolve().parent.parent / "data" / "sv_SE"
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rebuild data/misc/port.tsv from the IANA Service Name and Transport Protocol Port Number Registry.
|
||||||
|
|
||||||
|
data-import/port.py [--source URL_OR_FILE] [--cache DIR] [--out FILE]
|
||||||
|
|
||||||
|
A row needs a service name and a numeric TCP port. A port the registry lists more than
|
||||||
|
once keeps the first service it describes, or the first of them where it describes none,
|
||||||
|
so a number selects one row.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import source
|
||||||
|
import tsv
|
||||||
|
|
||||||
|
SOURCE = "https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.csv"
|
||||||
|
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "port.tsv"
|
||||||
|
CACHE = Path(__file__).resolve().parent / "cache"
|
||||||
|
COLUMNS = ["number", "service"]
|
||||||
|
FLOOR = 4000
|
||||||
|
|
||||||
|
|
||||||
|
def rows(text):
|
||||||
|
best, described = {}, set()
|
||||||
|
for r in csv.DictReader(io.StringIO(text)):
|
||||||
|
service, number = (r["Service Name"] or "").strip(), (r["Port Number"] or "").strip()
|
||||||
|
transport, description = (r["Transport Protocol"] or "").strip(), (r["Description"] or "").strip()
|
||||||
|
if transport != "tcp" or not service or not number.isdigit():
|
||||||
|
continue
|
||||||
|
if not 1 <= int(number) <= 65535:
|
||||||
|
sys.exit(f"{service}: port {number} is outside 1-65535")
|
||||||
|
if number not in best or (description and number not in described):
|
||||||
|
best[number] = {"number": number, "service": service}
|
||||||
|
if description:
|
||||||
|
described.add(number)
|
||||||
|
return sorted(best.values(), key=lambda r: int(r["number"]))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
|
p.add_argument("--cache", default=str(CACHE))
|
||||||
|
p.add_argument("--source", default=SOURCE)
|
||||||
|
p.add_argument("--out", default=str(OUT))
|
||||||
|
a = p.parse_args()
|
||||||
|
table = rows(source.fetch(a.source, a.cache, "service-names-port-numbers.csv").decode("utf-8"))
|
||||||
|
if len(table) < FLOOR:
|
||||||
|
sys.exit(f"only {len(table)} ports named a TCP service; the registry's columns have moved")
|
||||||
|
tsv.write(a.out, COLUMNS, table)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rebuild data/misc/protocol.tsv from the IANA Protocol Numbers registry.
|
||||||
|
|
||||||
|
data-import/protocol.py [--source URL_OR_FILE] [--cache DIR] [--out FILE]
|
||||||
|
|
||||||
|
A row needs a number and a keyword, which drops the seven numbers assigned to a class of
|
||||||
|
protocols rather than to one; a keyword the registry reserves or marks deprecated goes
|
||||||
|
too. Where the registry names no protocol beside the keyword, the keyword is the name.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import source
|
||||||
|
import tsv
|
||||||
|
|
||||||
|
SOURCE = "https://www.iana.org/assignments/protocol-numbers/protocol-numbers-1.csv"
|
||||||
|
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "protocol.tsv"
|
||||||
|
CACHE = Path(__file__).resolve().parent / "cache"
|
||||||
|
COLUMNS = ["keyword", "name", "number"]
|
||||||
|
SKIP = ("Reserved", "deprecated")
|
||||||
|
SELECTOR = '[]{}"|' # mirrors inSelector in internal/grammar/path.go
|
||||||
|
|
||||||
|
|
||||||
|
def rows(text):
|
||||||
|
for r in csv.DictReader(io.StringIO(text)):
|
||||||
|
number, keyword = (r["Decimal"] or "").strip(), (r["Keyword"] or "").strip()
|
||||||
|
if not number.isdigit() or not keyword or any(s in keyword for s in SKIP):
|
||||||
|
continue
|
||||||
|
if not 0 <= int(number) <= 255:
|
||||||
|
sys.exit(f"{keyword}: protocol number {number} is outside 0-255")
|
||||||
|
yield {"keyword": keyword, "name": " ".join((r["Protocol"] or "").split()) or keyword, "number": number}
|
||||||
|
|
||||||
|
|
||||||
|
def refuse_an_unselectable_row(table):
|
||||||
|
"""What the loader refuses at New, and a name that names two rows rather than one."""
|
||||||
|
keywords, names = {}, {}
|
||||||
|
for r in table:
|
||||||
|
for column in ("keyword", "name"):
|
||||||
|
if set(r[column]) & set(SELECTOR):
|
||||||
|
sys.exit(f"{r['keyword']}: {column} {r[column]!r} holds one of {SELECTOR}, which a selector cannot spell")
|
||||||
|
if r["keyword"] in keywords:
|
||||||
|
sys.exit(f"{r['keyword']}: repeats the keyword of protocol {keywords[r['keyword']]}")
|
||||||
|
keywords[r["keyword"]] = r["number"]
|
||||||
|
for r in table:
|
||||||
|
if r["name"] != r["keyword"] and r["name"] in keywords:
|
||||||
|
sys.exit(f"{r['keyword']}: name {r['name']!r} is another protocol's keyword")
|
||||||
|
if r["name"] in names:
|
||||||
|
sys.exit(f"{r['keyword']}: name {r['name']!r} repeats {names[r['name']]}'s, so it selects neither")
|
||||||
|
names[r["name"]] = r["keyword"]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
|
p.add_argument("--cache", default=str(CACHE))
|
||||||
|
p.add_argument("--source", default=SOURCE)
|
||||||
|
p.add_argument("--out", default=str(OUT))
|
||||||
|
a = p.parse_args()
|
||||||
|
table = sorted(rows(source.fetch(a.source, a.cache, "protocol-numbers-1.csv").decode("utf-8")), key=lambda r: int(r["number"]))
|
||||||
|
refuse_an_unselectable_row(table)
|
||||||
|
tsv.write(a.out, COLUMNS, table)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -11,10 +11,10 @@ def fetch(source, cache, name, magic=b"", data=None, headers=None):
|
|||||||
if not re.match(r"^https?://", source):
|
if not re.match(r"^https?://", source):
|
||||||
return Path(source).read_bytes()
|
return Path(source).read_bytes()
|
||||||
path = Path(cache) / name
|
path = Path(cache) / name
|
||||||
|
if path.exists():
|
||||||
|
return path.read_bytes()
|
||||||
for attempt in range(1, 6):
|
for attempt in range(1, 6):
|
||||||
if path.exists():
|
req = urllib.request.Request(source, data=data, headers={"User-Agent": "fejkdata-data-import (https://github.com/larvit/fejkdata)", **(headers or {})})
|
||||||
return path.read_bytes()
|
|
||||||
req = urllib.request.Request(source, data=data, headers={"User-Agent": "fejkdata data-import", **(headers or {})})
|
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(req, timeout=600) as r:
|
with urllib.request.urlopen(req, timeout=600) as r:
|
||||||
body = r.read()
|
body = r.read()
|
||||||
@@ -23,6 +23,7 @@ def fetch(source, cache, name, magic=b"", data=None, headers=None):
|
|||||||
if body and body.startswith(magic) and b"Request Rejected" not in body[:512]:
|
if body and body.startswith(magic) and b"Request Rejected" not in body[:512]:
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.write_bytes(body)
|
path.write_bytes(body)
|
||||||
elif attempt < 5:
|
return body
|
||||||
|
if attempt < 5:
|
||||||
time.sleep(10 * attempt)
|
time.sleep(10 * attempt)
|
||||||
sys.exit(f"{source}: no valid download in 5 attempts")
|
sys.exit(f"{source}: no valid download in 5 attempts")
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Rebuild data/misc/country.tsv from datasets/country-codes (PDDL).
|
"""Rebuild data/misc/territory.tsv from datasets/country-codes (PDDL).
|
||||||
|
|
||||||
data-import/country.py [--source URL_OR_FILE] [--cache DIR] [--out FILE]
|
data-import/territory.py [--source URL_OR_FILE] [--cache DIR] [--out FILE]
|
||||||
"""
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import source
|
import source
|
||||||
import tsv
|
import tsv
|
||||||
|
|
||||||
SOURCE = "https://raw.githubusercontent.com/datasets/country-codes/main/data/country-codes.csv"
|
SOURCE = "https://raw.githubusercontent.com/datasets/country-codes/main/data/country-codes.csv"
|
||||||
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "country.tsv"
|
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "territory.tsv"
|
||||||
CACHE = Path(__file__).resolve().parent / "cache"
|
CACHE = Path(__file__).resolve().parent / "cache"
|
||||||
COLUMNS = ["alpha2", "alpha3", "calling-code", "capital", "currency", "flag", "languages", "name", "numeric", "tld"]
|
COLUMNS = ["alpha2", "alpha3", "calling-code", "capital", "country", "currency", "flag", "languages", "name", "numeric", "tld"]
|
||||||
|
SOVEREIGN = re.compile(r"^(?:Part of|Territor(?:y|ies) of|Crown dependency of|Commonwealth of|Associated with) ([A-Z]{2})$")
|
||||||
|
STANDALONE = {"In contention", "International"}
|
||||||
# Gaps in the source, keyed by alpha2.
|
# Gaps in the source, keyed by alpha2.
|
||||||
FIXUPS = {"TR": {"currency": "TRY"}}
|
FIXUPS = {"TR": {"currency": "TRY"}}
|
||||||
|
|
||||||
@@ -33,6 +36,16 @@ def languages(field):
|
|||||||
return ",".join(p for p in field.split(",") if p)
|
return ",".join(p for p in field.split(",") if p)
|
||||||
|
|
||||||
|
|
||||||
|
def country(alpha2, independent):
|
||||||
|
"""The sovereign state the register records; a territory it records none for stands alone."""
|
||||||
|
m = SOVEREIGN.match(independent)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
if independent == "Yes" or independent in STANDALONE:
|
||||||
|
return alpha2
|
||||||
|
sys.exit(f"{alpha2}: is_independent {independent!r} names no sovereign this script can read")
|
||||||
|
|
||||||
|
|
||||||
def rows(text):
|
def rows(text):
|
||||||
for r in csv.DictReader(io.StringIO(text)):
|
for r in csv.DictReader(io.StringIO(text)):
|
||||||
alpha2 = r["ISO3166-1-Alpha-2"]
|
alpha2 = r["ISO3166-1-Alpha-2"]
|
||||||
@@ -41,6 +54,7 @@ def rows(text):
|
|||||||
"alpha3": r["ISO3166-1-Alpha-3"],
|
"alpha3": r["ISO3166-1-Alpha-3"],
|
||||||
"calling-code": calling_code(r["Dial"]),
|
"calling-code": calling_code(r["Dial"]),
|
||||||
"capital": r["Capital"],
|
"capital": r["Capital"],
|
||||||
|
"country": country(alpha2, r["is_independent"]),
|
||||||
"currency": r["ISO4217-currency_alphabetic_code"].split(",")[0],
|
"currency": r["ISO4217-currency_alphabetic_code"].split(",")[0],
|
||||||
"flag": flag(alpha2),
|
"flag": flag(alpha2),
|
||||||
"languages": languages(r["Languages"]),
|
"languages": languages(r["Languages"]),
|
||||||
@@ -59,8 +73,12 @@ def main():
|
|||||||
p.add_argument("--source", default=SOURCE)
|
p.add_argument("--source", default=SOURCE)
|
||||||
p.add_argument("--out", default=str(OUT))
|
p.add_argument("--out", default=str(OUT))
|
||||||
a = p.parse_args()
|
a = p.parse_args()
|
||||||
table = rows(source.fetch(a.source, a.cache, "country-codes.csv").decode("utf-8"))
|
table = sorted(rows(source.fetch(a.source, a.cache, "country-codes.csv").decode("utf-8")), key=lambda r: r["alpha2"])
|
||||||
tsv.write(a.out, COLUMNS, sorted(table, key=lambda r: r["alpha2"]))
|
shipped = {r["alpha2"] for r in table}
|
||||||
|
for r in table:
|
||||||
|
if r["country"] not in shipped:
|
||||||
|
sys.exit(f"{r['alpha2']}: country {r['country']} is no shipped territory; a sovereign the set drops leaves its territories unreadable")
|
||||||
|
tsv.write(a.out, COLUMNS, table)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rebuild data/misc/timezone.tsv from the IANA tzdb tarball (public domain) and GeoNames (CC BY 4.0).
|
||||||
|
|
||||||
|
data-import/timezone.py [--source URL_OR_FILE] [--cities URL_OR_FILE] [--cache DIR] [--out FILE] [--territories FILE]
|
||||||
|
|
||||||
|
The offset is the zone's standard offset, the first field of its Zone rule's last
|
||||||
|
continuation line, with a Link resolved to its target. The weight is the population
|
||||||
|
GeoNames records in the zone's cities of 15,000 or more, never below that threshold,
|
||||||
|
so a drawn zone is one people live in.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import tarfile
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import source
|
||||||
|
import tsv
|
||||||
|
|
||||||
|
SOURCE = "https://data.iana.org/time-zones/tzdata-latest.tar.gz"
|
||||||
|
CITIES = "https://download.geonames.org/export/dump/cities15000.zip"
|
||||||
|
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "timezone.tsv"
|
||||||
|
TERRITORIES = Path(__file__).resolve().parent.parent / "data" / "misc" / "territory.tsv"
|
||||||
|
CACHE = Path(__file__).resolve().parent / "cache"
|
||||||
|
COLUMNS = ["offset", "territory", "weight", "zone"]
|
||||||
|
REGIONS = ["africa", "antarctica", "asia", "australasia", "backward", "etcetera", "europe", "northamerica", "southamerica"]
|
||||||
|
# GeoNames' own cutoff, so a zone it records no city in weighs less than one would.
|
||||||
|
FLOOR = 15000
|
||||||
|
|
||||||
|
|
||||||
|
def member(tar, name):
|
||||||
|
try:
|
||||||
|
f = tar.extractfile(name)
|
||||||
|
except KeyError:
|
||||||
|
f = None
|
||||||
|
if f is None:
|
||||||
|
sys.exit(f"{name}: the tarball no longer holds it as a file; the tzdb layout has moved")
|
||||||
|
return f.read().decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def offsets(tar):
|
||||||
|
"""Every zone's standard offset, and every link's target."""
|
||||||
|
std, links = {}, {}
|
||||||
|
for name in REGIONS:
|
||||||
|
zone = None
|
||||||
|
for raw in member(tar, name).splitlines():
|
||||||
|
line = raw.split("#")[0].rstrip()
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
if line.startswith("Zone"):
|
||||||
|
f = line.split()
|
||||||
|
zone, std[f[1]] = f[1], f[2]
|
||||||
|
elif line.startswith("Link"):
|
||||||
|
f = line.split()
|
||||||
|
links[f[2]], zone = f[1], None
|
||||||
|
elif line[0] in " \t" and zone:
|
||||||
|
std[zone] = line.split()[0]
|
||||||
|
else:
|
||||||
|
zone = None
|
||||||
|
return std, links
|
||||||
|
|
||||||
|
|
||||||
|
def resolve(zone, std, links):
|
||||||
|
for _ in range(10):
|
||||||
|
if zone in std:
|
||||||
|
return std[zone]
|
||||||
|
if zone not in links:
|
||||||
|
return None
|
||||||
|
zone = links[zone]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def utc_offset(raw):
|
||||||
|
"""±HH:MM from a tzdb STDOFF field, which writes the minutes and seconds only when it has them."""
|
||||||
|
m = re.match(r"^(-)?(\d{1,2})(?::(\d{2})(?::(\d{2}))?)?$", raw)
|
||||||
|
if not m or (m.group(4) or "00") != "00":
|
||||||
|
return None
|
||||||
|
return f"{'-' if m.group(1) else '+'}{int(m.group(2)):02d}:{m.group(3) or '00'}"
|
||||||
|
|
||||||
|
|
||||||
|
def population(body):
|
||||||
|
"""The population GeoNames records per timezone, summed over its cities."""
|
||||||
|
with zipfile.ZipFile(io.BytesIO(body)) as z:
|
||||||
|
text = z.read("cities15000.txt").decode("utf-8")
|
||||||
|
per = {}
|
||||||
|
for city in csv.reader(io.StringIO(text), delimiter="\t", quoting=csv.QUOTE_NONE):
|
||||||
|
if len(city) > 17 and city[17] and city[14].isdigit():
|
||||||
|
per[city[17]] = per.get(city[17], 0) + int(city[14])
|
||||||
|
return per
|
||||||
|
|
||||||
|
|
||||||
|
def rows(tar, territories, pop):
|
||||||
|
std, links = offsets(tar)
|
||||||
|
for line in member(tar, "zone.tab").splitlines():
|
||||||
|
if line.startswith("#") or not line.strip():
|
||||||
|
continue
|
||||||
|
fields = line.split("\t")
|
||||||
|
if len(fields) < 3:
|
||||||
|
sys.exit(f"zone.tab: {line!r} has {len(fields)} fields; a row names a territory, a location and a zone")
|
||||||
|
territory, zone = fields[0], fields[2]
|
||||||
|
if territory not in territories:
|
||||||
|
continue
|
||||||
|
raw = resolve(zone, std, links)
|
||||||
|
if raw is None:
|
||||||
|
sys.exit(f"{zone}: the tarball gives it no Zone rule and no Link to one")
|
||||||
|
offset = utc_offset(raw)
|
||||||
|
if offset is None:
|
||||||
|
sys.exit(f"{zone}: standard offset {raw!r} is not a ±HH:MM the table can spell")
|
||||||
|
yield {"offset": offset, "territory": territory, "weight": max(pop.get(zone, 0), FLOOR), "zone": zone}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
|
p.add_argument("--cache", default=str(CACHE))
|
||||||
|
p.add_argument("--source", default=SOURCE)
|
||||||
|
p.add_argument("--out", default=str(OUT))
|
||||||
|
p.add_argument("--cities", default=CITIES)
|
||||||
|
p.add_argument("--territories", default=str(TERRITORIES))
|
||||||
|
a = p.parse_args()
|
||||||
|
shipped = {r["alpha2"] for r in csv.DictReader(io.StringIO(Path(a.territories).read_text(encoding="utf-8")), delimiter="\t")}
|
||||||
|
pop = population(source.fetch(a.cities, a.cache, "cities15000.zip", magic=b"PK"))
|
||||||
|
body = source.fetch(a.source, a.cache, "tzdata-latest.tar.gz", magic=b"\x1f\x8b")
|
||||||
|
with tarfile.open(fileobj=io.BytesIO(body)) as tar:
|
||||||
|
table = sorted(rows(tar, shipped, pop), key=lambda r: r["zone"])
|
||||||
|
print(f"tzdb {tar.extractfile('version').read().decode('utf-8').strip()}", file=sys.stderr)
|
||||||
|
linked = {r["territory"] for r in table}
|
||||||
|
if missing := shipped - linked:
|
||||||
|
sys.exit(f"no zone for {sorted(missing)}; a parent row without a child is a load error")
|
||||||
|
if (floored := sum(1 for r in table if r["weight"] == FLOOR)) > len(table) // 2:
|
||||||
|
sys.exit(f"{floored} of {len(table)} zones fell to the floor; GeoNames' population or timezone column has moved")
|
||||||
|
tsv.write(a.out, COLUMNS, table)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rebuild data/misc/tld.tsv from the IANA Root Zone Database.
|
||||||
|
|
||||||
|
data-import/tld.py [--source URL_OR_FILE] [--zone URL_OR_FILE] [--cache DIR] [--out FILE]
|
||||||
|
|
||||||
|
A TLD ships when the database names a manager for it; one it marks "Not assigned" is a
|
||||||
|
delegation the root zone no longer holds, and the zone file is fetched to prove the two
|
||||||
|
agree. The key is the A-label the root zone holds; `unicode` is the form the database
|
||||||
|
displays, less the bidi marks it wraps a right-to-left label in, and proved to punycode
|
||||||
|
back to the key.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import html
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import source
|
||||||
|
import tsv
|
||||||
|
|
||||||
|
SOURCE = "https://www.iana.org/domains/root/db"
|
||||||
|
ZONE = "https://data.iana.org/TLD/tlds-alpha-by-domain.txt"
|
||||||
|
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "tld.tsv"
|
||||||
|
CACHE = Path(__file__).resolve().parent / "cache"
|
||||||
|
COLUMNS = ["tld", "type", "unicode"]
|
||||||
|
TYPES = ("country-code", "generic", "generic-restricted", "infrastructure", "sponsored")
|
||||||
|
BIDI = {0x200E: None, 0x200F: None}
|
||||||
|
ROW = re.compile(r'<a href="/domains/root/db/([^"]+)\.html">([^<]+)</a>.*?</td>\s*<td>([^<]*)</td>\s*<td>(.*?)</td>', re.S)
|
||||||
|
|
||||||
|
|
||||||
|
def rows(page):
|
||||||
|
for a_label, shown, kind, manager in ROW.findall(page):
|
||||||
|
if "Not assigned" in manager:
|
||||||
|
continue
|
||||||
|
kind = kind.strip()
|
||||||
|
if kind not in TYPES:
|
||||||
|
sys.exit(f"{a_label}: the database types it {kind!r}, which is none of {TYPES}")
|
||||||
|
yield {"tld": "." + a_label, "type": kind, "unicode": html.unescape(shown).strip().translate(BIDI)}
|
||||||
|
|
||||||
|
|
||||||
|
def prove_each_unicode_form_is_its_key(table):
|
||||||
|
"""The displayed form is the key's own spelling, which punycode encodes back to."""
|
||||||
|
for r in table:
|
||||||
|
label = r["unicode"][1:]
|
||||||
|
encoded = label if label.isascii() else "xn--" + label.encode("punycode").decode("ascii")
|
||||||
|
if "." + encoded != r["tld"]:
|
||||||
|
sys.exit(f"{r['tld']}: the database displays {r['unicode']!r}, which is no spelling of it")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
|
p.add_argument("--cache", default=str(CACHE))
|
||||||
|
p.add_argument("--source", default=SOURCE)
|
||||||
|
p.add_argument("--zone", default=ZONE)
|
||||||
|
p.add_argument("--out", default=str(OUT))
|
||||||
|
a = p.parse_args()
|
||||||
|
table = sorted(rows(source.fetch(a.source, a.cache, "root-db.html").decode("utf-8")), key=lambda r: r["tld"])
|
||||||
|
prove_each_unicode_form_is_its_key(table)
|
||||||
|
listing = source.fetch(a.zone, a.cache, "tlds-alpha-by-domain.txt").decode("utf-8")
|
||||||
|
delegated = {"." + line.strip().lower() for line in listing.splitlines() if line.strip() and not line.startswith("#")}
|
||||||
|
shipped = {r["tld"] for r in table}
|
||||||
|
if len(table) != len(delegated) or shipped != delegated:
|
||||||
|
sys.exit(f"the database assigns {len(table)} rows where the root zone holds {len(delegated)} TLDs: {sorted(shipped ^ delegated)[:10]}")
|
||||||
|
tsv.write(a.out, COLUMNS, table)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+4
-2
@@ -5,12 +5,14 @@ from pathlib import Path
|
|||||||
|
|
||||||
|
|
||||||
def write(path, columns, rows):
|
def write(path, columns, rows):
|
||||||
"""Write the rows as a TSV; every cell must be non-empty and free of tabs, newlines and braces."""
|
"""Write the rows as a TSV of a header and at least one row, each cell stripped, non-empty and free of tabs, newlines and braces."""
|
||||||
lines = ["\t".join(columns)]
|
lines = ["\t".join(columns)]
|
||||||
for row in rows:
|
for row in rows:
|
||||||
cells = [str(row[c]) for c in columns]
|
cells = [str(row[c]).strip() for c in columns]
|
||||||
if not all(cells) or any(re.search(r"[\t\n{}]", c) for c in cells):
|
if not all(cells) or any(re.search(r"[\t\n{}]", c) for c in cells):
|
||||||
raise ValueError(f"{path}: a cell is empty or holds a tab, newline or brace: {row}")
|
raise ValueError(f"{path}: a cell is empty or holds a tab, newline or brace: {row}")
|
||||||
lines.append("\t".join(cells))
|
lines.append("\t".join(cells))
|
||||||
|
if len(lines) < 2:
|
||||||
|
raise ValueError(f"{path}: has no rows below its header; a table holds at least one row")
|
||||||
Path(path).write_text("\n".join(lines) + "\n", encoding="utf-8")
|
Path(path).write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
print(f"{path}: {len(lines) - 1} rows", file=sys.stderr)
|
print(f"{path}: {len(lines) - 1} rows", file=sys.stderr)
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rebuild data/misc/useragent.tsv from microlinkhq/top-user-agents (MIT).
|
||||||
|
|
||||||
|
data-import/useragent.py [--source URL] [--cache DIR] [--out FILE]
|
||||||
|
|
||||||
|
The desktop and mobile lists partition the whole, so `device` is read rather than
|
||||||
|
parsed. A row ships when the string names a browser and an operating system both.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import source
|
||||||
|
import tsv
|
||||||
|
|
||||||
|
SOURCE = "https://raw.githubusercontent.com/microlinkhq/top-user-agents/master/src"
|
||||||
|
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "useragent.tsv"
|
||||||
|
CACHE = Path(__file__).resolve().parent / "cache"
|
||||||
|
COLUMNS = ["browser", "device", "os", "ua"]
|
||||||
|
# First match wins: a Chromium fork this list does not name ships as Chrome.
|
||||||
|
BROWSERS = [("Edge", r"Edg(A|iOS)?/"), ("Opera", r"OPR/"), ("Samsung Internet", r"SamsungBrowser/"),
|
||||||
|
("Chrome", r"(Chrome|CriOS)/"), ("Firefox", r"(Firefox|FxiOS)/"), ("Safari", r"Version/[\d.]+ .*Safari")]
|
||||||
|
SYSTEMS = [("iOS", r"iPhone|iPad|CPU OS "), ("Android", r"Android"), ("ChromeOS", r"CrOS"),
|
||||||
|
("Windows", r"Windows NT"), ("macOS", r"Macintosh|Mac OS X"), ("Linux", r"X11.*Linux|Ubuntu")]
|
||||||
|
|
||||||
|
|
||||||
|
def named(table, ua):
|
||||||
|
for name, pattern in table:
|
||||||
|
if re.search(pattern, ua):
|
||||||
|
return name
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def rows(desktop, mobile):
|
||||||
|
seen, kept = set(), []
|
||||||
|
for device, uas in (("desktop", desktop), ("mobile", mobile)):
|
||||||
|
for ua in uas:
|
||||||
|
browser, os = named(BROWSERS, ua), named(SYSTEMS, ua)
|
||||||
|
if not browser or not os:
|
||||||
|
print(f"dropped, naming no {'browser' if not browser else 'operating system'}: {ua}", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
if ua in seen:
|
||||||
|
sys.exit(f"{ua}: listed twice, which would draw it twice as often")
|
||||||
|
seen.add(ua)
|
||||||
|
kept.append({"browser": browser, "device": device, "os": os, "ua": ua})
|
||||||
|
if len(kept) < 0.8 * (len(desktop) + len(mobile)):
|
||||||
|
sys.exit(f"only {len(kept)} of {len(desktop) + len(mobile)} strings named both; the tokens have moved")
|
||||||
|
return kept
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
|
p.add_argument("--cache", default=str(CACHE))
|
||||||
|
p.add_argument("--source", default=SOURCE)
|
||||||
|
p.add_argument("--out", default=str(OUT))
|
||||||
|
a = p.parse_args()
|
||||||
|
lists = [json.loads(source.fetch(f"{a.source}/{n}.json", a.cache, f"top-user-agents-{n}.json")) for n in ("desktop", "mobile")]
|
||||||
|
tsv.write(a.out, COLUMNS, sorted(rows(*lists), key=lambda r: (r["browser"], r["os"], r["ua"])))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,194 +1,175 @@
|
|||||||
package fejkdata
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/larvit/fejkdata/internal/datafiles"
|
||||||
)
|
)
|
||||||
|
|
||||||
// dataSource is one tree to load: an fs.FS and the directory in it to start from.
|
// loadSources merges sources in order, the last winning a clash. A source whose manifest
|
||||||
// label prefixes file names in errors; onDisk marks path as a directory that must
|
// carries an index leaves its categories unloaded; every other source loads here, with
|
||||||
// exist.
|
// the unloaded categories it needs.
|
||||||
type dataSource struct {
|
// docs/decisions.md#a-source-whose-manifest-carries-an-index-loads-each-category-on-first-reach-any-other-loads-in-new-with-what-it-reads
|
||||||
fsys fs.FS
|
func loadSources(sources []datafiles.Source) (folder, error) {
|
||||||
label string
|
root := folder{children: map[string]node{}}
|
||||||
onDisk bool
|
|
||||||
path string
|
|
||||||
root string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s dataSource) name(p string) string {
|
|
||||||
if s.label == "" {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
return path.Join(s.label, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadData loads every source into one namespace tree and returns its root
|
|
||||||
// children. A directory becomes a folder; each *.json file in it compiles to a node
|
|
||||||
// keyed by its base name (address.json -> "address"); each subdirectory becomes a
|
|
||||||
// nested folder, so folders turn into dot-path segments. Sources merge left to right:
|
|
||||||
// matching folders merge by their children, and any other clash is won by the last
|
|
||||||
// source loaded. Once merged, linkRefs binds every reference against the
|
|
||||||
// final tree.
|
|
||||||
func loadData(sources []dataSource) (map[string]node, error) {
|
|
||||||
root := map[string]node{}
|
|
||||||
for _, src := range sources {
|
for _, src := range sources {
|
||||||
if src.onDisk {
|
m, err := src.Manifest()
|
||||||
if src.path == "" {
|
|
||||||
return nil, fmt.Errorf("a data path is empty")
|
|
||||||
}
|
|
||||||
info, err := os.Stat(src.path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !info.IsDir() {
|
|
||||||
return nil, fmt.Errorf("%s is not a directory", src.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dir := src.root
|
|
||||||
if dir == "" {
|
|
||||||
dir = "."
|
|
||||||
}
|
|
||||||
g, err := loadDir(src, dir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return folder{}, err
|
||||||
|
}
|
||||||
|
if err := addSource(&root, src, m); err != nil {
|
||||||
|
return folder{}, err
|
||||||
}
|
}
|
||||||
mergeChildren(root, g.children)
|
|
||||||
}
|
}
|
||||||
if len(root) == 0 {
|
return root, loadReached(&root, categorySites(&root), nil)
|
||||||
return nil, fmt.Errorf("no .json data found")
|
|
||||||
}
|
|
||||||
if err := linkTables(root); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := linkRefs(root); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := checkNoCycles(root); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := checkScope(treeScope(root)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return root, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadDir compiles one directory into a folder. fs.ReadDir yields entries sorted
|
func addSource(root *folder, src datafiles.Source, m datafiles.Manifest) error {
|
||||||
// by name, so the tree is built deterministically. Empty subdirectories (no JSON
|
src = src.Mounted(m.Namespace)
|
||||||
// anywhere under them) are skipped rather than added as empty namespaces.
|
if m.Index != nil {
|
||||||
func loadDir(src dataSource, dir string) (*folder, error) {
|
placeIndex(root, src, m.Index)
|
||||||
entries, err := fs.ReadDir(src.fsys, dir)
|
return nil
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: %w", src.name(dir), err)
|
|
||||||
}
|
}
|
||||||
g := &folder{children: map[string]node{}}
|
g := &folder{children: map[string]node{}}
|
||||||
files := &categoryFiles{src: src, dir: dir, tsv: map[string]bool{}}
|
if err := src.Walk(compileInto(func(dir []string) *folder { return madeFolder(g, dir) })); err != nil {
|
||||||
for _, e := range entries {
|
|
||||||
if strings.HasSuffix(e.Name(), ".tsv") && !e.IsDir() {
|
|
||||||
files.tsv[e.Name()] = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, e := range entries {
|
|
||||||
if strings.HasPrefix(e.Name(), ".") { // hidden: a checkout or an editor's file, never data
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
full := path.Join(dir, e.Name())
|
|
||||||
if e.IsDir() {
|
|
||||||
if err := loadFolder(src, g, full, e.Name()); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := loadFile(src, g, full, e.Name(), files); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for name, named := range files.tsv {
|
|
||||||
if !named && !strings.HasPrefix(name, ".") {
|
|
||||||
return nil, fmt.Errorf("%s: no category names it in its rows; a table's rows file sits beside a category file naming it", src.name(path.Join(dir, name)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return g, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// categoryFiles is what a category may name beside itself: the rows files of its
|
|
||||||
// directory, each marked once a category names it.
|
|
||||||
type categoryFiles struct {
|
|
||||||
src dataSource
|
|
||||||
dir string
|
|
||||||
tsv map[string]bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// rows reads the rows file a category names beside it.
|
|
||||||
func (c *categoryFiles) rows(name string) (string, error) {
|
|
||||||
if _, present := c.tsv[name]; !present {
|
|
||||||
return "", fmt.Errorf("rows names %s, which is not beside it in %s", name, c.src.name(c.dir))
|
|
||||||
}
|
|
||||||
c.tsv[name] = true
|
|
||||||
b, err := fs.ReadFile(c.src.fsys, path.Join(c.dir, name))
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("%s: %w", c.src.name(path.Join(c.dir, name)), err)
|
|
||||||
}
|
|
||||||
return string(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadFolder adds a subdirectory as a nested folder, unless nothing under it is data.
|
|
||||||
func loadFolder(src dataSource, g *folder, full, name string) error {
|
|
||||||
child, err := loadDir(src, full)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(child.children) == 0 {
|
mergeFolder(root, g)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := checkName(name); err != nil {
|
|
||||||
return fmt.Errorf("%s: folder %w", src.name(full), err)
|
|
||||||
}
|
|
||||||
g.children[name] = child
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadFile compiles a *.json file into a category named after it; any other file
|
func compileInto(place func(dir []string) *folder) func(datafiles.Category) error {
|
||||||
// is skipped.
|
return func(c datafiles.Category) error {
|
||||||
func loadFile(src dataSource, g *folder, full, file string, files *categoryFiles) error {
|
n, err := compileCategory(c)
|
||||||
if !strings.HasSuffix(file, ".json") {
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
place(c.Folders).put(c.Name, n)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
name := strings.TrimSuffix(file, ".json")
|
|
||||||
if err := checkName(name); err != nil {
|
|
||||||
return fmt.Errorf("%s: category %w", src.name(full), err)
|
|
||||||
}
|
|
||||||
b, err := fs.ReadFile(src.fsys, full)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", src.name(full), err)
|
|
||||||
}
|
|
||||||
var raw any
|
|
||||||
if err := json.Unmarshal(b, &raw); err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", src.name(full), err)
|
|
||||||
}
|
|
||||||
n, err := compileCategory(raw, name, files)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", src.name(full), err)
|
|
||||||
}
|
|
||||||
g.children[name] = n
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// mergeChildren overlays src onto dst. Two folders under the same key merge
|
// madeFolder is the folder that dir names under root, made where it is missing or a category.
|
||||||
// recursively (so locales/categories from several paths combine); every other
|
func madeFolder(root *folder, dir []string) *folder {
|
||||||
// key is replaced, making the last-loaded directory win on a conflict.
|
g := root
|
||||||
func mergeChildren(dst, src map[string]node) {
|
for _, seg := range dir {
|
||||||
for k, v := range src {
|
sub, isFolder := g.children[seg].(*folder)
|
||||||
if dg, ok := dst[k].(*folder); ok {
|
if !isFolder {
|
||||||
|
sub = &folder{children: map[string]node{}}
|
||||||
|
g.put(seg, sub)
|
||||||
|
}
|
||||||
|
g = sub
|
||||||
|
}
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeFolder(dst, src *folder) {
|
||||||
|
for k, v := range src.children {
|
||||||
|
if dg, ok := dst.children[k].(*folder); ok {
|
||||||
if sg, ok := v.(*folder); ok {
|
if sg, ok := v.(*folder); ok {
|
||||||
mergeChildren(dg.children, sg.children)
|
mergeFolder(dg, sg)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dst[k] = v
|
dst.put(k, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nodeSet is the set of nodes one validation pass covers: one load's categories, or a
|
||||||
|
// single inline node.
|
||||||
|
type nodeSet func(fn func(label string, n node) error) error
|
||||||
|
|
||||||
|
// categorySite is a loaded category, where it sits and its own dot path.
|
||||||
|
type categorySite struct {
|
||||||
|
categoryAt
|
||||||
|
n node
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// categorySites lists every category under root, folders and names in sorted order.
|
||||||
|
func categorySites(root *folder) []categorySite {
|
||||||
|
var out []categorySite
|
||||||
|
var walk func(dir []string, g *folder)
|
||||||
|
walk = func(dir []string, g *folder) {
|
||||||
|
for _, name := range sortedNames(g.children) {
|
||||||
|
if sub, isFolder := g.children[name].(*folder); isFolder {
|
||||||
|
walk(append(dir[:len(dir):len(dir)], name), sub)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, siteIn(dir, g, name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(nil, root)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func siteIn(dir []string, in *folder, name string) categorySite {
|
||||||
|
return categorySite{categoryAt: categoryAt{dir: dir, in: in, name: name}, n: in.children[name], path: categoryPath(dir, name)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func categoryPath(dir []string, name string) string { return join(strings.Join(dir, "."), name) }
|
||||||
|
|
||||||
|
func siteNodes(sites []categorySite) nodeSet {
|
||||||
|
return func(fn func(label string, n node) error) error {
|
||||||
|
for _, s := range sites {
|
||||||
|
if err := eachNode(s.n, s.path, fn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func inlineNodes(n node, label string) nodeSet {
|
||||||
|
return func(fn func(label string, m node) error) error { return eachNode(n, label, fn) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// pipeline is the one load pipeline, run over one nodeSet; its fields are where
|
||||||
|
// entry points differ.
|
||||||
|
type pipeline struct {
|
||||||
|
nodes nodeSet
|
||||||
|
resolve func() error
|
||||||
|
// Set by a caller that proves the columns against their Go types itself.
|
||||||
|
typedByGo bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// categoryPipeline is the pipeline over the categories of one load, their references
|
||||||
|
// resolving against root.
|
||||||
|
func categoryPipeline(sites []categorySite, root map[string]node) pipeline {
|
||||||
|
return pipeline{
|
||||||
|
nodes: siteNodes(sites),
|
||||||
|
resolve: func() error {
|
||||||
|
if err := linkTables(sites); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return resolveCategoryTemplates(sites, root)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p pipeline) run() error {
|
||||||
|
if err := p.resolve(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := checkNoCycles(p.nodes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
settleRecords(p.nodes)
|
||||||
|
if !p.typedByGo {
|
||||||
|
if err := checkColumns(p.nodes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return checkNodeFences(p.nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkNodeFences runs the per-node fences over s, each over every node before the next,
|
||||||
|
// so which of several broken nodes is reported does not depend on the walk. Its walks
|
||||||
|
// recurse unguarded, so pipeline.run refuses cycles first.
|
||||||
|
func checkNodeFences(s nodeSet) error {
|
||||||
|
mem := renderCounts{}
|
||||||
|
if err := s(func(label string, n node) error { return repeatCheck(label, n, mem) }); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s((&valueProof{}).checkDatatype)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"index": {
|
||||||
|
"en_US.address": {"paths":["","locality","postal-code","region","street","street-number"]},
|
||||||
|
"en_US.color": {"paths":[""]},
|
||||||
|
"en_US.company": {"paths":["","base","suffix"]},
|
||||||
|
"en_US.date": {"paths":[""]},
|
||||||
|
"en_US.email": {"paths":["","domain","local","local.n"]},
|
||||||
|
"en_US.first-name": {"parent":"sex","paths":["","count","name","sex"]},
|
||||||
|
"en_US.ip": {"paths":[""]},
|
||||||
|
"en_US.itin": {"paths":["","area","group","serial"]},
|
||||||
|
"en_US.last-name": {"paths":["","count","name"]},
|
||||||
|
"en_US.person": {"paths":["","first","last","prefix","sex"]},
|
||||||
|
"en_US.phone": {"paths":["","area","exchange","line"]},
|
||||||
|
"en_US.phone-exchange": {"paths":[""]},
|
||||||
|
"en_US.price": {"paths":["","amt","cents"]},
|
||||||
|
"en_US.sentence": {"paths":["","adj","noun","prep","verb"]},
|
||||||
|
"en_US.sex": {"paths":["","code","first-name","first-name.count","first-name.name","first-name.sex","name","title","title.name","title.sex","title.share"]},
|
||||||
|
"en_US.ssn": {"paths":["","area","group","serial"]},
|
||||||
|
"en_US.time": {"paths":[""]},
|
||||||
|
"en_US.title": {"parent":"sex","paths":["","name","sex","share"]},
|
||||||
|
"en_US.url": {"paths":["","host","path"]},
|
||||||
|
"en_US.username": {"paths":["","n"]},
|
||||||
|
"en_US.version": {"paths":["","n","pre","suffix"]},
|
||||||
|
"en_US.word": {"paths":[""]}
|
||||||
|
},
|
||||||
|
"namespace": "en_US"
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"format": "{street-number} {street}\n{locality}, {region} {postal-code}",
|
"format": "{/geo.US.address as a}{a}",
|
||||||
"street": "{/geo.US.address.street}",
|
"street": "{a.street}",
|
||||||
"street-number": "{/geo.US.address.street-number}",
|
"street-number": "{a.street-number}",
|
||||||
"locality": "{/geo.US.address.locality}",
|
"locality": "{a.locality}",
|
||||||
"region": "{/geo.US.address.region}",
|
"region": "{a.region}",
|
||||||
"postal-code": "{/geo.US.address.postal-code}"
|
"postal-code": "{a.postal-code}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Package enus is fejkdata's en_US locale module, whose categories are en_US.*; its address reads
|
||||||
|
// geo.US, which package geous holds.
|
||||||
|
package enus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
|
|
||||||
|
geous "github.com/larvit/fejkdata/data/geo/US"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FS holds the locale alone, its categories and its manifest; pass Modules unless your own
|
||||||
|
// data stands in for geo.US.
|
||||||
|
//
|
||||||
|
//go:embed *.json *.tsv .fejkdata.json
|
||||||
|
var FS embed.FS
|
||||||
|
|
||||||
|
// Modules returns FS and every module it reads, to pass to fejkdata.WithDataFS.
|
||||||
|
func Modules() []fs.FS { return append([]fs.FS{FS}, geous.Modules()...) }
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package enus_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/larvit/fejkdata"
|
||||||
|
enus "github.com/larvit/fejkdata/data/en_US"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFSLoadsByItsManifest(t *testing.T) {
|
||||||
|
if _, err := fs.Stat(enus.FS, ".fejkdata.json"); err != nil {
|
||||||
|
t.Fatalf("FS holds no manifest: %v", err)
|
||||||
|
}
|
||||||
|
f, err := fejkdata.New(fejkdata.WithDataFS(enus.Modules()...), fejkdata.WithSeed(1))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New(WithDataFS(Modules()...)) = %v", err)
|
||||||
|
}
|
||||||
|
if len(f.List()) == 0 {
|
||||||
|
t.Error("List() is empty, want the module's categories")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"format": "9{digits(2)}-{group}-{serial}",
|
"format": "{area}-{group}-{serial}",
|
||||||
|
"area": "9{digits(2)}",
|
||||||
"group": [
|
"group": [
|
||||||
{ "format": "{int(50,65)}", "weight": 16 },
|
{ "format": "{int(50,65)}", "weight": 16 },
|
||||||
{ "format": "{int(70,88)}", "weight": 19 },
|
{ "format": "{int(70,88)}", "weight": 19 },
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"format": "{prefix}{first} {last}",
|
"format": "{prefix}{first} {last}",
|
||||||
"first": "{.first-name.name}",
|
"first": "{.first-name as n}{n.name}",
|
||||||
"last": "{.last-name.name}",
|
"last": "{.last-name.name}",
|
||||||
"prefix": ["", { "format": "{title} ", "title": ["Dr", "Mx", "Prof"], "weight": 0.1 }],
|
"prefix": [null, { "format": "{n..sex.title.name} ", "weight": 0.1 }],
|
||||||
"sex": "{.sex.name}"
|
"sex": "{n..sex.name}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[
|
||||||
|
{ "format": "{int(2,9)}0{digits(1)}", "weight": 80 },
|
||||||
|
{ "format": "{int(2,9)}1{int(2,9)}", "weight": 64 },
|
||||||
|
{ "format": "{int(2,9)}10", "weight": 8 },
|
||||||
|
{ "format": "{int(2,4)}{int(2,9)}{digits(1)}", "weight": 240 },
|
||||||
|
{ "format": "{int(6,8)}{int(2,9)}{digits(1)}", "weight": 240 },
|
||||||
|
{ "format": "5{int(2,4)}{digits(1)}", "weight": 30 },
|
||||||
|
{ "format": "5{int(6,9)}{digits(1)}", "weight": 40 },
|
||||||
|
{ "format": "55{int(0,4)}", "weight": 5 },
|
||||||
|
{ "format": "55{int(6,9)}", "weight": 4 },
|
||||||
|
{ "format": "9{int(2,4)}{digits(1)}", "weight": 30 },
|
||||||
|
{ "format": "9{int(8,9)}{digits(1)}", "weight": 20 },
|
||||||
|
{ "format": "95{int(1,7)}", "weight": 7 },
|
||||||
|
{ "format": "96{digits(1)}", "weight": 10 },
|
||||||
|
{ "format": "97{int(0,5)}", "weight": 6 },
|
||||||
|
{ "format": "97{int(7,9)}", "weight": 3 }
|
||||||
|
]
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"format": "({area}) {exch}-{line}",
|
"format": "({area}) {exchange}-{line}",
|
||||||
"weight": 2,
|
"weight": 2,
|
||||||
"area": ["202", "212", "213", "305", "312", "404", "415", "469", "503", "512", "602", "617", "646", "702", "718", "720", "737", "773", "786", "808", "917"],
|
"area": ["202", "212", "213", "305", "312", "404", "415", "469", "503", "512", "602", "617", "646", "702", "718", "720", "737", "773", "786", "808", "917"],
|
||||||
"exch": "{int(100,999)}",
|
"exchange": "{.phone-exchange}",
|
||||||
"line": "{digits(4)}"
|
"line": "{digits(4)}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"format": "{area}-{exch}-{line}",
|
"format": "{area}-{exchange}-{line}",
|
||||||
"area": ["202", "212", "213", "305", "312", "404", "415", "469", "503", "512", "602", "617", "646", "702", "718", "720", "737", "773", "786", "808", "917"],
|
"area": ["202", "212", "213", "305", "312", "404", "415", "469", "503", "512", "602", "617", "646", "702", "718", "720", "737", "773", "786", "808", "917"],
|
||||||
"exch": "{int(100,999)}",
|
"exchange": "{.phone-exchange}",
|
||||||
"line": "{digits(4)}"
|
"line": "{digits(4)}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{ "format": "{name}", "rows": "title.tsv", "name": "name", "parent": "sex", "weight": "share" }
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
name sex share
|
||||||
|
Dr f 6
|
||||||
|
Dr m 6
|
||||||
|
Miss f 4
|
||||||
|
Mr m 60
|
||||||
|
Mrs f 22
|
||||||
|
Ms f 30
|
||||||
|
Mx f 1
|
||||||
|
Mx m 1
|
||||||
|
Prof f 3
|
||||||
|
Prof m 3
|
||||||
|
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"index": {
|
||||||
|
"geo.SE.address": {"paths":["","locality","postal-code","street","street-number"]},
|
||||||
|
"geo.SE.locality": {"parent":"municipality","paths":["","lat","lon","municipality","name","population","postal-code","postal-code.code","postal-code.locality","street","street.locality","street.name","street.segments"]},
|
||||||
|
"geo.SE.municipality": {"parent":"region","paths":["","code","locality","locality.lat","locality.lon","locality.municipality","locality.name","locality.population","locality.postal-code","locality.postal-code.code","locality.postal-code.locality","locality.street","locality.street.locality","locality.street.name","locality.street.segments","name","population","region"]},
|
||||||
|
"geo.SE.postal-code": {"parent":"locality","paths":["","code","locality"]},
|
||||||
|
"geo.SE.region": {"paths":["","code","municipality","municipality.code","municipality.locality","municipality.locality.lat","municipality.locality.lon","municipality.locality.municipality","municipality.locality.name","municipality.locality.population","municipality.locality.postal-code","municipality.locality.postal-code.code","municipality.locality.postal-code.locality","municipality.locality.street","municipality.locality.street.locality","municipality.locality.street.name","municipality.locality.street.segments","municipality.name","municipality.population","municipality.region","name","population","timezone"]},
|
||||||
|
"geo.SE.street": {"parent":"locality","paths":["","locality","name","segments"]}
|
||||||
|
},
|
||||||
|
"namespace": "geo.SE"
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"format": "{street} {street-number}\n{postal-code} {locality}",
|
"format": "{street} {street-number}\n{postal-code} {locality}",
|
||||||
"street": "{.street.name}",
|
"street": "{.locality as l}{l.street.name}",
|
||||||
"street-number": [
|
"street-number": [
|
||||||
"{int(1,9)}",
|
"{int(1,9)}",
|
||||||
"{int(10,99)}",
|
"{int(10,99)}",
|
||||||
@@ -9,6 +9,6 @@
|
|||||||
{ "format": "{int(10,99)}{upper(1)}", "weight": 0.1 },
|
{ "format": "{int(10,99)}{upper(1)}", "weight": 0.1 },
|
||||||
{ "format": "{int(100,999)}{upper(1)}", "weight": 0.05 }
|
{ "format": "{int(100,999)}{upper(1)}", "weight": 0.05 }
|
||||||
],
|
],
|
||||||
"postal-code": "{.postal-code.code}",
|
"postal-code": "{l.postal-code.code}",
|
||||||
"locality": "{.locality.name}"
|
"locality": "{l.name}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// Package geose is fejkdata's Swedish places module, whose categories are geo.SE.*.
|
||||||
|
package geose
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FS holds the module's categories and its manifest.
|
||||||
|
//
|
||||||
|
//go:embed *.json *.tsv .fejkdata.json
|
||||||
|
var FS embed.FS
|
||||||
|
|
||||||
|
// Modules returns FS, which reads no other module, to pass to fejkdata.WithDataFS.
|
||||||
|
func Modules() []fs.FS { return []fs.FS{FS} }
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package geose_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/larvit/fejkdata"
|
||||||
|
geose "github.com/larvit/fejkdata/data/geo/SE"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFSLoadsByItsManifest(t *testing.T) {
|
||||||
|
if _, err := fs.Stat(geose.FS, ".fejkdata.json"); err != nil {
|
||||||
|
t.Fatalf("FS holds no manifest: %v", err)
|
||||||
|
}
|
||||||
|
f, err := fejkdata.New(fejkdata.WithDataFS(geose.Modules()...), fejkdata.WithSeed(1))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New(WithDataFS(Modules()...)) = %v", err)
|
||||||
|
}
|
||||||
|
if len(f.List()) == 0 {
|
||||||
|
t.Error("List() is empty, want the module's categories")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -866,7 +866,6 @@ Nynäshamn 0192 16447 58.9034 17.9479
|
|||||||
Nyvång 1277 200 56.1333 12.9000
|
Nyvång 1277 200 56.1333 12.9000
|
||||||
Nyåker 2403 200 63.7500 19.3167
|
Nyåker 2403 200 63.7500 19.3167
|
||||||
Nälden 2309 986 63.3500 14.2500
|
Nälden 2309 986 63.3500 14.2500
|
||||||
Nämdö 0120 200 59.1833 18.6833
|
|
||||||
Nässjö 0682 18639 57.6531 14.6968
|
Nässjö 0682 18639 57.6531 14.6968
|
||||||
Näsum 1272 1274 56.1667 14.4833
|
Näsum 1272 1274 56.1667 14.4833
|
||||||
Näsviken 2184 994 61.7667 16.8667
|
Näsviken 2184 994 61.7667 16.8667
|
||||||
|
|||||||
|
@@ -949,7 +949,6 @@ code locality
|
|||||||
130 25 Ingmarsö
|
130 25 Ingmarsö
|
||||||
130 33 Gällnöby
|
130 33 Gällnöby
|
||||||
130 34 Skälvik
|
130 34 Skälvik
|
||||||
130 36 Nämdö
|
|
||||||
130 37 Stavsnäs
|
130 37 Stavsnäs
|
||||||
130 38 Runmarö
|
130 38 Runmarö
|
||||||
130 39 Sandhamn
|
130 39 Sandhamn
|
||||||
|
|||||||
|
+159557
-5
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"index": {
|
||||||
|
"geo.US.address": {"paths":["","locality","postal-code","region","street","street-number"]},
|
||||||
|
"geo.US.locality": {"parent":"municipality","paths":["","code","lat","lon","municipality","name","population","postal-code","postal-code.addresses","postal-code.code","postal-code.locality","street","street.addresses","street.locality","street.name"]},
|
||||||
|
"geo.US.municipality": {"parent":"region","paths":["","code","locality","locality.code","locality.lat","locality.lon","locality.municipality","locality.name","locality.population","locality.postal-code","locality.postal-code.addresses","locality.postal-code.code","locality.postal-code.locality","locality.street","locality.street.addresses","locality.street.locality","locality.street.name","name","population","region"]},
|
||||||
|
"geo.US.postal-code": {"parent":"locality","paths":["","addresses","code","locality"]},
|
||||||
|
"geo.US.region": {"paths":["","abbr","code","municipality","municipality.code","municipality.locality","municipality.locality.code","municipality.locality.lat","municipality.locality.lon","municipality.locality.municipality","municipality.locality.name","municipality.locality.population","municipality.locality.postal-code","municipality.locality.postal-code.addresses","municipality.locality.postal-code.code","municipality.locality.postal-code.locality","municipality.locality.street","municipality.locality.street.addresses","municipality.locality.street.locality","municipality.locality.street.name","municipality.name","municipality.population","municipality.region","name","population","timezone"]},
|
||||||
|
"geo.US.street": {"parent":"locality","paths":["","addresses","locality","name"]}
|
||||||
|
},
|
||||||
|
"namespace": "geo.US"
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"format": "{street-number} {street}\n{locality}, {region} {postal-code}",
|
"format": "{street-number} {street}\n{locality}, {region} {postal-code}",
|
||||||
"street": "{.street.name}",
|
"street": "{.locality as l}{l.street.name}",
|
||||||
"street-number": [
|
"street-number": [
|
||||||
"{int(10,99)}",
|
"{int(10,99)}",
|
||||||
{ "format": "{int(100,999)}", "weight": 2 },
|
{ "format": "{int(100,999)}", "weight": 2 },
|
||||||
{ "format": "{int(1000,9999)}", "weight": 0.5 },
|
{ "format": "{int(1000,9999)}", "weight": 0.5 },
|
||||||
{ "format": "{int(10,99)}{int(100,999)}", "weight": 0.3 }
|
{ "format": "{int(10,99)}{int(100,999)}", "weight": 0.3 }
|
||||||
],
|
],
|
||||||
"locality": "{.locality.name}",
|
"locality": "{l.name}",
|
||||||
"region": "{.region.abbr}",
|
"region": "{l..municipality..region.abbr}",
|
||||||
"postal-code": "{.postal-code.code}"
|
"postal-code": "{l.postal-code.code}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// Package geous is fejkdata's US places module, whose categories are geo.US.*.
|
||||||
|
package geous
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FS holds the module's categories and its manifest.
|
||||||
|
//
|
||||||
|
//go:embed *.json *.tsv .fejkdata.json
|
||||||
|
var FS embed.FS
|
||||||
|
|
||||||
|
// Modules returns FS, which reads no other module, to pass to fejkdata.WithDataFS.
|
||||||
|
func Modules() []fs.FS { return []fs.FS{FS} }
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package geous_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/larvit/fejkdata"
|
||||||
|
geous "github.com/larvit/fejkdata/data/geo/US"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFSLoadsByItsManifest(t *testing.T) {
|
||||||
|
if _, err := fs.Stat(geous.FS, ".fejkdata.json"); err != nil {
|
||||||
|
t.Fatalf("FS holds no manifest: %v", err)
|
||||||
|
}
|
||||||
|
f, err := fejkdata.New(fejkdata.WithDataFS(geous.Modules()...), fejkdata.WithSeed(1))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New(WithDataFS(Modules()...)) = %v", err)
|
||||||
|
}
|
||||||
|
if len(f.List()) == 0 {
|
||||||
|
t.Error("List() is empty, want the module's categories")
|
||||||
|
}
|
||||||
|
}
|
||||||
+717
-585
File diff suppressed because it is too large
Load Diff
+353
-132
File diff suppressed because it is too large
Load Diff
+1113
-1389
File diff suppressed because it is too large
Load Diff
+465333
-5902
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"index": {
|
||||||
|
"misc.car": {"paths":["","make","model"]},
|
||||||
|
"misc.coordinate": {"paths":["","lat","lon"]},
|
||||||
|
"misc.creditcard": {"paths":["","d"]},
|
||||||
|
"misc.currency": {"paths":["","code","decimals","name","numeric","symbol"]},
|
||||||
|
"misc.datetime": {"paths":[""]},
|
||||||
|
"misc.emoji": {"paths":[""]},
|
||||||
|
"misc.httpmethod": {"paths":["","idempotent","method","safe"]},
|
||||||
|
"misc.httpstatus": {"paths":["","code","reason"]},
|
||||||
|
"misc.language": {"paths":["","code","code3","name"]},
|
||||||
|
"misc.loglevel": {"paths":["","code","keyword","severity"]},
|
||||||
|
"misc.mac": {"paths":[""]},
|
||||||
|
"misc.mimetype": {"paths":["","ext","type"]},
|
||||||
|
"misc.objectid": {"paths":[""]},
|
||||||
|
"misc.port": {"paths":["","number","service"]},
|
||||||
|
"misc.protocol": {"paths":["","keyword","name","number"]},
|
||||||
|
"misc.territory": {"paths":["","alpha2","alpha3","calling-code","capital","country","currency","flag","languages","name","numeric","timezone","timezone.offset","timezone.territory","timezone.weight","timezone.zone","tld"]},
|
||||||
|
"misc.timezone": {"parent":"territory","paths":["","offset","territory","weight","zone"]},
|
||||||
|
"misc.tld": {"paths":["","tld","type","unicode"]},
|
||||||
|
"misc.useragent": {"paths":["","browser","device","os","ua"]},
|
||||||
|
"misc.uuid": {"paths":["","variant"]}
|
||||||
|
},
|
||||||
|
"namespace": "misc"
|
||||||
|
}
|
||||||
+1
-10
@@ -1,10 +1 @@
|
|||||||
[
|
{ "format": "{make} {model}", "rows": "car.tsv" }
|
||||||
{ "format": "{maker} {model}", "maker": "BMW", "model": ["3 Series", "5 Series", "X3", "X5"] },
|
|
||||||
{ "format": "{maker} {model}", "maker": "Ford", "model": ["Fiesta", "Focus", "Mustang", "Explorer"] },
|
|
||||||
{ "format": "{maker} {model}", "maker": "Honda", "model": ["Civic", "Accord", "CR-V", "Jazz"] },
|
|
||||||
{ "format": "{maker} {model}", "maker": "Mercedes-Benz", "model": ["A-Class", "C-Class", "E-Class", "GLC"] },
|
|
||||||
{ "format": "{maker} {model}", "maker": "Tesla", "model": ["Model 3", "Model S", "Model X", "Model Y"] },
|
|
||||||
{ "format": "{maker} {model}", "maker": "Toyota", "model": ["Corolla", "Camry", "RAV4", "Yaris"] },
|
|
||||||
{ "format": "{maker} {model}", "maker": "Volkswagen", "model": ["Golf", "Passat", "Polo", "Tiguan"] },
|
|
||||||
{ "format": "{maker} {model}", "maker": "Volvo", "model": ["XC40", "XC60", "XC90", "V60"] }
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
make model
|
||||||
|
BMW 3 Series
|
||||||
|
BMW 5 Series
|
||||||
|
BMW X3
|
||||||
|
BMW X5
|
||||||
|
Ford Explorer
|
||||||
|
Ford Fiesta
|
||||||
|
Ford Focus
|
||||||
|
Ford Mustang
|
||||||
|
Honda Accord
|
||||||
|
Honda CR-V
|
||||||
|
Honda Civic
|
||||||
|
Honda Jazz
|
||||||
|
Mercedes-Benz A-Class
|
||||||
|
Mercedes-Benz C-Class
|
||||||
|
Mercedes-Benz E-Class
|
||||||
|
Mercedes-Benz GLC
|
||||||
|
Tesla Model 3
|
||||||
|
Tesla Model S
|
||||||
|
Tesla Model X
|
||||||
|
Tesla Model Y
|
||||||
|
Toyota Camry
|
||||||
|
Toyota Corolla
|
||||||
|
Toyota RAV4
|
||||||
|
Toyota Yaris
|
||||||
|
Volkswagen Golf
|
||||||
|
Volkswagen Passat
|
||||||
|
Volkswagen Polo
|
||||||
|
Volkswagen Tiguan
|
||||||
|
Volvo V60
|
||||||
|
Volvo XC40
|
||||||
|
Volvo XC60
|
||||||
|
Volvo XC90
|
||||||
|
@@ -1 +0,0 @@
|
|||||||
{ "format": "{name}", "rows": "country.tsv", "key": "alpha2", "name": "name" }
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
alpha2 alpha3 calling-code capital currency flag languages name numeric tld
|
|
||||||
AD AND 376 Andorra la Vella EUR 🇦🇩 ca Andorra 020 .ad
|
|
||||||
AE ARE 971 Abu Dhabi AED 🇦🇪 ar-AE,fa,en,hi,ur United Arab Emirates 784 .ae
|
|
||||||
AF AFG 93 Kabul AFN 🇦🇫 fa-AF,ps,uz-AF,tk Afghanistan 004 .af
|
|
||||||
AG ATG 1-268 St. John's XCD 🇦🇬 en-AG Antigua & Barbuda 028 .ag
|
|
||||||
AI AIA 1-264 The Valley XCD 🇦🇮 en-AI Anguilla 660 .ai
|
|
||||||
AL ALB 355 Tirana ALL 🇦🇱 sq,el Albania 008 .al
|
|
||||||
AM ARM 374 Yerevan AMD 🇦🇲 hy Armenia 051 .am
|
|
||||||
AO AGO 244 Luanda AOA 🇦🇴 pt-AO Angola 024 .ao
|
|
||||||
AR ARG 54 Buenos Aires ARS 🇦🇷 es-AR,en,it,de,fr,gn Argentina 032 .ar
|
|
||||||
AS ASM 1-684 Pago Pago USD 🇦🇸 en-AS,sm,to American Samoa 016 .as
|
|
||||||
AT AUT 43 Vienna EUR 🇦🇹 de-AT,hr,hu,sl Austria 040 .at
|
|
||||||
AU AUS 61 Canberra AUD 🇦🇺 en-AU Australia 036 .au
|
|
||||||
AW ABW 297 Oranjestad AWG 🇦🇼 nl-AW,pap,es,en Aruba 533 .aw
|
|
||||||
AX ALA 358 Mariehamn EUR 🇦🇽 sv-AX Åland Islands 248 .ax
|
|
||||||
AZ AZE 994 Baku AZN 🇦🇿 az,ru,hy Azerbaijan 031 .az
|
|
||||||
BA BIH 387 Sarajevo BAM 🇧🇦 bs,hr-BA,sr-BA Bosnia 070 .ba
|
|
||||||
BB BRB 1-246 Bridgetown BBD 🇧🇧 en-BB Barbados 052 .bb
|
|
||||||
BD BGD 880 Dhaka BDT 🇧🇩 bn-BD,en Bangladesh 050 .bd
|
|
||||||
BE BEL 32 Brussels EUR 🇧🇪 nl-BE,fr-BE,de-BE Belgium 056 .be
|
|
||||||
BF BFA 226 Ouagadougou XOF 🇧🇫 fr-BF,mos Burkina Faso 854 .bf
|
|
||||||
BG BGR 359 Sofia EUR 🇧🇬 bg,tr-BG,rom Bulgaria 100 .bg
|
|
||||||
BH BHR 973 Manama BHD 🇧🇭 ar-BH,en,fa,ur Bahrain 048 .bh
|
|
||||||
BI BDI 257 Gitega BIF 🇧🇮 fr-BI,rn Burundi 108 .bi
|
|
||||||
BJ BEN 229 Porto-Novo XOF 🇧🇯 fr-BJ Benin 204 .bj
|
|
||||||
BL BLM 590 Gustavia EUR 🇧🇱 fr St. Barthélemy 652 .gp
|
|
||||||
BM BMU 1-441 Hamilton BMD 🇧🇲 en-BM,pt Bermuda 060 .bm
|
|
||||||
BN BRN 673 Bandar Seri Begawan BND 🇧🇳 ms-BN,en-BN Brunei 096 .bn
|
|
||||||
BO BOL 591 Sucre BOB 🇧🇴 es-BO,qu,ay Bolivia 068 .bo
|
|
||||||
BR BRA 55 Brasilia BRL 🇧🇷 pt-BR,es,en,fr Brazil 076 .br
|
|
||||||
BS BHS 1-242 Nassau BSD 🇧🇸 en-BS Bahamas 044 .bs
|
|
||||||
BT BTN 975 Thimphu INR 🇧🇹 dz Bhutan 064 .bt
|
|
||||||
BW BWA 267 Gaborone BWP 🇧🇼 en-BW,tn-BW Botswana 072 .bw
|
|
||||||
BY BLR 375 Minsk BYN 🇧🇾 be,ru Belarus 112 .by
|
|
||||||
BZ BLZ 501 Belmopan BZD 🇧🇿 en-BZ,es Belize 084 .bz
|
|
||||||
CA CAN 1 Ottawa CAD 🇨🇦 en-CA,fr-CA,iu Canada 124 .ca
|
|
||||||
CC CCK 61 West Island AUD 🇨🇨 ms-CC,en Cocos Islands 166 .cc
|
|
||||||
CD COD 243 Kinshasa CDF 🇨🇩 fr-CD,ln,ktu,kg,sw,lua Congo - Kinshasa 180 .cd
|
|
||||||
CF CAF 236 Bangui XAF 🇨🇫 fr-CF,sg,ln,kg Central African Republic 140 .cf
|
|
||||||
CG COG 242 Brazzaville XAF 🇨🇬 fr-CG,kg,ln-CG Congo - Brazzaville 178 .cg
|
|
||||||
CH CHE 41 Bern CHF 🇨🇭 de-CH,fr-CH,it-CH,rm Switzerland 756 .ch
|
|
||||||
CI CIV 225 Yamoussoukro XOF 🇨🇮 fr-CI Côte d’Ivoire 384 .ci
|
|
||||||
CK COK 682 Avarua NZD 🇨🇰 en-CK,mi Cook Islands 184 .ck
|
|
||||||
CL CHL 56 Santiago CLP 🇨🇱 es-CL Chile 152 .cl
|
|
||||||
CM CMR 237 Yaounde XAF 🇨🇲 en-CM,fr-CM Cameroon 120 .cm
|
|
||||||
CN CHN 86 Beijing CNY 🇨🇳 zh-CN,yue,wuu,dta,ug,za China 156 .cn
|
|
||||||
CO COL 57 Bogota COP 🇨🇴 es-CO Colombia 170 .co
|
|
||||||
CR CRI 506 San Jose CRC 🇨🇷 es-CR,en Costa Rica 188 .cr
|
|
||||||
CU CUB 53 Havana CUP 🇨🇺 es-CU,pap Cuba 192 .cu
|
|
||||||
CV CPV 238 Praia CVE 🇨🇻 pt-CV Cape Verde 132 .cv
|
|
||||||
CW CUW 599 Willemstad XCG 🇨🇼 nl,pap Curaçao 531 .cw
|
|
||||||
CX CXR 61 Flying Fish Cove AUD 🇨🇽 en,zh,ms-CX Christmas Island 162 .cx
|
|
||||||
CY CYP 357 Nicosia EUR 🇨🇾 el-CY,tr-CY,en Cyprus 196 .cy
|
|
||||||
CZ CZE 420 Prague CZK 🇨🇿 cs,sk Czechia 203 .cz
|
|
||||||
DE DEU 49 Berlin EUR 🇩🇪 de Germany 276 .de
|
|
||||||
DJ DJI 253 Djibouti DJF 🇩🇯 fr-DJ,ar,so-DJ,aa Djibouti 262 .dj
|
|
||||||
DK DNK 45 Copenhagen DKK 🇩🇰 da-DK,en,fo,de-DK Denmark 208 .dk
|
|
||||||
DM DMA 1-767 Roseau XCD 🇩🇲 en-DM Dominica 212 .dm
|
|
||||||
DO DOM 1-809 Santo Domingo DOP 🇩🇴 es-DO Dominican Republic 214 .do
|
|
||||||
DZ DZA 213 Algiers DZD 🇩🇿 ar-DZ Algeria 012 .dz
|
|
||||||
EC ECU 593 Quito USD 🇪🇨 es-EC Ecuador 218 .ec
|
|
||||||
EE EST 372 Tallinn EUR 🇪🇪 et,ru Estonia 233 .ee
|
|
||||||
EG EGY 20 Cairo EGP 🇪🇬 ar-EG,en,fr Egypt 818 .eg
|
|
||||||
EH ESH 212 El-Aaiun MAD 🇪🇭 ar,mey Western Sahara 732 .eh
|
|
||||||
ER ERI 291 Asmara ERN 🇪🇷 aa-ER,ar,tig,kun,ti-ER Eritrea 232 .er
|
|
||||||
ES ESP 34 Madrid EUR 🇪🇸 es-ES,ca,gl,eu,oc Spain 724 .es
|
|
||||||
ET ETH 251 Addis Ababa ETB 🇪🇹 am,en-ET,om-ET,ti-ET,so-ET,sid Ethiopia 231 .et
|
|
||||||
FI FIN 358 Helsinki EUR 🇫🇮 fi-FI,sv-FI,smn Finland 246 .fi
|
|
||||||
FJ FJI 679 Suva FJD 🇫🇯 en-FJ,fj Fiji 242 .fj
|
|
||||||
FK FLK 500 Stanley FKP 🇫🇰 en-FK Falkland Islands 238 .fk
|
|
||||||
FM FSM 691 Palikir USD 🇫🇲 en-FM,chk,pon,yap,kos,uli,woe,nkr,kpg Micronesia 583 .fm
|
|
||||||
FO FRO 298 Torshavn DKK 🇫🇴 fo,da-FO Faroe Islands 234 .fo
|
|
||||||
FR FRA 33 Paris EUR 🇫🇷 fr-FR,frp,br,co,ca,eu,oc France 250 .fr
|
|
||||||
GA GAB 241 Libreville XAF 🇬🇦 fr-GA Gabon 266 .ga
|
|
||||||
GB GBR 44 London GBP 🇬🇧 en-GB,cy-GB,gd UK 826 .uk
|
|
||||||
GD GRD 1-473 St. George's XCD 🇬🇩 en-GD Grenada 308 .gd
|
|
||||||
GE GEO 995 Tbilisi GEL 🇬🇪 ka,ru,hy,az Georgia 268 .ge
|
|
||||||
GF GUF 594 Cayenne EUR 🇬🇫 fr-GF French Guiana 254 .gf
|
|
||||||
GG GGY 44 St Peter Port GBP 🇬🇬 en,nrf Guernsey 831 .gg
|
|
||||||
GH GHA 233 Accra GHS 🇬🇭 en-GH,ak,ee,tw Ghana 288 .gh
|
|
||||||
GI GIB 350 Gibraltar GIP 🇬🇮 en-GI,es,it,pt Gibraltar 292 .gi
|
|
||||||
GL GRL 299 Nuuk DKK 🇬🇱 kl,da-GL,en Greenland 304 .gl
|
|
||||||
GM GMB 220 Banjul GMD 🇬🇲 en-GM,mnk,wof,wo,ff Gambia 270 .gm
|
|
||||||
GN GIN 224 Conakry GNF 🇬🇳 fr-GN Guinea 324 .gn
|
|
||||||
GP GLP 590 Basse-Terre EUR 🇬🇵 fr-GP Guadeloupe 312 .gp
|
|
||||||
GQ GNQ 240 Ciudad de la Paz XAF 🇬🇶 es-GQ,fr,pt Equatorial Guinea 226 .gq
|
|
||||||
GR GRC 30 Athens EUR 🇬🇷 el-GR,en,fr Greece 300 .gr
|
|
||||||
GT GTM 502 Guatemala City GTQ 🇬🇹 es-GT Guatemala 320 .gt
|
|
||||||
GU GUM 1-671 Hagatna USD 🇬🇺 en-GU,ch-GU Guam 316 .gu
|
|
||||||
GW GNB 245 Bissau XOF 🇬🇼 pt-GW,pov Guinea-Bissau 624 .gw
|
|
||||||
GY GUY 592 Georgetown GYD 🇬🇾 en-GY Guyana 328 .gy
|
|
||||||
HK HKG 852 Hong Kong HKD 🇭🇰 zh-HK,yue,zh,en Hong Kong 344 .hk
|
|
||||||
HN HND 504 Tegucigalpa HNL 🇭🇳 es-HN,cab,miq Honduras 340 .hn
|
|
||||||
HR HRV 385 Zagreb EUR 🇭🇷 hr-HR,sr Croatia 191 .hr
|
|
||||||
HT HTI 509 Port-au-Prince HTG 🇭🇹 ht,fr-HT Haiti 332 .ht
|
|
||||||
HU HUN 36 Budapest HUF 🇭🇺 hu-HU Hungary 348 .hu
|
|
||||||
ID IDN 62 Jakarta IDR 🇮🇩 id,en,nl,jv Indonesia 360 .id
|
|
||||||
IE IRL 353 Dublin EUR 🇮🇪 en-IE,ga-IE Ireland 372 .ie
|
|
||||||
IL ISR 972 Jerusalem ILS 🇮🇱 he,ar-IL,en-IL Israel 376 .il
|
|
||||||
IM IMN 44 Douglas GBP 🇮🇲 en,gv Isle of Man 833 .im
|
|
||||||
IN IND 91 New Delhi INR 🇮🇳 en-IN,hi,bn,te,mr,ta,ur,gu,kn,ml,or,pa,as,bh,sat,ks,ne,sd,kok,doi,mni,sit,sa,fr,lus,inc India 356 .in
|
|
||||||
IO IOT 246 Diego Garcia USD 🇮🇴 en-IO British Indian Ocean Territory 086 .io
|
|
||||||
IQ IRQ 964 Baghdad IQD 🇮🇶 ar-IQ,ku,hy Iraq 368 .iq
|
|
||||||
IR IRN 98 Tehran IRR 🇮🇷 fa-IR,ku Iran 364 .ir
|
|
||||||
IS ISL 354 Reykjavik ISK 🇮🇸 is,en,de,da,sv,no Iceland 352 .is
|
|
||||||
IT ITA 39 Rome EUR 🇮🇹 it-IT,de-IT,fr-IT,sc,ca,co,sl Italy 380 .it
|
|
||||||
JE JEY 44 Saint Helier GBP 🇯🇪 en,fr,nrf Jersey 832 .je
|
|
||||||
JM JAM 1-876 Kingston JMD 🇯🇲 en-JM Jamaica 388 .jm
|
|
||||||
JO JOR 962 Amman JOD 🇯🇴 ar-JO,en Jordan 400 .jo
|
|
||||||
JP JPN 81 Tokyo JPY 🇯🇵 ja Japan 392 .jp
|
|
||||||
KE KEN 254 Nairobi KES 🇰🇪 en-KE,sw-KE Kenya 404 .ke
|
|
||||||
KG KGZ 996 Bishkek KGS 🇰🇬 ky,uz,ru Kyrgyzstan 417 .kg
|
|
||||||
KH KHM 855 Phnom Penh KHR 🇰🇭 km,fr,en Cambodia 116 .kh
|
|
||||||
KI KIR 686 Tarawa AUD 🇰🇮 en-KI,gil Kiribati 296 .ki
|
|
||||||
KM COM 269 Moroni KMF 🇰🇲 ar,fr-KM Comoros 174 .km
|
|
||||||
KN KNA 1-869 Basseterre XCD 🇰🇳 en-KN St. Kitts & Nevis 659 .kn
|
|
||||||
KP PRK 850 Pyongyang KPW 🇰🇵 ko-KP North Korea 408 .kp
|
|
||||||
KR KOR 82 Seoul KRW 🇰🇷 ko-KR,en South Korea 410 .kr
|
|
||||||
KW KWT 965 Kuwait City KWD 🇰🇼 ar-KW,en Kuwait 414 .kw
|
|
||||||
KY CYM 1-345 George Town KYD 🇰🇾 en-KY Cayman Islands 136 .ky
|
|
||||||
KZ KAZ 7 Nur-Sultan KZT 🇰🇿 kk,ru Kazakhstan 398 .kz
|
|
||||||
LA LAO 856 Vientiane LAK 🇱🇦 lo,fr,en Laos 418 .la
|
|
||||||
LB LBN 961 Beirut LBP 🇱🇧 ar-LB,fr-LB,en,hy Lebanon 422 .lb
|
|
||||||
LC LCA 1-758 Castries XCD 🇱🇨 en-LC St. Lucia 662 .lc
|
|
||||||
LI LIE 423 Vaduz CHF 🇱🇮 de-LI Liechtenstein 438 .li
|
|
||||||
LK LKA 94 Colombo LKR 🇱🇰 si,ta,en Sri Lanka 144 .lk
|
|
||||||
LR LBR 231 Monrovia LRD 🇱🇷 en-LR Liberia 430 .lr
|
|
||||||
LS LSO 266 Maseru LSL 🇱🇸 en-LS,st,zu,xh Lesotho 426 .ls
|
|
||||||
LT LTU 370 Vilnius EUR 🇱🇹 lt,ru,pl Lithuania 440 .lt
|
|
||||||
LU LUX 352 Luxembourg EUR 🇱🇺 lb,de-LU,fr-LU Luxembourg 442 .lu
|
|
||||||
LV LVA 371 Riga EUR 🇱🇻 lv,ru,lt Latvia 428 .lv
|
|
||||||
LY LBY 218 Tripoli LYD 🇱🇾 ar-LY,it,en Libya 434 .ly
|
|
||||||
MA MAR 212 Rabat MAD 🇲🇦 ar-MA,ber,fr Morocco 504 .ma
|
|
||||||
MC MCO 377 Monaco EUR 🇲🇨 fr-MC,en,it Monaco 492 .mc
|
|
||||||
MD MDA 373 Chisinau MDL 🇲🇩 ro,ru,gag,tr Moldova 498 .md
|
|
||||||
ME MNE 382 Podgorica EUR 🇲🇪 sr,hu,bs,sq,hr,rom Montenegro 499 .me
|
|
||||||
MF MAF 590 Marigot EUR 🇲🇫 fr St. Martin 663 .gp
|
|
||||||
MG MDG 261 Antananarivo MGA 🇲🇬 fr-MG,mg Madagascar 450 .mg
|
|
||||||
MH MHL 692 Majuro USD 🇲🇭 mh,en-MH Marshall Islands 584 .mh
|
|
||||||
MK MKD 389 Skopje MKD 🇲🇰 mk,sq,tr,rmm,sr North Macedonia 807 .mk
|
|
||||||
ML MLI 223 Bamako XOF 🇲🇱 fr-ML,bm Mali 466 .ml
|
|
||||||
MM MMR 95 Nay Pyi Taw MMK 🇲🇲 my Myanmar 104 .mm
|
|
||||||
MN MNG 976 Ulaanbaatar MNT 🇲🇳 mn,ru Mongolia 496 .mn
|
|
||||||
MO MAC 853 Macao MOP 🇲🇴 zh,zh-MO,pt Macao 446 .mo
|
|
||||||
MP MNP 1-670 Saipan USD 🇲🇵 fil,tl,zh,ch-MP,en-MP Northern Mariana Islands 580 .mp
|
|
||||||
MQ MTQ 596 Fort-de-France EUR 🇲🇶 fr-MQ Martinique 474 .mq
|
|
||||||
MR MRT 222 Nouakchott MRU 🇲🇷 ar-MR,fuc,snk,fr,mey,wo Mauritania 478 .mr
|
|
||||||
MS MSR 1-664 Plymouth XCD 🇲🇸 en-MS Montserrat 500 .ms
|
|
||||||
MT MLT 356 Valletta EUR 🇲🇹 mt,en-MT Malta 470 .mt
|
|
||||||
MU MUS 230 Port Louis MUR 🇲🇺 en-MU,bho,fr Mauritius 480 .mu
|
|
||||||
MV MDV 960 Male MVR 🇲🇻 dv,en Maldives 462 .mv
|
|
||||||
MW MWI 265 Lilongwe MWK 🇲🇼 ny,yao,tum,swk Malawi 454 .mw
|
|
||||||
MX MEX 52 Mexico City MXN 🇲🇽 es-MX Mexico 484 .mx
|
|
||||||
MY MYS 60 Kuala Lumpur MYR 🇲🇾 ms-MY,en,zh,ta,te,ml,pa,th Malaysia 458 .my
|
|
||||||
MZ MOZ 258 Maputo MZN 🇲🇿 pt-MZ,vmw Mozambique 508 .mz
|
|
||||||
NA NAM 264 Windhoek NAD 🇳🇦 en-NA,af,de,hz,naq Namibia 516 .na
|
|
||||||
NC NCL 687 Noumea XPF 🇳🇨 fr-NC New Caledonia 540 .nc
|
|
||||||
NE NER 227 Niamey XOF 🇳🇪 fr-NE,ha,kr,dje Niger 562 .ne
|
|
||||||
NF NFK 672 Kingston AUD 🇳🇫 en-NF Norfolk Island 574 .nf
|
|
||||||
NG NGA 234 Abuja NGN 🇳🇬 en-NG,ha,yo,ig,ff Nigeria 566 .ng
|
|
||||||
NI NIC 505 Managua NIO 🇳🇮 es-NI,en Nicaragua 558 .ni
|
|
||||||
NL NLD 31 Amsterdam EUR 🇳🇱 nl-NL,fy-NL Netherlands 528 .nl
|
|
||||||
NO NOR 47 Oslo NOK 🇳🇴 no,nb,nn,se,fi Norway 578 .no
|
|
||||||
NP NPL 977 Kathmandu NPR 🇳🇵 ne,en Nepal 524 .np
|
|
||||||
NR NRU 674 Yaren AUD 🇳🇷 na,en-NR Nauru 520 .nr
|
|
||||||
NU NIU 683 Alofi NZD 🇳🇺 niu,en-NU Niue 570 .nu
|
|
||||||
NZ NZL 64 Wellington NZD 🇳🇿 en-NZ,mi New Zealand 554 .nz
|
|
||||||
OM OMN 968 Muscat OMR 🇴🇲 ar-OM,en,bal,ur Oman 512 .om
|
|
||||||
PA PAN 507 Panama City PAB 🇵🇦 es-PA,en Panama 591 .pa
|
|
||||||
PE PER 51 Lima PEN 🇵🇪 es-PE,qu,ay Peru 604 .pe
|
|
||||||
PF PYF 689 Papeete XPF 🇵🇫 fr-PF,ty French Polynesia 258 .pf
|
|
||||||
PG PNG 675 Port Moresby PGK 🇵🇬 en-PG,ho,meu,tpi Papua New Guinea 598 .pg
|
|
||||||
PH PHL 63 Manila PHP 🇵🇭 tl,en-PH,fil,ceb,ilo,hil,war,pam,bik,bcl,pag,mrw,tsg,mdh,cbk,krj,sgd,msb,akl,ibg,yka,mta,abx Philippines 608 .ph
|
|
||||||
PK PAK 92 Islamabad PKR 🇵🇰 ur-PK,en-PK,pa,sd,ps,brh Pakistan 586 .pk
|
|
||||||
PL POL 48 Warsaw PLN 🇵🇱 pl Poland 616 .pl
|
|
||||||
PM SPM 508 Saint-Pierre EUR 🇵🇲 fr-PM St. Pierre & Miquelon 666 .pm
|
|
||||||
PN PCN 870 Adamstown NZD 🇵🇳 en-PN Pitcairn 612 .pn
|
|
||||||
PR PRI 1 San Juan USD 🇵🇷 en-PR,es-PR Puerto Rico 630 .pr
|
|
||||||
PT PRT 351 Lisbon EUR 🇵🇹 pt-PT,mwl Portugal 620 .pt
|
|
||||||
PW PLW 680 Melekeok USD 🇵🇼 pau,sov,en-PW,tox,ja,fil,zh Palau 585 .pw
|
|
||||||
PY PRY 595 Asuncion PYG 🇵🇾 es-PY,gn Paraguay 600 .py
|
|
||||||
QA QAT 974 Doha QAR 🇶🇦 ar-QA,es Qatar 634 .qa
|
|
||||||
RE REU 262 Saint-Denis EUR 🇷🇪 fr-RE Réunion 638 .re
|
|
||||||
RO ROU 40 Bucharest RON 🇷🇴 ro,hu,rom Romania 642 .ro
|
|
||||||
RS SRB 381 Belgrade RSD 🇷🇸 sr,hu,bs,rom Serbia 688 .rs
|
|
||||||
RU RUS 7 Moscow RUB 🇷🇺 ru,tt,xal,cau,ady,kv,ce,tyv,cv,udm,tut,mns,bua,myv,mdf,chm,ba,inh,kbd,krc,av,sah,nog Russia 643 .ru
|
|
||||||
RW RWA 250 Kigali RWF 🇷🇼 rw,en-RW,fr-RW,sw Rwanda 646 .rw
|
|
||||||
SA SAU 966 Riyadh SAR 🇸🇦 ar-SA Saudi Arabia 682 .sa
|
|
||||||
SB SLB 677 Honiara SBD 🇸🇧 en-SB,tpi Solomon Islands 090 .sb
|
|
||||||
SC SYC 248 Victoria SCR 🇸🇨 en-SC,fr-SC Seychelles 690 .sc
|
|
||||||
SD SDN 249 Khartoum SDG 🇸🇩 ar-SD,en,fia Sudan 729 .sd
|
|
||||||
SE SWE 46 Stockholm SEK 🇸🇪 sv-SE,se,sma,fi-SE Sweden 752 .se
|
|
||||||
SG SGP 65 Singapore SGD 🇸🇬 cmn,en-SG,ms-SG,ta-SG,zh-SG Singapore 702 .sg
|
|
||||||
SH SHN 290 Jamestown SHP 🇸🇭 en-SH St. Helena 654 .sh
|
|
||||||
SI SVN 386 Ljubljana EUR 🇸🇮 sl,sh Slovenia 705 .si
|
|
||||||
SJ SJM 47 Longyearbyen NOK 🇸🇯 no,ru Svalbard & Jan Mayen 744 .sj
|
|
||||||
SK SVK 421 Bratislava EUR 🇸🇰 sk,hu Slovakia 703 .sk
|
|
||||||
SL SLE 232 Freetown SLE 🇸🇱 en-SL,men,tem Sierra Leone 694 .sl
|
|
||||||
SM SMR 378 San Marino EUR 🇸🇲 it-SM San Marino 674 .sm
|
|
||||||
SN SEN 221 Dakar XOF 🇸🇳 fr-SN,wo,fuc,mnk Senegal 686 .sn
|
|
||||||
SO SOM 252 Mogadishu SOS 🇸🇴 so-SO,ar-SO,it,en-SO Somalia 706 .so
|
|
||||||
SR SUR 597 Paramaribo SRD 🇸🇷 nl-SR,en,srn,hns,jv Suriname 740 .sr
|
|
||||||
SS SSD 211 Juba SSP 🇸🇸 en South Sudan 728 .ss
|
|
||||||
ST STP 239 Sao Tome STN 🇸🇹 pt-ST São Tomé & Príncipe 678 .st
|
|
||||||
SV SLV 503 San Salvador SVC 🇸🇻 es-SV El Salvador 222 .sv
|
|
||||||
SX SXM 1-721 Philipsburg XCG 🇸🇽 nl,en Sint Maarten 534 .sx
|
|
||||||
SY SYR 963 Damascus SYP 🇸🇾 ar-SY,ku,hy,arc,fr,en Syria 760 .sy
|
|
||||||
SZ SWZ 268 Mbabane SZL 🇸🇿 en-SZ,ss-SZ Eswatini 748 .sz
|
|
||||||
TC TCA 1-649 Cockburn Town USD 🇹🇨 en-TC Turks & Caicos Islands 796 .tc
|
|
||||||
TD TCD 235 N'Djamena XAF 🇹🇩 fr-TD,ar-TD,sre Chad 148 .td
|
|
||||||
TF ATF 262 Port-aux-Francais EUR 🇹🇫 fr French Southern Territories 260 .tf
|
|
||||||
TG TGO 228 Lome XOF 🇹🇬 fr-TG,ee,hna,kbp,dag,ha Togo 768 .tg
|
|
||||||
TH THA 66 Bangkok THB 🇹🇭 th,en Thailand 764 .th
|
|
||||||
TJ TJK 992 Dushanbe TJS 🇹🇯 tg,ru Tajikistan 762 .tj
|
|
||||||
TL TLS 670 Dili USD 🇹🇱 tet,pt-TL,id,en Timor-Leste 626 .tl
|
|
||||||
TM TKM 993 Ashgabat TMT 🇹🇲 tk,ru,uz Turkmenistan 795 .tm
|
|
||||||
TN TUN 216 Tunis TND 🇹🇳 ar-TN,fr Tunisia 788 .tn
|
|
||||||
TO TON 676 Nuku'alofa TOP 🇹🇴 to,en-TO Tonga 776 .to
|
|
||||||
TR TUR 90 Ankara TRY 🇹🇷 tr-TR,ku,diq,az,av Türkiye 792 .tr
|
|
||||||
TT TTO 1-868 Port of Spain TTD 🇹🇹 en-TT,hns,fr,es,zh Trinidad & Tobago 780 .tt
|
|
||||||
TV TUV 688 Funafuti AUD 🇹🇻 tvl,en,sm,gil Tuvalu 798 .tv
|
|
||||||
TW TWN 886 Taipei TWD 🇹🇼 zh-TW,zh,nan,hak Taiwan 158 .tw
|
|
||||||
TZ TZA 255 Dodoma TZS 🇹🇿 sw-TZ,en,ar Tanzania 834 .tz
|
|
||||||
UA UKR 380 Kyiv UAH 🇺🇦 uk,ru-UA,rom,pl,hu Ukraine 804 .ua
|
|
||||||
UG UGA 256 Kampala UGX 🇺🇬 en-UG,lg,sw,ar Uganda 800 .ug
|
|
||||||
US USA 1 Washington USD 🇺🇸 en-US,es-US,haw,fr US 840 .us
|
|
||||||
UY URY 598 Montevideo UYU 🇺🇾 es-UY Uruguay 858 .uy
|
|
||||||
UZ UZB 998 Tashkent UZS 🇺🇿 uz,ru,tg Uzbekistan 860 .uz
|
|
||||||
VA VAT 39 Vatican City EUR 🇻🇦 la,it,fr Vatican City 336 .va
|
|
||||||
VC VCT 1-784 Kingstown XCD 🇻🇨 en-VC,fr St. Vincent & Grenadines 670 .vc
|
|
||||||
VE VEN 58 Caracas VES 🇻🇪 es-VE Venezuela 862 .ve
|
|
||||||
VG VGB 1-284 Road Town USD 🇻🇬 en-VG British Virgin Islands 092 .vg
|
|
||||||
VI VIR 1-340 Charlotte Amalie USD 🇻🇮 en-VI U.S. Virgin Islands 850 .vi
|
|
||||||
VN VNM 84 Hanoi VND 🇻🇳 vi,en,fr,zh,km Vietnam 704 .vn
|
|
||||||
VU VUT 678 Port Vila VUV 🇻🇺 bi,en-VU,fr-VU Vanuatu 548 .vu
|
|
||||||
WF WLF 681 Mata Utu XPF 🇼🇫 wls,fud,fr-WF Wallis & Futuna 876 .wf
|
|
||||||
WS WSM 685 Apia WST 🇼🇸 sm,en-WS Samoa 882 .ws
|
|
||||||
YE YEM 967 Sanaa YER 🇾🇪 ar-YE Yemen 887 .ye
|
|
||||||
YT MYT 262 Mamoudzou EUR 🇾🇹 fr-YT Mayotte 175 .yt
|
|
||||||
ZA ZAF 27 Pretoria ZAR 🇿🇦 zu,xh,af,nso,en-ZA,tn,st,ts,ss,ve,nr South Africa 710 .za
|
|
||||||
ZM ZMB 260 Lusaka ZMW 🇿🇲 en-ZM,bem,loz,lun,lue,ny,toi Zambia 894 .zm
|
|
||||||
ZW ZWE 263 Harare ZWG 🇿🇼 en-ZW,sn,nr,nd Zimbabwe 716 .zw
|
|
||||||
|
@@ -71,7 +71,7 @@ JPY 0 Yen 392 ¥
|
|||||||
KES 2 Kenyan Shilling 404 KES
|
KES 2 Kenyan Shilling 404 KES
|
||||||
KGS 2 Som 417 ⃀
|
KGS 2 Som 417 ⃀
|
||||||
KHR 2 Riel 116 ៛
|
KHR 2 Riel 116 ៛
|
||||||
KMF 0 Comorian Franc 174 CF
|
KMF 0 Comorian Franc 174 CF
|
||||||
KPW 2 North Korean Won 408 ₩
|
KPW 2 North Korean Won 408 ₩
|
||||||
KRW 0 Won 410 ₩
|
KRW 0 Won 410 ₩
|
||||||
KWD 3 Kuwaiti Dinar 414 KWD
|
KWD 3 Kuwaiti Dinar 414 KWD
|
||||||
|
|||||||
|
@@ -0,0 +1,15 @@
|
|||||||
|
// Package misc is fejkdata's locale-neutral module, whose categories are misc.*.
|
||||||
|
package misc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FS holds the module's categories and its manifest.
|
||||||
|
//
|
||||||
|
//go:embed *.json *.tsv .fejkdata.json
|
||||||
|
var FS embed.FS
|
||||||
|
|
||||||
|
// Modules returns FS, which reads no other module, to pass to fejkdata.WithDataFS.
|
||||||
|
func Modules() []fs.FS { return []fs.FS{FS} }
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package misc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/larvit/fejkdata"
|
||||||
|
"github.com/larvit/fejkdata/data/misc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFSLoadsByItsManifest(t *testing.T) {
|
||||||
|
if _, err := fs.Stat(misc.FS, ".fejkdata.json"); err != nil {
|
||||||
|
t.Fatalf("FS holds no manifest: %v", err)
|
||||||
|
}
|
||||||
|
f, err := fejkdata.New(fejkdata.WithDataFS(misc.Modules()...), fejkdata.WithSeed(1))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New(WithDataFS(Modules()...)) = %v", err)
|
||||||
|
}
|
||||||
|
if len(f.List()) == 0 {
|
||||||
|
t.Error("List() is empty, want the module's categories")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{ "format": "{method}", "rows": "httpmethod.tsv", "key": "method" }
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
idempotent method safe
|
||||||
|
false CONNECT false
|
||||||
|
true DELETE false
|
||||||
|
true GET true
|
||||||
|
true HEAD true
|
||||||
|
true OPTIONS true
|
||||||
|
false PATCH false
|
||||||
|
false POST false
|
||||||
|
true PUT false
|
||||||
|
true TRACE true
|
||||||
|
@@ -1,17 +1,61 @@
|
|||||||
code reason
|
code reason
|
||||||
|
100 Continue
|
||||||
|
101 Switching Protocols
|
||||||
|
102 Processing
|
||||||
|
103 Early Hints
|
||||||
200 OK
|
200 OK
|
||||||
201 Created
|
201 Created
|
||||||
|
202 Accepted
|
||||||
|
203 Non-Authoritative Information
|
||||||
204 No Content
|
204 No Content
|
||||||
|
205 Reset Content
|
||||||
|
206 Partial Content
|
||||||
|
207 Multi-Status
|
||||||
|
208 Already Reported
|
||||||
|
226 IM Used
|
||||||
|
300 Multiple Choices
|
||||||
301 Moved Permanently
|
301 Moved Permanently
|
||||||
302 Found
|
302 Found
|
||||||
|
303 See Other
|
||||||
304 Not Modified
|
304 Not Modified
|
||||||
|
305 Use Proxy
|
||||||
|
307 Temporary Redirect
|
||||||
|
308 Permanent Redirect
|
||||||
400 Bad Request
|
400 Bad Request
|
||||||
401 Unauthorized
|
401 Unauthorized
|
||||||
|
402 Payment Required
|
||||||
403 Forbidden
|
403 Forbidden
|
||||||
404 Not Found
|
404 Not Found
|
||||||
|
405 Method Not Allowed
|
||||||
|
406 Not Acceptable
|
||||||
|
407 Proxy Authentication Required
|
||||||
|
408 Request Timeout
|
||||||
409 Conflict
|
409 Conflict
|
||||||
422 Unprocessable Entity
|
410 Gone
|
||||||
|
411 Length Required
|
||||||
|
412 Precondition Failed
|
||||||
|
413 Content Too Large
|
||||||
|
414 URI Too Long
|
||||||
|
415 Unsupported Media Type
|
||||||
|
416 Range Not Satisfiable
|
||||||
|
417 Expectation Failed
|
||||||
|
421 Misdirected Request
|
||||||
|
422 Unprocessable Content
|
||||||
|
423 Locked
|
||||||
|
424 Failed Dependency
|
||||||
|
425 Too Early
|
||||||
|
426 Upgrade Required
|
||||||
|
428 Precondition Required
|
||||||
429 Too Many Requests
|
429 Too Many Requests
|
||||||
|
431 Request Header Fields Too Large
|
||||||
|
451 Unavailable For Legal Reasons
|
||||||
500 Internal Server Error
|
500 Internal Server Error
|
||||||
|
501 Not Implemented
|
||||||
502 Bad Gateway
|
502 Bad Gateway
|
||||||
503 Service Unavailable
|
503 Service Unavailable
|
||||||
|
504 Gateway Timeout
|
||||||
|
505 HTTP Version Not Supported
|
||||||
|
506 Variant Also Negotiates
|
||||||
|
507 Insufficient Storage
|
||||||
|
508 Loop Detected
|
||||||
|
511 Network Authentication Required
|
||||||
|
|||||||
|
+184
-17
@@ -1,17 +1,184 @@
|
|||||||
code name
|
code code3 name
|
||||||
ar Arabic
|
aa aar Afar
|
||||||
zh Chinese
|
ab abk Abkhazian
|
||||||
da Danish
|
ae ave Avestan
|
||||||
nl Dutch
|
af afr Afrikaans
|
||||||
en English
|
ak aka Akan
|
||||||
fi Finnish
|
am amh Amharic
|
||||||
fr French
|
an arg Aragonese
|
||||||
de German
|
ar ara Arabic
|
||||||
it Italian
|
as asm Assamese
|
||||||
ja Japanese
|
av ava Avaric
|
||||||
no Norwegian
|
ay aym Aymara
|
||||||
pl Polish
|
az aze Azerbaijani
|
||||||
pt Portuguese
|
ba bak Bashkir
|
||||||
ru Russian
|
be bel Belarusian
|
||||||
es Spanish
|
bg bul Bulgarian
|
||||||
sv Swedish
|
bi bis Bislama
|
||||||
|
bm bam Bambara
|
||||||
|
bn ben Bengali
|
||||||
|
bo bod Tibetan
|
||||||
|
br bre Breton
|
||||||
|
bs bos Bosnian
|
||||||
|
ca cat Catalan
|
||||||
|
ce che Chechen
|
||||||
|
ch cha Chamorro
|
||||||
|
co cos Corsican
|
||||||
|
cr cre Cree
|
||||||
|
cs ces Czech
|
||||||
|
cu chu Church Slavic
|
||||||
|
cv chv Chuvash
|
||||||
|
cy cym Welsh
|
||||||
|
da dan Danish
|
||||||
|
de deu German
|
||||||
|
dv div Divehi
|
||||||
|
dz dzo Dzongkha
|
||||||
|
ee ewe Ewe
|
||||||
|
el ell Modern Greek
|
||||||
|
en eng English
|
||||||
|
eo epo Esperanto
|
||||||
|
es spa Spanish
|
||||||
|
et est Estonian
|
||||||
|
eu eus Basque
|
||||||
|
fa fas Persian
|
||||||
|
ff ful Fulah
|
||||||
|
fi fin Finnish
|
||||||
|
fj fij Fijian
|
||||||
|
fo fao Faroese
|
||||||
|
fr fra French
|
||||||
|
fy fry Western Frisian
|
||||||
|
ga gle Irish
|
||||||
|
gd gla Gaelic
|
||||||
|
gl glg Galician
|
||||||
|
gn grn Guarani
|
||||||
|
gu guj Gujarati
|
||||||
|
gv glv Manx
|
||||||
|
ha hau Hausa
|
||||||
|
he heb Hebrew
|
||||||
|
hi hin Hindi
|
||||||
|
ho hmo Hiri Motu
|
||||||
|
hr hrv Croatian
|
||||||
|
ht hat Haitian
|
||||||
|
hu hun Hungarian
|
||||||
|
hy hye Armenian
|
||||||
|
hz her Herero
|
||||||
|
ia ina Interlingua
|
||||||
|
id ind Indonesian
|
||||||
|
ie ile Interlingue
|
||||||
|
ig ibo Igbo
|
||||||
|
ii iii Sichuan Yi
|
||||||
|
ik ipk Inupiaq
|
||||||
|
io ido Ido
|
||||||
|
is isl Icelandic
|
||||||
|
it ita Italian
|
||||||
|
iu iku Inuktitut
|
||||||
|
ja jpn Japanese
|
||||||
|
jv jav Javanese
|
||||||
|
ka kat Georgian
|
||||||
|
kg kon Kongo
|
||||||
|
ki kik Kikuyu
|
||||||
|
kj kua Kuanyama
|
||||||
|
kk kaz Kazakh
|
||||||
|
kl kal Kalaallisut
|
||||||
|
km khm Central Khmer
|
||||||
|
kn kan Kannada
|
||||||
|
ko kor Korean
|
||||||
|
kr kau Kanuri
|
||||||
|
ks kas Kashmiri
|
||||||
|
ku kur Kurdish
|
||||||
|
kv kom Komi
|
||||||
|
kw cor Cornish
|
||||||
|
ky kir Kirghiz
|
||||||
|
la lat Latin
|
||||||
|
lb ltz Luxembourgish
|
||||||
|
lg lug Ganda
|
||||||
|
li lim Limburgan
|
||||||
|
ln lin Lingala
|
||||||
|
lo lao Lao
|
||||||
|
lt lit Lithuanian
|
||||||
|
lu lub Luba-Katanga
|
||||||
|
lv lav Latvian
|
||||||
|
mg mlg Malagasy
|
||||||
|
mh mah Marshallese
|
||||||
|
mi mri Maori
|
||||||
|
mk mkd Macedonian
|
||||||
|
ml mal Malayalam
|
||||||
|
mn mon Mongolian
|
||||||
|
mr mar Marathi
|
||||||
|
ms msa Malay
|
||||||
|
mt mlt Maltese
|
||||||
|
my mya Burmese
|
||||||
|
na nau Nauru
|
||||||
|
nb nob Norwegian Bokmål
|
||||||
|
nd nde North Ndebele
|
||||||
|
ne nep Nepali
|
||||||
|
ng ndo Ndonga
|
||||||
|
nl nld Dutch
|
||||||
|
nn nno Norwegian Nynorsk
|
||||||
|
no nor Norwegian
|
||||||
|
nr nbl South Ndebele
|
||||||
|
nv nav Navajo
|
||||||
|
ny nya Chichewa
|
||||||
|
oc oci Occitan
|
||||||
|
oj oji Ojibwa
|
||||||
|
om orm Oromo
|
||||||
|
or ori Oriya
|
||||||
|
os oss Ossetian
|
||||||
|
pa pan Panjabi
|
||||||
|
pi pli Pali
|
||||||
|
pl pol Polish
|
||||||
|
ps pus Pushto
|
||||||
|
pt por Portuguese
|
||||||
|
qu que Quechua
|
||||||
|
rm roh Romansh
|
||||||
|
rn run Rundi
|
||||||
|
ro ron Romanian
|
||||||
|
ru rus Russian
|
||||||
|
rw kin Kinyarwanda
|
||||||
|
sa san Sanskrit
|
||||||
|
sc srd Sardinian
|
||||||
|
sd snd Sindhi
|
||||||
|
se sme Northern Sami
|
||||||
|
sg sag Sango
|
||||||
|
si sin Sinhala
|
||||||
|
sk slk Slovak
|
||||||
|
sl slv Slovenian
|
||||||
|
sm smo Samoan
|
||||||
|
sn sna Shona
|
||||||
|
so som Somali
|
||||||
|
sq sqi Albanian
|
||||||
|
sr srp Serbian
|
||||||
|
ss ssw Swati
|
||||||
|
st sot Sotho, Southern
|
||||||
|
su sun Sundanese
|
||||||
|
sv swe Swedish
|
||||||
|
sw swa Swahili
|
||||||
|
ta tam Tamil
|
||||||
|
te tel Telugu
|
||||||
|
tg tgk Tajik
|
||||||
|
th tha Thai
|
||||||
|
ti tir Tigrinya
|
||||||
|
tk tuk Turkmen
|
||||||
|
tl tgl Tagalog
|
||||||
|
tn tsn Tswana
|
||||||
|
to ton Tonga
|
||||||
|
tr tur Turkish
|
||||||
|
ts tso Tsonga
|
||||||
|
tt tat Tatar
|
||||||
|
tw twi Twi
|
||||||
|
ty tah Tahitian
|
||||||
|
ug uig Uighur
|
||||||
|
uk ukr Ukrainian
|
||||||
|
ur urd Urdu
|
||||||
|
uz uzb Uzbek
|
||||||
|
ve ven Venda
|
||||||
|
vi vie Vietnamese
|
||||||
|
vo vol Volapük
|
||||||
|
wa wln Walloon
|
||||||
|
wo wol Wolof
|
||||||
|
xh xho Xhosa
|
||||||
|
yi yid Yiddish
|
||||||
|
yo yor Yoruba
|
||||||
|
za zha Zhuang
|
||||||
|
zh zho Chinese
|
||||||
|
zu zul Zulu
|
||||||
|
|||||||
|
@@ -0,0 +1 @@
|
|||||||
|
{ "format": "{keyword}", "rows": "loglevel.tsv", "key": "code", "name": "keyword" }
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
code keyword severity
|
||||||
|
0 emerg Emergency
|
||||||
|
1 alert Alert
|
||||||
|
2 crit Critical
|
||||||
|
3 err Error
|
||||||
|
4 warning Warning
|
||||||
|
5 notice Notice
|
||||||
|
6 info Informational
|
||||||
|
7 debug Debug
|
||||||
|
@@ -1,16 +1,702 @@
|
|||||||
ext type
|
ext type
|
||||||
|
.ez application/andrew-inset
|
||||||
|
.atom application/atom+xml
|
||||||
|
.atomcat application/atomcat+xml
|
||||||
|
.atomdeleted application/atomdeleted+xml
|
||||||
|
.atomsvc application/atomsvc+xml
|
||||||
|
.dwd application/atsc-dwd+xml
|
||||||
|
.held application/atsc-held+xml
|
||||||
|
.rsat application/atsc-rsat+xml
|
||||||
|
.aml application/automationml-aml+xml
|
||||||
|
.amlx application/automationml-amlx+zip
|
||||||
|
.xcs application/calendar+xml
|
||||||
|
.ccxml application/ccxml+xml
|
||||||
|
.cdfx application/cdfx+xml
|
||||||
|
.cdmia application/cdmi-capability
|
||||||
|
.cdmic application/cdmi-container
|
||||||
|
.cdmid application/cdmi-domain
|
||||||
|
.cdmio application/cdmi-object
|
||||||
|
.cdmiq application/cdmi-queue
|
||||||
|
.cpl application/cpl+xml
|
||||||
|
.cwl application/cwl
|
||||||
|
.mpd application/dash+xml
|
||||||
|
.mpp application/dash-patch+xml
|
||||||
|
.davmount application/davmount+xml
|
||||||
|
.dcm application/dicom
|
||||||
|
.dssc application/dssc+der
|
||||||
|
.xdssc application/dssc+xml
|
||||||
|
.emma application/emma+xml
|
||||||
|
.emotionml application/emotionml+xml
|
||||||
|
.epub application/epub+zip
|
||||||
|
.exi application/exi
|
||||||
|
.exp application/express
|
||||||
|
.fdf application/fdf
|
||||||
|
.fdt application/fdt+xml
|
||||||
|
.pfr application/font-tdpfr
|
||||||
|
.geojson application/geo+json
|
||||||
|
.gml application/gml+xml
|
||||||
.gz application/gzip
|
.gz application/gzip
|
||||||
|
.stk application/hyperstudio
|
||||||
|
.ink application/inkml+xml
|
||||||
|
.ipfix application/ipfix
|
||||||
|
.its application/its+xml
|
||||||
|
.jar application/java-archive
|
||||||
.json application/json
|
.json application/json
|
||||||
|
.kbl application/kbl+xml
|
||||||
|
.jsonld application/ld+json
|
||||||
|
.lgr application/lgr+xml
|
||||||
|
.lostxml application/lost+xml
|
||||||
|
.hqx application/mac-binhex40
|
||||||
|
.mads application/mads+xml
|
||||||
|
.webmanifest application/manifest+json
|
||||||
|
.mrc application/marc
|
||||||
|
.mrcx application/marcxml+xml
|
||||||
|
.ma application/mathematica
|
||||||
|
.mathml application/mathml+xml
|
||||||
|
.mbox application/mbox
|
||||||
|
.mpf application/media-policy-dataset+xml
|
||||||
|
.mscml application/mediaservercontrol+xml
|
||||||
|
.meta4 application/metalink4+xml
|
||||||
|
.mets application/mets+xml
|
||||||
|
.maei application/mmt-aei+xml
|
||||||
|
.musd application/mmt-usd+xml
|
||||||
|
.mods application/mods+xml
|
||||||
|
.m21 application/mp21
|
||||||
|
.mp4s application/mp4
|
||||||
|
.doc application/msword
|
||||||
|
.mxf application/mxf
|
||||||
|
.nq application/n-quads
|
||||||
|
.nt application/n-triples
|
||||||
|
.cjs application/node
|
||||||
|
.bin application/octet-stream
|
||||||
|
.oda application/oda
|
||||||
|
.opf application/oebps-package+xml
|
||||||
|
.ogx application/ogg
|
||||||
|
.oxps application/oxps
|
||||||
|
.relo application/p2p-overlay+xml
|
||||||
|
.xer application/patch-ops-error+xml
|
||||||
.pdf application/pdf
|
.pdf application/pdf
|
||||||
|
.pgp application/pgp-encrypted
|
||||||
|
.asc application/pgp-keys
|
||||||
|
.sig application/pgp-signature
|
||||||
|
.p10 application/pkcs10
|
||||||
|
.p7m application/pkcs7-mime
|
||||||
|
.p7s application/pkcs7-signature
|
||||||
|
.p8 application/pkcs8
|
||||||
|
.ac application/pkix-attr-cert
|
||||||
|
.cer application/pkix-cert
|
||||||
|
.crl application/pkix-crl
|
||||||
|
.pkipath application/pkix-pkipath
|
||||||
|
.pki application/pkixcmp
|
||||||
|
.pls application/pls+xml
|
||||||
|
.ai application/postscript
|
||||||
|
.provx application/provenance+xml
|
||||||
|
.cww application/prs.cww
|
||||||
|
.xsf application/prs.xsf+xml
|
||||||
|
.pskcxml application/pskc+xml
|
||||||
|
.rdf application/rdf+xml
|
||||||
|
.rif application/reginfo+xml
|
||||||
|
.rnc application/relax-ng-compact-syntax
|
||||||
|
.rl application/resource-lists+xml
|
||||||
|
.rld application/resource-lists-diff+xml
|
||||||
|
.rs application/rls-services+xml
|
||||||
|
.rapd application/route-apd+xml
|
||||||
|
.sls application/route-s-tsid+xml
|
||||||
|
.rusd application/route-usd+xml
|
||||||
|
.gbr application/rpki-ghostbusters
|
||||||
|
.mft application/rpki-manifest
|
||||||
|
.roa application/rpki-roa
|
||||||
|
.rtf application/rtf
|
||||||
|
.sbml application/sbml+xml
|
||||||
|
.scq application/scvp-cv-request
|
||||||
|
.scs application/scvp-cv-response
|
||||||
|
.spq application/scvp-vp-request
|
||||||
|
.spp application/scvp-vp-response
|
||||||
|
.sdp application/sdp
|
||||||
|
.senmlx application/senml+xml
|
||||||
|
.sensmlx application/sensml+xml
|
||||||
|
.setpay application/set-payment-initiation
|
||||||
|
.setreg application/set-registration-initiation
|
||||||
|
.shf application/shf+xml
|
||||||
|
.siv application/sieve
|
||||||
|
.smi application/smil+xml
|
||||||
|
.rq application/sparql-query
|
||||||
|
.srx application/sparql-results+xml
|
||||||
|
.sql application/sql
|
||||||
|
.gram application/srgs
|
||||||
|
.grxml application/srgs+xml
|
||||||
|
.sru application/sru+xml
|
||||||
|
.ssml application/ssml+xml
|
||||||
|
.swidtag application/swid+xml
|
||||||
|
.tei application/tei+xml
|
||||||
|
.tfi application/thraud+xml
|
||||||
|
.tsd application/timestamped-data
|
||||||
|
.toml application/toml
|
||||||
|
.trig application/trig
|
||||||
|
.ttml application/ttml+xml
|
||||||
|
.rsheet application/urc-ressheet+xml
|
||||||
|
.td application/urc-targetdesc+xml
|
||||||
|
.vec application/vec+xml
|
||||||
|
.1km application/vnd.1000minds.decision-model+xml
|
||||||
|
.plb application/vnd.3gpp.pic-bw-large
|
||||||
|
.psb application/vnd.3gpp.pic-bw-small
|
||||||
|
.pvb application/vnd.3gpp.pic-bw-var
|
||||||
|
.tcap application/vnd.3gpp2.tcap
|
||||||
|
.pwn application/vnd.3m.post-it-notes
|
||||||
|
.aso application/vnd.accpac.simply.aso
|
||||||
|
.imp application/vnd.accpac.simply.imp
|
||||||
|
.acu application/vnd.acucobol
|
||||||
|
.atc application/vnd.acucorp
|
||||||
|
.fcdt application/vnd.adobe.formscentral.fcdt
|
||||||
|
.fxp application/vnd.adobe.fxp
|
||||||
|
.xdp application/vnd.adobe.xdp+xml
|
||||||
|
.age application/vnd.age
|
||||||
|
.ahead application/vnd.ahead.space
|
||||||
|
.azf application/vnd.airzip.filesecure.azf
|
||||||
|
.azs application/vnd.airzip.filesecure.azs
|
||||||
|
.acc application/vnd.americandynamics.acc
|
||||||
|
.ami application/vnd.amiga.ami
|
||||||
|
.cii application/vnd.anser-web-certificate-issue-initiation
|
||||||
|
.atx application/vnd.antix.game-component
|
||||||
|
.parquet application/vnd.apache.parquet
|
||||||
|
.mpkg application/vnd.apple.installer+xml
|
||||||
|
.key application/vnd.apple.keynote
|
||||||
|
.m3u8 application/vnd.apple.mpegurl
|
||||||
|
.numbers application/vnd.apple.numbers
|
||||||
|
.pages application/vnd.apple.pages
|
||||||
|
.swi application/vnd.aristanetworks.swi
|
||||||
|
.iota application/vnd.astraea-software.iota
|
||||||
|
.aep application/vnd.audiograph
|
||||||
|
.bmml application/vnd.balsamiq.bmml+xml
|
||||||
|
.mpm application/vnd.blueice.multipass
|
||||||
|
.bmi application/vnd.bmi
|
||||||
|
.rep application/vnd.businessobjects
|
||||||
|
.cdxml application/vnd.chemdraw+xml
|
||||||
|
.mmd application/vnd.chipnuts.karaoke-mmd
|
||||||
|
.cdy application/vnd.cinderella
|
||||||
|
.csl application/vnd.citationstyles.style+xml
|
||||||
|
.cla application/vnd.claymore
|
||||||
|
.rp9 application/vnd.cloanto.rp9
|
||||||
|
.c4g application/vnd.clonk.c4group
|
||||||
|
.c11amc application/vnd.cluetrust.cartomobile-config
|
||||||
|
.c11amz application/vnd.cluetrust.cartomobile-config-pkg
|
||||||
|
.csp application/vnd.commonspace
|
||||||
|
.cdbcmsg application/vnd.contact.cmsg
|
||||||
|
.cmc application/vnd.cosmocaller
|
||||||
|
.clkx application/vnd.crick.clicker
|
||||||
|
.clkk application/vnd.crick.clicker.keyboard
|
||||||
|
.clkp application/vnd.crick.clicker.palette
|
||||||
|
.clkt application/vnd.crick.clicker.template
|
||||||
|
.clkw application/vnd.crick.clicker.wordbank
|
||||||
|
.wbs application/vnd.criticaltools.wbs+xml
|
||||||
|
.pml application/vnd.ctc-posml
|
||||||
|
.ppd application/vnd.cups-ppd
|
||||||
|
.dart application/vnd.dart
|
||||||
|
.rdz application/vnd.data-vision.rdz
|
||||||
|
.dbf application/vnd.dbf
|
||||||
|
.dcmp application/vnd.dcmp+xml
|
||||||
|
.uvf application/vnd.dece.data
|
||||||
|
.uvt application/vnd.dece.ttml+xml
|
||||||
|
.uvx application/vnd.dece.unspecified
|
||||||
|
.uvz application/vnd.dece.zip
|
||||||
|
.fe_launch application/vnd.denovo.fcselayout-link
|
||||||
|
.dna application/vnd.dna
|
||||||
|
.dpg application/vnd.dpgraph
|
||||||
|
.dfac application/vnd.dreamfactory
|
||||||
|
.ait application/vnd.dvb.ait
|
||||||
|
.svc application/vnd.dvb.service
|
||||||
|
.geo application/vnd.dynageo
|
||||||
|
.mag application/vnd.ecowin.chart
|
||||||
|
.nml application/vnd.enliven
|
||||||
|
.esf application/vnd.epson.esf
|
||||||
|
.msf application/vnd.epson.msf
|
||||||
|
.qam application/vnd.epson.quickanime
|
||||||
|
.slt application/vnd.epson.salt
|
||||||
|
.ssf application/vnd.epson.ssf
|
||||||
|
.es3 application/vnd.eszigno3+xml
|
||||||
|
.ez2 application/vnd.ezpix-album
|
||||||
|
.ez3 application/vnd.ezpix-package
|
||||||
|
.mseed application/vnd.fdsn.mseed
|
||||||
|
.seed application/vnd.fdsn.seed
|
||||||
|
.gph application/vnd.flographit
|
||||||
|
.ftc application/vnd.fluxtime.clip
|
||||||
|
.fm application/vnd.framemaker
|
||||||
|
.fsc application/vnd.fsc.weblaunch
|
||||||
|
.oas application/vnd.fujitsu.oasys
|
||||||
|
.oa2 application/vnd.fujitsu.oasys2
|
||||||
|
.oa3 application/vnd.fujitsu.oasys3
|
||||||
|
.fg5 application/vnd.fujitsu.oasysgp
|
||||||
|
.bh2 application/vnd.fujitsu.oasysprs
|
||||||
|
.ddd application/vnd.fujixerox.ddd
|
||||||
|
.xdw application/vnd.fujixerox.docuworks
|
||||||
|
.xbd application/vnd.fujixerox.docuworks.binder
|
||||||
|
.fzs application/vnd.fuzzysheet
|
||||||
|
.txd application/vnd.genomatix.tuxedo
|
||||||
|
.ggb application/vnd.geogebra.file
|
||||||
|
.ggs application/vnd.geogebra.slides
|
||||||
|
.ggt application/vnd.geogebra.tool
|
||||||
|
.gex application/vnd.geometry-explorer
|
||||||
|
.gxt application/vnd.geonext
|
||||||
|
.g2w application/vnd.geoplan
|
||||||
|
.g3w application/vnd.geospace
|
||||||
|
.gmx application/vnd.gmx
|
||||||
|
.kml application/vnd.google-earth.kml+xml
|
||||||
|
.kmz application/vnd.google-earth.kmz
|
||||||
|
.xdcf application/vnd.gov.sk.xmldatacontainer+xml
|
||||||
|
.gqf application/vnd.grafeq
|
||||||
|
.gac application/vnd.groove-account
|
||||||
|
.ghf application/vnd.groove-help
|
||||||
|
.gim application/vnd.groove-identity-message
|
||||||
|
.grv application/vnd.groove-injector
|
||||||
|
.gtm application/vnd.groove-tool-message
|
||||||
|
.tpl application/vnd.groove-tool-template
|
||||||
|
.vcg application/vnd.groove-vcard
|
||||||
|
.hal application/vnd.hal+xml
|
||||||
|
.zmm application/vnd.handheld-entertainment+xml
|
||||||
|
.hbci application/vnd.hbci
|
||||||
|
.les application/vnd.hhe.lesson-player
|
||||||
|
.hpgl application/vnd.hp-hpgl
|
||||||
|
.hpid application/vnd.hp-hpid
|
||||||
|
.hps application/vnd.hp-hps
|
||||||
|
.jlt application/vnd.hp-jlyt
|
||||||
|
.pcl application/vnd.hp-pcl
|
||||||
|
.pclxl application/vnd.hp-pclxl
|
||||||
|
.sfd-hdstx application/vnd.hydrostatix.sof-data
|
||||||
|
.mpy application/vnd.ibm.minipay
|
||||||
|
.irm application/vnd.ibm.rights-management
|
||||||
|
.sc application/vnd.ibm.secure-container
|
||||||
|
.icc application/vnd.iccprofile
|
||||||
|
.igl application/vnd.igloader
|
||||||
|
.ivp application/vnd.immervision-ivp
|
||||||
|
.ivu application/vnd.immervision-ivu
|
||||||
|
.igm application/vnd.insors.igm
|
||||||
|
.xpw application/vnd.intercon.formnet
|
||||||
|
.i2g application/vnd.intergeo
|
||||||
|
.qbo application/vnd.intu.qbo
|
||||||
|
.qfx application/vnd.intu.qfx
|
||||||
|
.rcprofile application/vnd.ipunplugged.rcprofile
|
||||||
|
.irp application/vnd.irepository.package+xml
|
||||||
|
.xpr application/vnd.is-xpr
|
||||||
|
.fcs application/vnd.isac.fcs
|
||||||
|
.jam application/vnd.jam
|
||||||
|
.rms application/vnd.jcp.javame.midlet-rms
|
||||||
|
.jisp application/vnd.jisp
|
||||||
|
.joda application/vnd.joost.joda-archive
|
||||||
|
.ktz application/vnd.kahootz
|
||||||
|
.karbon application/vnd.kde.karbon
|
||||||
|
.chrt application/vnd.kde.kchart
|
||||||
|
.kfo application/vnd.kde.kformula
|
||||||
|
.flw application/vnd.kde.kivio
|
||||||
|
.kon application/vnd.kde.kontour
|
||||||
|
.kpr application/vnd.kde.kpresenter
|
||||||
|
.ksp application/vnd.kde.kspread
|
||||||
|
.kwd application/vnd.kde.kword
|
||||||
|
.htke application/vnd.kenameaapp
|
||||||
|
.kia application/vnd.kidspiration
|
||||||
|
.kne application/vnd.kinar
|
||||||
|
.skp application/vnd.koan
|
||||||
|
.sse application/vnd.kodak-descriptor
|
||||||
|
.lasxml application/vnd.las.las+xml
|
||||||
|
.lbd application/vnd.llamagraphics.life-balance.desktop
|
||||||
|
.lbe application/vnd.llamagraphics.life-balance.exchange+xml
|
||||||
|
.123 application/vnd.lotus-1-2-3
|
||||||
|
.apr application/vnd.lotus-approach
|
||||||
|
.pre application/vnd.lotus-freelance
|
||||||
|
.nsf application/vnd.lotus-notes
|
||||||
|
.org application/vnd.lotus-organizer
|
||||||
|
.scm application/vnd.lotus-screencam
|
||||||
|
.lwp application/vnd.lotus-wordpro
|
||||||
|
.portpkg application/vnd.macports.portpkg
|
||||||
|
.mvt application/vnd.mapbox-vector-tile
|
||||||
|
.mcd application/vnd.mcd
|
||||||
|
.mc1 application/vnd.medcalcdata
|
||||||
|
.cdkey application/vnd.mediastation.cdkey
|
||||||
|
.mwf application/vnd.mfer
|
||||||
|
.mfm application/vnd.mfmp
|
||||||
|
.flo application/vnd.micrografx.flo
|
||||||
|
.igx application/vnd.micrografx.igx
|
||||||
|
.mif application/vnd.mif
|
||||||
|
.daf application/vnd.mobius.daf
|
||||||
|
.dis application/vnd.mobius.dis
|
||||||
|
.mbk application/vnd.mobius.mbk
|
||||||
|
.mqy application/vnd.mobius.mqy
|
||||||
|
.msl application/vnd.mobius.msl
|
||||||
|
.plc application/vnd.mobius.plc
|
||||||
|
.txf application/vnd.mobius.txf
|
||||||
|
.mpn application/vnd.mophun.application
|
||||||
|
.mpc application/vnd.mophun.certificate
|
||||||
|
.xul application/vnd.mozilla.xul+xml
|
||||||
|
.cil application/vnd.ms-artgalry
|
||||||
|
.cab application/vnd.ms-cab-compressed
|
||||||
|
.xls application/vnd.ms-excel
|
||||||
|
.xlam application/vnd.ms-excel.addin.macroenabled.12
|
||||||
|
.xlsb application/vnd.ms-excel.sheet.binary.macroenabled.12
|
||||||
|
.xlsm application/vnd.ms-excel.sheet.macroenabled.12
|
||||||
|
.xltm application/vnd.ms-excel.template.macroenabled.12
|
||||||
|
.eot application/vnd.ms-fontobject
|
||||||
|
.chm application/vnd.ms-htmlhelp
|
||||||
|
.ims application/vnd.ms-ims
|
||||||
|
.lrm application/vnd.ms-lrm
|
||||||
|
.thmx application/vnd.ms-officetheme
|
||||||
|
.ppt application/vnd.ms-powerpoint
|
||||||
|
.ppam application/vnd.ms-powerpoint.addin.macroenabled.12
|
||||||
|
.pptm application/vnd.ms-powerpoint.presentation.macroenabled.12
|
||||||
|
.sldm application/vnd.ms-powerpoint.slide.macroenabled.12
|
||||||
|
.ppsm application/vnd.ms-powerpoint.slideshow.macroenabled.12
|
||||||
|
.potm application/vnd.ms-powerpoint.template.macroenabled.12
|
||||||
|
.mpp application/vnd.ms-project
|
||||||
|
.docm application/vnd.ms-word.document.macroenabled.12
|
||||||
|
.dotm application/vnd.ms-word.template.macroenabled.12
|
||||||
|
.wps application/vnd.ms-works
|
||||||
|
.wpl application/vnd.ms-wpl
|
||||||
|
.xps application/vnd.ms-xpsdocument
|
||||||
|
.mseq application/vnd.mseq
|
||||||
|
.mus application/vnd.musician
|
||||||
|
.msty application/vnd.muvee.style
|
||||||
|
.taglet application/vnd.mynfc
|
||||||
|
.bdo application/vnd.nato.bindingdataobject+xml
|
||||||
|
.nlu application/vnd.neurolanguage.nlu
|
||||||
|
.ntf application/vnd.nitf
|
||||||
|
.nnd application/vnd.noblenet-directory
|
||||||
|
.nns application/vnd.noblenet-sealer
|
||||||
|
.nnw application/vnd.noblenet-web
|
||||||
|
.ac application/vnd.nokia.n-gage.ac+xml
|
||||||
|
.ngdat application/vnd.nokia.n-gage.data
|
||||||
|
.rpst application/vnd.nokia.radio-preset
|
||||||
|
.rpss application/vnd.nokia.radio-presets
|
||||||
|
.edm application/vnd.novadigm.edm
|
||||||
|
.edx application/vnd.novadigm.edx
|
||||||
|
.ext application/vnd.novadigm.ext
|
||||||
|
.odc application/vnd.oasis.opendocument.chart
|
||||||
|
.otc application/vnd.oasis.opendocument.chart-template
|
||||||
|
.odf application/vnd.oasis.opendocument.formula
|
||||||
|
.odft application/vnd.oasis.opendocument.formula-template
|
||||||
|
.odg application/vnd.oasis.opendocument.graphics
|
||||||
|
.otg application/vnd.oasis.opendocument.graphics-template
|
||||||
|
.odi application/vnd.oasis.opendocument.image
|
||||||
|
.oti application/vnd.oasis.opendocument.image-template
|
||||||
|
.odp application/vnd.oasis.opendocument.presentation
|
||||||
|
.otp application/vnd.oasis.opendocument.presentation-template
|
||||||
|
.ods application/vnd.oasis.opendocument.spreadsheet
|
||||||
|
.ots application/vnd.oasis.opendocument.spreadsheet-template
|
||||||
|
.odt application/vnd.oasis.opendocument.text
|
||||||
|
.odm application/vnd.oasis.opendocument.text-master
|
||||||
|
.ott application/vnd.oasis.opendocument.text-template
|
||||||
|
.oth application/vnd.oasis.opendocument.text-web
|
||||||
|
.xo application/vnd.olpc-sugar
|
||||||
|
.dd2 application/vnd.oma.dd2+xml
|
||||||
|
.obgx application/vnd.openblox.game+xml
|
||||||
|
.osm application/vnd.openstreetmap.data+xml
|
||||||
|
.pptx application/vnd.openxmlformats-officedocument.presentationml.presentation
|
||||||
|
.sldx application/vnd.openxmlformats-officedocument.presentationml.slide
|
||||||
|
.ppsx application/vnd.openxmlformats-officedocument.presentationml.slideshow
|
||||||
|
.potx application/vnd.openxmlformats-officedocument.presentationml.template
|
||||||
|
.xlsx application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||||
|
.xltx application/vnd.openxmlformats-officedocument.spreadsheetml.template
|
||||||
|
.docx application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||||
|
.dotx application/vnd.openxmlformats-officedocument.wordprocessingml.template
|
||||||
|
.mgp application/vnd.osgeo.mapguide.package
|
||||||
|
.dp application/vnd.osgi.dp
|
||||||
|
.esa application/vnd.osgi.subsystem
|
||||||
|
.pdb application/vnd.palm
|
||||||
|
.paw application/vnd.pawaafile
|
||||||
|
.str application/vnd.pg.format
|
||||||
|
.ei6 application/vnd.pg.osasli
|
||||||
|
.efif application/vnd.picsel
|
||||||
|
.wg application/vnd.pmi.widget
|
||||||
|
.plf application/vnd.pocketlearn
|
||||||
|
.pbd application/vnd.powerbuilder6
|
||||||
|
.systemverify application/vnd.pp.systemverify+xml
|
||||||
|
.box application/vnd.previewsystems.box
|
||||||
|
.mgz application/vnd.proteus.magazine
|
||||||
|
.qps application/vnd.publishare-delta-tree
|
||||||
|
.ptid application/vnd.pvi.ptid1
|
||||||
|
.xhtm application/vnd.pwg-xhtml-print+xml
|
||||||
|
.qxd application/vnd.quark.quarkxpress
|
||||||
|
.rar application/vnd.rar
|
||||||
|
.bed application/vnd.realvnc.bed
|
||||||
|
.mxl application/vnd.recordare.musicxml
|
||||||
|
.musicxml application/vnd.recordare.musicxml+xml
|
||||||
|
.cryptonote application/vnd.rig.cryptonote
|
||||||
|
.link66 application/vnd.route66.link66+xml
|
||||||
|
.st application/vnd.sailingtracker.track
|
||||||
|
.see application/vnd.seemail
|
||||||
|
.sema application/vnd.sema
|
||||||
|
.semd application/vnd.semd
|
||||||
|
.semf application/vnd.semf
|
||||||
|
.ifm application/vnd.shana.informed.formdata
|
||||||
|
.itp application/vnd.shana.informed.formtemplate
|
||||||
|
.iif application/vnd.shana.informed.interchange
|
||||||
|
.ipk application/vnd.shana.informed.package
|
||||||
|
.twd application/vnd.simtech-mindmapper
|
||||||
|
.mmf application/vnd.smaf
|
||||||
|
.teacher application/vnd.smart.teacher
|
||||||
|
.fo application/vnd.software602.filler.form+xml
|
||||||
|
.sdkm application/vnd.solent.sdkm+xml
|
||||||
|
.dxp application/vnd.spotfire.dxp
|
||||||
|
.sfs application/vnd.spotfire.sfs
|
||||||
|
.sqlite application/vnd.sqlite3
|
||||||
|
.smzip application/vnd.stepmania.package
|
||||||
|
.sm application/vnd.stepmania.stepchart
|
||||||
|
.wadl application/vnd.sun.wadl+xml
|
||||||
|
.sus application/vnd.sus-calendar
|
||||||
|
.svd application/vnd.svd
|
||||||
|
.xsm application/vnd.syncml+xml
|
||||||
|
.bdm application/vnd.syncml.dm+wbxml
|
||||||
|
.xdm application/vnd.syncml.dm+xml
|
||||||
|
.ddf application/vnd.syncml.dmddf+xml
|
||||||
|
.tao application/vnd.tao.intent-module-archive
|
||||||
|
.pcap application/vnd.tcpdump.pcap
|
||||||
|
.tmo application/vnd.tmobile-livetv
|
||||||
|
.tpt application/vnd.trid.tpt
|
||||||
|
.mxs application/vnd.triscape.mxs
|
||||||
|
.tra application/vnd.trueapp
|
||||||
|
.ufd application/vnd.ufdl
|
||||||
|
.utz application/vnd.uiq.theme
|
||||||
|
.umj application/vnd.umajin
|
||||||
|
.unityweb application/vnd.unity
|
||||||
|
.uoml application/vnd.uoml+xml
|
||||||
|
.vcx application/vnd.vcx
|
||||||
|
.vsd application/vnd.visio
|
||||||
|
.vis application/vnd.visionary
|
||||||
|
.vsf application/vnd.vsf
|
||||||
|
.wbxml application/vnd.wap.wbxml
|
||||||
|
.wmlc application/vnd.wap.wmlc
|
||||||
|
.wmlsc application/vnd.wap.wmlscriptc
|
||||||
|
.wtb application/vnd.webturbo
|
||||||
|
.nbp application/vnd.wolfram.player
|
||||||
|
.wpd application/vnd.wordperfect
|
||||||
|
.wqd application/vnd.wqd
|
||||||
|
.stf application/vnd.wt.stf
|
||||||
|
.xar application/vnd.xara
|
||||||
|
.xfdl application/vnd.xfdl
|
||||||
|
.hvd application/vnd.yamaha.hv-dic
|
||||||
|
.hvs application/vnd.yamaha.hv-script
|
||||||
|
.hvp application/vnd.yamaha.hv-voice
|
||||||
|
.osf application/vnd.yamaha.openscoreformat
|
||||||
|
.osfpvg application/vnd.yamaha.openscoreformat.osfpvg+xml
|
||||||
|
.saf application/vnd.yamaha.smaf-audio
|
||||||
|
.spf application/vnd.yamaha.smaf-phrase
|
||||||
|
.cmp application/vnd.yellowriver-custom-menu
|
||||||
|
.zir application/vnd.zul
|
||||||
|
.zaz application/vnd.zzazz.deck+xml
|
||||||
|
.vxml application/voicexml+xml
|
||||||
|
.wasm application/wasm
|
||||||
|
.wif application/watcherinfo+xml
|
||||||
|
.wgt application/widget
|
||||||
|
.wsdl application/wsdl+xml
|
||||||
|
.wspolicy application/wspolicy+xml
|
||||||
|
.der application/x-x509-ca-cert
|
||||||
|
.xav application/xcap-att+xml
|
||||||
|
.xca application/xcap-caps+xml
|
||||||
|
.xdf application/xcap-diff+xml
|
||||||
|
.xel application/xcap-el+xml
|
||||||
|
.xns application/xcap-ns+xml
|
||||||
|
.xenc application/xenc+xml
|
||||||
|
.xfdf application/xfdf
|
||||||
|
.xhtml application/xhtml+xml
|
||||||
|
.xlf application/xliff+xml
|
||||||
.xml application/xml
|
.xml application/xml
|
||||||
|
.dtd application/xml-dtd
|
||||||
|
.xop application/xop+xml
|
||||||
|
.xsl application/xslt+xml
|
||||||
|
.mxml application/xv+xml
|
||||||
|
.yang application/yang
|
||||||
|
.yin application/yin+xml
|
||||||
.zip application/zip
|
.zip application/zip
|
||||||
|
.3gpp audio/3gpp
|
||||||
|
.adts audio/aac
|
||||||
|
.amr audio/amr
|
||||||
|
.au audio/basic
|
||||||
|
.mka audio/matroska
|
||||||
|
.mxmf audio/mobile-xmf
|
||||||
|
.m4a audio/mp4
|
||||||
.mp3 audio/mpeg
|
.mp3 audio/mpeg
|
||||||
|
.ogg audio/ogg
|
||||||
|
.uva audio/vnd.dece.audio
|
||||||
|
.eol audio/vnd.digital-winds
|
||||||
|
.dra audio/vnd.dra
|
||||||
|
.dts audio/vnd.dts
|
||||||
|
.dtshd audio/vnd.dts.hd
|
||||||
|
.lvp audio/vnd.lucent.voice
|
||||||
|
.pya audio/vnd.ms-playready.media.pya
|
||||||
|
.ecelp4800 audio/vnd.nuera.ecelp4800
|
||||||
|
.ecelp7470 audio/vnd.nuera.ecelp7470
|
||||||
|
.ecelp9600 audio/vnd.nuera.ecelp9600
|
||||||
|
.rip audio/vnd.rip
|
||||||
|
.ttc font/collection
|
||||||
|
.otf font/otf
|
||||||
|
.ttf font/ttf
|
||||||
|
.woff font/woff
|
||||||
|
.woff2 font/woff2
|
||||||
|
.exr image/aces
|
||||||
|
.apng image/apng
|
||||||
|
.avci image/avci
|
||||||
|
.avcs image/avcs
|
||||||
|
.avif image/avif
|
||||||
|
.bmp image/bmp
|
||||||
|
.cgm image/cgm
|
||||||
|
.drle image/dicom-rle
|
||||||
|
.dpx image/dpx
|
||||||
|
.emf image/emf
|
||||||
|
.fits image/fits
|
||||||
|
.g3 image/g3fax
|
||||||
.gif image/gif
|
.gif image/gif
|
||||||
|
.heic image/heic
|
||||||
|
.heics image/heic-sequence
|
||||||
|
.heif image/heif
|
||||||
|
.heifs image/heif-sequence
|
||||||
|
.hej2 image/hej2k
|
||||||
|
.ief image/ief
|
||||||
|
.jaii image/jaii
|
||||||
|
.jais image/jais
|
||||||
|
.jls image/jls
|
||||||
|
.jp2 image/jp2
|
||||||
.jpg image/jpeg
|
.jpg image/jpeg
|
||||||
|
.jph image/jph
|
||||||
|
.jhc image/jphc
|
||||||
|
.jpm image/jpm
|
||||||
|
.jpx image/jpx
|
||||||
|
.jxl image/jxl
|
||||||
|
.jxr image/jxr
|
||||||
|
.jxra image/jxra
|
||||||
|
.jxrs image/jxrs
|
||||||
|
.jxs image/jxs
|
||||||
|
.jxsc image/jxsc
|
||||||
|
.jxsi image/jxsi
|
||||||
|
.jxss image/jxss
|
||||||
|
.ktx image/ktx
|
||||||
|
.ktx2 image/ktx2
|
||||||
.png image/png
|
.png image/png
|
||||||
|
.btif image/prs.btif
|
||||||
|
.pti image/prs.pti
|
||||||
.svg image/svg+xml
|
.svg image/svg+xml
|
||||||
|
.t38 image/t38
|
||||||
|
.tif image/tiff
|
||||||
|
.tfx image/tiff-fx
|
||||||
|
.psd image/vnd.adobe.photoshop
|
||||||
|
.azv image/vnd.airzip.accelerator.azv
|
||||||
|
.facti image/vnd.blockfact.facti
|
||||||
|
.uvi image/vnd.dece.graphic
|
||||||
|
.djvu image/vnd.djvu
|
||||||
|
.sub image/vnd.dvb.subtitle
|
||||||
|
.dwg image/vnd.dwg
|
||||||
|
.dxf image/vnd.dxf
|
||||||
|
.fbs image/vnd.fastbidsheet
|
||||||
|
.fpx image/vnd.fpx
|
||||||
|
.fst image/vnd.fst
|
||||||
|
.mmr image/vnd.fujixerox.edmics-mmr
|
||||||
|
.rlc image/vnd.fujixerox.edmics-rlc
|
||||||
|
.ico image/vnd.microsoft.icon
|
||||||
|
.mdi image/vnd.ms-modi
|
||||||
|
.npx image/vnd.net-fpx
|
||||||
|
.b16 image/vnd.pco.b16
|
||||||
|
.tap image/vnd.tencent.tap
|
||||||
|
.vtf image/vnd.valve.source.texture
|
||||||
|
.wbmp image/vnd.wap.wbmp
|
||||||
|
.xif image/vnd.xiff
|
||||||
|
.pcx image/vnd.zbrush.pcx
|
||||||
.webp image/webp
|
.webp image/webp
|
||||||
|
.wmf image/wmf
|
||||||
|
.disposition-notification message/disposition-notification
|
||||||
|
.u8msg message/global
|
||||||
|
.u8dsn message/global-delivery-status
|
||||||
|
.u8mdn message/global-disposition-notification
|
||||||
|
.u8hdr message/global-headers
|
||||||
|
.eml message/rfc822
|
||||||
|
.wsc message/vnd.wfa.wsc
|
||||||
|
.3mf model/3mf
|
||||||
|
.gltf model/gltf+json
|
||||||
|
.glb model/gltf-binary
|
||||||
|
.igs model/iges
|
||||||
|
.jt model/jt
|
||||||
|
.msh model/mesh
|
||||||
|
.mtl model/mtl
|
||||||
|
.obj model/obj
|
||||||
|
.prc model/prc
|
||||||
|
.step model/step
|
||||||
|
.stpx model/step+xml
|
||||||
|
.stpz model/step+zip
|
||||||
|
.stpxz model/step-xml+zip
|
||||||
|
.stl model/stl
|
||||||
|
.u3d model/u3d
|
||||||
|
.bary model/vnd.bary
|
||||||
|
.cld model/vnd.cld
|
||||||
|
.dae model/vnd.collada+xml
|
||||||
|
.dwf model/vnd.dwf
|
||||||
|
.gdl model/vnd.gdl
|
||||||
|
.gtw model/vnd.gtw
|
||||||
|
.mts model/vnd.mts
|
||||||
|
.ogex model/vnd.opengex
|
||||||
|
.x_b model/vnd.parasolid.transmit.binary
|
||||||
|
.x_t model/vnd.parasolid.transmit.text
|
||||||
|
.pyo model/vnd.pytha.pyox
|
||||||
|
.vds model/vnd.sap.vds
|
||||||
|
.usda model/vnd.usda
|
||||||
|
.usdz model/vnd.usdz+zip
|
||||||
|
.bsp model/vnd.valve.source.compiled-map
|
||||||
|
.vtu model/vnd.vtu
|
||||||
|
.wrl model/vrml
|
||||||
|
.x3db model/x3d+fastinfoset
|
||||||
|
.x3d model/x3d+xml
|
||||||
|
.x3dv model/x3d-vrml
|
||||||
|
.appcache text/cache-manifest
|
||||||
|
.ics text/calendar
|
||||||
|
.css text/css
|
||||||
.csv text/csv
|
.csv text/csv
|
||||||
.html text/html
|
.html text/html
|
||||||
|
.js text/javascript
|
||||||
|
.md text/markdown
|
||||||
|
.n3 text/n3
|
||||||
.txt text/plain
|
.txt text/plain
|
||||||
|
.dsc text/prs.lines.tag
|
||||||
|
.rtx text/richtext
|
||||||
|
.rtf text/rtf
|
||||||
|
.sgml text/sgml
|
||||||
|
.shex text/shex
|
||||||
|
.spdx text/spdx
|
||||||
|
.tsv text/tab-separated-values
|
||||||
|
.t text/troff
|
||||||
|
.ttl text/turtle
|
||||||
|
.uri text/uri-list
|
||||||
|
.vcard text/vcard
|
||||||
|
.curl text/vnd.curl
|
||||||
|
.sub text/vnd.dvb.subtitle
|
||||||
|
.ged text/vnd.familysearch.gedcom
|
||||||
|
.fly text/vnd.fly
|
||||||
|
.flx text/vnd.fmi.flexstor
|
||||||
|
.gv text/vnd.graphviz
|
||||||
|
.3dml text/vnd.in3d.3dml
|
||||||
|
.spot text/vnd.in3d.spot
|
||||||
|
.jad text/vnd.sun.j2me.app-descriptor
|
||||||
|
.wml text/vnd.wap.wml
|
||||||
|
.wmls text/vnd.wap.wmlscript
|
||||||
|
.vtt text/vtt
|
||||||
|
.wgsl text/wgsl
|
||||||
|
.xml text/xml
|
||||||
|
.3gp video/3gpp
|
||||||
|
.3g2 video/3gpp2
|
||||||
|
.h261 video/h261
|
||||||
|
.h263 video/h263
|
||||||
|
.h264 video/h264
|
||||||
|
.m4s video/iso.segment
|
||||||
|
.jpgv video/jpeg
|
||||||
|
.mkv video/matroska
|
||||||
|
.mk3d video/matroska-3d
|
||||||
|
.mj2 video/mj2
|
||||||
|
.ts video/mp2t
|
||||||
.mp4 video/mp4
|
.mp4 video/mp4
|
||||||
|
.mpeg video/mpeg
|
||||||
|
.ogv video/ogg
|
||||||
|
.mov video/quicktime
|
||||||
|
.uvh video/vnd.dece.hd
|
||||||
|
.uvm video/vnd.dece.mobile
|
||||||
|
.uvp video/vnd.dece.pd
|
||||||
|
.uvs video/vnd.dece.sd
|
||||||
|
.uvv video/vnd.dece.video
|
||||||
|
.dvb video/vnd.dvb.file
|
||||||
|
.fvt video/vnd.fvt
|
||||||
|
.mxu video/vnd.mpegurl
|
||||||
|
.pyv video/vnd.ms-playready.media.pyv
|
||||||
|
.uvu video/vnd.uvvu.mp4
|
||||||
|
.viv video/vnd.vivo
|
||||||
|
|||||||
|
@@ -0,0 +1 @@
|
|||||||
|
{ "format": "{number}", "rows": "port.tsv", "key": "number" }
|
||||||
+5890
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
{ "format": "{keyword}", "rows": "protocol.tsv", "key": "keyword", "name": "name" }
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
keyword name number
|
||||||
|
HOPOPT IPv6 Hop-by-Hop Option 0
|
||||||
|
ICMP Internet Control Message 1
|
||||||
|
IGMP Internet Group Management 2
|
||||||
|
GGP Gateway-to-Gateway 3
|
||||||
|
IPv4 IPv4 encapsulation 4
|
||||||
|
ST Stream 5
|
||||||
|
TCP Transmission Control 6
|
||||||
|
CBT CBT 7
|
||||||
|
EGP Exterior Gateway Protocol 8
|
||||||
|
IGP any private interior gateway (used by Cisco for their IGRP) 9
|
||||||
|
BBN-RCC-MON BBN RCC Monitoring 10
|
||||||
|
NVP-II Network Voice Protocol 11
|
||||||
|
PUP PUP 12
|
||||||
|
EMCON EMCON 14
|
||||||
|
XNET Cross Net Debugger 15
|
||||||
|
CHAOS Chaos 16
|
||||||
|
UDP User Datagram 17
|
||||||
|
MUX Multiplexing 18
|
||||||
|
DCN-MEAS DCN Measurement Subsystems 19
|
||||||
|
HMP Host Monitoring 20
|
||||||
|
PRM Packet Radio Measurement 21
|
||||||
|
XNS-IDP XEROX NS IDP 22
|
||||||
|
TRUNK-1 Trunk-1 23
|
||||||
|
TRUNK-2 Trunk-2 24
|
||||||
|
LEAF-1 Leaf-1 25
|
||||||
|
LEAF-2 Leaf-2 26
|
||||||
|
RDP Reliable Data Protocol 27
|
||||||
|
IRTP Internet Reliable Transaction 28
|
||||||
|
ISO-TP4 ISO Transport Protocol Class 4 29
|
||||||
|
NETBLT Bulk Data Transfer Protocol 30
|
||||||
|
MFE-NSP MFE Network Services Protocol 31
|
||||||
|
MERIT-INP MERIT Internodal Protocol 32
|
||||||
|
DCCP Datagram Congestion Control Protocol 33
|
||||||
|
3PC Third Party Connect Protocol 34
|
||||||
|
IDPR Inter-Domain Policy Routing Protocol 35
|
||||||
|
XTP XTP 36
|
||||||
|
DDP Datagram Delivery Protocol 37
|
||||||
|
IDPR-CMTP IDPR Control Message Transport Proto 38
|
||||||
|
TP++ TP++ Transport Protocol 39
|
||||||
|
IL IL Transport Protocol 40
|
||||||
|
IPv6 IPv6 encapsulation 41
|
||||||
|
SDRP Source Demand Routing Protocol 42
|
||||||
|
IPv6-Route Routing Header for IPv6 43
|
||||||
|
IPv6-Frag Fragment Header for IPv6 44
|
||||||
|
IDRP Inter-Domain Routing Protocol 45
|
||||||
|
RSVP Reservation Protocol 46
|
||||||
|
GRE Generic Routing Encapsulation 47
|
||||||
|
DSR Dynamic Source Routing Protocol 48
|
||||||
|
BNA BNA 49
|
||||||
|
ESP Encap Security Payload 50
|
||||||
|
AH Authentication Header 51
|
||||||
|
I-NLSP Integrated Net Layer Security TUBA 52
|
||||||
|
NARP NBMA Address Resolution Protocol 54
|
||||||
|
Min-IPv4 Minimal IPv4 Encapsulation 55
|
||||||
|
TLSP Transport Layer Security Protocol using Kryptonet key management 56
|
||||||
|
SKIP SKIP 57
|
||||||
|
IPv6-ICMP ICMP for IPv6 58
|
||||||
|
IPv6-NoNxt No Next Header for IPv6 59
|
||||||
|
IPv6-Opts Destination Options for IPv6 60
|
||||||
|
CFTP CFTP 62
|
||||||
|
SAT-EXPAK SATNET and Backroom EXPAK 64
|
||||||
|
KRYPTOLAN Kryptolan 65
|
||||||
|
RVD MIT Remote Virtual Disk Protocol 66
|
||||||
|
IPPC Internet Pluribus Packet Core 67
|
||||||
|
SAT-MON SATNET Monitoring 69
|
||||||
|
VISA VISA Protocol 70
|
||||||
|
IPCV Internet Packet Core Utility 71
|
||||||
|
CPNX Computer Protocol Network Executive 72
|
||||||
|
CPHB Computer Protocol Heart Beat 73
|
||||||
|
WSN Wang Span Network 74
|
||||||
|
PVP Packet Video Protocol 75
|
||||||
|
BR-SAT-MON Backroom SATNET Monitoring 76
|
||||||
|
SUN-ND SUN ND PROTOCOL-Temporary 77
|
||||||
|
WB-MON WIDEBAND Monitoring 78
|
||||||
|
WB-EXPAK WIDEBAND EXPAK 79
|
||||||
|
ISO-IP ISO Internet Protocol 80
|
||||||
|
VMTP VMTP 81
|
||||||
|
SECURE-VMTP SECURE-VMTP 82
|
||||||
|
VINES VINES 83
|
||||||
|
IPTM Internet Protocol Traffic Manager 84
|
||||||
|
NSFNET-IGP NSFNET-IGP 85
|
||||||
|
DGP Dissimilar Gateway Protocol 86
|
||||||
|
TCF TCF 87
|
||||||
|
EIGRP EIGRP 88
|
||||||
|
OSPFIGP OSPFIGP 89
|
||||||
|
Sprite-RPC Sprite RPC Protocol 90
|
||||||
|
LARP Locus Address Resolution Protocol 91
|
||||||
|
MTP Multicast Transport Protocol 92
|
||||||
|
AX.25 AX.25 Frames 93
|
||||||
|
IPIP IP-within-IP Encapsulation Protocol 94
|
||||||
|
SCC-SP Semaphore Communications Sec. Pro. 96
|
||||||
|
ETHERIP Ethernet-within-IP Encapsulation 97
|
||||||
|
ENCAP Encapsulation Header 98
|
||||||
|
GMTP GMTP 100
|
||||||
|
IFMP Ipsilon Flow Management Protocol 101
|
||||||
|
PNNI PNNI over IP 102
|
||||||
|
PIM Protocol Independent Multicast 103
|
||||||
|
ARIS ARIS 104
|
||||||
|
SCPS SCPS 105
|
||||||
|
QNX QNX 106
|
||||||
|
A/N Active Networks 107
|
||||||
|
IPComp IP Payload Compression Protocol 108
|
||||||
|
SNP Sitara Networks Protocol 109
|
||||||
|
Compaq-Peer Compaq Peer Protocol 110
|
||||||
|
IPX-in-IP IPX in IP 111
|
||||||
|
VRRP Virtual Router Redundancy Protocol 112
|
||||||
|
PGM PGM Reliable Transport Protocol 113
|
||||||
|
L2TP Layer Two Tunneling Protocol 115
|
||||||
|
DDX D-II Data Exchange (DDX) 116
|
||||||
|
IATP Interactive Agent Transfer Protocol 117
|
||||||
|
STP Schedule Transfer Protocol 118
|
||||||
|
SRP SpectraLink Radio Protocol 119
|
||||||
|
UTI UTI 120
|
||||||
|
SMP Simple Message Protocol 121
|
||||||
|
PTP Performance Transparency Protocol 123
|
||||||
|
ISIS over IPv4 ISIS over IPv4 124
|
||||||
|
FIRE FIRE 125
|
||||||
|
CRTP Combat Radio Transport Protocol 126
|
||||||
|
CRUDP Combat Radio User Datagram 127
|
||||||
|
SSCOPMCE SSCOPMCE 128
|
||||||
|
IPLT IPLT 129
|
||||||
|
SPS Secure Packet Shield 130
|
||||||
|
PIPE Private IP Encapsulation within IP 131
|
||||||
|
SCTP Stream Control Transmission Protocol 132
|
||||||
|
FC Fibre Channel 133
|
||||||
|
RSVP-E2E-IGNORE RSVP-E2E-IGNORE 134
|
||||||
|
Mobility Header Mobility Header 135
|
||||||
|
UDPLite UDPLite 136
|
||||||
|
MPLS-in-IP MPLS-in-IP 137
|
||||||
|
manet MANET Protocols 138
|
||||||
|
HIP Host Identity Protocol 139
|
||||||
|
Shim6 Shim6 Protocol 140
|
||||||
|
WESP Wrapped Encapsulating Security Payload 141
|
||||||
|
ROHC Robust Header Compression 142
|
||||||
|
Ethernet Ethernet 143
|
||||||
|
AGGFRAG AGGFRAG encapsulation payload for ESP 144
|
||||||
|
NSH Network Service Header 145
|
||||||
|
Homa Homa 146
|
||||||
|
BIT-EMU Bit-stream Emulation 147
|
||||||
|
@@ -0,0 +1 @@
|
|||||||
|
{ "format": "{name}", "rows": "territory.tsv", "key": "alpha2", "name": "name" }
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
alpha2 alpha3 calling-code capital country currency flag languages name numeric tld
|
||||||
|
AD AND 376 Andorra la Vella AD EUR 🇦🇩 ca Andorra 020 .ad
|
||||||
|
AE ARE 971 Abu Dhabi AE AED 🇦🇪 ar-AE,fa,en,hi,ur United Arab Emirates 784 .ae
|
||||||
|
AF AFG 93 Kabul AF AFN 🇦🇫 fa-AF,ps,uz-AF,tk Afghanistan 004 .af
|
||||||
|
AG ATG 1-268 St. John's AG XCD 🇦🇬 en-AG Antigua & Barbuda 028 .ag
|
||||||
|
AI AIA 1-264 The Valley GB XCD 🇦🇮 en-AI Anguilla 660 .ai
|
||||||
|
AL ALB 355 Tirana AL ALL 🇦🇱 sq,el Albania 008 .al
|
||||||
|
AM ARM 374 Yerevan AM AMD 🇦🇲 hy Armenia 051 .am
|
||||||
|
AO AGO 244 Luanda AO AOA 🇦🇴 pt-AO Angola 024 .ao
|
||||||
|
AR ARG 54 Buenos Aires AR ARS 🇦🇷 es-AR,en,it,de,fr,gn Argentina 032 .ar
|
||||||
|
AS ASM 1-684 Pago Pago US USD 🇦🇸 en-AS,sm,to American Samoa 016 .as
|
||||||
|
AT AUT 43 Vienna AT EUR 🇦🇹 de-AT,hr,hu,sl Austria 040 .at
|
||||||
|
AU AUS 61 Canberra AU AUD 🇦🇺 en-AU Australia 036 .au
|
||||||
|
AW ABW 297 Oranjestad NL AWG 🇦🇼 nl-AW,pap,es,en Aruba 533 .aw
|
||||||
|
AX ALA 358 Mariehamn FI EUR 🇦🇽 sv-AX Åland Islands 248 .ax
|
||||||
|
AZ AZE 994 Baku AZ AZN 🇦🇿 az,ru,hy Azerbaijan 031 .az
|
||||||
|
BA BIH 387 Sarajevo BA BAM 🇧🇦 bs,hr-BA,sr-BA Bosnia 070 .ba
|
||||||
|
BB BRB 1-246 Bridgetown BB BBD 🇧🇧 en-BB Barbados 052 .bb
|
||||||
|
BD BGD 880 Dhaka BD BDT 🇧🇩 bn-BD,en Bangladesh 050 .bd
|
||||||
|
BE BEL 32 Brussels BE EUR 🇧🇪 nl-BE,fr-BE,de-BE Belgium 056 .be
|
||||||
|
BF BFA 226 Ouagadougou BF XOF 🇧🇫 fr-BF,mos Burkina Faso 854 .bf
|
||||||
|
BG BGR 359 Sofia BG EUR 🇧🇬 bg,tr-BG,rom Bulgaria 100 .bg
|
||||||
|
BH BHR 973 Manama BH BHD 🇧🇭 ar-BH,en,fa,ur Bahrain 048 .bh
|
||||||
|
BI BDI 257 Gitega BI BIF 🇧🇮 fr-BI,rn Burundi 108 .bi
|
||||||
|
BJ BEN 229 Porto-Novo BJ XOF 🇧🇯 fr-BJ Benin 204 .bj
|
||||||
|
BL BLM 590 Gustavia FR EUR 🇧🇱 fr St. Barthélemy 652 .gp
|
||||||
|
BM BMU 1-441 Hamilton GB BMD 🇧🇲 en-BM,pt Bermuda 060 .bm
|
||||||
|
BN BRN 673 Bandar Seri Begawan BN BND 🇧🇳 ms-BN,en-BN Brunei 096 .bn
|
||||||
|
BO BOL 591 Sucre BO BOB 🇧🇴 es-BO,qu,ay Bolivia 068 .bo
|
||||||
|
BR BRA 55 Brasilia BR BRL 🇧🇷 pt-BR,es,en,fr Brazil 076 .br
|
||||||
|
BS BHS 1-242 Nassau BS BSD 🇧🇸 en-BS Bahamas 044 .bs
|
||||||
|
BT BTN 975 Thimphu BT INR 🇧🇹 dz Bhutan 064 .bt
|
||||||
|
BW BWA 267 Gaborone BW BWP 🇧🇼 en-BW,tn-BW Botswana 072 .bw
|
||||||
|
BY BLR 375 Minsk BY BYN 🇧🇾 be,ru Belarus 112 .by
|
||||||
|
BZ BLZ 501 Belmopan BZ BZD 🇧🇿 en-BZ,es Belize 084 .bz
|
||||||
|
CA CAN 1 Ottawa CA CAD 🇨🇦 en-CA,fr-CA,iu Canada 124 .ca
|
||||||
|
CC CCK 61 West Island AU AUD 🇨🇨 ms-CC,en Cocos Islands 166 .cc
|
||||||
|
CD COD 243 Kinshasa CD CDF 🇨🇩 fr-CD,ln,ktu,kg,sw,lua Congo - Kinshasa 180 .cd
|
||||||
|
CF CAF 236 Bangui CF XAF 🇨🇫 fr-CF,sg,ln,kg Central African Republic 140 .cf
|
||||||
|
CG COG 242 Brazzaville CG XAF 🇨🇬 fr-CG,kg,ln-CG Congo - Brazzaville 178 .cg
|
||||||
|
CH CHE 41 Bern CH CHF 🇨🇭 de-CH,fr-CH,it-CH,rm Switzerland 756 .ch
|
||||||
|
CI CIV 225 Yamoussoukro CI XOF 🇨🇮 fr-CI Côte d’Ivoire 384 .ci
|
||||||
|
CK COK 682 Avarua NZ NZD 🇨🇰 en-CK,mi Cook Islands 184 .ck
|
||||||
|
CL CHL 56 Santiago CL CLP 🇨🇱 es-CL Chile 152 .cl
|
||||||
|
CM CMR 237 Yaounde CM XAF 🇨🇲 en-CM,fr-CM Cameroon 120 .cm
|
||||||
|
CN CHN 86 Beijing CN CNY 🇨🇳 zh-CN,yue,wuu,dta,ug,za China 156 .cn
|
||||||
|
CO COL 57 Bogota CO COP 🇨🇴 es-CO Colombia 170 .co
|
||||||
|
CR CRI 506 San Jose CR CRC 🇨🇷 es-CR,en Costa Rica 188 .cr
|
||||||
|
CU CUB 53 Havana CU CUP 🇨🇺 es-CU,pap Cuba 192 .cu
|
||||||
|
CV CPV 238 Praia CV CVE 🇨🇻 pt-CV Cape Verde 132 .cv
|
||||||
|
CW CUW 599 Willemstad NL XCG 🇨🇼 nl,pap Curaçao 531 .cw
|
||||||
|
CX CXR 61 Flying Fish Cove AU AUD 🇨🇽 en,zh,ms-CX Christmas Island 162 .cx
|
||||||
|
CY CYP 357 Nicosia CY EUR 🇨🇾 el-CY,tr-CY,en Cyprus 196 .cy
|
||||||
|
CZ CZE 420 Prague CZ CZK 🇨🇿 cs,sk Czechia 203 .cz
|
||||||
|
DE DEU 49 Berlin DE EUR 🇩🇪 de Germany 276 .de
|
||||||
|
DJ DJI 253 Djibouti DJ DJF 🇩🇯 fr-DJ,ar,so-DJ,aa Djibouti 262 .dj
|
||||||
|
DK DNK 45 Copenhagen DK DKK 🇩🇰 da-DK,en,fo,de-DK Denmark 208 .dk
|
||||||
|
DM DMA 1-767 Roseau DM XCD 🇩🇲 en-DM Dominica 212 .dm
|
||||||
|
DO DOM 1-809 Santo Domingo DO DOP 🇩🇴 es-DO Dominican Republic 214 .do
|
||||||
|
DZ DZA 213 Algiers DZ DZD 🇩🇿 ar-DZ Algeria 012 .dz
|
||||||
|
EC ECU 593 Quito EC USD 🇪🇨 es-EC Ecuador 218 .ec
|
||||||
|
EE EST 372 Tallinn EE EUR 🇪🇪 et,ru Estonia 233 .ee
|
||||||
|
EG EGY 20 Cairo EG EGP 🇪🇬 ar-EG,en,fr Egypt 818 .eg
|
||||||
|
EH ESH 212 El-Aaiun EH MAD 🇪🇭 ar,mey Western Sahara 732 .eh
|
||||||
|
ER ERI 291 Asmara ER ERN 🇪🇷 aa-ER,ar,tig,kun,ti-ER Eritrea 232 .er
|
||||||
|
ES ESP 34 Madrid ES EUR 🇪🇸 es-ES,ca,gl,eu,oc Spain 724 .es
|
||||||
|
ET ETH 251 Addis Ababa ET ETB 🇪🇹 am,en-ET,om-ET,ti-ET,so-ET,sid Ethiopia 231 .et
|
||||||
|
FI FIN 358 Helsinki FI EUR 🇫🇮 fi-FI,sv-FI,smn Finland 246 .fi
|
||||||
|
FJ FJI 679 Suva FJ FJD 🇫🇯 en-FJ,fj Fiji 242 .fj
|
||||||
|
FK FLK 500 Stanley GB FKP 🇫🇰 en-FK Falkland Islands 238 .fk
|
||||||
|
FM FSM 691 Palikir FM USD 🇫🇲 en-FM,chk,pon,yap,kos,uli,woe,nkr,kpg Micronesia 583 .fm
|
||||||
|
FO FRO 298 Torshavn DK DKK 🇫🇴 fo,da-FO Faroe Islands 234 .fo
|
||||||
|
FR FRA 33 Paris FR EUR 🇫🇷 fr-FR,frp,br,co,ca,eu,oc France 250 .fr
|
||||||
|
GA GAB 241 Libreville GA XAF 🇬🇦 fr-GA Gabon 266 .ga
|
||||||
|
GB GBR 44 London GB GBP 🇬🇧 en-GB,cy-GB,gd UK 826 .uk
|
||||||
|
GD GRD 1-473 St. George's GD XCD 🇬🇩 en-GD Grenada 308 .gd
|
||||||
|
GE GEO 995 Tbilisi GE GEL 🇬🇪 ka,ru,hy,az Georgia 268 .ge
|
||||||
|
GF GUF 594 Cayenne FR EUR 🇬🇫 fr-GF French Guiana 254 .gf
|
||||||
|
GG GGY 44 St Peter Port GB GBP 🇬🇬 en,nrf Guernsey 831 .gg
|
||||||
|
GH GHA 233 Accra GH GHS 🇬🇭 en-GH,ak,ee,tw Ghana 288 .gh
|
||||||
|
GI GIB 350 Gibraltar GB GIP 🇬🇮 en-GI,es,it,pt Gibraltar 292 .gi
|
||||||
|
GL GRL 299 Nuuk DK DKK 🇬🇱 kl,da-GL,en Greenland 304 .gl
|
||||||
|
GM GMB 220 Banjul GM GMD 🇬🇲 en-GM,mnk,wof,wo,ff Gambia 270 .gm
|
||||||
|
GN GIN 224 Conakry GN GNF 🇬🇳 fr-GN Guinea 324 .gn
|
||||||
|
GP GLP 590 Basse-Terre FR EUR 🇬🇵 fr-GP Guadeloupe 312 .gp
|
||||||
|
GQ GNQ 240 Ciudad de la Paz GQ XAF 🇬🇶 es-GQ,fr,pt Equatorial Guinea 226 .gq
|
||||||
|
GR GRC 30 Athens GR EUR 🇬🇷 el-GR,en,fr Greece 300 .gr
|
||||||
|
GT GTM 502 Guatemala City GT GTQ 🇬🇹 es-GT Guatemala 320 .gt
|
||||||
|
GU GUM 1-671 Hagatna US USD 🇬🇺 en-GU,ch-GU Guam 316 .gu
|
||||||
|
GW GNB 245 Bissau GW XOF 🇬🇼 pt-GW,pov Guinea-Bissau 624 .gw
|
||||||
|
GY GUY 592 Georgetown GY GYD 🇬🇾 en-GY Guyana 328 .gy
|
||||||
|
HK HKG 852 Hong Kong CN HKD 🇭🇰 zh-HK,yue,zh,en Hong Kong 344 .hk
|
||||||
|
HN HND 504 Tegucigalpa HN HNL 🇭🇳 es-HN,cab,miq Honduras 340 .hn
|
||||||
|
HR HRV 385 Zagreb HR EUR 🇭🇷 hr-HR,sr Croatia 191 .hr
|
||||||
|
HT HTI 509 Port-au-Prince HT HTG 🇭🇹 ht,fr-HT Haiti 332 .ht
|
||||||
|
HU HUN 36 Budapest HU HUF 🇭🇺 hu-HU Hungary 348 .hu
|
||||||
|
ID IDN 62 Jakarta ID IDR 🇮🇩 id,en,nl,jv Indonesia 360 .id
|
||||||
|
IE IRL 353 Dublin IE EUR 🇮🇪 en-IE,ga-IE Ireland 372 .ie
|
||||||
|
IL ISR 972 Jerusalem IL ILS 🇮🇱 he,ar-IL,en-IL Israel 376 .il
|
||||||
|
IM IMN 44 Douglas GB GBP 🇮🇲 en,gv Isle of Man 833 .im
|
||||||
|
IN IND 91 New Delhi IN INR 🇮🇳 en-IN,hi,bn,te,mr,ta,ur,gu,kn,ml,or,pa,as,bh,sat,ks,ne,sd,kok,doi,mni,sit,sa,fr,lus,inc India 356 .in
|
||||||
|
IO IOT 246 Diego Garcia GB USD 🇮🇴 en-IO British Indian Ocean Territory 086 .io
|
||||||
|
IQ IRQ 964 Baghdad IQ IQD 🇮🇶 ar-IQ,ku,hy Iraq 368 .iq
|
||||||
|
IR IRN 98 Tehran IR IRR 🇮🇷 fa-IR,ku Iran 364 .ir
|
||||||
|
IS ISL 354 Reykjavik IS ISK 🇮🇸 is,en,de,da,sv,no Iceland 352 .is
|
||||||
|
IT ITA 39 Rome IT EUR 🇮🇹 it-IT,de-IT,fr-IT,sc,ca,co,sl Italy 380 .it
|
||||||
|
JE JEY 44 Saint Helier GB GBP 🇯🇪 en,fr,nrf Jersey 832 .je
|
||||||
|
JM JAM 1-876 Kingston JM JMD 🇯🇲 en-JM Jamaica 388 .jm
|
||||||
|
JO JOR 962 Amman JO JOD 🇯🇴 ar-JO,en Jordan 400 .jo
|
||||||
|
JP JPN 81 Tokyo JP JPY 🇯🇵 ja Japan 392 .jp
|
||||||
|
KE KEN 254 Nairobi KE KES 🇰🇪 en-KE,sw-KE Kenya 404 .ke
|
||||||
|
KG KGZ 996 Bishkek KG KGS 🇰🇬 ky,uz,ru Kyrgyzstan 417 .kg
|
||||||
|
KH KHM 855 Phnom Penh KH KHR 🇰🇭 km,fr,en Cambodia 116 .kh
|
||||||
|
KI KIR 686 Tarawa KI AUD 🇰🇮 en-KI,gil Kiribati 296 .ki
|
||||||
|
KM COM 269 Moroni KM KMF 🇰🇲 ar,fr-KM Comoros 174 .km
|
||||||
|
KN KNA 1-869 Basseterre KN XCD 🇰🇳 en-KN St. Kitts & Nevis 659 .kn
|
||||||
|
KP PRK 850 Pyongyang KP KPW 🇰🇵 ko-KP North Korea 408 .kp
|
||||||
|
KR KOR 82 Seoul KR KRW 🇰🇷 ko-KR,en South Korea 410 .kr
|
||||||
|
KW KWT 965 Kuwait City KW KWD 🇰🇼 ar-KW,en Kuwait 414 .kw
|
||||||
|
KY CYM 1-345 George Town GB KYD 🇰🇾 en-KY Cayman Islands 136 .ky
|
||||||
|
KZ KAZ 7 Nur-Sultan KZ KZT 🇰🇿 kk,ru Kazakhstan 398 .kz
|
||||||
|
LA LAO 856 Vientiane LA LAK 🇱🇦 lo,fr,en Laos 418 .la
|
||||||
|
LB LBN 961 Beirut LB LBP 🇱🇧 ar-LB,fr-LB,en,hy Lebanon 422 .lb
|
||||||
|
LC LCA 1-758 Castries LC XCD 🇱🇨 en-LC St. Lucia 662 .lc
|
||||||
|
LI LIE 423 Vaduz LI CHF 🇱🇮 de-LI Liechtenstein 438 .li
|
||||||
|
LK LKA 94 Colombo LK LKR 🇱🇰 si,ta,en Sri Lanka 144 .lk
|
||||||
|
LR LBR 231 Monrovia LR LRD 🇱🇷 en-LR Liberia 430 .lr
|
||||||
|
LS LSO 266 Maseru LS LSL 🇱🇸 en-LS,st,zu,xh Lesotho 426 .ls
|
||||||
|
LT LTU 370 Vilnius LT EUR 🇱🇹 lt,ru,pl Lithuania 440 .lt
|
||||||
|
LU LUX 352 Luxembourg LU EUR 🇱🇺 lb,de-LU,fr-LU Luxembourg 442 .lu
|
||||||
|
LV LVA 371 Riga LV EUR 🇱🇻 lv,ru,lt Latvia 428 .lv
|
||||||
|
LY LBY 218 Tripoli LY LYD 🇱🇾 ar-LY,it,en Libya 434 .ly
|
||||||
|
MA MAR 212 Rabat MA MAD 🇲🇦 ar-MA,ber,fr Morocco 504 .ma
|
||||||
|
MC MCO 377 Monaco MC EUR 🇲🇨 fr-MC,en,it Monaco 492 .mc
|
||||||
|
MD MDA 373 Chisinau MD MDL 🇲🇩 ro,ru,gag,tr Moldova 498 .md
|
||||||
|
ME MNE 382 Podgorica ME EUR 🇲🇪 sr,hu,bs,sq,hr,rom Montenegro 499 .me
|
||||||
|
MF MAF 590 Marigot FR EUR 🇲🇫 fr St. Martin 663 .gp
|
||||||
|
MG MDG 261 Antananarivo MG MGA 🇲🇬 fr-MG,mg Madagascar 450 .mg
|
||||||
|
MH MHL 692 Majuro MH USD 🇲🇭 mh,en-MH Marshall Islands 584 .mh
|
||||||
|
MK MKD 389 Skopje MK MKD 🇲🇰 mk,sq,tr,rmm,sr North Macedonia 807 .mk
|
||||||
|
ML MLI 223 Bamako ML XOF 🇲🇱 fr-ML,bm Mali 466 .ml
|
||||||
|
MM MMR 95 Nay Pyi Taw MM MMK 🇲🇲 my Myanmar 104 .mm
|
||||||
|
MN MNG 976 Ulaanbaatar MN MNT 🇲🇳 mn,ru Mongolia 496 .mn
|
||||||
|
MO MAC 853 Macao CN MOP 🇲🇴 zh,zh-MO,pt Macao 446 .mo
|
||||||
|
MP MNP 1-670 Saipan US USD 🇲🇵 fil,tl,zh,ch-MP,en-MP Northern Mariana Islands 580 .mp
|
||||||
|
MQ MTQ 596 Fort-de-France FR EUR 🇲🇶 fr-MQ Martinique 474 .mq
|
||||||
|
MR MRT 222 Nouakchott MR MRU 🇲🇷 ar-MR,fuc,snk,fr,mey,wo Mauritania 478 .mr
|
||||||
|
MS MSR 1-664 Plymouth GB XCD 🇲🇸 en-MS Montserrat 500 .ms
|
||||||
|
MT MLT 356 Valletta MT EUR 🇲🇹 mt,en-MT Malta 470 .mt
|
||||||
|
MU MUS 230 Port Louis MU MUR 🇲🇺 en-MU,bho,fr Mauritius 480 .mu
|
||||||
|
MV MDV 960 Male MV MVR 🇲🇻 dv,en Maldives 462 .mv
|
||||||
|
MW MWI 265 Lilongwe MW MWK 🇲🇼 ny,yao,tum,swk Malawi 454 .mw
|
||||||
|
MX MEX 52 Mexico City MX MXN 🇲🇽 es-MX Mexico 484 .mx
|
||||||
|
MY MYS 60 Kuala Lumpur MY MYR 🇲🇾 ms-MY,en,zh,ta,te,ml,pa,th Malaysia 458 .my
|
||||||
|
MZ MOZ 258 Maputo MZ MZN 🇲🇿 pt-MZ,vmw Mozambique 508 .mz
|
||||||
|
NA NAM 264 Windhoek NA NAD 🇳🇦 en-NA,af,de,hz,naq Namibia 516 .na
|
||||||
|
NC NCL 687 Noumea FR XPF 🇳🇨 fr-NC New Caledonia 540 .nc
|
||||||
|
NE NER 227 Niamey NE XOF 🇳🇪 fr-NE,ha,kr,dje Niger 562 .ne
|
||||||
|
NF NFK 672 Kingston AU AUD 🇳🇫 en-NF Norfolk Island 574 .nf
|
||||||
|
NG NGA 234 Abuja NG NGN 🇳🇬 en-NG,ha,yo,ig,ff Nigeria 566 .ng
|
||||||
|
NI NIC 505 Managua NI NIO 🇳🇮 es-NI,en Nicaragua 558 .ni
|
||||||
|
NL NLD 31 Amsterdam NL EUR 🇳🇱 nl-NL,fy-NL Netherlands 528 .nl
|
||||||
|
NO NOR 47 Oslo NO NOK 🇳🇴 no,nb,nn,se,fi Norway 578 .no
|
||||||
|
NP NPL 977 Kathmandu NP NPR 🇳🇵 ne,en Nepal 524 .np
|
||||||
|
NR NRU 674 Yaren NR AUD 🇳🇷 na,en-NR Nauru 520 .nr
|
||||||
|
NU NIU 683 Alofi NZ NZD 🇳🇺 niu,en-NU Niue 570 .nu
|
||||||
|
NZ NZL 64 Wellington NZ NZD 🇳🇿 en-NZ,mi New Zealand 554 .nz
|
||||||
|
OM OMN 968 Muscat OM OMR 🇴🇲 ar-OM,en,bal,ur Oman 512 .om
|
||||||
|
PA PAN 507 Panama City PA PAB 🇵🇦 es-PA,en Panama 591 .pa
|
||||||
|
PE PER 51 Lima PE PEN 🇵🇪 es-PE,qu,ay Peru 604 .pe
|
||||||
|
PF PYF 689 Papeete FR XPF 🇵🇫 fr-PF,ty French Polynesia 258 .pf
|
||||||
|
PG PNG 675 Port Moresby PG PGK 🇵🇬 en-PG,ho,meu,tpi Papua New Guinea 598 .pg
|
||||||
|
PH PHL 63 Manila PH PHP 🇵🇭 tl,en-PH,fil,ceb,ilo,hil,war,pam,bik,bcl,pag,mrw,tsg,mdh,cbk,krj,sgd,msb,akl,ibg,yka,mta,abx Philippines 608 .ph
|
||||||
|
PK PAK 92 Islamabad PK PKR 🇵🇰 ur-PK,en-PK,pa,sd,ps,brh Pakistan 586 .pk
|
||||||
|
PL POL 48 Warsaw PL PLN 🇵🇱 pl Poland 616 .pl
|
||||||
|
PM SPM 508 Saint-Pierre FR EUR 🇵🇲 fr-PM St. Pierre & Miquelon 666 .pm
|
||||||
|
PN PCN 870 Adamstown GB NZD 🇵🇳 en-PN Pitcairn 612 .pn
|
||||||
|
PR PRI 1 San Juan US USD 🇵🇷 en-PR,es-PR Puerto Rico 630 .pr
|
||||||
|
PT PRT 351 Lisbon PT EUR 🇵🇹 pt-PT,mwl Portugal 620 .pt
|
||||||
|
PW PLW 680 Melekeok PW USD 🇵🇼 pau,sov,en-PW,tox,ja,fil,zh Palau 585 .pw
|
||||||
|
PY PRY 595 Asuncion PY PYG 🇵🇾 es-PY,gn Paraguay 600 .py
|
||||||
|
QA QAT 974 Doha QA QAR 🇶🇦 ar-QA,es Qatar 634 .qa
|
||||||
|
RE REU 262 Saint-Denis FR EUR 🇷🇪 fr-RE Réunion 638 .re
|
||||||
|
RO ROU 40 Bucharest RO RON 🇷🇴 ro,hu,rom Romania 642 .ro
|
||||||
|
RS SRB 381 Belgrade RS RSD 🇷🇸 sr,hu,bs,rom Serbia 688 .rs
|
||||||
|
RU RUS 7 Moscow RU RUB 🇷🇺 ru,tt,xal,cau,ady,kv,ce,tyv,cv,udm,tut,mns,bua,myv,mdf,chm,ba,inh,kbd,krc,av,sah,nog Russia 643 .ru
|
||||||
|
RW RWA 250 Kigali RW RWF 🇷🇼 rw,en-RW,fr-RW,sw Rwanda 646 .rw
|
||||||
|
SA SAU 966 Riyadh SA SAR 🇸🇦 ar-SA Saudi Arabia 682 .sa
|
||||||
|
SB SLB 677 Honiara SB SBD 🇸🇧 en-SB,tpi Solomon Islands 090 .sb
|
||||||
|
SC SYC 248 Victoria SC SCR 🇸🇨 en-SC,fr-SC Seychelles 690 .sc
|
||||||
|
SD SDN 249 Khartoum SD SDG 🇸🇩 ar-SD,en,fia Sudan 729 .sd
|
||||||
|
SE SWE 46 Stockholm SE SEK 🇸🇪 sv-SE,se,sma,fi-SE Sweden 752 .se
|
||||||
|
SG SGP 65 Singapore SG SGD 🇸🇬 cmn,en-SG,ms-SG,ta-SG,zh-SG Singapore 702 .sg
|
||||||
|
SH SHN 290 Jamestown GB SHP 🇸🇭 en-SH St. Helena 654 .sh
|
||||||
|
SI SVN 386 Ljubljana SI EUR 🇸🇮 sl,sh Slovenia 705 .si
|
||||||
|
SJ SJM 47 Longyearbyen NO NOK 🇸🇯 no,ru Svalbard & Jan Mayen 744 .sj
|
||||||
|
SK SVK 421 Bratislava SK EUR 🇸🇰 sk,hu Slovakia 703 .sk
|
||||||
|
SL SLE 232 Freetown SL SLE 🇸🇱 en-SL,men,tem Sierra Leone 694 .sl
|
||||||
|
SM SMR 378 San Marino SM EUR 🇸🇲 it-SM San Marino 674 .sm
|
||||||
|
SN SEN 221 Dakar SN XOF 🇸🇳 fr-SN,wo,fuc,mnk Senegal 686 .sn
|
||||||
|
SO SOM 252 Mogadishu SO SOS 🇸🇴 so-SO,ar-SO,it,en-SO Somalia 706 .so
|
||||||
|
SR SUR 597 Paramaribo SR SRD 🇸🇷 nl-SR,en,srn,hns,jv Suriname 740 .sr
|
||||||
|
SS SSD 211 Juba SS SSP 🇸🇸 en South Sudan 728 .ss
|
||||||
|
ST STP 239 Sao Tome ST STN 🇸🇹 pt-ST São Tomé & Príncipe 678 .st
|
||||||
|
SV SLV 503 San Salvador SV SVC 🇸🇻 es-SV El Salvador 222 .sv
|
||||||
|
SX SXM 1-721 Philipsburg NL XCG 🇸🇽 nl,en Sint Maarten 534 .sx
|
||||||
|
SY SYR 963 Damascus SY SYP 🇸🇾 ar-SY,ku,hy,arc,fr,en Syria 760 .sy
|
||||||
|
SZ SWZ 268 Mbabane SZ SZL 🇸🇿 en-SZ,ss-SZ Eswatini 748 .sz
|
||||||
|
TC TCA 1-649 Cockburn Town GB USD 🇹🇨 en-TC Turks & Caicos Islands 796 .tc
|
||||||
|
TD TCD 235 N'Djamena TD XAF 🇹🇩 fr-TD,ar-TD,sre Chad 148 .td
|
||||||
|
TF ATF 262 Port-aux-Francais FR EUR 🇹🇫 fr French Southern Territories 260 .tf
|
||||||
|
TG TGO 228 Lome TG XOF 🇹🇬 fr-TG,ee,hna,kbp,dag,ha Togo 768 .tg
|
||||||
|
TH THA 66 Bangkok TH THB 🇹🇭 th,en Thailand 764 .th
|
||||||
|
TJ TJK 992 Dushanbe TJ TJS 🇹🇯 tg,ru Tajikistan 762 .tj
|
||||||
|
TL TLS 670 Dili TL USD 🇹🇱 tet,pt-TL,id,en Timor-Leste 626 .tl
|
||||||
|
TM TKM 993 Ashgabat TM TMT 🇹🇲 tk,ru,uz Turkmenistan 795 .tm
|
||||||
|
TN TUN 216 Tunis TN TND 🇹🇳 ar-TN,fr Tunisia 788 .tn
|
||||||
|
TO TON 676 Nuku'alofa TO TOP 🇹🇴 to,en-TO Tonga 776 .to
|
||||||
|
TR TUR 90 Ankara TR TRY 🇹🇷 tr-TR,ku,diq,az,av Türkiye 792 .tr
|
||||||
|
TT TTO 1-868 Port of Spain TT TTD 🇹🇹 en-TT,hns,fr,es,zh Trinidad & Tobago 780 .tt
|
||||||
|
TV TUV 688 Funafuti TV AUD 🇹🇻 tvl,en,sm,gil Tuvalu 798 .tv
|
||||||
|
TW TWN 886 Taipei TW TWD 🇹🇼 zh-TW,zh,nan,hak Taiwan 158 .tw
|
||||||
|
TZ TZA 255 Dodoma TZ TZS 🇹🇿 sw-TZ,en,ar Tanzania 834 .tz
|
||||||
|
UA UKR 380 Kyiv UA UAH 🇺🇦 uk,ru-UA,rom,pl,hu Ukraine 804 .ua
|
||||||
|
UG UGA 256 Kampala UG UGX 🇺🇬 en-UG,lg,sw,ar Uganda 800 .ug
|
||||||
|
US USA 1 Washington US USD 🇺🇸 en-US,es-US,haw,fr US 840 .us
|
||||||
|
UY URY 598 Montevideo UY UYU 🇺🇾 es-UY Uruguay 858 .uy
|
||||||
|
UZ UZB 998 Tashkent UZ UZS 🇺🇿 uz,ru,tg Uzbekistan 860 .uz
|
||||||
|
VA VAT 39 Vatican City VA EUR 🇻🇦 la,it,fr Vatican City 336 .va
|
||||||
|
VC VCT 1-784 Kingstown VC XCD 🇻🇨 en-VC,fr St. Vincent & Grenadines 670 .vc
|
||||||
|
VE VEN 58 Caracas VE VES 🇻🇪 es-VE Venezuela 862 .ve
|
||||||
|
VG VGB 1-284 Road Town GB USD 🇻🇬 en-VG British Virgin Islands 092 .vg
|
||||||
|
VI VIR 1-340 Charlotte Amalie US USD 🇻🇮 en-VI U.S. Virgin Islands 850 .vi
|
||||||
|
VN VNM 84 Hanoi VN VND 🇻🇳 vi,en,fr,zh,km Vietnam 704 .vn
|
||||||
|
VU VUT 678 Port Vila VU VUV 🇻🇺 bi,en-VU,fr-VU Vanuatu 548 .vu
|
||||||
|
WF WLF 681 Mata Utu FR XPF 🇼🇫 wls,fud,fr-WF Wallis & Futuna 876 .wf
|
||||||
|
WS WSM 685 Apia WS WST 🇼🇸 sm,en-WS Samoa 882 .ws
|
||||||
|
YE YEM 967 Sanaa YE YER 🇾🇪 ar-YE Yemen 887 .ye
|
||||||
|
YT MYT 262 Mamoudzou FR EUR 🇾🇹 fr-YT Mayotte 175 .yt
|
||||||
|
ZA ZAF 27 Pretoria ZA ZAR 🇿🇦 zu,xh,af,nso,en-ZA,tn,st,ts,ss,ve,nr South Africa 710 .za
|
||||||
|
ZM ZMB 260 Lusaka ZM ZMW 🇿🇲 en-ZM,bem,loz,lun,lue,ny,toi Zambia 894 .zm
|
||||||
|
ZW ZWE 263 Harare ZW ZWG 🇿🇼 en-ZW,sn,nr,nd Zimbabwe 716 .zw
|
||||||
|
@@ -1 +1 @@
|
|||||||
["UTC", "Africa/Cairo", "Africa/Johannesburg", "America/Chicago", "America/Denver", "America/Los_Angeles", "America/Mexico_City", "America/New_York", "America/Sao_Paulo", "America/Toronto", "Asia/Dubai", "Asia/Hong_Kong", "Asia/Kolkata", "Asia/Shanghai", "Asia/Singapore", "Asia/Tokyo", "Australia/Sydney", "Europe/Amsterdam", "Europe/Berlin", "Europe/London", "Europe/Madrid", "Europe/Moscow", "Europe/Oslo", "Europe/Paris", "Europe/Stockholm", "Europe/Warsaw", "Pacific/Auckland"]
|
{ "format": "{zone}", "rows": "timezone.tsv", "key": "zone", "parent": "territory", "weight": "weight" }
|
||||||
|
|||||||
@@ -0,0 +1,402 @@
|
|||||||
|
offset territory weight zone
|
||||||
|
+00:00 CI 17496811 Africa/Abidjan
|
||||||
|
+00:00 GH 11277813 Africa/Accra
|
||||||
|
+03:00 ET 18690487 Africa/Addis_Ababa
|
||||||
|
+01:00 DZ 20111539 Africa/Algiers
|
||||||
|
+03:00 ER 798575 Africa/Asmara
|
||||||
|
+00:00 ML 7673493 Africa/Bamako
|
||||||
|
+01:00 CF 2582076 Africa/Bangui
|
||||||
|
+00:00 GM 1305981 Africa/Banjul
|
||||||
|
+00:00 GW 904776 Africa/Bissau
|
||||||
|
+02:00 MW 3030905 Africa/Blantyre
|
||||||
|
+01:00 CG 3748808 Africa/Brazzaville
|
||||||
|
+02:00 BI 1251675 Africa/Bujumbura
|
||||||
|
+02:00 EG 47242427 Africa/Cairo
|
||||||
|
+00:00 MA 22808531 Africa/Casablanca
|
||||||
|
+01:00 ES 85985 Africa/Ceuta
|
||||||
|
+00:00 GN 6679241 Africa/Conakry
|
||||||
|
+00:00 SN 10055356 Africa/Dakar
|
||||||
|
+03:00 TZ 21350525 Africa/Dar_es_Salaam
|
||||||
|
+03:00 DJ 1316108 Africa/Djibouti
|
||||||
|
+01:00 CM 8330801 Africa/Douala
|
||||||
|
+00:00 EH 374107 Africa/El_Aaiun
|
||||||
|
+00:00 SL 1752243 Africa/Freetown
|
||||||
|
+02:00 BW 1262880 Africa/Gaborone
|
||||||
|
+02:00 ZW 4508424 Africa/Harare
|
||||||
|
+02:00 ZA 47939803 Africa/Johannesburg
|
||||||
|
+02:00 SS 1679674 Africa/Juba
|
||||||
|
+03:00 UG 9664318 Africa/Kampala
|
||||||
|
+02:00 SD 11781926 Africa/Khartoum
|
||||||
|
+02:00 RW 2910190 Africa/Kigali
|
||||||
|
+01:00 CD 20651243 Africa/Kinshasa
|
||||||
|
+01:00 NG 75725576 Africa/Lagos
|
||||||
|
+01:00 GA 1731692 Africa/Libreville
|
||||||
|
+00:00 TG 3089219 Africa/Lome
|
||||||
|
+01:00 AO 23567341 Africa/Luanda
|
||||||
|
+02:00 CD 16660579 Africa/Lubumbashi
|
||||||
|
+02:00 ZM 8422414 Africa/Lusaka
|
||||||
|
+01:00 GQ 355840 Africa/Malabo
|
||||||
|
+02:00 MZ 8598836 Africa/Maputo
|
||||||
|
+02:00 LS 739682 Africa/Maseru
|
||||||
|
+02:00 SZ 191312 Africa/Mbabane
|
||||||
|
+03:00 SO 6165052 Africa/Mogadishu
|
||||||
|
+00:00 LR 2127307 Africa/Monrovia
|
||||||
|
+03:00 KE 16920989 Africa/Nairobi
|
||||||
|
+01:00 TD 3363418 Africa/Ndjamena
|
||||||
|
+01:00 NE 3689982 Africa/Niamey
|
||||||
|
+00:00 MR 2157288 Africa/Nouakchott
|
||||||
|
+00:00 BF 6125427 Africa/Ouagadougou
|
||||||
|
+01:00 BJ 4001576 Africa/Porto-Novo
|
||||||
|
+00:00 ST 53300 Africa/Sao_Tome
|
||||||
|
+02:00 LY 6105792 Africa/Tripoli
|
||||||
|
+01:00 TN 7314557 Africa/Tunis
|
||||||
|
+02:00 NA 983097 Africa/Windhoek
|
||||||
|
-10:00 US 15000 America/Adak
|
||||||
|
-09:00 US 366200 America/Anchorage
|
||||||
|
-04:00 AI 15000 America/Anguilla
|
||||||
|
-04:00 AG 51737 America/Antigua
|
||||||
|
-03:00 BR 858988 America/Araguaina
|
||||||
|
-03:00 AR 11817784 America/Argentina/Buenos_Aires
|
||||||
|
-03:00 AR 580969 America/Argentina/Catamarca
|
||||||
|
-03:00 AR 8998034 America/Argentina/Cordoba
|
||||||
|
-03:00 AR 413866 America/Argentina/Jujuy
|
||||||
|
-03:00 AR 237745 America/Argentina/La_Rioja
|
||||||
|
-03:00 AR 695484 America/Argentina/Mendoza
|
||||||
|
-03:00 AR 241619 America/Argentina/Rio_Gallegos
|
||||||
|
-03:00 AR 2120781 America/Argentina/Salta
|
||||||
|
-03:00 AR 518624 America/Argentina/San_Juan
|
||||||
|
-03:00 AR 266728 America/Argentina/San_Luis
|
||||||
|
-03:00 AR 788916 America/Argentina/Tucuman
|
||||||
|
-03:00 AR 109506 America/Argentina/Ushuaia
|
||||||
|
-04:00 AW 90927 America/Aruba
|
||||||
|
-03:00 PY 3653883 America/Asuncion
|
||||||
|
-05:00 CA 15000 America/Atikokan
|
||||||
|
-03:00 BR 13131907 America/Bahia
|
||||||
|
-06:00 MX 36253 America/Bahia_Banderas
|
||||||
|
-04:00 BB 98511 America/Barbados
|
||||||
|
-03:00 BR 7977349 America/Belem
|
||||||
|
-06:00 BZ 132559 America/Belize
|
||||||
|
-04:00 CA 15000 America/Blanc-Sablon
|
||||||
|
-04:00 BR 550434 America/Boa_Vista
|
||||||
|
-05:00 CO 37243567 America/Bogota
|
||||||
|
-07:00 US 748802 America/Boise
|
||||||
|
-07:00 CA 15000 America/Cambridge_Bay
|
||||||
|
-04:00 BR 2429053 America/Campo_Grande
|
||||||
|
-05:00 MX 1328961 America/Cancun
|
||||||
|
-04:00 VE 28324666 America/Caracas
|
||||||
|
-03:00 GF 155245 America/Cayenne
|
||||||
|
-05:00 KY 44705 America/Cayman
|
||||||
|
-06:00 US 55510713 America/Chicago
|
||||||
|
-06:00 MX 1533843 America/Chihuahua
|
||||||
|
-07:00 MX 1512450 America/Ciudad_Juarez
|
||||||
|
-06:00 CR 1458629 America/Costa_Rica
|
||||||
|
-03:00 CL 62723 America/Coyhaique
|
||||||
|
-07:00 CA 15000 America/Creston
|
||||||
|
-04:00 BR 2960376 America/Cuiaba
|
||||||
|
-04:00 CW 145838 America/Curacao
|
||||||
|
+00:00 GL 15000 America/Danmarkshavn
|
||||||
|
-07:00 CA 15000 America/Dawson
|
||||||
|
-07:00 CA 21465 America/Dawson_Creek
|
||||||
|
-07:00 US 9069711 America/Denver
|
||||||
|
-05:00 US 4344725 America/Detroit
|
||||||
|
-04:00 DM 16571 America/Dominica
|
||||||
|
-06:00 CA 3518059 America/Edmonton
|
||||||
|
-05:00 BR 142900 America/Eirunepe
|
||||||
|
-06:00 SV 2571611 America/El_Salvador
|
||||||
|
-07:00 CA 15000 America/Fort_Nelson
|
||||||
|
-03:00 BR 21824546 America/Fortaleza
|
||||||
|
-04:00 CA 122883 America/Glace_Bay
|
||||||
|
-04:00 CA 15000 America/Goose_Bay
|
||||||
|
-05:00 TC 27489 America/Grand_Turk
|
||||||
|
-04:00 GD 15000 America/Grenada
|
||||||
|
-04:00 GP 250260 America/Guadeloupe
|
||||||
|
-06:00 GT 7302205 America/Guatemala
|
||||||
|
-05:00 EC 11564603 America/Guayaquil
|
||||||
|
-04:00 GY 314746 America/Guyana
|
||||||
|
-04:00 CA 843888 America/Halifax
|
||||||
|
-05:00 CU 11972822 America/Havana
|
||||||
|
-07:00 MX 2215048 America/Hermosillo
|
||||||
|
-05:00 US 2672398 America/Indiana/Indianapolis
|
||||||
|
-06:00 US 15000 America/Indiana/Knox
|
||||||
|
-05:00 US 15000 America/Indiana/Marengo
|
||||||
|
-05:00 US 15000 America/Indiana/Petersburg
|
||||||
|
-06:00 US 15000 America/Indiana/Tell_City
|
||||||
|
-05:00 US 15000 America/Indiana/Vevay
|
||||||
|
-05:00 US 15451 America/Indiana/Vincennes
|
||||||
|
-05:00 US 15000 America/Indiana/Winamac
|
||||||
|
-06:00 CA 15000 America/Inuvik
|
||||||
|
-05:00 CA 15000 America/Iqaluit
|
||||||
|
-05:00 JM 2069735 America/Jamaica
|
||||||
|
-09:00 US 31555 America/Juneau
|
||||||
|
-05:00 US 909379 America/Kentucky/Louisville
|
||||||
|
-05:00 US 15000 America/Kentucky/Monticello
|
||||||
|
-04:00 BO 6958595 America/La_Paz
|
||||||
|
-05:00 PE 21406340 America/Lima
|
||||||
|
-08:00 US 46301242 America/Los_Angeles
|
||||||
|
-04:00 SX 15000 America/Lower_Princes
|
||||||
|
-03:00 BR 4805317 America/Maceio
|
||||||
|
-06:00 NI 2621115 America/Managua
|
||||||
|
-04:00 BR 3918958 America/Manaus
|
||||||
|
-04:00 MF 15000 America/Marigot
|
||||||
|
-04:00 MQ 243860 America/Martinique
|
||||||
|
-06:00 MX 1899165 America/Matamoros
|
||||||
|
-07:00 MX 2951216 America/Mazatlan
|
||||||
|
-06:00 US 15000 America/Menominee
|
||||||
|
-06:00 MX 4154924 America/Merida
|
||||||
|
-09:00 US 15000 America/Metlakatla
|
||||||
|
-06:00 MX 61603325 America/Mexico_City
|
||||||
|
-03:00 PM 15000 America/Miquelon
|
||||||
|
-04:00 CA 381536 America/Moncton
|
||||||
|
-06:00 MX 9714766 America/Monterrey
|
||||||
|
-03:00 UY 2457462 America/Montevideo
|
||||||
|
-04:00 MS 15000 America/Montserrat
|
||||||
|
-05:00 BS 319054 America/Nassau
|
||||||
|
-05:00 US 88988896 America/New_York
|
||||||
|
-09:00 US 15000 America/Nome
|
||||||
|
-02:00 BR 15000 America/Noronha
|
||||||
|
-06:00 US 15000 America/North_Dakota/Beulah
|
||||||
|
-06:00 US 15000 America/North_Dakota/Center
|
||||||
|
-06:00 US 21382 America/North_Dakota/New_Salem
|
||||||
|
-02:00 GL 15000 America/Nuuk
|
||||||
|
-06:00 MX 50784 America/Ojinaga
|
||||||
|
-05:00 PA 1692726 America/Panama
|
||||||
|
-03:00 SR 454064 America/Paramaribo
|
||||||
|
-07:00 US 6541994 America/Phoenix
|
||||||
|
-05:00 HT 5328623 America/Port-au-Prince
|
||||||
|
-04:00 TT 456832 America/Port_of_Spain
|
||||||
|
-04:00 BR 1551818 America/Porto_Velho
|
||||||
|
-04:00 PR 1605968 America/Puerto_Rico
|
||||||
|
-03:00 CL 135935 America/Punta_Arenas
|
||||||
|
-06:00 CA 15000 America/Rankin_Inlet
|
||||||
|
-03:00 BR 8832691 America/Recife
|
||||||
|
-06:00 CA 982810 America/Regina
|
||||||
|
-06:00 CA 15000 America/Resolute
|
||||||
|
-05:00 BR 794527 America/Rio_Branco
|
||||||
|
-03:00 BR 1403419 America/Santarem
|
||||||
|
-04:00 CL 15912038 America/Santiago
|
||||||
|
-04:00 DO 7521391 America/Santo_Domingo
|
||||||
|
-03:00 BR 129186469 America/Sao_Paulo
|
||||||
|
-02:00 GL 15000 America/Scoresbysund
|
||||||
|
-09:00 US 15000 America/Sitka
|
||||||
|
-04:00 BL 15000 America/St_Barthelemy
|
||||||
|
-03:30 CA 202443 America/St_Johns
|
||||||
|
-04:00 KN 15000 America/St_Kitts
|
||||||
|
-04:00 LC 45210 America/St_Lucia
|
||||||
|
-04:00 VI 70601 America/St_Thomas
|
||||||
|
-04:00 VC 48723 America/St_Vincent
|
||||||
|
-06:00 CA 16604 America/Swift_Current
|
||||||
|
-06:00 HN 4808928 America/Tegucigalpa
|
||||||
|
-04:00 GL 15000 America/Thule
|
||||||
|
-08:00 MX 3797218 America/Tijuana
|
||||||
|
-05:00 CA 24763654 America/Toronto
|
||||||
|
-04:00 VG 15000 America/Tortola
|
||||||
|
-07:00 CA 6340886 America/Vancouver
|
||||||
|
-07:00 CA 28201 America/Whitehorse
|
||||||
|
-06:00 CA 922566 America/Winnipeg
|
||||||
|
-09:00 US 15000 America/Yakutat
|
||||||
|
+10:00 AU 15000 Antarctica/Macquarie
|
||||||
|
+01:00 SJ 15000 Arctic/Longyearbyen
|
||||||
|
+03:00 YE 6904010 Asia/Aden
|
||||||
|
+05:00 KZ 9055499 Asia/Almaty
|
||||||
|
+03:00 JO 4950859 Asia/Amman
|
||||||
|
+12:00 RU 15604 Asia/Anadyr
|
||||||
|
+05:00 KZ 282409 Asia/Aqtau
|
||||||
|
+05:00 KZ 598827 Asia/Aqtobe
|
||||||
|
+05:00 TM 2706037 Asia/Ashgabat
|
||||||
|
+05:00 KZ 388248 Asia/Atyrau
|
||||||
|
+03:00 IQ 29681856 Asia/Baghdad
|
||||||
|
+03:00 BH 815447 Asia/Bahrain
|
||||||
|
+04:00 AZ 5491302 Asia/Baku
|
||||||
|
+07:00 TH 49896297 Asia/Bangkok
|
||||||
|
+07:00 RU 1413356 Asia/Barnaul
|
||||||
|
+02:00 LB 4467748 Asia/Beirut
|
||||||
|
+06:00 KG 2233412 Asia/Bishkek
|
||||||
|
+08:00 BN 230416 Asia/Brunei
|
||||||
|
+09:00 RU 486311 Asia/Chita
|
||||||
|
+05:30 LK 4297175 Asia/Colombo
|
||||||
|
+03:00 SY 10939175 Asia/Damascus
|
||||||
|
+06:00 BD 35955225 Asia/Dhaka
|
||||||
|
+09:00 TL 311685 Asia/Dili
|
||||||
|
+04:00 AE 12931222 Asia/Dubai
|
||||||
|
+05:00 TJ 2578933 Asia/Dushanbe
|
||||||
|
+02:00 CY 109959 Asia/Famagusta
|
||||||
|
+07:00 VN 42129036 Asia/Ho_Chi_Minh
|
||||||
|
+08:00 HK 24678261 Asia/Hong_Kong
|
||||||
|
+07:00 MN 122174 Asia/Hovd
|
||||||
|
+08:00 RU 2231790 Asia/Irkutsk
|
||||||
|
+07:00 ID 68794489 Asia/Jakarta
|
||||||
|
+09:00 ID 2512221 Asia/Jayapura
|
||||||
|
+02:00 IL 7901151 Asia/Jerusalem
|
||||||
|
+04:30 AF 8764027 Asia/Kabul
|
||||||
|
+12:00 RU 247112 Asia/Kamchatka
|
||||||
|
+05:00 PK 88744953 Asia/Karachi
|
||||||
|
+05:45 NP 5913313 Asia/Kathmandu
|
||||||
|
+09:00 RU 15000 Asia/Khandyga
|
||||||
|
+05:30 IN 365373326 Asia/Kolkata
|
||||||
|
+07:00 RU 2615869 Asia/Krasnoyarsk
|
||||||
|
+08:00 MY 33322164 Asia/Kuala_Lumpur
|
||||||
|
+08:00 MY 4013140 Asia/Kuching
|
||||||
|
+03:00 KW 1662180 Asia/Kuwait
|
||||||
|
+08:00 MO 1236695 Asia/Macau
|
||||||
|
+11:00 RU 92782 Asia/Magadan
|
||||||
|
+08:00 ID 12458359 Asia/Makassar
|
||||||
|
+08:00 PH 64227523 Asia/Manila
|
||||||
|
+04:00 OM 3290021 Asia/Muscat
|
||||||
|
+02:00 CY 679420 Asia/Nicosia
|
||||||
|
+07:00 RU 2355475 Asia/Novokuznetsk
|
||||||
|
+07:00 RU 2119272 Asia/Novosibirsk
|
||||||
|
+06:00 RU 1243165 Asia/Omsk
|
||||||
|
+05:00 KZ 364799 Asia/Oral
|
||||||
|
+07:00 KH 4111780 Asia/Phnom_Penh
|
||||||
|
+07:00 ID 1819584 Asia/Pontianak
|
||||||
|
+09:00 KP 12876883 Asia/Pyongyang
|
||||||
|
+03:00 QA 1566088 Asia/Qatar
|
||||||
|
+05:00 KZ 529104 Asia/Qostanay
|
||||||
|
+05:00 KZ 511391 Asia/Qyzylorda
|
||||||
|
+03:00 SA 23913997 Asia/Riyadh
|
||||||
|
+11:00 RU 326982 Asia/Sakhalin
|
||||||
|
+05:00 UZ 3864475 Asia/Samarkand
|
||||||
|
+09:00 KR 47400555 Asia/Seoul
|
||||||
|
+08:00 CN 861293141 Asia/Shanghai
|
||||||
|
+08:00 SG 11621053 Asia/Singapore
|
||||||
|
+11:00 RU 15000 Asia/Srednekolymsk
|
||||||
|
+08:00 TW 26394699 Asia/Taipei
|
||||||
|
+05:00 UZ 7452125 Asia/Tashkent
|
||||||
|
+04:00 GE 1827911 Asia/Tbilisi
|
||||||
|
+03:30 IR 50648371 Asia/Tehran
|
||||||
|
+06:00 BT 170210 Asia/Thimphu
|
||||||
|
+09:00 JP 141330654 Asia/Tokyo
|
||||||
|
+07:00 RU 783576 Asia/Tomsk
|
||||||
|
+08:00 MN 1438738 Asia/Ulaanbaatar
|
||||||
|
+06:00 CN 12667986 Asia/Urumqi
|
||||||
|
+10:00 RU 15000 Asia/Ust-Nera
|
||||||
|
+07:00 LA 1561716 Asia/Vientiane
|
||||||
|
+10:00 RU 2534711 Asia/Vladivostok
|
||||||
|
+09:00 RU 902983 Asia/Yakutsk
|
||||||
|
+06:30 MM 18836338 Asia/Yangon
|
||||||
|
+05:00 RU 15407217 Asia/Yekaterinburg
|
||||||
|
+04:00 AM 2880620 Asia/Yerevan
|
||||||
|
-01:00 PT 20056 Atlantic/Azores
|
||||||
|
-04:00 BM 15000 Atlantic/Bermuda
|
||||||
|
+00:00 ES 2054431 Atlantic/Canary
|
||||||
|
-01:00 CV 252678 Atlantic/Cape_Verde
|
||||||
|
+00:00 FO 15000 Atlantic/Faroe
|
||||||
|
+00:00 PT 213794 Atlantic/Madeira
|
||||||
|
+00:00 IS 245356 Atlantic/Reykjavik
|
||||||
|
+00:00 SH 15000 Atlantic/St_Helena
|
||||||
|
-03:00 FK 15000 Atlantic/Stanley
|
||||||
|
+09:30 AU 1706678 Australia/Adelaide
|
||||||
|
+10:00 AU 5807246 Australia/Brisbane
|
||||||
|
+09:30 AU 17456 Australia/Broken_Hill
|
||||||
|
+09:30 AU 199509 Australia/Darwin
|
||||||
|
+08:45 AU 15000 Australia/Eucla
|
||||||
|
+10:00 AU 392450 Australia/Hobart
|
||||||
|
+10:00 AU 15000 Australia/Lindeman
|
||||||
|
+10:30 AU 15000 Australia/Lord_Howe
|
||||||
|
+10:00 AU 9304158 Australia/Melbourne
|
||||||
|
+08:00 AU 3086441 Australia/Perth
|
||||||
|
+10:00 AU 9352313 Australia/Sydney
|
||||||
|
+01:00 NL 13892513 Europe/Amsterdam
|
||||||
|
+01:00 AD 36283 Europe/Andorra
|
||||||
|
+04:00 RU 644812 Europe/Astrakhan
|
||||||
|
+02:00 GR 6286690 Europe/Athens
|
||||||
|
+01:00 RS 5756549 Europe/Belgrade
|
||||||
|
+01:00 DE 62740682 Europe/Berlin
|
||||||
|
+01:00 SK 2548401 Europe/Bratislava
|
||||||
|
+01:00 BE 9038306 Europe/Brussels
|
||||||
|
+02:00 RO 11070040 Europe/Bucharest
|
||||||
|
+01:00 HU 9366333 Europe/Budapest
|
||||||
|
+01:00 DE 15000 Europe/Busingen
|
||||||
|
+02:00 MD 1460347 Europe/Chisinau
|
||||||
|
+01:00 DK 3885672 Europe/Copenhagen
|
||||||
|
+01:00 IE 2720758 Europe/Dublin
|
||||||
|
+01:00 GI 26544 Europe/Gibraltar
|
||||||
|
+00:00 GG 16488 Europe/Guernsey
|
||||||
|
+02:00 FI 5265184 Europe/Helsinki
|
||||||
|
+00:00 IM 26218 Europe/Isle_of_Man
|
||||||
|
+03:00 TR 77554557 Europe/Istanbul
|
||||||
|
+00:00 JE 28000 Europe/Jersey
|
||||||
|
+02:00 RU 664545 Europe/Kaliningrad
|
||||||
|
+03:00 RU 782604 Europe/Kirov
|
||||||
|
+02:00 UA 35633582 Europe/Kyiv
|
||||||
|
+00:00 PT 6372176 Europe/Lisbon
|
||||||
|
+01:00 SI 552127 Europe/Ljubljana
|
||||||
|
+00:00 GB 57817786 Europe/London
|
||||||
|
+01:00 LU 131322 Europe/Luxembourg
|
||||||
|
+01:00 ES 42918247 Europe/Madrid
|
||||||
|
+01:00 MT 141714 Europe/Malta
|
||||||
|
+02:00 AX 15000 Europe/Mariehamn
|
||||||
|
+03:00 BY 6105410 Europe/Minsk
|
||||||
|
+01:00 MC 48977 Europe/Monaco
|
||||||
|
+03:00 RU 69342062 Europe/Moscow
|
||||||
|
+01:00 NO 3296711 Europe/Oslo
|
||||||
|
+01:00 FR 33093827 Europe/Paris
|
||||||
|
+01:00 ME 398494 Europe/Podgorica
|
||||||
|
+01:00 CZ 5924565 Europe/Prague
|
||||||
|
+02:00 LV 1206422 Europe/Riga
|
||||||
|
+01:00 IT 34033666 Europe/Rome
|
||||||
|
+04:00 RU 3546813 Europe/Samara
|
||||||
|
+01:00 SM 15000 Europe/San_Marino
|
||||||
|
+01:00 BA 1905798 Europe/Sarajevo
|
||||||
|
+04:00 RU 1705529 Europe/Saratov
|
||||||
|
+03:00 UA 1620906 Europe/Simferopol
|
||||||
|
+01:00 MK 1754303 Europe/Skopje
|
||||||
|
+02:00 BG 4111057 Europe/Sofia
|
||||||
|
+01:00 SE 7149541 Europe/Stockholm
|
||||||
|
+02:00 EE 1106943 Europe/Tallinn
|
||||||
|
+01:00 AL 1629643 Europe/Tirane
|
||||||
|
+04:00 RU 814189 Europe/Ulyanovsk
|
||||||
|
+01:00 LI 15000 Europe/Vaduz
|
||||||
|
+01:00 VA 15000 Europe/Vatican
|
||||||
|
+01:00 AT 5274040 Europe/Vienna
|
||||||
|
+02:00 LT 2103133 Europe/Vilnius
|
||||||
|
+03:00 RU 1837053 Europe/Volgograd
|
||||||
|
+01:00 PL 22720110 Europe/Warsaw
|
||||||
|
+01:00 HR 1650334 Europe/Zagreb
|
||||||
|
+01:00 CH 3461664 Europe/Zurich
|
||||||
|
+03:00 MG 6115943 Indian/Antananarivo
|
||||||
|
+06:00 IO 15000 Indian/Chagos
|
||||||
|
+07:00 CX 15000 Indian/Christmas
|
||||||
|
+06:30 CC 15000 Indian/Cocos
|
||||||
|
+03:00 KM 118353 Indian/Comoro
|
||||||
|
+05:00 TF 15000 Indian/Kerguelen
|
||||||
|
+04:00 SC 22881 Indian/Mahe
|
||||||
|
+05:00 MV 103693 Indian/Maldives
|
||||||
|
+04:00 MU 701493 Indian/Mauritius
|
||||||
|
+03:00 YT 138967 Indian/Mayotte
|
||||||
|
+04:00 RE 804329 Indian/Reunion
|
||||||
|
+13:00 WS 40407 Pacific/Apia
|
||||||
|
+12:00 NZ 5203989 Pacific/Auckland
|
||||||
|
+11:00 PG 40266 Pacific/Bougainville
|
||||||
|
+12:45 NZ 15000 Pacific/Chatham
|
||||||
|
+10:00 FM 15000 Pacific/Chuuk
|
||||||
|
-06:00 CL 15000 Pacific/Easter
|
||||||
|
+11:00 VU 35901 Pacific/Efate
|
||||||
|
+12:00 FJ 334403 Pacific/Fiji
|
||||||
|
+12:00 TV 15000 Pacific/Funafuti
|
||||||
|
-06:00 EC 15000 Pacific/Galapagos
|
||||||
|
-09:00 PF 15000 Pacific/Gambier
|
||||||
|
+11:00 SB 194707 Pacific/Guadalcanal
|
||||||
|
+10:00 GU 121094 Pacific/Guam
|
||||||
|
-10:00 US 1569453 Pacific/Honolulu
|
||||||
|
+13:00 KI 15000 Pacific/Kanton
|
||||||
|
+14:00 KI 15000 Pacific/Kiritimati
|
||||||
|
+11:00 FM 15000 Pacific/Kosrae
|
||||||
|
+12:00 MH 15000 Pacific/Kwajalein
|
||||||
|
+12:00 MH 45701 Pacific/Majuro
|
||||||
|
-09:30 PF 15000 Pacific/Marquesas
|
||||||
|
+12:00 NR 15000 Pacific/Nauru
|
||||||
|
-11:00 NU 15000 Pacific/Niue
|
||||||
|
+11:00 NF 15000 Pacific/Norfolk
|
||||||
|
+11:00 NC 137086 Pacific/Noumea
|
||||||
|
-11:00 AS 15000 Pacific/Pago_Pago
|
||||||
|
+09:00 PW 15000 Pacific/Palau
|
||||||
|
-08:00 PN 15000 Pacific/Pitcairn
|
||||||
|
+11:00 FM 15000 Pacific/Pohnpei
|
||||||
|
+10:00 PG 588589 Pacific/Port_Moresby
|
||||||
|
-10:00 CK 15000 Pacific/Rarotonga
|
||||||
|
+10:00 MP 48220 Pacific/Saipan
|
||||||
|
-10:00 PF 81958 Pacific/Tahiti
|
||||||
|
+12:00 KI 40311 Pacific/Tarawa
|
||||||
|
+13:00 TO 22400 Pacific/Tongatapu
|
||||||
|
+12:00 WF 15000 Pacific/Wallis
|
||||||
|
@@ -0,0 +1 @@
|
|||||||
|
{ "format": "{tld}", "rows": "tld.tsv", "key": "tld", "name": "unicode" }
|
||||||
+1439
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
|||||||
["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15", "Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0"]
|
{ "format": "{ua}", "rows": "useragent.tsv" }
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
browser device os ua
|
||||||
|
Chrome mobile Android Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Mobile Safari/537.36
|
||||||
|
Chrome mobile Android Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Mobile Safari/537.36
|
||||||
|
Chrome mobile Android Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Mobile Safari/537.36
|
||||||
|
Chrome mobile Android Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Mobile Safari/537.36
|
||||||
|
Chrome mobile Android Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Mobile Safari/537.36
|
||||||
|
Chrome mobile Android Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36
|
||||||
|
Chrome mobile Android Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Mobile Safari/537.36
|
||||||
|
Chrome mobile Android Mozilla/5.0 (Linux; Android 12; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36
|
||||||
|
Chrome desktop ChromeOS Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Linux Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Linux Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Linux Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Linux Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Linux Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Linux Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.132 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 YaBrowser/26.8.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.7922.34 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36
|
||||||
|
Chrome mobile iOS Mozilla/5.0 (iPhone; CPU iPhone OS 26_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/152.0.7977.64 Mobile/15E148 Safari/604.1
|
||||||
|
Chrome desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36
|
||||||
|
Chrome desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36
|
||||||
|
Chrome desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36
|
||||||
|
Edge desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36 Edg/151.0.0.0
|
||||||
|
Edge desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36 Edg/152.0.0.0
|
||||||
|
Edge desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36 Edg/153.0.0.0
|
||||||
|
Edge desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36 Edg/152.0.0.0
|
||||||
|
Firefox desktop Linux Mozilla/5.0 (X11; Linux x86_64; rv:155.0) Gecko/20100101 Firefox/155.0
|
||||||
|
Firefox desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0
|
||||||
|
Firefox desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:154.0) Gecko/20100101 Firefox/154.0
|
||||||
|
Firefox desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0
|
||||||
|
Firefox desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:154.0) Gecko/20100101 Firefox/154.0
|
||||||
|
Firefox desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:155.0) Gecko/20100101 Firefox/155.0
|
||||||
|
Opera desktop Windows Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36 OPR/135.0.0.0
|
||||||
|
Opera desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 OPR/134.0.0.0
|
||||||
|
Safari mobile iOS Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1
|
||||||
|
Safari mobile iOS Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.7.5 Mobile/15E148 Safari/604.1
|
||||||
|
Safari mobile iOS Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.2 Mobile/15E148 Safari/604.1
|
||||||
|
Safari mobile iOS Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.3 Mobile/15E148 Safari/604.1
|
||||||
|
Safari mobile iOS Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1
|
||||||
|
Safari mobile iOS Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5.2 Mobile/15E148 Safari/604.1
|
||||||
|
Safari mobile iOS Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1
|
||||||
|
Safari mobile iOS Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6.1 Mobile/15E148 Safari/604.1
|
||||||
|
Safari mobile iOS Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/27.0 Mobile/15E148 Safari/604.1
|
||||||
|
Safari desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15
|
||||||
|
Safari desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Safari/605.1.15
|
||||||
|
Safari desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.2 Safari/605.1.15
|
||||||
|
Safari desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Safari/605.1.15
|
||||||
|
Safari desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5.2 Safari/605.1.15
|
||||||
|
Safari desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15
|
||||||
|
Safari desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6.1 Safari/605.1.15
|
||||||
|
Safari desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6.2 Safari/605.1.15
|
||||||
|
Safari desktop macOS Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/27.0 Safari/605.1.15
|
||||||
|
Samsung Internet mobile Android Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/30.0 Chrome/143.0.0.0 Mobile Safari/537.36
|
||||||
|
@@ -0,0 +1,19 @@
|
|||||||
|
// Package data lists every module fejkdata ships.
|
||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
|
||||||
|
enus "github.com/larvit/fejkdata/data/en_US"
|
||||||
|
geose "github.com/larvit/fejkdata/data/geo/SE"
|
||||||
|
geous "github.com/larvit/fejkdata/data/geo/US"
|
||||||
|
"github.com/larvit/fejkdata/data/misc"
|
||||||
|
svse "github.com/larvit/fejkdata/data/sv_SE"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate env REGENERATE=1 go test -run ^TestShippedManifestsAreCurrent$ ..
|
||||||
|
|
||||||
|
// Modules returns every module fejkdata ships, to pass to fejkdata.WithDataFS.
|
||||||
|
func Modules() []fs.FS {
|
||||||
|
return []fs.FS{enus.FS, geose.FS, geous.FS, misc.FS, svse.FS}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"index": {
|
||||||
|
"sv_SE.address": {"paths":["","locality","postal-code","street","street-number"]},
|
||||||
|
"sv_SE.birth-number": {"parent":"sex","paths":["","number","sex"]},
|
||||||
|
"sv_SE.color": {"paths":[""]},
|
||||||
|
"sv_SE.company": {"paths":["","base","suffix"]},
|
||||||
|
"sv_SE.date": {"paths":[""]},
|
||||||
|
"sv_SE.email": {"paths":["","domain","local","local.n"]},
|
||||||
|
"sv_SE.first-name": {"parent":"sex","paths":["","count","name","sex"]},
|
||||||
|
"sv_SE.ip": {"paths":[""]},
|
||||||
|
"sv_SE.last-name": {"paths":["","count","name"]},
|
||||||
|
"sv_SE.person": {"paths":["","first","last","prefix","sex"]},
|
||||||
|
"sv_SE.personnummer": {"paths":[""]},
|
||||||
|
"sv_SE.phone": {"paths":["","a","b","c","prefix"]},
|
||||||
|
"sv_SE.price": {"paths":["","amt","ore"]},
|
||||||
|
"sv_SE.samordningsnummer": {"paths":[""]},
|
||||||
|
"sv_SE.sentence": {"paths":["","adj","noun","prep","verb"]},
|
||||||
|
"sv_SE.sex": {"paths":["","birth-number","birth-number.number","birth-number.sex","code","first-name","first-name.count","first-name.name","first-name.sex","name"]},
|
||||||
|
"sv_SE.time": {"paths":[""]},
|
||||||
|
"sv_SE.title": {"paths":["","name","share"]},
|
||||||
|
"sv_SE.url": {"paths":["","host","path"]},
|
||||||
|
"sv_SE.username": {"paths":["","n"]},
|
||||||
|
"sv_SE.version": {"paths":["","n","pre","suffix"]},
|
||||||
|
"sv_SE.word": {"paths":[""]}
|
||||||
|
},
|
||||||
|
"namespace": "sv_SE"
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"format": "{street} {street-number}\n{postal-code} {locality}",
|
"format": "{/geo.SE.address as a}{a}",
|
||||||
"street": "{/geo.SE.address.street}",
|
"street": "{a.street}",
|
||||||
"street-number": "{/geo.SE.address.street-number}",
|
"street-number": "{a.street-number}",
|
||||||
"postal-code": "{/geo.SE.address.postal-code}",
|
"postal-code": "{a.postal-code}",
|
||||||
"locality": "{/geo.SE.address.locality}"
|
"locality": "{a.locality}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Package svse is fejkdata's sv_SE locale module, whose categories are sv_SE.*; its address reads
|
||||||
|
// geo.SE, which package geose holds.
|
||||||
|
package svse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
|
|
||||||
|
geose "github.com/larvit/fejkdata/data/geo/SE"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FS holds the locale alone, its categories and its manifest; pass Modules unless your own
|
||||||
|
// data stands in for geo.SE.
|
||||||
|
//
|
||||||
|
//go:embed *.json *.tsv .fejkdata.json
|
||||||
|
var FS embed.FS
|
||||||
|
|
||||||
|
// Modules returns FS and every module it reads, to pass to fejkdata.WithDataFS.
|
||||||
|
func Modules() []fs.FS { return append([]fs.FS{FS}, geose.Modules()...) }
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package svse_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/larvit/fejkdata"
|
||||||
|
svse "github.com/larvit/fejkdata/data/sv_SE"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFSLoadsByItsManifest(t *testing.T) {
|
||||||
|
if _, err := fs.Stat(svse.FS, ".fejkdata.json"); err != nil {
|
||||||
|
t.Fatalf("FS holds no manifest: %v", err)
|
||||||
|
}
|
||||||
|
f, err := fejkdata.New(fejkdata.WithDataFS(svse.Modules()...), fejkdata.WithSeed(1))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New(WithDataFS(Modules()...)) = %v", err)
|
||||||
|
}
|
||||||
|
if len(f.List()) == 0 {
|
||||||
|
t.Error("List() is empty, want the module's categories")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -837,7 +837,6 @@ Cronholm 393
|
|||||||
Cronqvist 388
|
Cronqvist 388
|
||||||
Cronvall 199
|
Cronvall 199
|
||||||
Cruz 207
|
Cruz 207
|
||||||
Da Silva 287
|
|
||||||
Daabas 247
|
Daabas 247
|
||||||
Dabrowski 222
|
Dabrowski 222
|
||||||
Dag 327
|
Dag 327
|
||||||
@@ -4875,6 +4874,7 @@ Zimmermann 230
|
|||||||
Zingmark 337
|
Zingmark 337
|
||||||
Zivkovic 455
|
Zivkovic 455
|
||||||
af Ekenstam 204
|
af Ekenstam 204
|
||||||
|
da Silva 287
|
||||||
de Geer 200
|
de Geer 200
|
||||||
du Rietz 282
|
du Rietz 282
|
||||||
von Bahr 188
|
von Bahr 188
|
||||||
|
|||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"format": "{prefix}{first} {last}",
|
"format": "{prefix}{first} {last}",
|
||||||
"first": "{.first-name.name}",
|
"first": "{.first-name as n}{n.name}",
|
||||||
"last": "{.last-name.name}",
|
"last": "{.last-name.name}",
|
||||||
"prefix": ["", { "format": "{string} ", "string": ["dr", "prof"], "weight": 0.05 }],
|
"prefix": [null, { "format": "{.title.name} ", "weight": 0.05 }],
|
||||||
"sex": "{.sex.name}"
|
"sex": "{n..sex.name}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{ "format": "{name}", "rows": "title.tsv", "key": "name", "weight": "share" }
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
name share
|
||||||
|
dr 3
|
||||||
|
prof 1
|
||||||
|
+307
-51
@@ -1,7 +1,10 @@
|
|||||||
package fejkdata
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -48,7 +51,7 @@ func TestShippedDataCategories(t *testing.T) {
|
|||||||
regexp.MustCompile(`^\d{1,3}( \d{3})?(,\d{2})? kr$`)},
|
regexp.MustCompile(`^\d{1,3}( \d{3})?(,\d{2})? kr$`)},
|
||||||
}
|
}
|
||||||
|
|
||||||
f := newGenerator(t, "data", WithSeed(1))
|
f := shipped(t, WithSeed(1))
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
if v := fake(t, f, "en_US."+c.path); !c.en.MatchString(v) {
|
if v := fake(t, f, "en_US."+c.path); !c.en.MatchString(v) {
|
||||||
@@ -61,11 +64,11 @@ func TestShippedDataCategories(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestShippedMiscCategories covers the locale-neutral data/misc folder: a proper
|
// TestShippedMiscCategories covers the locale-neutral misc module: a proper
|
||||||
// v4 UUID (version/variant nibbles fixed), a MAC address, and credit-card numbers
|
// v4 UUID (version/variant nibbles fixed), a MAC address, and credit-card numbers
|
||||||
// whose trailing {luhn()} check passes.
|
// whose trailing {luhn()} check passes.
|
||||||
func TestShippedMiscCategories(t *testing.T) {
|
func TestShippedMiscCategories(t *testing.T) {
|
||||||
f := newGenerator(t, "data/misc", WithSeed(1))
|
f := newBareModule(t, "misc", WithSeed(1))
|
||||||
v4 := regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)
|
v4 := regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)
|
||||||
mac := regexp.MustCompile(`^([0-9a-f]{2}:){5}[0-9a-f]{2}$`)
|
mac := regexp.MustCompile(`^([0-9a-f]{2}:){5}[0-9a-f]{2}$`)
|
||||||
objectid := regexp.MustCompile(`^[0-9a-f]{24}$`)
|
objectid := regexp.MustCompile(`^[0-9a-f]{24}$`)
|
||||||
@@ -93,24 +96,46 @@ func TestShippedMiscCategories(t *testing.T) {
|
|||||||
// codes follow their standard shape, dotted sub-paths resolve, emoji is a real
|
// codes follow their standard shape, dotted sub-paths resolve, emoji is a real
|
||||||
// glyph, and a coordinate parses to a point within geographic bounds.
|
// glyph, and a coordinate parses to a point within geographic bounds.
|
||||||
func TestShippedMiscReferenceData(t *testing.T) {
|
func TestShippedMiscReferenceData(t *testing.T) {
|
||||||
f := newGenerator(t, "data/misc", WithSeed(1))
|
f := newBareModule(t, "misc", WithSeed(1))
|
||||||
re := map[string]*regexp.Regexp{
|
re := map[string]*regexp.Regexp{
|
||||||
"currency": regexp.MustCompile(`^[A-Z]{3}$`),
|
"currency": regexp.MustCompile(`^[A-Z]{3}$`),
|
||||||
"currency.name": regexp.MustCompile(`\p{L}`),
|
"currency.name": regexp.MustCompile(`\p{L}`),
|
||||||
"currency.symbol": regexp.MustCompile(`^\S+$`),
|
"currency.symbol": regexp.MustCompile(`^\S+$`),
|
||||||
"country": regexp.MustCompile(`\p{L}`),
|
"territory": regexp.MustCompile(`\p{L}`),
|
||||||
"country.alpha2": regexp.MustCompile(`^[A-Z]{2}$`),
|
"territory.alpha2": regexp.MustCompile(`^[A-Z]{2}$`),
|
||||||
"country.alpha3": regexp.MustCompile(`^[A-Z]{3}$`),
|
"territory.alpha3": regexp.MustCompile(`^[A-Z]{3}$`),
|
||||||
"language": regexp.MustCompile(`\p{L}`),
|
"territory.country": regexp.MustCompile(`^[A-Z]{2}$`),
|
||||||
"language.code": regexp.MustCompile(`^[a-z]{2}$`),
|
"language": regexp.MustCompile(`\p{L}`),
|
||||||
"timezone": regexp.MustCompile(`^([A-Za-z_]+(/[A-Za-z_]+)+|UTC)$`),
|
"language.code": regexp.MustCompile(`^[a-z]{2}$`),
|
||||||
"mimetype": regexp.MustCompile(`^[a-z]+/[a-z0-9.+-]+$`),
|
"language.code3": regexp.MustCompile(`^[a-z]{3}$`),
|
||||||
"mimetype.ext": regexp.MustCompile(`^\.[a-z0-9]+$`),
|
"timezone": regexp.MustCompile(`^[A-Za-z]+(/[A-Za-z0-9_+-]+)+$`),
|
||||||
"httpstatus": regexp.MustCompile(`^[1-5]\d{2} \S.*$`),
|
"timezone.offset": regexp.MustCompile(`^[+-](0\d|1[0-4]):[0-5]\d$`),
|
||||||
"httpstatus.code": regexp.MustCompile(`^[1-5]\d{2}$`),
|
"timezone.territory": regexp.MustCompile(`^[A-Z]{2}$`),
|
||||||
"useragent": regexp.MustCompile(`^Mozilla/5\.0 .+`),
|
"timezone.weight": regexp.MustCompile(`^[1-9]\d*$`),
|
||||||
"car": regexp.MustCompile(`^\S.* \S`),
|
"mimetype": regexp.MustCompile(`^[a-z]+/[a-z0-9.+-]+$`),
|
||||||
"car.maker": regexp.MustCompile(`^\S`),
|
"mimetype.ext": regexp.MustCompile(`^\.[a-z0-9_-]+$`),
|
||||||
|
"httpmethod": regexp.MustCompile(`^(CONNECT|DELETE|GET|HEAD|OPTIONS|PATCH|POST|PUT|TRACE)$`),
|
||||||
|
"httpmethod.safe": regexp.MustCompile(`^(true|false)$`),
|
||||||
|
"httpmethod.idempotent": regexp.MustCompile(`^(true|false)$`),
|
||||||
|
"port": regexp.MustCompile(`^[1-9]\d{0,4}$`),
|
||||||
|
"port.service": regexp.MustCompile(`^\S+$`),
|
||||||
|
"protocol": regexp.MustCompile(`^\S+( \S+)*$`),
|
||||||
|
"protocol.name": regexp.MustCompile(`\S`),
|
||||||
|
"protocol.number": regexp.MustCompile(`^\d{1,3}$`),
|
||||||
|
"loglevel": regexp.MustCompile(`^(alert|crit|debug|emerg|err|info|notice|warning)$`),
|
||||||
|
"loglevel.code": regexp.MustCompile(`^[0-7]$`),
|
||||||
|
"loglevel.severity": regexp.MustCompile(`^(Alert|Critical|Debug|Emergency|Error|Informational|Notice|Warning)$`),
|
||||||
|
"tld": regexp.MustCompile(`^\.[a-z0-9]([a-z0-9-]*[a-z0-9])?$`),
|
||||||
|
"tld.type": regexp.MustCompile(`^(country-code|generic|generic-restricted|infrastructure|sponsored)$`),
|
||||||
|
"tld.unicode": regexp.MustCompile(`^\.\S+$`),
|
||||||
|
"httpstatus": regexp.MustCompile(`^[1-5]\d{2} \S.*$`),
|
||||||
|
"httpstatus.code": regexp.MustCompile(`^[1-5]\d{2}$`),
|
||||||
|
"useragent": regexp.MustCompile(`^Mozilla/5\.0 .+`),
|
||||||
|
"useragent.browser": regexp.MustCompile(`^(Chrome|Edge|Firefox|Opera|Safari|Samsung Internet)$`),
|
||||||
|
"useragent.device": regexp.MustCompile(`^(desktop|mobile)$`),
|
||||||
|
"useragent.os": regexp.MustCompile(`^(Android|ChromeOS|Linux|Windows|iOS|macOS)$`),
|
||||||
|
"car": regexp.MustCompile(`^\S.* \S`),
|
||||||
|
"car.make": regexp.MustCompile(`^\S`),
|
||||||
}
|
}
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
for p, rx := range re {
|
for p, rx := range re {
|
||||||
@@ -134,7 +159,7 @@ func TestShippedMiscReferenceData(t *testing.T) {
|
|||||||
// TestSwedishPersonNamesHaveNoTripleLetter pins an orthographic rule the shape
|
// TestSwedishPersonNamesHaveNoTripleLetter pins an orthographic rule the shape
|
||||||
// regexes miss: no generated name repeats a character three times over.
|
// regexes miss: no generated name repeats a character three times over.
|
||||||
func TestSwedishPersonNamesHaveNoTripleLetter(t *testing.T) {
|
func TestSwedishPersonNamesHaveNoTripleLetter(t *testing.T) {
|
||||||
f := newGenerator(t, "data", WithSeed(11))
|
f := shipped(t, WithSeed(11))
|
||||||
for _, path := range []string{"sv_SE.person", "sv_SE.person.last"} {
|
for _, path := range []string{"sv_SE.person", "sv_SE.person.last"} {
|
||||||
for i := 0; i < 20000; i++ {
|
for i := 0; i < 20000; i++ {
|
||||||
name := fake(t, f, path)
|
name := fake(t, f, path)
|
||||||
@@ -153,7 +178,7 @@ func TestSwedishPersonNamesHaveNoTripleLetter(t *testing.T) {
|
|||||||
// male), the trailing digit is a Luhn checksum over the other nine, and a
|
// male), the trailing digit is a Luhn checksum over the other nine, and a
|
||||||
// samordningsnummer is the same with 60 added to the day.
|
// samordningsnummer is the same with 60 added to the day.
|
||||||
func TestSwedishPersonnummer(t *testing.T) {
|
func TestSwedishPersonnummer(t *testing.T) {
|
||||||
sv := newGenerator(t, "data", WithSeed(1))
|
sv := shipped(t, WithSeed(1))
|
||||||
re := regexp.MustCompile(`^\d{6}-23[89]\d$`)
|
re := regexp.MustCompile(`^\d{6}-23[89]\d$`)
|
||||||
sawLongMonthEnd, birth := false, map[string]bool{}
|
sawLongMonthEnd, birth := false, map[string]bool{}
|
||||||
for i := 0; i < 2000; i++ {
|
for i := 0; i < 2000; i++ {
|
||||||
@@ -186,19 +211,13 @@ func TestSwedishPersonnummer(t *testing.T) {
|
|||||||
if !birth["238"] || !birth["239"] {
|
if !birth["238"] || !birth["239"] {
|
||||||
t.Fatalf("birth numbers drawn %v, want both 238 and 239", birth)
|
t.Fatalf("birth numbers drawn %v, want both 238 and 239", birth)
|
||||||
}
|
}
|
||||||
for i := 0; i < 200; i++ {
|
|
||||||
got := fakeTemplate(t, sv, `{/sv_SE.person.sex} {/sv_SE.personnummer}`)
|
|
||||||
if f, m := strings.HasPrefix(got, "kvinna ") && got[14:17] == "238", strings.HasPrefix(got, "man ") && got[11:14] == "239"; !f && !m {
|
|
||||||
t.Fatalf("%q: a person and a personnummer in one render disagree on sex", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestShippedUSTaxIds pins the SSA and IRS ranges: an SSN's area is 001-899 but
|
// TestShippedUSTaxIds pins the SSA and IRS ranges: an SSN's area is 001-899 but
|
||||||
// 666, its group 01-99 and its serial 0001-9999; an ITIN is 9XX-GG-XXXX with GG
|
// 666, its group 01-99 and its serial 0001-9999; an ITIN is 9XX-GG-XXXX with GG
|
||||||
// in 50-65, 70-88, 90-92 or 94-99.
|
// in 50-65, 70-88, 90-92 or 94-99.
|
||||||
func TestShippedUSTaxIds(t *testing.T) {
|
func TestShippedUSTaxIds(t *testing.T) {
|
||||||
f := newGenerator(t, "data", WithSeed(5))
|
f := shipped(t, WithSeed(5))
|
||||||
ssn := regexp.MustCompile(`^(\d{3})-(\d{2})-(\d{4})$`)
|
ssn := regexp.MustCompile(`^(\d{3})-(\d{2})-(\d{4})$`)
|
||||||
itin := regexp.MustCompile(`^9\d{2}-(5\d|6[0-5]|7\d|8[0-8]|9[0-24-9])-\d{4}$`)
|
itin := regexp.MustCompile(`^9\d{2}-(5\d|6[0-5]|7\d|8[0-8]|9[0-24-9])-\d{4}$`)
|
||||||
for i := 0; i < 2000; i++ {
|
for i := 0; i < 2000; i++ {
|
||||||
@@ -222,7 +241,7 @@ func TestShippedUSTaxIds(t *testing.T) {
|
|||||||
// TestShippedPersonNames pins the name tables: a sex pins its first names, a name
|
// TestShippedPersonNames pins the name tables: a sex pins its first names, a name
|
||||||
// both sexes carry appears under both, and the record's sex agrees with its name.
|
// both sexes carry appears under both, and the record's sex agrees with its name.
|
||||||
func TestShippedPersonNames(t *testing.T) {
|
func TestShippedPersonNames(t *testing.T) {
|
||||||
f := newGenerator(t, "data", WithSeed(9))
|
f := shipped(t, WithSeed(9))
|
||||||
for path, want := range map[string]string{
|
for path, want := range map[string]string{
|
||||||
"sv_SE.sex[f]": "kvinna",
|
"sv_SE.sex[f]": "kvinna",
|
||||||
"sv_SE.sex[man].code": "m",
|
"sv_SE.sex[man].code": "m",
|
||||||
@@ -250,24 +269,41 @@ func TestShippedPersonNames(t *testing.T) {
|
|||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
for i := 0; i < 2000; i++ {
|
for i := 0; i < 2000; i++ {
|
||||||
seen[fake(t, f, locale+".sex[f].first-name")] = true
|
seen[fake(t, f, locale+".sex[f].first-name")] = true
|
||||||
first, sex := fake(t, f, locale+".person.first"), fake(t, f, locale+".person.sex")
|
got := fakeTemplate(t, f, `{/`+locale+`.person as p}{p.first}|{p.sex}`)
|
||||||
if first == "" || sex == "" {
|
first, sex, _ := strings.Cut(got, "|")
|
||||||
t.Fatalf("%s.person lacks a first name or a sex", locale)
|
fake(t, f, locale+".sex["+sex+"].first-name["+first+"]")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(seen) < 300 || !seen["Anna"] && locale == "sv_SE" || !seen["Mary"] && locale == "en_US" {
|
if len(seen) < 300 || !seen["Anna"] && locale == "sv_SE" || !seen["Mary"] && locale == "en_US" {
|
||||||
t.Fatalf("%s female first names: %d distinct in 2000, want a weighted register", locale, len(seen))
|
t.Fatalf("%s female first names: %d distinct in 2000, want a weighted register", locale, len(seen))
|
||||||
}
|
}
|
||||||
got := fakeTemplate(t, f, `{/`+locale+`.sex.code}|{/`+locale+`.person.first}`)
|
}
|
||||||
code, first, _ := strings.Cut(got, "|")
|
}
|
||||||
if v := fake(t, f, locale+".sex["+code+"].first-name["+first+"]"); v != first {
|
|
||||||
t.Fatalf("%s: %q drawn as a %s name is not one", locale, first, code)
|
// TestShippedPersonIsOneNamedPick pins that a person's first name, sex and title come
|
||||||
|
// from one pick, and that two names bound to one person category are two people.
|
||||||
|
func TestShippedPersonIsOneNamedPick(t *testing.T) {
|
||||||
|
f := shipped(t, WithSeed(9))
|
||||||
|
for _, locale := range []string{"sv_SE", "en_US"} {
|
||||||
|
apart := false
|
||||||
|
for i := 0; i < 300; i++ {
|
||||||
|
got := fakeTemplate(t, f, `{/`+locale+`.person as a}{/`+locale+`.person as b}{a.first}|{a.sex}|{a.prefix}|{b.first}|{b.sex}`)
|
||||||
|
parts := strings.Split(got, "|")
|
||||||
|
for _, p := range [][2]string{{parts[0], parts[1]}, {parts[3], parts[4]}} {
|
||||||
|
fake(t, f, locale+".sex["+p[1]+"].first-name["+p[0]+"]")
|
||||||
|
}
|
||||||
|
if title := strings.TrimSpace(parts[2]); title != "" && locale == "en_US" {
|
||||||
|
fake(t, f, "en_US.sex["+parts[1]+"].title["+title+"]")
|
||||||
|
}
|
||||||
|
apart = apart || parts[0] != parts[3]
|
||||||
|
}
|
||||||
|
if !apart {
|
||||||
|
t.Errorf("%s: two names bound to person drew one first name in 300 renders, want two people", locale)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShippedSwedishPhone(t *testing.T) {
|
func TestShippedSwedishPhone(t *testing.T) {
|
||||||
f := newGenerator(t, "data", WithSeed(11))
|
f := shipped(t, WithSeed(11))
|
||||||
re := regexp.MustCompile(`^0\d{1,2}-\d{3} \d{2} \d{2}$`)
|
re := regexp.MustCompile(`^0\d{1,2}-\d{3} \d{2} \d{2}$`)
|
||||||
for i := 0; i < 50; i++ {
|
for i := 0; i < 50; i++ {
|
||||||
if n := fake(t, f, "sv_SE.phone"); !re.MatchString(n) {
|
if n := fake(t, f, "sv_SE.phone"); !re.MatchString(n) {
|
||||||
@@ -277,7 +313,7 @@ func TestShippedSwedishPhone(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestShippedSwedishAddress(t *testing.T) {
|
func TestShippedSwedishAddress(t *testing.T) {
|
||||||
f := newGenerator(t, "data", WithSeed(3))
|
f := shipped(t, WithSeed(3))
|
||||||
digit := regexp.MustCompile(`\d`)
|
digit := regexp.MustCompile(`\d`)
|
||||||
for i := 0; i < 30; i++ {
|
for i := 0; i < 30; i++ {
|
||||||
a := fake(t, f, "sv_SE.address")
|
a := fake(t, f, "sv_SE.address")
|
||||||
@@ -294,8 +330,170 @@ func TestShippedSwedishAddress(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestShippedAddressIsOneNamedPick pins that an address's street, postal code,
|
||||||
|
// locality and region are one place in the geo tables, read through a name in a template or
|
||||||
|
// across a struct's tags, and that a reference beside the name is another address.
|
||||||
|
func TestShippedAddressIsOneNamedPick(t *testing.T) {
|
||||||
|
f := shipped(t, WithSeed(3))
|
||||||
|
for _, c := range []struct{ locale, country, template, largest string }{
|
||||||
|
{"sv_SE", "SE", "{a.street}|{a.postal-code}|{a.locality}|", "Stockholm"},
|
||||||
|
{"en_US", "US", "{a.street}|{a.postal-code}|{a.locality}|{a.region}", "New York"},
|
||||||
|
} {
|
||||||
|
places := geoPlaces(t, c.country)
|
||||||
|
apart, localities := false, map[string]int{}
|
||||||
|
for i := 0; i < 2000; i++ {
|
||||||
|
got := fakeTemplate(t, f, `{/`+c.locale+`.address as a}`+c.template+`|{/`+c.locale+`.address.street}`)
|
||||||
|
parts := strings.Split(got, "|")
|
||||||
|
if !places[strings.Join(parts[:4], "|")] {
|
||||||
|
t.Fatalf("%s.address: %q is no street, postal code, locality and region of one place", c.locale, got)
|
||||||
|
}
|
||||||
|
apart = apart || parts[0] != parts[4]
|
||||||
|
localities[parts[2]]++
|
||||||
|
}
|
||||||
|
if !apart {
|
||||||
|
t.Errorf("%s: a reference beside a name drew the name's street in 2000 renders, want two addresses", c.locale)
|
||||||
|
}
|
||||||
|
if c.country == "SE" {
|
||||||
|
var row struct {
|
||||||
|
Street string `fake:"{/sv_SE.address as a}{a.street}"`
|
||||||
|
PostalCode string `fake:"{a.postal-code}"`
|
||||||
|
Locality string `fake:"{a.locality}"`
|
||||||
|
}
|
||||||
|
for i := 0; i < 200; i++ {
|
||||||
|
if err := f.FakeStruct(&row); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := row.Street + "|" + row.PostalCode + "|" + row.Locality + "|"; !places[got] {
|
||||||
|
t.Fatalf("sv_SE.address: struct tags reading one name, %q, are no street, postal code and locality of one place", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for l, n := range localities {
|
||||||
|
if n > localities[c.largest] {
|
||||||
|
t.Errorf("%s.address drew %s %d times and %s %d, want the locality weighted by population", c.locale, l, n, c.largest, localities[c.largest])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0644000 is Los Angeles.
|
||||||
|
func TestShippedPlacesSpreadAddresses(t *testing.T) {
|
||||||
|
for country, city := range map[string]struct {
|
||||||
|
key string
|
||||||
|
least int
|
||||||
|
}{"SE": {"Göteborg", 1500}, "US": {"0644000", 2500}} {
|
||||||
|
streets := 0
|
||||||
|
for _, r := range geoRows(t, country, "street") {
|
||||||
|
if r[1] == city.key {
|
||||||
|
streets++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if streets < city.least {
|
||||||
|
t.Errorf("geo.%s: %s ships %d streets, want at least %d", country, city.key, streets, city.least)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
header, _, _ := strings.Cut(geoTSV(t, "US", "locality"), "\n")
|
||||||
|
population := slices.Index(strings.Split(header, "\t"), "population")
|
||||||
|
if population < 0 {
|
||||||
|
t.Fatal("geo.US locality.tsv has no population column")
|
||||||
|
}
|
||||||
|
for _, r := range geoRows(t, "US", "locality") {
|
||||||
|
n, err := strconv.Atoi(r[population])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if n < 2500 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Error("geo.US: every locality holds 2500 people or more, want places drawn down to villages")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestShippedCapitalsAndBigPlaces pins state capitals and places of 100,000 or more that a
|
||||||
|
// draw by population leaves out.
|
||||||
|
func TestShippedCapitalsAndBigPlaces(t *testing.T) {
|
||||||
|
state := map[string]string{}
|
||||||
|
for _, r := range geoRows(t, "US", "municipality") {
|
||||||
|
state[r[0]] = r[2]
|
||||||
|
}
|
||||||
|
shipped := map[string]bool{}
|
||||||
|
for _, r := range geoRows(t, "US", "locality") {
|
||||||
|
shipped[r[1]+", "+state[r[2]]] = true
|
||||||
|
}
|
||||||
|
for _, place := range []string{
|
||||||
|
"Albany, NY", "Augusta, ME", "Baton Rouge, LA", "Bismarck, ND", "Boise, ID", "Charleston, WV",
|
||||||
|
"Frankfort, KY", "Harrisburg, PA", "Helena, MT", "Juneau, AK", "Kansas City, KS", "Lafayette, LA",
|
||||||
|
"Lexington, KY", "Montpelier, VT", "New Orleans, LA", "Pierre, SD", "St. Paul, MN", "Stockton, CA",
|
||||||
|
"Ventura, CA",
|
||||||
|
} {
|
||||||
|
if !shipped[place] {
|
||||||
|
t.Errorf("geo.US: no locality %s, want every state capital and place of 100,000 or more", place)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShippedSwedishStreetsAreStreetNames(t *testing.T) {
|
||||||
|
refused := map[string]bool{
|
||||||
|
"Färjeled Henriksdalsbryggan - Barnängsbryggan": true, "HÄLLSÄTTERSLEDEN": true, "Kärrs VSF": true,
|
||||||
|
"Länsväg 694": true, "NN": true, "Risövägen,Dejstolvägen,Runvägen": true, "tornslingan": true,
|
||||||
|
"Öresundsvägen, Bondevägen": true,
|
||||||
|
}
|
||||||
|
for _, r := range geoRows(t, "SE", "street") {
|
||||||
|
if refused[r[0]] {
|
||||||
|
t.Errorf("geo.SE: street %q in %s, want no road number, ferry route, label, placeholder, two names or miscased name", r[0], r[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func geoTSV(t *testing.T, country, table string) string {
|
||||||
|
b, err := os.ReadFile("data/geo/" + country + "/" + table + ".tsv")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func geoRows(t *testing.T, country, table string) [][]string {
|
||||||
|
var out [][]string
|
||||||
|
for _, line := range strings.Split(geoTSV(t, country, table), "\n")[1:] {
|
||||||
|
out = append(out, strings.Split(line, "\t"))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// geoPlaces reads a country's geo tables into every "street|postal-code|locality|region"
|
||||||
|
// one locality holds, the region empty for SE, whose address carries none.
|
||||||
|
func geoPlaces(t *testing.T, country string) map[string]bool {
|
||||||
|
rows := func(table string) [][]string { return geoRows(t, country, table) }
|
||||||
|
name, region := map[string]string{}, map[string]string{}
|
||||||
|
if country == "US" {
|
||||||
|
county := map[string]string{}
|
||||||
|
for _, r := range rows("municipality") {
|
||||||
|
county[r[0]] = r[2]
|
||||||
|
}
|
||||||
|
for _, r := range rows("locality") {
|
||||||
|
name[r[0]], region[r[0]] = r[1], county[r[2]]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, r := range rows("locality") {
|
||||||
|
name[r[0]] = r[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
codes := map[string][]string{}
|
||||||
|
for _, r := range rows("postal-code") {
|
||||||
|
codes[r[1]] = append(codes[r[1]], r[0])
|
||||||
|
}
|
||||||
|
places := map[string]bool{}
|
||||||
|
for _, r := range rows("street") {
|
||||||
|
for _, code := range codes[r[1]] {
|
||||||
|
places[strings.Join([]string{r[0], code, name[r[1]], region[r[1]]}, "|")] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return places
|
||||||
|
}
|
||||||
|
|
||||||
func TestShippedPersonHasParts(t *testing.T) {
|
func TestShippedPersonHasParts(t *testing.T) {
|
||||||
f := newGenerator(t, "data", WithSeed(7))
|
f := shipped(t, WithSeed(7))
|
||||||
for _, path := range []string{"sv_SE.person", "en_US.person"} {
|
for _, path := range []string{"sv_SE.person", "en_US.person"} {
|
||||||
for i := 0; i < 30; i++ {
|
for i := 0; i < 30; i++ {
|
||||||
if name := fake(t, f, path); len(name) < 3 || !regexp.MustCompile(`\S \S`).MatchString(name) {
|
if name := fake(t, f, path); len(name) < 3 || !regexp.MustCompile(`\S \S`).MatchString(name) {
|
||||||
@@ -305,20 +503,50 @@ func TestShippedPersonHasParts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NANPA's central office code records mark N11, 555, 950, 958, 959 and 976 unassignable.
|
||||||
func TestShippedUSPhone(t *testing.T) {
|
func TestShippedUSPhone(t *testing.T) {
|
||||||
f := newGenerator(t, "data", WithSeed(11))
|
f := shipped(t, WithSeed(11))
|
||||||
re := regexp.MustCompile(`^(\(\d{3}\) \d{3}-\d{4}|\d{3}-\d{3}-\d{4})$`)
|
re := regexp.MustCompile(`^(?:\(\d{3}\) |\d{3}-)([2-9]\d{2})-\d{4}$`)
|
||||||
for i := 0; i < 50; i++ {
|
unassignable := map[string]bool{"555": true, "950": true, "958": true, "959": true, "976": true}
|
||||||
if n := fake(t, f, "en_US.phone"); !re.MatchString(n) {
|
for i := 0; i < 2000; i++ {
|
||||||
|
n := fake(t, f, "en_US.phone")
|
||||||
|
m := re.FindStringSubmatch(n)
|
||||||
|
if m == nil {
|
||||||
t.Fatalf("phone %q does not match %s", n, re)
|
t.Fatalf("phone %q does not match %s", n, re)
|
||||||
}
|
}
|
||||||
|
if exchange := m[1]; exchange[1:] == "11" || unassignable[exchange] {
|
||||||
|
t.Fatalf("phone %q has the unassignable exchange %s", n, exchange)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestShippedNamespacedTree loads the whole data/ tree (not a single locale) and
|
func TestShippedTableCellsAreTrimmed(t *testing.T) {
|
||||||
// reaches each locale through its folder segment: data/sv_SE/person -> sv_SE.person.
|
err := fs.WalkDir(os.DirFS("data"), ".", func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil || !strings.HasSuffix(path, ".tsv") {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b, err := fs.ReadFile(os.DirFS("data"), path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, line := range strings.Split(string(b), "\n") {
|
||||||
|
for _, cell := range strings.Split(line, "\t") {
|
||||||
|
if cell != strings.TrimSpace(cell) {
|
||||||
|
t.Errorf("data/%s:%d: cell %q is padded", path, i+1, cell)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestShippedNamespacedTree loads every shipped module and reaches each locale through its
|
||||||
|
// namespace: data/sv_SE/person.json, mounted under sv_SE, is sv_SE.person.
|
||||||
func TestShippedNamespacedTree(t *testing.T) {
|
func TestShippedNamespacedTree(t *testing.T) {
|
||||||
f := newGenerator(t, "data", WithSeed(1))
|
f := shipped(t, WithSeed(1))
|
||||||
for _, path := range []string{"sv_SE.person", "en_US.person", "sv_SE.address.locality"} {
|
for _, path := range []string{"sv_SE.person", "en_US.person", "sv_SE.address.locality"} {
|
||||||
if got := fake(t, f, path); got == "" {
|
if got := fake(t, f, path); got == "" {
|
||||||
t.Fatalf("Fake(%q) returned empty", path)
|
t.Fatalf("Fake(%q) returned empty", path)
|
||||||
@@ -362,7 +590,7 @@ func luhnValid(s string) bool {
|
|||||||
var swedishName = regexp.MustCompile(`^\p{L}+([ -]\p{L}+)*$`)
|
var swedishName = regexp.MustCompile(`^\p{L}+([ -]\p{L}+)*$`)
|
||||||
|
|
||||||
func TestShippedStreetIsARegisteredName(t *testing.T) {
|
func TestShippedStreetIsARegisteredName(t *testing.T) {
|
||||||
f := newGenerator(t, "data", WithSeed(5))
|
f := shipped(t, WithSeed(5))
|
||||||
street := regexp.MustCompile(`^\p{L}[\p{L}\d:.-]*([ -][\p{L}\d:.-]+)*$`)
|
street := regexp.MustCompile(`^\p{L}[\p{L}\d:.-]*([ -][\p{L}\d:.-]+)*$`)
|
||||||
for i := 0; i < 300; i++ {
|
for i := 0; i < 300; i++ {
|
||||||
if s := fake(t, f, "sv_SE.address.street"); !street.MatchString(s) {
|
if s := fake(t, f, "sv_SE.address.street"); !street.MatchString(s) {
|
||||||
@@ -374,7 +602,7 @@ func TestShippedStreetIsARegisteredName(t *testing.T) {
|
|||||||
func TestShippedLastNameComposition(t *testing.T) {
|
func TestShippedLastNameComposition(t *testing.T) {
|
||||||
// last is a choice of patronymic {first}sson templates, compound
|
// last is a choice of patronymic {first}sson templates, compound
|
||||||
// {first}{last} templates and literal surnames.
|
// {first}{last} templates and literal surnames.
|
||||||
f := newGenerator(t, "data", WithSeed(6))
|
f := shipped(t, WithSeed(6))
|
||||||
for i := 0; i < 300; i++ {
|
for i := 0; i < 300; i++ {
|
||||||
if s := fake(t, f, "sv_SE.person.last"); !swedishName.MatchString(s) {
|
if s := fake(t, f, "sv_SE.person.last"); !swedishName.MatchString(s) {
|
||||||
t.Fatalf("last name %q is not a Swedish surname", s)
|
t.Fatalf("last name %q is not a Swedish surname", s)
|
||||||
@@ -384,7 +612,7 @@ func TestShippedLastNameComposition(t *testing.T) {
|
|||||||
|
|
||||||
func TestShippedStreetNumberFormats(t *testing.T) {
|
func TestShippedStreetNumberFormats(t *testing.T) {
|
||||||
// Reachable via a hyphenated path; covers all five weighted number variants.
|
// Reachable via a hyphenated path; covers all five weighted number variants.
|
||||||
f := newGenerator(t, "data", WithSeed(8))
|
f := shipped(t, WithSeed(8))
|
||||||
re := regexp.MustCompile(`^[1-9]\d{0,2}[A-Z]?$`)
|
re := regexp.MustCompile(`^[1-9]\d{0,2}[A-Z]?$`)
|
||||||
for i := 0; i < 300; i++ {
|
for i := 0; i < 300; i++ {
|
||||||
if n := fake(t, f, "sv_SE.address.street-number"); !re.MatchString(n) {
|
if n := fake(t, f, "sv_SE.address.street-number"); !re.MatchString(n) {
|
||||||
@@ -392,3 +620,31 @@ func TestShippedStreetNumberFormats(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestShippedUSTitleAgreesWithSex pins that one person's title and sex agree, and
|
||||||
|
// that the everyday titles are reachable.
|
||||||
|
func TestShippedUSTitleAgreesWithSex(t *testing.T) {
|
||||||
|
f := shipped(t, WithSeed(4))
|
||||||
|
female := map[string]bool{"Miss": true, "Mrs": true, "Ms": true}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for i := 0; i < 3000; i++ {
|
||||||
|
got := fakeTemplate(t, f, `{/en_US.person as p}{p.prefix}|{p.sex}`)
|
||||||
|
title, sex, _ := strings.Cut(got, "|")
|
||||||
|
title = strings.TrimSpace(title)
|
||||||
|
seen[title] = true
|
||||||
|
if title == "Mr" && sex != "male" || female[title] && sex != "female" {
|
||||||
|
t.Fatalf("%q: the title contradicts the sex", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, want := range []string{"", "Mr", "Ms"} {
|
||||||
|
if !seen[want] {
|
||||||
|
t.Errorf("en_US.person.prefix never drew %q in 3000 draws", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
swedish := map[string]bool{"dr": true, "prof": true}
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
if got := fake(t, f, "sv_SE.title"); !swedish[got] {
|
||||||
|
t.Fatalf("sv_SE.title = %q, want an unsexed Swedish honorific", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user