A human-friendly DSL for ATProto Lexicons
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Better validation

+1072 -120
+645 -10
Cargo.lock
··· 3 3 version = 4 4 4 5 5 [[package]] 6 + name = "abnf" 7 + version = "0.13.0" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "087113bd50d9adce24850eed5d0476c7d199d532fce8fab5173650331e09033a" 10 + dependencies = [ 11 + "abnf-core", 12 + "nom 7.1.3", 13 + ] 14 + 15 + [[package]] 16 + name = "abnf-core" 17 + version = "0.5.0" 18 + source = "registry+https://github.com/rust-lang/crates.io-index" 19 + checksum = "c44e09c43ae1c368fb91a03a566472d0087c26cf7e1b9e8e289c14ede681dd7d" 20 + dependencies = [ 21 + "nom 7.1.3", 22 + ] 23 + 24 + [[package]] 6 25 name = "addr2line" 7 26 version = "0.25.1" 8 27 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 107 126 checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" 108 127 109 128 [[package]] 129 + name = "block-buffer" 130 + version = "0.10.4" 131 + source = "registry+https://github.com/rust-lang/crates.io-index" 132 + checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 133 + dependencies = [ 134 + "generic-array", 135 + ] 136 + 137 + [[package]] 138 + name = "btree-range-map" 139 + version = "0.7.2" 140 + source = "registry+https://github.com/rust-lang/crates.io-index" 141 + checksum = "1be5c9672446d3800bcbcaabaeba121fe22f1fb25700c4562b22faf76d377c33" 142 + dependencies = [ 143 + "btree-slab", 144 + "cc-traits", 145 + "range-traits", 146 + "serde", 147 + "slab", 148 + ] 149 + 150 + [[package]] 151 + name = "btree-slab" 152 + version = "0.6.1" 153 + source = "registry+https://github.com/rust-lang/crates.io-index" 154 + checksum = "7a2b56d3029f075c4fa892428a098425b86cef5c89ae54073137ece416aef13c" 155 + dependencies = [ 156 + "cc-traits", 157 + "slab", 158 + "smallvec", 159 + ] 160 + 161 + [[package]] 110 162 name = "bumpalo" 111 163 version = "3.19.0" 112 164 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 123 175 ] 124 176 125 177 [[package]] 178 + name = "cc-traits" 179 + version = "2.0.0" 180 + source = "registry+https://github.com/rust-lang/crates.io-index" 181 + checksum = "060303ef31ef4a522737e1b1ab68c67916f2a787bb2f4f54f383279adba962b5" 182 + dependencies = [ 183 + "slab", 184 + ] 185 + 186 + [[package]] 126 187 name = "cfg-if" 127 188 version = "1.0.3" 128 189 source = "registry+https://github.com/rust-lang/crates.io-index" 129 190 checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 130 191 131 192 [[package]] 193 + name = "ciborium" 194 + version = "0.2.2" 195 + source = "registry+https://github.com/rust-lang/crates.io-index" 196 + checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" 197 + dependencies = [ 198 + "ciborium-io", 199 + "ciborium-ll", 200 + "serde", 201 + ] 202 + 203 + [[package]] 204 + name = "ciborium-io" 205 + version = "0.2.2" 206 + source = "registry+https://github.com/rust-lang/crates.io-index" 207 + checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" 208 + 209 + [[package]] 210 + name = "ciborium-ll" 211 + version = "0.2.2" 212 + source = "registry+https://github.com/rust-lang/crates.io-index" 213 + checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" 214 + dependencies = [ 215 + "ciborium-io", 216 + "half", 217 + ] 218 + 219 + [[package]] 132 220 name = "clap" 133 221 version = "4.5.48" 134 222 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 159 247 "heck", 160 248 "proc-macro2", 161 249 "quote", 162 - "syn", 250 + "syn 2.0.106", 163 251 ] 164 252 165 253 [[package]] ··· 175 263 checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 176 264 177 265 [[package]] 266 + name = "cpufeatures" 267 + version = "0.2.17" 268 + source = "registry+https://github.com/rust-lang/crates.io-index" 269 + checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 270 + dependencies = [ 271 + "libc", 272 + ] 273 + 274 + [[package]] 275 + name = "crunchy" 276 + version = "0.2.4" 277 + source = "registry+https://github.com/rust-lang/crates.io-index" 278 + checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" 279 + 280 + [[package]] 281 + name = "crypto-common" 282 + version = "0.1.6" 283 + source = "registry+https://github.com/rust-lang/crates.io-index" 284 + checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 285 + dependencies = [ 286 + "generic-array", 287 + "typenum", 288 + ] 289 + 290 + [[package]] 291 + name = "deranged" 292 + version = "0.5.4" 293 + source = "registry+https://github.com/rust-lang/crates.io-index" 294 + checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" 295 + dependencies = [ 296 + "powerfmt", 297 + ] 298 + 299 + [[package]] 300 + name = "digest" 301 + version = "0.10.7" 302 + source = "registry+https://github.com/rust-lang/crates.io-index" 303 + checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 304 + dependencies = [ 305 + "block-buffer", 306 + "crypto-common", 307 + ] 308 + 309 + [[package]] 310 + name = "displaydoc" 311 + version = "0.2.5" 312 + source = "registry+https://github.com/rust-lang/crates.io-index" 313 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 314 + dependencies = [ 315 + "proc-macro2", 316 + "quote", 317 + "syn 2.0.106", 318 + ] 319 + 320 + [[package]] 178 321 name = "equivalent" 179 322 version = "1.0.2" 180 323 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 195 338 version = "0.1.3" 196 339 source = "registry+https://github.com/rust-lang/crates.io-index" 197 340 checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" 341 + 342 + [[package]] 343 + name = "form_urlencoded" 344 + version = "1.2.2" 345 + source = "registry+https://github.com/rust-lang/crates.io-index" 346 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 347 + dependencies = [ 348 + "percent-encoding", 349 + ] 350 + 351 + [[package]] 352 + name = "generic-array" 353 + version = "0.14.7" 354 + source = "registry+https://github.com/rust-lang/crates.io-index" 355 + checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 356 + dependencies = [ 357 + "typenum", 358 + "version_check", 359 + ] 198 360 199 361 [[package]] 200 362 name = "gimli" ··· 209 371 checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" 210 372 211 373 [[package]] 374 + name = "half" 375 + version = "2.6.0" 376 + source = "registry+https://github.com/rust-lang/crates.io-index" 377 + checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" 378 + dependencies = [ 379 + "cfg-if", 380 + "crunchy", 381 + ] 382 + 383 + [[package]] 212 384 name = "hashbrown" 213 385 version = "0.16.0" 214 386 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 221 393 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 222 394 223 395 [[package]] 396 + name = "hex_fmt" 397 + version = "0.3.0" 398 + source = "registry+https://github.com/rust-lang/crates.io-index" 399 + checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" 400 + 401 + [[package]] 402 + name = "icu_collections" 403 + version = "2.0.0" 404 + source = "registry+https://github.com/rust-lang/crates.io-index" 405 + checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 406 + dependencies = [ 407 + "displaydoc", 408 + "potential_utf", 409 + "yoke", 410 + "zerofrom", 411 + "zerovec", 412 + ] 413 + 414 + [[package]] 415 + name = "icu_locale_core" 416 + version = "2.0.0" 417 + source = "registry+https://github.com/rust-lang/crates.io-index" 418 + checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 419 + dependencies = [ 420 + "displaydoc", 421 + "litemap", 422 + "tinystr", 423 + "writeable", 424 + "zerovec", 425 + ] 426 + 427 + [[package]] 428 + name = "icu_normalizer" 429 + version = "2.0.0" 430 + source = "registry+https://github.com/rust-lang/crates.io-index" 431 + checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 432 + dependencies = [ 433 + "displaydoc", 434 + "icu_collections", 435 + "icu_normalizer_data", 436 + "icu_properties", 437 + "icu_provider", 438 + "smallvec", 439 + "zerovec", 440 + ] 441 + 442 + [[package]] 443 + name = "icu_normalizer_data" 444 + version = "2.0.0" 445 + source = "registry+https://github.com/rust-lang/crates.io-index" 446 + checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 447 + 448 + [[package]] 449 + name = "icu_properties" 450 + version = "2.0.1" 451 + source = "registry+https://github.com/rust-lang/crates.io-index" 452 + checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 453 + dependencies = [ 454 + "displaydoc", 455 + "icu_collections", 456 + "icu_locale_core", 457 + "icu_properties_data", 458 + "icu_provider", 459 + "potential_utf", 460 + "zerotrie", 461 + "zerovec", 462 + ] 463 + 464 + [[package]] 465 + name = "icu_properties_data" 466 + version = "2.0.1" 467 + source = "registry+https://github.com/rust-lang/crates.io-index" 468 + checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 469 + 470 + [[package]] 471 + name = "icu_provider" 472 + version = "2.0.0" 473 + source = "registry+https://github.com/rust-lang/crates.io-index" 474 + checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 475 + dependencies = [ 476 + "displaydoc", 477 + "icu_locale_core", 478 + "stable_deref_trait", 479 + "tinystr", 480 + "writeable", 481 + "yoke", 482 + "zerofrom", 483 + "zerotrie", 484 + "zerovec", 485 + ] 486 + 487 + [[package]] 488 + name = "idna" 489 + version = "1.1.0" 490 + source = "registry+https://github.com/rust-lang/crates.io-index" 491 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 492 + dependencies = [ 493 + "idna_adapter", 494 + "smallvec", 495 + "utf8_iter", 496 + ] 497 + 498 + [[package]] 499 + name = "idna_adapter" 500 + version = "1.2.1" 501 + source = "registry+https://github.com/rust-lang/crates.io-index" 502 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 503 + dependencies = [ 504 + "icu_normalizer", 505 + "icu_properties", 506 + ] 507 + 508 + [[package]] 224 509 name = "include_dir" 225 510 version = "0.7.4" 226 511 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 250 535 ] 251 536 252 537 [[package]] 538 + name = "indoc" 539 + version = "2.0.6" 540 + source = "registry+https://github.com/rust-lang/crates.io-index" 541 + checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 542 + 543 + [[package]] 253 544 name = "is_ci" 254 545 version = "1.2.0" 255 546 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 278 569 ] 279 570 280 571 [[package]] 572 + name = "langtag" 573 + version = "0.4.0" 574 + source = "registry+https://github.com/rust-lang/crates.io-index" 575 + checksum = "9ecb4c689a30e48ebeaa14237f34037e300dd072e6ad21a9ec72e810ff3c6600" 576 + dependencies = [ 577 + "static-regular-grammar", 578 + "thiserror 1.0.69", 579 + ] 580 + 581 + [[package]] 281 582 name = "libc" 282 583 version = "0.2.176" 283 584 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 290 591 checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 291 592 292 593 [[package]] 594 + name = "litemap" 595 + version = "0.8.0" 596 + source = "registry+https://github.com/rust-lang/crates.io-index" 597 + checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 598 + 599 + [[package]] 293 600 name = "log" 294 601 version = "0.4.28" 295 602 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 328 635 dependencies = [ 329 636 "proc-macro2", 330 637 "quote", 331 - "syn", 638 + "syn 2.0.106", 332 639 ] 333 640 334 641 [[package]] ··· 342 649 ] 343 650 344 651 [[package]] 652 + name = "minimal-lexical" 653 + version = "0.2.1" 654 + source = "registry+https://github.com/rust-lang/crates.io-index" 655 + checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 656 + 657 + [[package]] 345 658 name = "miniz_oxide" 346 659 version = "0.8.9" 347 660 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 363 676 "mlf-validation", 364 677 "serde", 365 678 "serde_json", 366 - "thiserror", 679 + "thiserror 2.0.17", 367 680 ] 368 681 369 682 [[package]] ··· 387 700 version = "0.1.0" 388 701 dependencies = [ 389 702 "include_dir", 390 - "nom", 703 + "nom 8.0.0", 391 704 ] 392 705 393 706 [[package]] 394 707 name = "mlf-validation" 395 708 version = "0.1.0" 396 709 dependencies = [ 710 + "langtag", 397 711 "mlf-lang", 712 + "regex", 398 713 "serde_json", 714 + "time", 715 + "unicode-segmentation", 716 + "url", 399 717 ] 400 718 401 719 [[package]] ··· 414 732 415 733 [[package]] 416 734 name = "nom" 735 + version = "7.1.3" 736 + source = "registry+https://github.com/rust-lang/crates.io-index" 737 + checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 738 + dependencies = [ 739 + "memchr", 740 + "minimal-lexical", 741 + ] 742 + 743 + [[package]] 744 + name = "nom" 417 745 version = "8.0.0" 418 746 source = "registry+https://github.com/rust-lang/crates.io-index" 419 747 checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" 420 748 dependencies = [ 421 749 "memchr", 422 750 ] 751 + 752 + [[package]] 753 + name = "num-conv" 754 + version = "0.1.0" 755 + source = "registry+https://github.com/rust-lang/crates.io-index" 756 + checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 423 757 424 758 [[package]] 425 759 name = "object" ··· 449 783 checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" 450 784 451 785 [[package]] 786 + name = "percent-encoding" 787 + version = "2.3.2" 788 + source = "registry+https://github.com/rust-lang/crates.io-index" 789 + checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 790 + 791 + [[package]] 792 + name = "potential_utf" 793 + version = "0.1.3" 794 + source = "registry+https://github.com/rust-lang/crates.io-index" 795 + checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" 796 + dependencies = [ 797 + "zerovec", 798 + ] 799 + 800 + [[package]] 801 + name = "powerfmt" 802 + version = "0.2.0" 803 + source = "registry+https://github.com/rust-lang/crates.io-index" 804 + checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 805 + 806 + [[package]] 807 + name = "proc-macro-error" 808 + version = "1.0.4" 809 + source = "registry+https://github.com/rust-lang/crates.io-index" 810 + checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 811 + dependencies = [ 812 + "proc-macro-error-attr", 813 + "proc-macro2", 814 + "quote", 815 + "syn 1.0.109", 816 + "version_check", 817 + ] 818 + 819 + [[package]] 820 + name = "proc-macro-error-attr" 821 + version = "1.0.4" 822 + source = "registry+https://github.com/rust-lang/crates.io-index" 823 + checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 824 + dependencies = [ 825 + "proc-macro2", 826 + "quote", 827 + "version_check", 828 + ] 829 + 830 + [[package]] 452 831 name = "proc-macro2" 453 832 version = "1.0.101" 454 833 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 465 844 dependencies = [ 466 845 "proc-macro2", 467 846 ] 847 + 848 + [[package]] 849 + name = "range-traits" 850 + version = "0.3.2" 851 + source = "registry+https://github.com/rust-lang/crates.io-index" 852 + checksum = "d20581732dd76fa913c7dff1a2412b714afe3573e94d41c34719de73337cc8ab" 468 853 469 854 [[package]] 470 855 name = "regex" ··· 573 958 dependencies = [ 574 959 "proc-macro2", 575 960 "quote", 576 - "syn", 961 + "syn 2.0.106", 577 962 ] 578 963 579 964 [[package]] ··· 591 976 ] 592 977 593 978 [[package]] 979 + name = "sha2" 980 + version = "0.10.9" 981 + source = "registry+https://github.com/rust-lang/crates.io-index" 982 + checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 983 + dependencies = [ 984 + "cfg-if", 985 + "cpufeatures", 986 + "digest", 987 + ] 988 + 989 + [[package]] 594 990 name = "shlex" 595 991 version = "1.3.0" 596 992 source = "registry+https://github.com/rust-lang/crates.io-index" 597 993 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 598 994 599 995 [[package]] 996 + name = "slab" 997 + version = "0.4.11" 998 + source = "registry+https://github.com/rust-lang/crates.io-index" 999 + checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" 1000 + 1001 + [[package]] 1002 + name = "smallvec" 1003 + version = "1.15.1" 1004 + source = "registry+https://github.com/rust-lang/crates.io-index" 1005 + checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 1006 + 1007 + [[package]] 1008 + name = "stable_deref_trait" 1009 + version = "1.2.0" 1010 + source = "registry+https://github.com/rust-lang/crates.io-index" 1011 + checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1012 + 1013 + [[package]] 1014 + name = "static-regular-grammar" 1015 + version = "2.0.2" 1016 + source = "registry+https://github.com/rust-lang/crates.io-index" 1017 + checksum = "4f4a6c40247579acfbb138c3cd7de3dab113ab4ac6227f1b7de7d626ee667957" 1018 + dependencies = [ 1019 + "abnf", 1020 + "btree-range-map", 1021 + "ciborium", 1022 + "hex_fmt", 1023 + "indoc", 1024 + "proc-macro-error", 1025 + "proc-macro2", 1026 + "quote", 1027 + "serde", 1028 + "sha2", 1029 + "syn 2.0.106", 1030 + "thiserror 1.0.69", 1031 + ] 1032 + 1033 + [[package]] 600 1034 name = "strsim" 601 1035 version = "0.11.1" 602 1036 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 625 1059 626 1060 [[package]] 627 1061 name = "syn" 1062 + version = "1.0.109" 1063 + source = "registry+https://github.com/rust-lang/crates.io-index" 1064 + checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1065 + dependencies = [ 1066 + "proc-macro2", 1067 + "unicode-ident", 1068 + ] 1069 + 1070 + [[package]] 1071 + name = "syn" 628 1072 version = "2.0.106" 629 1073 source = "registry+https://github.com/rust-lang/crates.io-index" 630 1074 checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" ··· 635 1079 ] 636 1080 637 1081 [[package]] 1082 + name = "synstructure" 1083 + version = "0.13.2" 1084 + source = "registry+https://github.com/rust-lang/crates.io-index" 1085 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 1086 + dependencies = [ 1087 + "proc-macro2", 1088 + "quote", 1089 + "syn 2.0.106", 1090 + ] 1091 + 1092 + [[package]] 638 1093 name = "terminal_size" 639 1094 version = "0.4.3" 640 1095 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 656 1111 657 1112 [[package]] 658 1113 name = "thiserror" 1114 + version = "1.0.69" 1115 + source = "registry+https://github.com/rust-lang/crates.io-index" 1116 + checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1117 + dependencies = [ 1118 + "thiserror-impl 1.0.69", 1119 + ] 1120 + 1121 + [[package]] 1122 + name = "thiserror" 659 1123 version = "2.0.17" 660 1124 source = "registry+https://github.com/rust-lang/crates.io-index" 661 1125 checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 662 1126 dependencies = [ 663 - "thiserror-impl", 1127 + "thiserror-impl 2.0.17", 1128 + ] 1129 + 1130 + [[package]] 1131 + name = "thiserror-impl" 1132 + version = "1.0.69" 1133 + source = "registry+https://github.com/rust-lang/crates.io-index" 1134 + checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1135 + dependencies = [ 1136 + "proc-macro2", 1137 + "quote", 1138 + "syn 2.0.106", 664 1139 ] 665 1140 666 1141 [[package]] ··· 671 1146 dependencies = [ 672 1147 "proc-macro2", 673 1148 "quote", 674 - "syn", 1149 + "syn 2.0.106", 1150 + ] 1151 + 1152 + [[package]] 1153 + name = "time" 1154 + version = "0.3.44" 1155 + source = "registry+https://github.com/rust-lang/crates.io-index" 1156 + checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" 1157 + dependencies = [ 1158 + "deranged", 1159 + "num-conv", 1160 + "powerfmt", 1161 + "serde", 1162 + "time-core", 1163 + "time-macros", 1164 + ] 1165 + 1166 + [[package]] 1167 + name = "time-core" 1168 + version = "0.1.6" 1169 + source = "registry+https://github.com/rust-lang/crates.io-index" 1170 + checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" 1171 + 1172 + [[package]] 1173 + name = "time-macros" 1174 + version = "0.2.24" 1175 + source = "registry+https://github.com/rust-lang/crates.io-index" 1176 + checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" 1177 + dependencies = [ 1178 + "num-conv", 1179 + "time-core", 1180 + ] 1181 + 1182 + [[package]] 1183 + name = "tinystr" 1184 + version = "0.8.1" 1185 + source = "registry+https://github.com/rust-lang/crates.io-index" 1186 + checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 1187 + dependencies = [ 1188 + "displaydoc", 1189 + "zerovec", 675 1190 ] 676 1191 677 1192 [[package]] ··· 693 1208 ] 694 1209 695 1210 [[package]] 1211 + name = "typenum" 1212 + version = "1.19.0" 1213 + source = "registry+https://github.com/rust-lang/crates.io-index" 1214 + checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" 1215 + 1216 + [[package]] 696 1217 name = "unicode-ident" 697 1218 version = "1.0.19" 698 1219 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 705 1226 checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" 706 1227 707 1228 [[package]] 1229 + name = "unicode-segmentation" 1230 + version = "1.12.0" 1231 + source = "registry+https://github.com/rust-lang/crates.io-index" 1232 + checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1233 + 1234 + [[package]] 708 1235 name = "unicode-width" 709 1236 version = "0.1.14" 710 1237 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 717 1244 checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" 718 1245 719 1246 [[package]] 1247 + name = "url" 1248 + version = "2.5.7" 1249 + source = "registry+https://github.com/rust-lang/crates.io-index" 1250 + checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 1251 + dependencies = [ 1252 + "form_urlencoded", 1253 + "idna", 1254 + "percent-encoding", 1255 + "serde", 1256 + ] 1257 + 1258 + [[package]] 1259 + name = "utf8_iter" 1260 + version = "1.0.4" 1261 + source = "registry+https://github.com/rust-lang/crates.io-index" 1262 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1263 + 1264 + [[package]] 720 1265 name = "utf8parse" 721 1266 version = "0.2.2" 722 1267 source = "registry+https://github.com/rust-lang/crates.io-index" 723 1268 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 724 1269 725 1270 [[package]] 1271 + name = "version_check" 1272 + version = "0.9.5" 1273 + source = "registry+https://github.com/rust-lang/crates.io-index" 1274 + checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1275 + 1276 + [[package]] 726 1277 name = "walkdir" 727 1278 version = "2.5.0" 728 1279 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 755 1306 "log", 756 1307 "proc-macro2", 757 1308 "quote", 758 - "syn", 1309 + "syn 2.0.106", 759 1310 "wasm-bindgen-shared", 760 1311 ] 761 1312 ··· 790 1341 dependencies = [ 791 1342 "proc-macro2", 792 1343 "quote", 793 - "syn", 1344 + "syn 2.0.106", 794 1345 "wasm-bindgen-backend", 795 1346 "wasm-bindgen-shared", 796 1347 ] ··· 825 1376 dependencies = [ 826 1377 "proc-macro2", 827 1378 "quote", 828 - "syn", 1379 + "syn 2.0.106", 829 1380 ] 830 1381 831 1382 [[package]] ··· 926 1477 version = "0.53.0" 927 1478 source = "registry+https://github.com/rust-lang/crates.io-index" 928 1479 checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 1480 + 1481 + [[package]] 1482 + name = "writeable" 1483 + version = "0.6.1" 1484 + source = "registry+https://github.com/rust-lang/crates.io-index" 1485 + checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 1486 + 1487 + [[package]] 1488 + name = "yoke" 1489 + version = "0.8.0" 1490 + source = "registry+https://github.com/rust-lang/crates.io-index" 1491 + checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 1492 + dependencies = [ 1493 + "serde", 1494 + "stable_deref_trait", 1495 + "yoke-derive", 1496 + "zerofrom", 1497 + ] 1498 + 1499 + [[package]] 1500 + name = "yoke-derive" 1501 + version = "0.8.0" 1502 + source = "registry+https://github.com/rust-lang/crates.io-index" 1503 + checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 1504 + dependencies = [ 1505 + "proc-macro2", 1506 + "quote", 1507 + "syn 2.0.106", 1508 + "synstructure", 1509 + ] 1510 + 1511 + [[package]] 1512 + name = "zerofrom" 1513 + version = "0.1.6" 1514 + source = "registry+https://github.com/rust-lang/crates.io-index" 1515 + checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 1516 + dependencies = [ 1517 + "zerofrom-derive", 1518 + ] 1519 + 1520 + [[package]] 1521 + name = "zerofrom-derive" 1522 + version = "0.1.6" 1523 + source = "registry+https://github.com/rust-lang/crates.io-index" 1524 + checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 1525 + dependencies = [ 1526 + "proc-macro2", 1527 + "quote", 1528 + "syn 2.0.106", 1529 + "synstructure", 1530 + ] 1531 + 1532 + [[package]] 1533 + name = "zerotrie" 1534 + version = "0.2.2" 1535 + source = "registry+https://github.com/rust-lang/crates.io-index" 1536 + checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 1537 + dependencies = [ 1538 + "displaydoc", 1539 + "yoke", 1540 + "zerofrom", 1541 + ] 1542 + 1543 + [[package]] 1544 + name = "zerovec" 1545 + version = "0.11.4" 1546 + source = "registry+https://github.com/rust-lang/crates.io-index" 1547 + checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" 1548 + dependencies = [ 1549 + "yoke", 1550 + "zerofrom", 1551 + "zerovec-derive", 1552 + ] 1553 + 1554 + [[package]] 1555 + name = "zerovec-derive" 1556 + version = "0.11.1" 1557 + source = "registry+https://github.com/rust-lang/crates.io-index" 1558 + checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 1559 + dependencies = [ 1560 + "proc-macro2", 1561 + "quote", 1562 + "syn 2.0.106", 1563 + ]
+5
mlf-validation/Cargo.toml
··· 7 7 [dependencies] 8 8 mlf-lang = { path = "../mlf-lang" } 9 9 serde_json = "1" 10 + unicode-segmentation = "1.12" 11 + regex = { version = "1", default-features = false, features = ["std", "unicode-perl"] } 12 + url = "2" 13 + time = { version = "0.3", features = ["parsing", "macros"] } 14 + langtag = "0.4"
+422 -110
mlf-validation/src/lib.rs
··· 1 1 use mlf_lang::ast::*; 2 2 use serde_json::Value as JsonValue; 3 3 use std::fmt; 4 + use unicode_segmentation::UnicodeSegmentation; 5 + use regex::Regex; 6 + use url::Url; 7 + use time::format_description::well_known::Rfc3339; 8 + use time::OffsetDateTime; 9 + use langtag::LangTag; 4 10 5 11 #[derive(Debug, Clone)] 6 12 pub struct ValidationError { ··· 243 249 message: format!("String too short: {} bytes (min: {})", s.len(), min), 244 250 }); 245 251 } 252 + } else if let Some(arr) = value.as_array() { 253 + // MinLength can also apply to arrays (element count) 254 + if arr.len() < *min { 255 + errors.push(ValidationError { 256 + path: path.to_string(), 257 + message: format!("Array too short: {} elements (min: {})", arr.len(), min), 258 + }); 259 + } 246 260 } 247 261 } 248 262 Constraint::MaxLength { value: max, .. } => { ··· 253 267 message: format!("String too long: {} bytes (max: {})", s.len(), max), 254 268 }); 255 269 } 270 + } else if let Some(arr) = value.as_array() { 271 + // MaxLength can also apply to arrays (element count) 272 + if arr.len() > *max { 273 + errors.push(ValidationError { 274 + path: path.to_string(), 275 + message: format!("Array too long: {} elements (max: {})", arr.len(), max), 276 + }); 277 + } 256 278 } 257 279 } 258 280 Constraint::MinGraphemes { value: min, .. } => { 259 281 if let Some(s) = value.as_str() { 260 - let count = s.chars().count(); // Simplified grapheme count 282 + // Use proper Unicode grapheme cluster counting 283 + let count = s.graphemes(true).count(); 261 284 if count < *min { 262 285 errors.push(ValidationError { 263 286 path: path.to_string(), ··· 268 291 } 269 292 Constraint::MaxGraphemes { value: max, .. } => { 270 293 if let Some(s) = value.as_str() { 271 - let count = s.chars().count(); // Simplified grapheme count 294 + // Use proper Unicode grapheme cluster counting 295 + let count = s.graphemes(true).count(); 272 296 if count > *max { 273 297 errors.push(ValidationError { 274 298 path: path.to_string(), ··· 362 386 path: &str, 363 387 errors: &mut Vec<ValidationError>, 364 388 ) { 365 - // TODO: Implement proper format validation according to ATProto spec 366 - // These are currently basic checks and should be replaced with proper validation: 367 - // - datetime: RFC 3339 datetime validation 368 - // - uri: RFC 3986 URI validation 369 - // - at-uri: ATProto AT-URI validation 370 - // - did: DID spec validation (did:plc:, did:web:, etc.) 371 - // - handle: ATProto handle validation 372 - // - nsid: NSID validation (reverse domain name) 373 - // - cid: CID validation (multibase-encoded multihash) 374 - // - at-identifier: either a DID or handle 375 - // - language: BCP 47 language tag validation 376 - // - tid: TID validation (base32-sortable timestamp) 377 - // - record-key: Record key validation 389 + let is_valid = match format { 390 + "datetime" => validate_datetime(value), 391 + "uri" => validate_uri(value), 392 + "at-uri" => validate_at_uri(value), 393 + "did" => validate_did(value), 394 + "handle" => validate_handle(value), 395 + "nsid" => validate_nsid(value), 396 + "cid" => validate_cid(value), 397 + "at-identifier" => validate_at_identifier(value), 398 + "language" => validate_language(value), 399 + "tid" => validate_tid(value), 400 + "record-key" => validate_record_key(value), 401 + _ => true, // Unknown format, pass validation 402 + }; 378 403 379 - match format { 380 - "datetime" => { 381 - // Basic ISO 8601 datetime validation 382 - if !value.contains('T') && !value.contains('Z') && !value.contains('+') && !value.contains('-') { 383 - errors.push(ValidationError { 384 - path: path.to_string(), 385 - message: format!("Invalid datetime format: '{}' (expected ISO 8601)", value), 386 - }); 387 - } 388 - } 389 - "uri" => { 390 - if !value.starts_with("http://") && !value.starts_with("https://") && !value.starts_with("ftp://") { 391 - errors.push(ValidationError { 392 - path: path.to_string(), 393 - message: format!("Invalid URI: '{}' (expected http://, https://, or ftp://)", value), 394 - }); 395 - } 396 - } 397 - "at-uri" => { 398 - if !value.starts_with("at://") { 399 - errors.push(ValidationError { 400 - path: path.to_string(), 401 - message: format!("Invalid AT-URI: '{}' (expected at://)", value), 402 - }); 403 - } 404 - } 405 - "did" => { 406 - if !value.starts_with("did:") { 407 - errors.push(ValidationError { 408 - path: path.to_string(), 409 - message: format!("Invalid DID: '{}' (expected did:)", value), 410 - }); 411 - } 412 - } 413 - "handle" => { 414 - // Basic handle validation (domain-like) 415 - if !value.contains('.') || value.starts_with('.') || value.ends_with('.') { 416 - errors.push(ValidationError { 417 - path: path.to_string(), 418 - message: format!("Invalid handle: '{}' (expected domain format)", value), 419 - }); 420 - } 421 - } 422 - "nsid" => { 423 - // NSID format: authority.name(.name)* 424 - let parts: Vec<&str> = value.split('.').collect(); 425 - if parts.len() < 2 { 426 - errors.push(ValidationError { 427 - path: path.to_string(), 428 - message: format!("Invalid NSID: '{}' (expected at least 2 segments)", value), 429 - }); 430 - } 431 - } 432 - "cid" => { 433 - // CID validation is complex, just check not empty 434 - if value.is_empty() { 435 - errors.push(ValidationError { 436 - path: path.to_string(), 437 - message: "Invalid CID: empty string".to_string(), 438 - }); 439 - } 440 - } 441 - "at-identifier" => { 442 - // Can be either DID or handle 443 - let is_did = value.starts_with("did:"); 444 - let is_handle = value.contains('.') && !value.starts_with('.') && !value.ends_with('.'); 445 - 446 - if !is_did && !is_handle { 447 - errors.push(ValidationError { 448 - path: path.to_string(), 449 - message: format!("Invalid AT-identifier: '{}' (expected DID or handle)", value), 450 - }); 451 - } 452 - } 453 - "language" => { 454 - // Basic BCP 47 validation (simplified) 455 - if value.len() < 2 || !value.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') { 456 - errors.push(ValidationError { 457 - path: path.to_string(), 458 - message: format!("Invalid language code: '{}'", value), 459 - }); 460 - } 461 - } 462 - "tid" | "record-key" => { 463 - // Basic validation for TID and record-key 464 - if value.is_empty() { 465 - errors.push(ValidationError { 466 - path: path.to_string(), 467 - message: format!("Invalid {}: empty string", format), 468 - }); 469 - } 470 - } 471 - _ => { 472 - // Unknown format, skip validation 473 - } 404 + if !is_valid { 405 + errors.push(ValidationError { 406 + path: path.to_string(), 407 + message: format!("Invalid {} format: '{}'", format, value), 408 + }); 474 409 } 475 410 } 476 411 ··· 569 504 JsonValue::Object(_) => "object", 570 505 } 571 506 } 507 + 508 + // Format validators 509 + 510 + /// Validate datetime format (RFC 3339 / ISO 8601) 511 + fn validate_datetime(value: &str) -> bool { 512 + // Use time crate for proper RFC 3339 parsing 513 + OffsetDateTime::parse(value, &Rfc3339).is_ok() 514 + } 515 + 516 + /// Validate URI format (RFC 3986) 517 + fn validate_uri(value: &str) -> bool { 518 + // Use url crate for proper URI parsing 519 + Url::parse(value).is_ok() 520 + } 521 + 522 + /// Validate AT-URI format (at://did:plc:xyz/com.example.foo/record-key) 523 + fn validate_at_uri(value: &str) -> bool { 524 + // AT-URI format: at://authority/collection/rkey 525 + // authority is a DID or handle 526 + // collection is an NSID 527 + // rkey is optional 528 + 529 + if !value.starts_with("at://") { 530 + return false; 531 + } 532 + 533 + // Strip the scheme 534 + let without_scheme = &value[5..]; 535 + 536 + // Split by first slash to get authority and path 537 + let (authority, path) = match without_scheme.split_once('/') { 538 + Some((auth, p)) => (auth, Some(p)), 539 + None => (without_scheme, None), 540 + }; 541 + 542 + // Authority must be a DID or handle 543 + if !validate_did(authority) && !validate_handle(authority) { 544 + return false; 545 + } 546 + 547 + // Path validation (if present) 548 + if let Some(path_str) = path { 549 + if !path_str.is_empty() { 550 + // Path should be collection or collection/rkey 551 + let parts: Vec<&str> = path_str.split('/').filter(|s| !s.is_empty()).collect(); 552 + if parts.is_empty() || parts.len() > 2 { 553 + return false; 554 + } 555 + // Collection should be an NSID 556 + if !validate_nsid(parts[0]) { 557 + return false; 558 + } 559 + // Record key validation (if present) 560 + if parts.len() == 2 && !validate_record_key(parts[1]) { 561 + return false; 562 + } 563 + } 564 + } 565 + 566 + true 567 + } 568 + 569 + /// Validate DID format (did:method:identifier) 570 + fn validate_did(value: &str) -> bool { 571 + // DID format: did:method:method-specific-id 572 + // method: lowercase letters, numbers 573 + // method-specific-id: alphanumeric plus . - _ : 574 + let re = Regex::new( 575 + r"^did:[a-z0-9]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$" 576 + ).unwrap(); 577 + re.is_match(value) 578 + } 579 + 580 + /// Validate handle format (domain name) 581 + fn validate_handle(value: &str) -> bool { 582 + // Handle is a domain name: segment.segment.segment 583 + // Each segment: alphanumeric and hyphen, can't start or end with hyphen 584 + // Must have at least one dot 585 + if !value.contains('.') || value.starts_with('.') || value.ends_with('.') { 586 + return false; 587 + } 588 + 589 + // Check each segment 590 + for segment in value.split('.') { 591 + if segment.is_empty() 592 + || segment.starts_with('-') 593 + || segment.ends_with('-') 594 + || segment.len() > 63 { 595 + return false; 596 + } 597 + if !segment.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') { 598 + return false; 599 + } 600 + } 601 + 602 + // Total length check 603 + value.len() <= 253 604 + } 605 + 606 + /// Validate NSID format (namespaced identifier) 607 + fn validate_nsid(value: &str) -> bool { 608 + // NSID format: authority.name(.name)* 609 + // authority: domain name (reversed) 610 + // name: lowercase alphanumeric, max 63 chars per segment 611 + // Total: 3-317 chars 612 + if value.len() < 3 || value.len() > 317 { 613 + return false; 614 + } 615 + 616 + let parts: Vec<&str> = value.split('.').collect(); 617 + if parts.len() < 3 { 618 + return false; 619 + } 620 + 621 + // Check each segment 622 + for part in &parts { 623 + if part.is_empty() || part.len() > 63 { 624 + return false; 625 + } 626 + // NSID segments must be lowercase alphanumeric (and hyphen for domain parts) 627 + if !part.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') { 628 + return false; 629 + } 630 + // Can't start with digit 631 + if part.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) { 632 + return false; 633 + } 634 + } 635 + 636 + true 637 + } 638 + 639 + /// Validate CID format (Content Identifier) 640 + fn validate_cid(value: &str) -> bool { 641 + // CID format is complex (multibase encoded multihash) 642 + // Basic validation: non-empty, starts with base58btc or base32 prefix 643 + // Full validation would require parsing the multibase/multihash 644 + if value.is_empty() { 645 + return false; 646 + } 647 + 648 + // CIDv0: starts with 'Qm' (base58btc) 649 + // CIDv1: starts with 'b' (base32) or 'z' (base58btc) followed by version 650 + if value.starts_with("Qm") && value.len() == 46 { 651 + // CIDv0 - all base58btc chars 652 + return value.chars().all(|c| { 653 + c.is_ascii_alphanumeric() && c != '0' && c != 'O' && c != 'I' && c != 'l' 654 + }); 655 + } 656 + 657 + if (value.starts_with('b') || value.starts_with('z')) && value.len() > 10 { 658 + // CIDv1 - basic check for valid multibase chars 659 + return value.chars().all(|c| c.is_ascii_alphanumeric()); 660 + } 661 + 662 + false 663 + } 664 + 665 + /// Validate AT-identifier format (DID or handle) 666 + fn validate_at_identifier(value: &str) -> bool { 667 + validate_did(value) || validate_handle(value) 668 + } 669 + 670 + /// Validate language code format (BCP 47) 671 + fn validate_language(value: &str) -> bool { 672 + // Use langtag crate for proper BCP 47 / RFC 5646 validation 673 + LangTag::new(value).is_ok() 674 + } 675 + 676 + /// Validate TID format (Timestamp Identifier) 677 + fn validate_tid(value: &str) -> bool { 678 + // TID: 13 character base32-sortable timestamp 679 + // Uses a-z2-7 character set (no 0,1,8,9) 680 + if value.len() != 13 { 681 + return false; 682 + } 683 + 684 + value.chars().all(|c| { 685 + matches!(c, 'a'..='z' | '2'..='7') 686 + }) 687 + } 688 + 689 + /// Validate record-key format 690 + fn validate_record_key(value: &str) -> bool { 691 + // Record key: alphanumeric, dot, underscore, tilde, hyphen 692 + // 1-512 characters 693 + // Can be TID or custom key 694 + if value.is_empty() || value.len() > 512 { 695 + return false; 696 + } 697 + 698 + // If it looks like a TID, validate as TID 699 + if value.len() == 13 && value.chars().all(|c| matches!(c, 'a'..='z' | '2'..='7')) { 700 + return validate_tid(value); 701 + } 702 + 703 + // Otherwise, general record key validation 704 + value.chars().all(|c| { 705 + c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '~' || c == '-' 706 + }) 707 + } 708 + 709 + #[cfg(test)] 710 + mod tests { 711 + use super::*; 712 + 713 + #[test] 714 + fn test_validate_datetime() { 715 + // Valid datetimes 716 + assert!(validate_datetime("2024-01-15T10:30:00Z")); 717 + assert!(validate_datetime("2024-01-15T10:30:00.123Z")); 718 + assert!(validate_datetime("2024-01-15T10:30:00+05:30")); 719 + assert!(validate_datetime("2024-01-15T10:30:00-08:00")); 720 + 721 + // Invalid datetimes 722 + assert!(!validate_datetime("2024-01-15")); 723 + assert!(!validate_datetime("2024-01-15 10:30:00")); 724 + assert!(!validate_datetime("not-a-date")); 725 + } 726 + 727 + #[test] 728 + fn test_validate_uri() { 729 + // Valid URIs 730 + assert!(validate_uri("https://example.com")); 731 + assert!(validate_uri("http://example.com/path")); 732 + assert!(validate_uri("ftp://example.com")); 733 + assert!(validate_uri("custom-scheme://something")); 734 + 735 + // Invalid URIs 736 + assert!(!validate_uri("not a uri")); 737 + assert!(!validate_uri("://missing-scheme")); 738 + assert!(!validate_uri("")); 739 + } 740 + 741 + #[test] 742 + fn test_validate_at_uri() { 743 + // Valid AT-URIs 744 + assert!(validate_at_uri("at://did:plc:abc123")); 745 + assert!(validate_at_uri("at://did:plc:abc123/com.example.foo")); 746 + assert!(validate_at_uri("at://did:plc:abc123/com.example.foo/abc123")); 747 + assert!(validate_at_uri("at://alice.example.com/com.example.post/abc")); 748 + 749 + // Invalid AT-URIs 750 + assert!(!validate_at_uri("https://example.com")); 751 + assert!(!validate_at_uri("at://")); 752 + assert!(!validate_at_uri("not-at-uri")); 753 + } 754 + 755 + #[test] 756 + fn test_validate_did() { 757 + // Valid DIDs 758 + assert!(validate_did("did:plc:abc123xyz")); 759 + assert!(validate_did("did:web:example.com")); 760 + assert!(validate_did("did:key:abc123")); 761 + 762 + // Invalid DIDs 763 + assert!(!validate_did("not-a-did")); 764 + assert!(!validate_did("did:")); 765 + assert!(!validate_did("did:UPPERCASE:test")); // method must be lowercase 766 + } 767 + 768 + #[test] 769 + fn test_validate_handle() { 770 + // Valid handles 771 + assert!(validate_handle("example.com")); 772 + assert!(validate_handle("alice.example.com")); 773 + assert!(validate_handle("my-site.example.com")); 774 + 775 + // Invalid handles 776 + assert!(!validate_handle("nodomainext")); 777 + assert!(!validate_handle(".example.com")); 778 + assert!(!validate_handle("example.com.")); 779 + assert!(!validate_handle("-invalid.com")); 780 + assert!(!validate_handle("invalid-.com")); 781 + } 782 + 783 + #[test] 784 + fn test_validate_nsid() { 785 + // Valid NSIDs 786 + assert!(validate_nsid("com.example.foo")); 787 + assert!(validate_nsid("com.example.foo.bar")); 788 + assert!(validate_nsid("io.github.user.action")); 789 + 790 + // Invalid NSIDs 791 + assert!(!validate_nsid("com.example")); // need at least 3 segments 792 + assert!(!validate_nsid("COM.EXAMPLE.FOO")); // must be lowercase 793 + assert!(!validate_nsid("com.123invalid.foo")); // can't start with digit 794 + assert!(!validate_nsid("co")); // too short 795 + } 796 + 797 + #[test] 798 + fn test_validate_cid() { 799 + // Valid CIDs (examples) 800 + assert!(validate_cid("QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG")); // CIDv0 801 + assert!(validate_cid("bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku")); // CIDv1 802 + 803 + // Invalid CIDs 804 + assert!(!validate_cid("")); 805 + assert!(!validate_cid("not-a-cid")); 806 + assert!(!validate_cid("Qm123")); // too short 807 + } 808 + 809 + #[test] 810 + fn test_validate_at_identifier() { 811 + // Valid (DIDs) 812 + assert!(validate_at_identifier("did:plc:abc123")); 813 + 814 + // Valid (handles) 815 + assert!(validate_at_identifier("example.com")); 816 + assert!(validate_at_identifier("alice.example.com")); 817 + 818 + // Invalid 819 + assert!(!validate_at_identifier("not-valid")); 820 + assert!(!validate_at_identifier("")); 821 + } 822 + 823 + #[test] 824 + fn test_validate_language() { 825 + // Valid language codes (BCP 47 / RFC 5646) 826 + assert!(validate_language("en")); 827 + assert!(validate_language("en-US")); 828 + assert!(validate_language("zh-Hans-CN")); 829 + assert!(validate_language("fr-CA")); 830 + assert!(validate_language("en-GB")); 831 + assert!(validate_language("de-DE")); 832 + 833 + // Invalid language codes 834 + assert!(!validate_language("e")); // too short 835 + assert!(!validate_language("en_US")); // wrong separator (underscore) 836 + assert!(!validate_language("")); // empty 837 + assert!(!validate_language("123")); // starts with digit 838 + assert!(!validate_language("en--US")); // double separator 839 + } 840 + 841 + #[test] 842 + fn test_validate_tid() { 843 + // Valid TIDs (13 chars, base32-sortable) 844 + assert!(validate_tid("3jui7kd54zh2y")); 845 + assert!(validate_tid("3k2a4dqudbbz2")); 846 + 847 + // Invalid TIDs 848 + assert!(!validate_tid("3jui7kd54zh2")); // too short 849 + assert!(!validate_tid("3jui7kd54zh2yy")); // too long 850 + assert!(!validate_tid("3jui7kd54zh2Y")); // uppercase not allowed 851 + assert!(!validate_tid("3jui0kd54zh2y")); // 0 not allowed 852 + } 853 + 854 + #[test] 855 + fn test_validate_record_key() { 856 + // Valid record keys 857 + assert!(validate_record_key("3jui7kd54zh2y")); // TID 858 + assert!(validate_record_key("my-record-key")); 859 + assert!(validate_record_key("key.with.dots")); 860 + assert!(validate_record_key("key_with_underscores")); 861 + assert!(validate_record_key("key~with~tildes")); 862 + 863 + // Invalid record keys 864 + assert!(!validate_record_key("")); // empty 865 + assert!(!validate_record_key(&"a".repeat(513))); // too long 866 + assert!(!validate_record_key("key with spaces")); // spaces not allowed 867 + } 868 + 869 + #[test] 870 + fn test_grapheme_counting() { 871 + // Unicode grapheme cluster counting test 872 + use unicode_segmentation::UnicodeSegmentation; 873 + 874 + let text = "👨‍👩‍👧‍👦"; // Family emoji (1 grapheme cluster) 875 + assert_eq!(text.graphemes(true).count(), 1); 876 + 877 + let text = "hello"; // 5 graphemes 878 + assert_eq!(text.graphemes(true).count(), 5); 879 + 880 + let text = "नमस्ते"; // Devanagari (3 grapheme clusters) 881 + assert_eq!(text.graphemes(true).count(), 3); 882 + } 883 + }