A better Rust ATProto crate

better error types, building out value type

Orual 516176e9 d0d85afb

+504 -2
Cargo.lock
··· 102 102 checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 103 103 104 104 [[package]] 105 + name = "base64" 106 + version = "0.22.1" 107 + source = "registry+https://github.com/rust-lang/crates.io-index" 108 + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 109 + 110 + [[package]] 105 111 name = "block-buffer" 106 112 version = "0.10.4" 107 113 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 150 156 checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 151 157 152 158 [[package]] 159 + name = "bytes" 160 + version = "1.10.1" 161 + source = "registry+https://github.com/rust-lang/crates.io-index" 162 + checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 163 + 164 + [[package]] 153 165 name = "cc" 154 166 version = "1.2.39" 155 167 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 189 201 "iana-time-zone", 190 202 "js-sys", 191 203 "num-traits", 204 + "serde", 192 205 "wasm-bindgen", 193 206 "windows-link", 194 207 ] ··· 321 334 ] 322 335 323 336 [[package]] 337 + name = "darling" 338 + version = "0.21.3" 339 + source = "registry+https://github.com/rust-lang/crates.io-index" 340 + checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" 341 + dependencies = [ 342 + "darling_core", 343 + "darling_macro", 344 + ] 345 + 346 + [[package]] 347 + name = "darling_core" 348 + version = "0.21.3" 349 + source = "registry+https://github.com/rust-lang/crates.io-index" 350 + checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" 351 + dependencies = [ 352 + "fnv", 353 + "ident_case", 354 + "proc-macro2", 355 + "quote", 356 + "strsim", 357 + "syn 2.0.106", 358 + ] 359 + 360 + [[package]] 361 + name = "darling_macro" 362 + version = "0.21.3" 363 + source = "registry+https://github.com/rust-lang/crates.io-index" 364 + checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" 365 + dependencies = [ 366 + "darling_core", 367 + "quote", 368 + "syn 2.0.106", 369 + ] 370 + 371 + [[package]] 324 372 name = "data-encoding" 325 373 version = "2.9.0" 326 374 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 347 395 ] 348 396 349 397 [[package]] 398 + name = "deranged" 399 + version = "0.5.4" 400 + source = "registry+https://github.com/rust-lang/crates.io-index" 401 + checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" 402 + dependencies = [ 403 + "powerfmt", 404 + "serde_core", 405 + ] 406 + 407 + [[package]] 350 408 name = "digest" 351 409 version = "0.10.7" 352 410 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 357 415 ] 358 416 359 417 [[package]] 418 + name = "displaydoc" 419 + version = "0.2.5" 420 + source = "registry+https://github.com/rust-lang/crates.io-index" 421 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 422 + dependencies = [ 423 + "proc-macro2", 424 + "quote", 425 + "syn 2.0.106", 426 + ] 427 + 428 + [[package]] 429 + name = "dyn-clone" 430 + version = "1.0.20" 431 + source = "registry+https://github.com/rust-lang/crates.io-index" 432 + checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" 433 + 434 + [[package]] 435 + name = "enum_dispatch" 436 + version = "0.3.13" 437 + source = "registry+https://github.com/rust-lang/crates.io-index" 438 + checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" 439 + dependencies = [ 440 + "once_cell", 441 + "proc-macro2", 442 + "quote", 443 + "syn 2.0.106", 444 + ] 445 + 446 + [[package]] 360 447 name = "equivalent" 361 448 version = "1.0.2" 362 449 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 367 454 version = "0.1.2" 368 455 source = "registry+https://github.com/rust-lang/crates.io-index" 369 456 checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" 457 + 458 + [[package]] 459 + name = "fnv" 460 + version = "1.0.7" 461 + source = "registry+https://github.com/rust-lang/crates.io-index" 462 + checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 370 463 371 464 [[package]] 372 465 name = "form_urlencoded" ··· 399 492 400 493 [[package]] 401 494 name = "hashbrown" 495 + version = "0.12.3" 496 + source = "registry+https://github.com/rust-lang/crates.io-index" 497 + checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 498 + 499 + [[package]] 500 + name = "hashbrown" 402 501 version = "0.16.0" 403 502 source = "registry+https://github.com/rust-lang/crates.io-index" 404 503 checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" ··· 408 507 version = "0.5.0" 409 508 source = "registry+https://github.com/rust-lang/crates.io-index" 410 509 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 510 + 511 + [[package]] 512 + name = "hex" 513 + version = "0.4.3" 514 + source = "registry+https://github.com/rust-lang/crates.io-index" 515 + checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 411 516 412 517 [[package]] 413 518 name = "hex_fmt" ··· 440 545 ] 441 546 442 547 [[package]] 548 + name = "icu_collections" 549 + version = "2.0.0" 550 + source = "registry+https://github.com/rust-lang/crates.io-index" 551 + checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 552 + dependencies = [ 553 + "displaydoc", 554 + "potential_utf", 555 + "yoke", 556 + "zerofrom", 557 + "zerovec", 558 + ] 559 + 560 + [[package]] 561 + name = "icu_locale_core" 562 + version = "2.0.0" 563 + source = "registry+https://github.com/rust-lang/crates.io-index" 564 + checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 565 + dependencies = [ 566 + "displaydoc", 567 + "litemap", 568 + "tinystr", 569 + "writeable", 570 + "zerovec", 571 + ] 572 + 573 + [[package]] 574 + name = "icu_normalizer" 575 + version = "2.0.0" 576 + source = "registry+https://github.com/rust-lang/crates.io-index" 577 + checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 578 + dependencies = [ 579 + "displaydoc", 580 + "icu_collections", 581 + "icu_normalizer_data", 582 + "icu_properties", 583 + "icu_provider", 584 + "smallvec", 585 + "zerovec", 586 + ] 587 + 588 + [[package]] 589 + name = "icu_normalizer_data" 590 + version = "2.0.0" 591 + source = "registry+https://github.com/rust-lang/crates.io-index" 592 + checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 593 + 594 + [[package]] 595 + name = "icu_properties" 596 + version = "2.0.1" 597 + source = "registry+https://github.com/rust-lang/crates.io-index" 598 + checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 599 + dependencies = [ 600 + "displaydoc", 601 + "icu_collections", 602 + "icu_locale_core", 603 + "icu_properties_data", 604 + "icu_provider", 605 + "potential_utf", 606 + "zerotrie", 607 + "zerovec", 608 + ] 609 + 610 + [[package]] 611 + name = "icu_properties_data" 612 + version = "2.0.1" 613 + source = "registry+https://github.com/rust-lang/crates.io-index" 614 + checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 615 + 616 + [[package]] 617 + name = "icu_provider" 618 + version = "2.0.0" 619 + source = "registry+https://github.com/rust-lang/crates.io-index" 620 + checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 621 + dependencies = [ 622 + "displaydoc", 623 + "icu_locale_core", 624 + "stable_deref_trait", 625 + "tinystr", 626 + "writeable", 627 + "yoke", 628 + "zerofrom", 629 + "zerotrie", 630 + "zerovec", 631 + ] 632 + 633 + [[package]] 634 + name = "ident_case" 635 + version = "1.0.1" 636 + source = "registry+https://github.com/rust-lang/crates.io-index" 637 + checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 638 + 639 + [[package]] 640 + name = "idna" 641 + version = "1.1.0" 642 + source = "registry+https://github.com/rust-lang/crates.io-index" 643 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 644 + dependencies = [ 645 + "idna_adapter", 646 + "smallvec", 647 + "utf8_iter", 648 + ] 649 + 650 + [[package]] 651 + name = "idna_adapter" 652 + version = "1.2.1" 653 + source = "registry+https://github.com/rust-lang/crates.io-index" 654 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 655 + dependencies = [ 656 + "icu_normalizer", 657 + "icu_properties", 658 + ] 659 + 660 + [[package]] 661 + name = "indexmap" 662 + version = "1.9.3" 663 + source = "registry+https://github.com/rust-lang/crates.io-index" 664 + checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 665 + dependencies = [ 666 + "autocfg", 667 + "hashbrown 0.12.3", 668 + "serde", 669 + ] 670 + 671 + [[package]] 443 672 name = "indexmap" 444 673 version = "2.11.4" 445 674 source = "registry+https://github.com/rust-lang/crates.io-index" 446 675 checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" 447 676 dependencies = [ 448 677 "equivalent", 449 - "hashbrown", 678 + "hashbrown 0.16.0", 679 + "serde", 680 + "serde_core", 450 681 ] 451 682 452 683 [[package]] ··· 479 710 name = "jacquard-common" 480 711 version = "0.1.0" 481 712 dependencies = [ 713 + "bytes", 482 714 "chrono", 483 715 "cid", 716 + "enum_dispatch", 484 717 "langtag", 485 718 "miette", 486 719 "multibase", ··· 489 722 "serde", 490 723 "serde_html_form", 491 724 "serde_json", 725 + "serde_with", 492 726 "smol_str", 493 727 "thiserror 2.0.16", 728 + "url", 494 729 ] 495 730 496 731 [[package]] 732 + name = "jacquard-lexicon" 733 + version = "0.1.0" 734 + 735 + [[package]] 497 736 name = "js-sys" 498 737 version = "0.3.81" 499 738 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 519 758 version = "0.2.176" 520 759 source = "registry+https://github.com/rust-lang/crates.io-index" 521 760 checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" 761 + 762 + [[package]] 763 + name = "litemap" 764 + version = "0.8.0" 765 + source = "registry+https://github.com/rust-lang/crates.io-index" 766 + checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 522 767 523 768 [[package]] 524 769 name = "log" ··· 591 836 "memchr", 592 837 "minimal-lexical", 593 838 ] 839 + 840 + [[package]] 841 + name = "num-conv" 842 + version = "0.1.0" 843 + source = "registry+https://github.com/rust-lang/crates.io-index" 844 + checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 594 845 595 846 [[package]] 596 847 name = "num-traits" ··· 620 871 checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 621 872 622 873 [[package]] 874 + name = "potential_utf" 875 + version = "0.1.3" 876 + source = "registry+https://github.com/rust-lang/crates.io-index" 877 + checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" 878 + dependencies = [ 879 + "zerovec", 880 + ] 881 + 882 + [[package]] 883 + name = "powerfmt" 884 + version = "0.2.0" 885 + source = "registry+https://github.com/rust-lang/crates.io-index" 886 + checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 887 + 888 + [[package]] 623 889 name = "proc-macro-error" 624 890 version = "1.0.4" 625 891 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 668 934 checksum = "d20581732dd76fa913c7dff1a2412b714afe3573e94d41c34719de73337cc8ab" 669 935 670 936 [[package]] 937 + name = "ref-cast" 938 + version = "1.0.24" 939 + source = "registry+https://github.com/rust-lang/crates.io-index" 940 + checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" 941 + dependencies = [ 942 + "ref-cast-impl", 943 + ] 944 + 945 + [[package]] 946 + name = "ref-cast-impl" 947 + version = "1.0.24" 948 + source = "registry+https://github.com/rust-lang/crates.io-index" 949 + checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" 950 + dependencies = [ 951 + "proc-macro2", 952 + "quote", 953 + "syn 2.0.106", 954 + ] 955 + 956 + [[package]] 671 957 name = "regex" 672 958 version = "1.11.3" 673 959 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 709 995 checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 710 996 711 997 [[package]] 998 + name = "schemars" 999 + version = "0.9.0" 1000 + source = "registry+https://github.com/rust-lang/crates.io-index" 1001 + checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" 1002 + dependencies = [ 1003 + "dyn-clone", 1004 + "ref-cast", 1005 + "serde", 1006 + "serde_json", 1007 + ] 1008 + 1009 + [[package]] 1010 + name = "schemars" 1011 + version = "1.0.4" 1012 + source = "registry+https://github.com/rust-lang/crates.io-index" 1013 + checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" 1014 + dependencies = [ 1015 + "dyn-clone", 1016 + "ref-cast", 1017 + "serde", 1018 + "serde_json", 1019 + ] 1020 + 1021 + [[package]] 712 1022 name = "serde" 713 1023 version = "1.0.227" 714 1024 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 754 1064 checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" 755 1065 dependencies = [ 756 1066 "form_urlencoded", 757 - "indexmap", 1067 + "indexmap 2.11.4", 758 1068 "itoa", 759 1069 "ryu", 760 1070 "serde_core", ··· 774 1084 ] 775 1085 776 1086 [[package]] 1087 + name = "serde_with" 1088 + version = "3.14.1" 1089 + source = "registry+https://github.com/rust-lang/crates.io-index" 1090 + checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" 1091 + dependencies = [ 1092 + "base64", 1093 + "chrono", 1094 + "hex", 1095 + "indexmap 1.9.3", 1096 + "indexmap 2.11.4", 1097 + "schemars 0.9.0", 1098 + "schemars 1.0.4", 1099 + "serde", 1100 + "serde_derive", 1101 + "serde_json", 1102 + "serde_with_macros", 1103 + "time", 1104 + ] 1105 + 1106 + [[package]] 1107 + name = "serde_with_macros" 1108 + version = "3.14.1" 1109 + source = "registry+https://github.com/rust-lang/crates.io-index" 1110 + checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" 1111 + dependencies = [ 1112 + "darling", 1113 + "proc-macro2", 1114 + "quote", 1115 + "syn 2.0.106", 1116 + ] 1117 + 1118 + [[package]] 777 1119 name = "sha2" 778 1120 version = "0.10.9" 779 1121 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 813 1155 ] 814 1156 815 1157 [[package]] 1158 + name = "stable_deref_trait" 1159 + version = "1.2.0" 1160 + source = "registry+https://github.com/rust-lang/crates.io-index" 1161 + checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1162 + 1163 + [[package]] 816 1164 name = "static-regular-grammar" 817 1165 version = "2.0.2" 818 1166 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 860 1208 ] 861 1209 862 1210 [[package]] 1211 + name = "synstructure" 1212 + version = "0.13.2" 1213 + source = "registry+https://github.com/rust-lang/crates.io-index" 1214 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 1215 + dependencies = [ 1216 + "proc-macro2", 1217 + "quote", 1218 + "syn 2.0.106", 1219 + ] 1220 + 1221 + [[package]] 863 1222 name = "thiserror" 864 1223 version = "1.0.69" 865 1224 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 900 1259 ] 901 1260 902 1261 [[package]] 1262 + name = "time" 1263 + version = "0.3.44" 1264 + source = "registry+https://github.com/rust-lang/crates.io-index" 1265 + checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" 1266 + dependencies = [ 1267 + "deranged", 1268 + "itoa", 1269 + "num-conv", 1270 + "powerfmt", 1271 + "serde", 1272 + "time-core", 1273 + "time-macros", 1274 + ] 1275 + 1276 + [[package]] 1277 + name = "time-core" 1278 + version = "0.1.6" 1279 + source = "registry+https://github.com/rust-lang/crates.io-index" 1280 + checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" 1281 + 1282 + [[package]] 1283 + name = "time-macros" 1284 + version = "0.2.24" 1285 + source = "registry+https://github.com/rust-lang/crates.io-index" 1286 + checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" 1287 + dependencies = [ 1288 + "num-conv", 1289 + "time-core", 1290 + ] 1291 + 1292 + [[package]] 1293 + name = "tinystr" 1294 + version = "0.8.1" 1295 + source = "registry+https://github.com/rust-lang/crates.io-index" 1296 + checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 1297 + dependencies = [ 1298 + "displaydoc", 1299 + "zerovec", 1300 + ] 1301 + 1302 + [[package]] 903 1303 name = "typenum" 904 1304 version = "1.18.0" 905 1305 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 922 1322 version = "0.8.0" 923 1323 source = "registry+https://github.com/rust-lang/crates.io-index" 924 1324 checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" 1325 + 1326 + [[package]] 1327 + name = "url" 1328 + version = "2.5.7" 1329 + source = "registry+https://github.com/rust-lang/crates.io-index" 1330 + checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 1331 + dependencies = [ 1332 + "form_urlencoded", 1333 + "idna", 1334 + "percent-encoding", 1335 + "serde", 1336 + ] 1337 + 1338 + [[package]] 1339 + name = "utf8_iter" 1340 + version = "1.0.4" 1341 + source = "registry+https://github.com/rust-lang/crates.io-index" 1342 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 925 1343 926 1344 [[package]] 927 1345 name = "utf8parse" ··· 1126 1544 version = "0.53.0" 1127 1545 source = "registry+https://github.com/rust-lang/crates.io-index" 1128 1546 checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 1547 + 1548 + [[package]] 1549 + name = "writeable" 1550 + version = "0.6.1" 1551 + source = "registry+https://github.com/rust-lang/crates.io-index" 1552 + checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 1553 + 1554 + [[package]] 1555 + name = "yoke" 1556 + version = "0.8.0" 1557 + source = "registry+https://github.com/rust-lang/crates.io-index" 1558 + checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 1559 + dependencies = [ 1560 + "serde", 1561 + "stable_deref_trait", 1562 + "yoke-derive", 1563 + "zerofrom", 1564 + ] 1565 + 1566 + [[package]] 1567 + name = "yoke-derive" 1568 + version = "0.8.0" 1569 + source = "registry+https://github.com/rust-lang/crates.io-index" 1570 + checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 1571 + dependencies = [ 1572 + "proc-macro2", 1573 + "quote", 1574 + "syn 2.0.106", 1575 + "synstructure", 1576 + ] 1577 + 1578 + [[package]] 1579 + name = "zerofrom" 1580 + version = "0.1.6" 1581 + source = "registry+https://github.com/rust-lang/crates.io-index" 1582 + checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 1583 + dependencies = [ 1584 + "zerofrom-derive", 1585 + ] 1586 + 1587 + [[package]] 1588 + name = "zerofrom-derive" 1589 + version = "0.1.6" 1590 + source = "registry+https://github.com/rust-lang/crates.io-index" 1591 + checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 1592 + dependencies = [ 1593 + "proc-macro2", 1594 + "quote", 1595 + "syn 2.0.106", 1596 + "synstructure", 1597 + ] 1598 + 1599 + [[package]] 1600 + name = "zerotrie" 1601 + version = "0.2.2" 1602 + source = "registry+https://github.com/rust-lang/crates.io-index" 1603 + checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 1604 + dependencies = [ 1605 + "displaydoc", 1606 + "yoke", 1607 + "zerofrom", 1608 + ] 1609 + 1610 + [[package]] 1611 + name = "zerovec" 1612 + version = "0.11.4" 1613 + source = "registry+https://github.com/rust-lang/crates.io-index" 1614 + checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" 1615 + dependencies = [ 1616 + "yoke", 1617 + "zerofrom", 1618 + "zerovec-derive", 1619 + ] 1620 + 1621 + [[package]] 1622 + name = "zerovec-derive" 1623 + version = "0.11.1" 1624 + source = "registry+https://github.com/rust-lang/crates.io-index" 1625 + checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 1626 + dependencies = [ 1627 + "proc-macro2", 1628 + "quote", 1629 + "syn 2.0.106", 1630 + ]
+10
crates/jacquard-common/Cargo.toml
··· 3 3 edition.workspace = true 4 4 version.workspace = true 5 5 authors.workspace = true 6 + repository.workspace = true 7 + keywords.workspace = true 8 + categories.workspace = true 9 + readme.workspace = true 10 + documentation.workspace = true 11 + exclude.workspace = true 6 12 description.workspace = true 7 13 8 14 [dependencies] 15 + bytes = "1.10.1" 9 16 chrono = "0.4.42" 10 17 cid = { version = "0.11.1", features = ["serde", "std"] } 18 + enum_dispatch = "0.3.13" 11 19 langtag = { version = "0.4.0", features = ["serde"] } 12 20 miette = "7.6.0" 13 21 multibase = "0.9.1" ··· 16 24 serde = { version = "1.0.227", features = ["derive"] } 17 25 serde_html_form = "0.2.8" 18 26 serde_json = "1.0.145" 27 + serde_with = "3.14.1" 19 28 smol_str = { version = "0.3.2", features = ["serde"] } 20 29 thiserror = "2.0.16" 30 + url = "2.5.7"
+3
crates/jacquard-common/src/lib.rs
··· 7 7 8 8 pub use cowstr::CowStr; 9 9 pub use into_static::IntoStatic; 10 + 11 + pub use smol_str; 12 + pub use url;
+90
crates/jacquard-common/src/types.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + 1 3 pub mod aturi; 2 4 pub mod blob; 3 5 pub mod cid; ··· 11 13 pub mod link; 12 14 pub mod nsid; 13 15 pub mod recordkey; 16 + pub mod string; 14 17 pub mod tid; 18 + pub mod uri; 19 + pub mod value; 15 20 16 21 /// Trait for a constant string literal type 17 22 pub trait Literal: Clone + Copy + PartialEq + Eq + Send + Sync + 'static { 18 23 /// The string literal 19 24 const LITERAL: &'static str; 20 25 } 26 + 27 + pub const DISALLOWED_TLDS: &[&str] = &[ 28 + ".local", 29 + ".arpa", 30 + ".invalid", 31 + ".localhost", 32 + ".internal", 33 + ".example", 34 + ".alt", 35 + // policy could concievably change on ".onion" some day 36 + ".onion", 37 + // NOTE: .test is allowed in testing and devopment. In practical terms 38 + // "should" "never" actually resolve and get registered in production 39 + ]; 40 + 41 + pub fn ends_with(string: impl AsRef<str>, list: &[&str]) -> bool { 42 + let string = string.as_ref(); 43 + for item in list { 44 + if string.ends_with(item) { 45 + return true; 46 + } 47 + } 48 + false 49 + } 50 + 51 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)] 52 + #[serde(rename_all = "kebab-case")] 53 + pub enum DataModelType { 54 + Null, 55 + Boolean, 56 + Integer, 57 + Bytes, 58 + CidLink, 59 + Blob, 60 + Array, 61 + Object, 62 + #[serde(untagged)] 63 + String(LexiconStringType), 64 + } 65 + 66 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)] 67 + #[serde(rename_all = "kebab-case")] 68 + pub enum LexiconType { 69 + Params, 70 + Token, 71 + Ref, 72 + Union, 73 + Unknown, 74 + Record, 75 + Query, 76 + Procedure, 77 + Subscription, 78 + #[serde(untagged)] 79 + DataModel(DataModelType), 80 + } 81 + 82 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)] 83 + #[serde(rename_all = "kebab-case")] 84 + pub enum LexiconStringType { 85 + Datetime, 86 + AtUri, 87 + Did, 88 + Handle, 89 + AtIdentifier, 90 + Nsid, 91 + Cid, 92 + Language, 93 + Tid, 94 + RecordKey, 95 + Uri(UriType), 96 + #[serde(untagged)] 97 + String, 98 + } 99 + 100 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] 101 + #[serde(tag = "type")] 102 + pub enum UriType { 103 + Did, 104 + At, 105 + Https, 106 + Wss, 107 + Cid, 108 + Dns, 109 + Any, 110 + }
+84 -22
crates/jacquard-common/src/types/aturi.rs
··· 2 2 use crate::types::ident::AtIdentifier; 3 3 use crate::types::nsid::Nsid; 4 4 use crate::types::recordkey::{RecordKey, Rkey}; 5 + use crate::types::string::AtStrError; 5 6 use regex::Regex; 6 7 use serde::Serializer; 7 8 use serde::{Deserialize, Deserializer, Serialize, de::Error}; 8 - use smol_str::ToSmolStr; 9 + use smol_str::{SmolStr, ToSmolStr}; 9 10 use std::fmt; 10 11 use std::sync::LazyLock; 11 12 use std::{ops::Deref, str::FromStr}; ··· 41 42 42 43 impl<'u> AtUri<'u> { 43 44 /// Fallible constructor, validates, borrows from input 44 - pub fn new(uri: &'u str) -> Result<Self, &'static str> { 45 + pub fn new(uri: &'u str) -> Result<Self, AtStrError> { 45 46 if let Some(parts) = ATURI_REGEX.captures(uri) { 46 47 if let Some(authority) = parts.name("authority") { 47 - let authority = AtIdentifier::new(authority.as_str())?; 48 + let authority = AtIdentifier::new(authority.as_str()) 49 + .map_err(|e| AtStrError::wrap("at-uri-scheme", uri.to_string(), e))?; 48 50 let path = if let Some(collection) = parts.name("collection") { 49 - let collection = Nsid::new(collection.as_str())?; 51 + let collection = Nsid::new(collection.as_str()) 52 + .map_err(|e| AtStrError::wrap("at-uri-scheme", uri.to_string(), e))?; 50 53 let rkey = if let Some(rkey) = parts.name("rkey") { 51 - let rkey = RecordKey::from(Rkey::new(rkey.as_str())?); 54 + let rkey = 55 + RecordKey::from(Rkey::new(rkey.as_str()).map_err(|e| { 56 + AtStrError::wrap("at-uri-scheme", uri.to_string(), e) 57 + })?); 52 58 Some(rkey) 53 59 } else { 54 60 None ··· 68 74 fragment, 69 75 }) 70 76 } else { 71 - Err("at:// URI missing authority") 77 + Err(AtStrError::missing("at-uri-scheme", uri, "authority")) 72 78 } 73 79 } else { 74 - Err("Invalid at:// URI via regex") 80 + Err(AtStrError::regex( 81 + "at-uri-scheme", 82 + uri, 83 + SmolStr::new_static("doesn't match schema"), 84 + )) 75 85 } 76 86 } 77 87 78 - pub fn new_owned(uri: impl AsRef<str>) -> Result<Self, &'static str> { 88 + pub fn raw(uri: &'u str) -> Self { 89 + if let Some(parts) = ATURI_REGEX.captures(uri) { 90 + if let Some(authority) = parts.name("authority") { 91 + let authority = AtIdentifier::raw(authority.as_str()); 92 + let path = if let Some(collection) = parts.name("collection") { 93 + let collection = Nsid::raw(collection.as_str()); 94 + let rkey = if let Some(rkey) = parts.name("rkey") { 95 + let rkey = RecordKey::from(Rkey::raw(rkey.as_str())); 96 + Some(rkey) 97 + } else { 98 + None 99 + }; 100 + Some(UriPath { collection, rkey }) 101 + } else { 102 + None 103 + }; 104 + let fragment = parts.name("fragment").map(|fragment| { 105 + let fragment = CowStr::Borrowed(fragment.as_str()); 106 + fragment 107 + }); 108 + AtUri { 109 + uri: CowStr::Borrowed(uri), 110 + authority, 111 + path, 112 + fragment, 113 + } 114 + } else { 115 + panic!("at:// URI missing authority") 116 + } 117 + } else { 118 + panic!("Invalid at:// URI via regex") 119 + } 120 + } 121 + 122 + pub fn new_owned(uri: impl AsRef<str>) -> Result<Self, AtStrError> { 79 123 let uri = uri.as_ref(); 80 124 if let Some(parts) = ATURI_REGEX.captures(uri) { 81 125 if let Some(authority) = parts.name("authority") { 82 - let authority = AtIdentifier::new_owned(authority.as_str())?; 126 + let authority = AtIdentifier::new_owned(authority.as_str()) 127 + .map_err(|e| AtStrError::wrap("at-uri-scheme", uri.to_string(), e))?; 83 128 let path = if let Some(collection) = parts.name("collection") { 84 - let collection = Nsid::new_owned(collection.as_str())?; 129 + let collection = Nsid::new_owned(collection.as_str()) 130 + .map_err(|e| AtStrError::wrap("at-uri-scheme", uri.to_string(), e))?; 85 131 let rkey = if let Some(rkey) = parts.name("rkey") { 86 - let rkey = RecordKey::from(Rkey::new_owned(rkey.as_str())?); 132 + let rkey = 133 + RecordKey::from(Rkey::new_owned(rkey.as_str()).map_err(|e| { 134 + AtStrError::wrap("at-uri-scheme", uri.to_string(), e) 135 + })?); 87 136 Some(rkey) 88 137 } else { 89 138 None ··· 103 152 fragment, 104 153 }) 105 154 } else { 106 - Err("at:// URI missing authority") 155 + Err(AtStrError::missing("at-uri-scheme", uri, "authority")) 107 156 } 108 157 } else { 109 - Err("Invalid at:// URI via regex") 158 + Err(AtStrError::regex( 159 + "at-uri-scheme", 160 + uri, 161 + SmolStr::new_static("doesn't match schema"), 162 + )) 110 163 } 111 164 } 112 165 113 - pub fn new_static(uri: &'static str) -> Result<AtUri<'static>, &'static str> { 166 + pub fn new_static(uri: &'static str) -> Result<AtUri<'static>, AtStrError> { 114 167 let uri = uri.as_ref(); 115 168 if let Some(parts) = ATURI_REGEX.captures(uri) { 116 169 if let Some(authority) = parts.name("authority") { 117 - let authority = AtIdentifier::new_static(authority.as_str())?; 170 + let authority = AtIdentifier::new_static(authority.as_str()) 171 + .map_err(|e| AtStrError::wrap("at-uri-scheme", uri.to_string(), e))?; 118 172 let path = if let Some(collection) = parts.name("collection") { 119 - let collection = Nsid::new_static(collection.as_str())?; 173 + let collection = Nsid::new_static(collection.as_str()) 174 + .map_err(|e| AtStrError::wrap("at-uri-scheme", uri.to_string(), e))?; 120 175 let rkey = if let Some(rkey) = parts.name("rkey") { 121 - let rkey = RecordKey::from(Rkey::new_static(rkey.as_str())?); 176 + let rkey = 177 + RecordKey::from(Rkey::new_static(rkey.as_str()).map_err(|e| { 178 + AtStrError::wrap("at-uri-scheme", uri.to_string(), e) 179 + })?); 122 180 Some(rkey) 123 181 } else { 124 182 None ··· 138 196 fragment, 139 197 }) 140 198 } else { 141 - Err("at:// URI missing authority") 199 + Err(AtStrError::missing("at-uri-scheme", uri, "authority")) 142 200 } 143 201 } else { 144 - Err("Invalid at:// URI via regex") 202 + Err(AtStrError::regex( 203 + "at-uri-scheme", 204 + uri, 205 + SmolStr::new_static("doesn't match schema"), 206 + )) 145 207 } 146 208 } 147 209 ··· 198 260 } 199 261 200 262 impl FromStr for AtUri<'_> { 201 - type Err = &'static str; 263 + type Err = AtStrError; 202 264 203 265 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 204 266 /// Prefer `AtUri::new()` or `AtUri::raw()` if you want to borrow. ··· 245 307 } 246 308 247 309 impl TryFrom<String> for AtUri<'static> { 248 - type Error = &'static str; 310 + type Error = AtStrError; 249 311 250 312 fn try_from(value: String) -> Result<Self, Self::Error> { 251 313 Self::new_owned(&value) ··· 253 315 } 254 316 255 317 impl<'d> TryFrom<CowStr<'d>> for AtUri<'d> { 256 - type Error = &'static str; 318 + type Error = AtStrError; 257 319 /// TODO: rewrite to avoid taking ownership/cloning 258 320 fn try_from(value: CowStr<'d>) -> Result<Self, Self::Error> { 259 321 Self::new_owned(value)
+1 -1
crates/jacquard-common/src/types/blob.rs
··· 12 12 str::FromStr, 13 13 }; 14 14 15 - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 15 + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] 16 16 #[serde(rename_all = "camelCase")] 17 17 pub struct Blob<'b> { 18 18 pub r#ref: Cid<'b>,
+12 -2
crates/jacquard-common/src/types/cid.rs
··· 40 40 Ok(Self::Str(cid_str)) 41 41 } 42 42 } 43 - pub fn ipld(cid: IpldCid) -> Self { 43 + 44 + pub fn new_owned(cid: &[u8]) -> Result<Cid<'static>, Error> { 45 + if let Ok(cid) = IpldCid::try_from(cid.as_ref()) { 46 + Ok(Self::ipld(cid)) 47 + } else { 48 + let cid_str = CowStr::from_utf8(cid)?; 49 + Ok(Cid::Str(cid_str.into_static())) 50 + } 51 + } 52 + 53 + pub fn ipld(cid: IpldCid) -> Cid<'static> { 44 54 let s = CowStr::Owned( 45 55 cid.to_string_of_base(ATP_CID_BASE) 46 56 .unwrap_or_default() 47 57 .to_smolstr(), 48 58 ); 49 - Self::Ipld { cid, s } 59 + Cid::Ipld { cid, s } 50 60 } 51 61 52 62 pub fn str(cid: &'c str) -> Self {
+2 -2
crates/jacquard-common/src/types/datetime.rs
··· 13 13 }); 14 14 15 15 /// A Lexicon timestamp. 16 - #[derive(Clone, Debug, Eq)] 16 + #[derive(Clone, Debug, Eq, Hash)] 17 17 pub struct Datetime { 18 18 /// Serialized form. Preserved during parsing to ensure round-trip re-serialization. 19 19 serialized: CowStr<'static>, ··· 98 98 if ISO8601_REGEX.is_match(s) { 99 99 let dt = chrono::DateTime::parse_from_rfc3339(s)?; 100 100 Ok(Self { 101 - serialized: CowStr::Borrowed(s).into_static(), 101 + serialized: CowStr::Owned(s.to_smolstr()), 102 102 dt, 103 103 }) 104 104 } else {
+24 -11
crates/jacquard-common/src/types/did.rs
··· 1 + use crate::types::string::AtStrError; 1 2 use crate::{CowStr, IntoStatic}; 2 3 use regex::Regex; 3 4 use serde::{Deserialize, Deserializer, Serialize, de::Error}; 4 - use smol_str::ToSmolStr; 5 + use smol_str::{SmolStr, ToSmolStr}; 5 6 use std::fmt; 6 7 use std::sync::LazyLock; 7 8 use std::{ops::Deref, str::FromStr}; ··· 16 17 17 18 impl<'d> Did<'d> { 18 19 /// Fallible constructor, validates, borrows from input 19 - pub fn new(did: &'d str) -> Result<Self, &'static str> { 20 + pub fn new(did: &'d str) -> Result<Self, AtStrError> { 20 21 let did = did.strip_prefix("at://").unwrap_or(did); 21 22 if did.len() > 2048 { 22 - Err("DID too long") 23 + Err(AtStrError::too_long("did", did, 2048, did.len())) 23 24 } else if !DID_REGEX.is_match(did) { 24 - Err("Invalid DID") 25 + Err(AtStrError::regex( 26 + "did", 27 + did, 28 + SmolStr::new_static("invalid"), 29 + )) 25 30 } else { 26 31 Ok(Self(CowStr::Borrowed(did))) 27 32 } 28 33 } 29 34 30 35 /// Fallible constructor, validates, takes ownership 31 - pub fn new_owned(did: impl AsRef<str>) -> Result<Self, &'static str> { 36 + pub fn new_owned(did: impl AsRef<str>) -> Result<Self, AtStrError> { 32 37 let did = did.as_ref(); 33 38 let did = did.strip_prefix("at://").unwrap_or(did); 34 39 if did.len() > 2048 { 35 - Err("DID too long") 40 + Err(AtStrError::too_long("did", did, 2048, did.len())) 36 41 } else if !DID_REGEX.is_match(did) { 37 - Err("Invalid DID") 42 + Err(AtStrError::regex( 43 + "did", 44 + did, 45 + SmolStr::new_static("invalid"), 46 + )) 38 47 } else { 39 48 Ok(Self(CowStr::Owned(did.to_smolstr()))) 40 49 } 41 50 } 42 51 43 52 /// Fallible constructor, validates, doesn't allocate 44 - pub fn new_static(did: &'static str) -> Result<Self, &'static str> { 53 + pub fn new_static(did: &'static str) -> Result<Self, AtStrError> { 45 54 let did = did.strip_prefix("at://").unwrap_or(did); 46 55 if did.len() > 2048 { 47 - Err("DID too long") 56 + Err(AtStrError::too_long("did", did, 2048, did.len())) 48 57 } else if !DID_REGEX.is_match(did) { 49 - Err("Invalid DID") 58 + Err(AtStrError::regex( 59 + "did", 60 + did, 61 + SmolStr::new_static("invalid"), 62 + )) 50 63 } else { 51 64 Ok(Self(CowStr::new_static(did))) 52 65 } ··· 82 95 } 83 96 84 97 impl FromStr for Did<'_> { 85 - type Err = &'static str; 98 + type Err = AtStrError; 86 99 87 100 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 88 101 /// Prefer `Did::new()` or `Did::raw` if you want to borrow.
+35 -43
crates/jacquard-common/src/types/handle.rs
··· 1 + use crate::types::string::AtStrError; 2 + use crate::types::{DISALLOWED_TLDS, ends_with}; 1 3 use crate::{CowStr, IntoStatic}; 2 4 use regex::Regex; 3 5 use serde::{Deserialize, Deserializer, Serialize, de::Error}; 4 - use smol_str::ToSmolStr; 6 + use smol_str::{SmolStr, ToSmolStr}; 5 7 use std::fmt; 6 8 use std::sync::LazyLock; 7 9 use std::{ops::Deref, str::FromStr}; ··· 20 22 /// Fallible constructor, validates, borrows from input 21 23 /// 22 24 /// Accepts (and strips) preceding '@' if present 23 - pub fn new(handle: &'h str) -> Result<Self, &'static str> { 25 + pub fn new(handle: &'h str) -> Result<Self, AtStrError> { 24 26 let handle = handle 25 27 .strip_prefix("at://") 26 28 .unwrap_or(handle) 27 29 .strip_prefix('@') 28 30 .unwrap_or(handle); 29 31 if handle.len() > 253 { 30 - Err("handle too long") 32 + Err(AtStrError::too_long("handle", handle, 253, handle.len())) 31 33 } else if !HANDLE_REGEX.is_match(handle) { 32 - Err("Invalid handle") 34 + Err(AtStrError::regex( 35 + "handle", 36 + handle, 37 + SmolStr::new_static("invalid"), 38 + )) 39 + } else if ends_with(handle, DISALLOWED_TLDS) { 40 + Err(AtStrError::disallowed("handle", handle, DISALLOWED_TLDS)) 33 41 } else { 34 42 Ok(Self(CowStr::Borrowed(handle))) 35 43 } 36 44 } 37 45 38 46 /// Fallible constructor, validates, takes ownership 39 - pub fn new_owned(handle: impl AsRef<str>) -> Result<Self, &'static str> { 47 + pub fn new_owned(handle: impl AsRef<str>) -> Result<Self, AtStrError> { 40 48 let handle = handle.as_ref(); 41 49 let handle = handle 42 50 .strip_prefix("at://") ··· 44 52 .strip_prefix('@') 45 53 .unwrap_or(handle); 46 54 if handle.len() > 253 { 47 - Err("handle too long") 55 + Err(AtStrError::too_long("handle", handle, 253, handle.len())) 48 56 } else if !HANDLE_REGEX.is_match(handle) { 49 - Err("Invalid handle") 57 + Err(AtStrError::regex( 58 + "handle", 59 + handle, 60 + SmolStr::new_static("invalid"), 61 + )) 62 + } else if ends_with(handle, DISALLOWED_TLDS) { 63 + Err(AtStrError::disallowed("handle", handle, DISALLOWED_TLDS)) 50 64 } else { 51 65 Ok(Self(CowStr::Owned(handle.to_smolstr()))) 52 66 } 53 67 } 54 68 55 69 /// Fallible constructor, validates, doesn't allocate 56 - pub fn new_static(handle: &'static str) -> Result<Self, &'static str> { 70 + pub fn new_static(handle: &'static str) -> Result<Self, AtStrError> { 57 71 let handle = handle 58 72 .strip_prefix("at://") 59 73 .unwrap_or(handle) 60 74 .strip_prefix('@') 61 75 .unwrap_or(handle); 62 76 if handle.len() > 253 { 63 - Err("handle too long") 77 + Err(AtStrError::too_long("handle", handle, 253, handle.len())) 64 78 } else if !HANDLE_REGEX.is_match(handle) { 65 - Err("Invalid handle") 79 + Err(AtStrError::regex( 80 + "handle", 81 + handle, 82 + SmolStr::new_static("invalid"), 83 + )) 84 + } else if ends_with(handle, DISALLOWED_TLDS) { 85 + Err(AtStrError::disallowed("handle", handle, DISALLOWED_TLDS)) 66 86 } else { 67 87 Ok(Self(CowStr::new_static(handle))) 68 88 } ··· 83 103 panic!("handle too long") 84 104 } else if !HANDLE_REGEX.is_match(handle) { 85 105 panic!("Invalid handle") 106 + } else if ends_with(handle, DISALLOWED_TLDS) { 107 + panic!("top-level domain not allowed in handles") 86 108 } else { 87 109 Self(CowStr::Borrowed(handle)) 88 110 } ··· 110 132 } 111 133 112 134 impl FromStr for Handle<'_> { 113 - type Err = &'static str; 135 + type Err = AtStrError; 114 136 115 137 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 116 138 /// Prefer `Handle::new()` or `Handle::raw` if you want to borrow. ··· 163 185 164 186 impl From<String> for Handle<'static> { 165 187 fn from(value: String) -> Self { 166 - let value = if let Some(handle) = value 167 - .strip_prefix("at://") 168 - .unwrap_or(&value) 169 - .strip_prefix('@') 170 - { 171 - CowStr::Borrowed(handle) 172 - } else { 173 - value.into() 174 - }; 175 - if value.len() > 253 { 176 - panic!("handle too long") 177 - } else if !HANDLE_REGEX.is_match(&value) { 178 - panic!("Invalid handle") 179 - } else { 180 - Self(value.into_static()) 181 - } 188 + Self::new_owned(value).unwrap() 182 189 } 183 190 } 184 191 185 192 impl<'h> From<CowStr<'h>> for Handle<'h> { 186 193 fn from(value: CowStr<'h>) -> Self { 187 - let value = if let Some(handle) = value 188 - .strip_prefix("at://") 189 - .unwrap_or(&value) 190 - .strip_prefix('@') 191 - { 192 - CowStr::Borrowed(handle) 193 - } else { 194 - value 195 - }; 196 - if value.len() > 253 { 197 - panic!("handle too long") 198 - } else if !HANDLE_REGEX.is_match(&value) { 199 - panic!("Invalid handle") 200 - } else { 201 - Self(value.into_static()) 202 - } 194 + Self::new_owned(value).unwrap() 203 195 } 204 196 } 205 197
+5 -4
crates/jacquard-common/src/types/ident.rs
··· 1 1 use crate::types::handle::Handle; 2 + use crate::types::string::AtStrError; 2 3 use crate::{IntoStatic, types::did::Did}; 3 4 use std::fmt; 4 5 use std::str::FromStr; ··· 18 19 19 20 impl<'i> AtIdentifier<'i> { 20 21 /// Fallible constructor, validates, borrows from input 21 - pub fn new(ident: &'i str) -> Result<Self, &'static str> { 22 + pub fn new(ident: &'i str) -> Result<Self, AtStrError> { 22 23 if let Ok(did) = ident.parse() { 23 24 Ok(AtIdentifier::Did(did)) 24 25 } else { ··· 27 28 } 28 29 29 30 /// Fallible constructor, validates, takes ownership 30 - pub fn new_owned(ident: impl AsRef<str>) -> Result<Self, &'static str> { 31 + pub fn new_owned(ident: impl AsRef<str>) -> Result<Self, AtStrError> { 31 32 let ident = ident.as_ref(); 32 33 if let Ok(did) = Did::new_owned(ident) { 33 34 Ok(AtIdentifier::Did(did)) ··· 37 38 } 38 39 39 40 /// Fallible constructor, validates, doesn't allocate 40 - pub fn new_static(ident: &'static str) -> Result<AtIdentifier<'static>, &'static str> { 41 + pub fn new_static(ident: &'static str) -> Result<AtIdentifier<'static>, AtStrError> { 41 42 if let Ok(did) = Did::new_static(ident) { 42 43 Ok(AtIdentifier::Did(did)) 43 44 } else { ··· 93 94 } 94 95 95 96 impl FromStr for AtIdentifier<'_> { 96 - type Err = &'static str; 97 + type Err = AtStrError; 97 98 98 99 fn from_str(s: &str) -> Result<Self, Self::Err> { 99 100 if let Ok(did) = s.parse() {
+19 -17
crates/jacquard-common/src/types/language.rs
··· 5 5 6 6 use crate::CowStr; 7 7 8 - /// A [Timestamp Identifier]. 8 + /// An IETF language tag. 9 + /// 10 + /// Uses langtag crate for validation, but is stored as a SmolStr for size/avoiding allocations 9 11 /// 10 - /// [Timestamp Identifier]: https://atproto.com/specs/lang 12 + /// TODO: Implement langtag-style semantic matching for this type, delegating to langtag 11 13 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)] 12 14 #[serde(transparent)] 13 15 #[repr(transparent)] 14 - pub struct Lang(SmolStr); 16 + pub struct Language(SmolStr); 15 17 16 - impl Lang { 18 + impl Language { 17 19 /// Parses an IETF language tag from the given string. 18 20 pub fn new<T>(lang: &T) -> Result<Self, langtag::InvalidLangTag<&T>> 19 21 where 20 22 T: AsRef<str> + ?Sized, 21 23 { 22 24 let tag = langtag::LangTag::new(lang)?; 23 - Ok(Lang(SmolStr::new_inline(tag.as_str()))) 25 + Ok(Language(SmolStr::new_inline(tag.as_str()))) 24 26 } 25 27 26 28 /// Infallible constructor for when you *know* the string is a valid IETF language tag. ··· 30 32 pub fn raw(lang: impl AsRef<str>) -> Self { 31 33 let lang = lang.as_ref(); 32 34 let tag = langtag::LangTag::new(lang).expect("valid IETF language tag"); 33 - Lang(SmolStr::new_inline(tag.as_str())) 35 + Language(SmolStr::new_inline(tag.as_str())) 34 36 } 35 37 36 38 /// Infallible constructor for when you *know* the string is a valid IETF language tag. ··· 49 51 } 50 52 } 51 53 52 - impl FromStr for Lang { 54 + impl FromStr for Language { 53 55 type Err = SmolStr; 54 56 55 57 fn from_str(s: &str) -> Result<Self, Self::Err> { ··· 57 59 } 58 60 } 59 61 60 - impl<'de> Deserialize<'de> for Lang { 62 + impl<'de> Deserialize<'de> for Language { 61 63 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 62 64 where 63 65 D: Deserializer<'de>, ··· 67 69 } 68 70 } 69 71 70 - impl fmt::Display for Lang { 72 + impl fmt::Display for Language { 71 73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 72 74 f.write_str(&self.0) 73 75 } 74 76 } 75 77 76 - impl From<Lang> for String { 77 - fn from(value: Lang) -> Self { 78 + impl From<Language> for String { 79 + fn from(value: Language) -> Self { 78 80 value.0.to_string() 79 81 } 80 82 } 81 83 82 - impl From<Lang> for SmolStr { 83 - fn from(value: Lang) -> Self { 84 + impl From<Language> for SmolStr { 85 + fn from(value: Language) -> Self { 84 86 value.0 85 87 } 86 88 } 87 89 88 - impl From<String> for Lang { 90 + impl From<String> for Language { 89 91 fn from(value: String) -> Self { 90 92 Self::raw(&value) 91 93 } 92 94 } 93 95 94 - impl<'t> From<CowStr<'t>> for Lang { 96 + impl<'t> From<CowStr<'t>> for Language { 95 97 fn from(value: CowStr<'t>) -> Self { 96 98 Self::raw(&value) 97 99 } 98 100 } 99 101 100 - impl AsRef<str> for Lang { 102 + impl AsRef<str> for Language { 101 103 fn as_ref(&self) -> &str { 102 104 self.as_str() 103 105 } 104 106 } 105 107 106 - impl Deref for Lang { 108 + impl Deref for Language { 107 109 type Target = str; 108 110 109 111 fn deref(&self) -> &Self::Target {
+23 -10
crates/jacquard-common/src/types/nsid.rs
··· 1 1 use crate::types::recordkey::RecordKeyType; 2 + use crate::types::string::AtStrError; 2 3 use crate::{CowStr, IntoStatic}; 3 4 use regex::Regex; 4 5 use serde::{Deserialize, Deserializer, Serialize, de::Error}; ··· 22 23 23 24 impl<'n> Nsid<'n> { 24 25 /// Fallible constructor, validates, borrows from input 25 - pub fn new(nsid: &'n str) -> Result<Self, &'static str> { 26 + pub fn new(nsid: &'n str) -> Result<Self, AtStrError> { 26 27 if nsid.len() > 317 { 27 - Err("NSID too long") 28 + Err(AtStrError::too_long("nsid", nsid, 317, nsid.len())) 28 29 } else if !NSID_REGEX.is_match(nsid) { 29 - Err("Invalid NSID") 30 + Err(AtStrError::regex( 31 + "nsid", 32 + nsid, 33 + SmolStr::new_static("invalid"), 34 + )) 30 35 } else { 31 36 Ok(Self(CowStr::Borrowed(nsid))) 32 37 } 33 38 } 34 39 35 40 /// Fallible constructor, validates, borrows from input 36 - pub fn new_owned(nsid: impl AsRef<str>) -> Result<Self, &'static str> { 41 + pub fn new_owned(nsid: impl AsRef<str>) -> Result<Self, AtStrError> { 37 42 let nsid = nsid.as_ref(); 38 43 if nsid.len() > 317 { 39 - Err("NSID too long") 44 + Err(AtStrError::too_long("nsid", nsid, 317, nsid.len())) 40 45 } else if !NSID_REGEX.is_match(nsid) { 41 - Err("Invalid NSID") 46 + Err(AtStrError::regex( 47 + "nsid", 48 + nsid, 49 + SmolStr::new_static("invalid"), 50 + )) 42 51 } else { 43 52 Ok(Self(CowStr::Owned(nsid.to_smolstr()))) 44 53 } 45 54 } 46 55 47 56 /// Fallible constructor, validates, doesn't allocate 48 - pub fn new_static(nsid: &'static str) -> Result<Self, &'static str> { 57 + pub fn new_static(nsid: &'static str) -> Result<Self, AtStrError> { 49 58 if nsid.len() > 317 { 50 - Err("NSID too long") 59 + Err(AtStrError::too_long("nsid", nsid, 317, nsid.len())) 51 60 } else if !NSID_REGEX.is_match(nsid) { 52 - Err("Invalid NSID") 61 + Err(AtStrError::regex( 62 + "nsid", 63 + nsid, 64 + SmolStr::new_static("invalid"), 65 + )) 53 66 } else { 54 67 Ok(Self(CowStr::new_static(nsid))) 55 68 } ··· 96 109 } 97 110 98 111 impl<'n> FromStr for Nsid<'n> { 99 - type Err = &'static str; 112 + type Err = AtStrError; 100 113 101 114 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 102 115 /// Prefer `Nsid::new()` or `Nsid::raw` if you want to borrow.
+29 -12
crates/jacquard-common/src/types/recordkey.rs
··· 1 1 use crate::types::Literal; 2 + use crate::types::string::AtStrError; 2 3 use crate::{CowStr, IntoStatic}; 3 4 use regex::Regex; 4 5 use serde::{Deserialize, Deserializer, Serialize, de::Error}; ··· 74 75 /// AT Protocol rkey 75 76 impl<'r> Rkey<'r> { 76 77 /// Fallible constructor, validates, borrows from input 77 - pub fn new(rkey: &'r str) -> Result<Self, &'static str> { 78 + pub fn new(rkey: &'r str) -> Result<Self, AtStrError> { 78 79 if [".", ".."].contains(&rkey) { 79 - Err("Disallowed rkey") 80 + Err(AtStrError::disallowed("record-key", rkey, &[".", ".."])) 80 81 } else if !RKEY_REGEX.is_match(rkey) { 81 - Err("Invalid rkey") 82 + Err(AtStrError::regex( 83 + "record-key", 84 + rkey, 85 + SmolStr::new_static("doesn't match 'any' schema"), 86 + )) 82 87 } else { 83 88 Ok(Self(CowStr::Borrowed(rkey))) 84 89 } 85 90 } 86 91 87 92 /// Fallible constructor, validates, borrows from input 88 - pub fn new_owned(rkey: impl AsRef<str>) -> Result<Self, &'static str> { 93 + pub fn new_owned(rkey: impl AsRef<str>) -> Result<Self, AtStrError> { 89 94 let rkey = rkey.as_ref(); 90 95 if [".", ".."].contains(&rkey) { 91 - Err("Disallowed rkey") 96 + Err(AtStrError::disallowed("record-key", rkey, &[".", ".."])) 92 97 } else if !RKEY_REGEX.is_match(rkey) { 93 - Err("Invalid rkey") 98 + Err(AtStrError::regex( 99 + "record-key", 100 + rkey, 101 + SmolStr::new_static("doesn't match 'any' schema"), 102 + )) 94 103 } else { 95 104 Ok(Self(CowStr::Owned(rkey.to_smolstr()))) 96 105 } 97 106 } 98 107 99 108 /// Fallible constructor, validates, doesn't allocate 100 - pub fn new_static(rkey: &'static str) -> Result<Self, &'static str> { 109 + pub fn new_static(rkey: &'static str) -> Result<Self, AtStrError> { 101 110 if [".", ".."].contains(&rkey) { 102 - Err("Disallowed rkey") 111 + Err(AtStrError::disallowed("record-key", rkey, &[".", ".."])) 103 112 } else if !RKEY_REGEX.is_match(rkey) { 104 - Err("Invalid rkey") 113 + Err(AtStrError::regex( 114 + "record-key", 115 + rkey, 116 + SmolStr::new_static("doesn't match 'any' schema"), 117 + )) 105 118 } else { 106 119 Ok(Self(CowStr::new_static(rkey))) 107 120 } ··· 136 149 } 137 150 138 151 impl<'r> FromStr for Rkey<'r> { 139 - type Err = &'static str; 152 + type Err = AtStrError; 140 153 141 154 fn from_str(s: &str) -> Result<Self, Self::Err> { 142 155 if [".", ".."].contains(&s) { 143 - Err("Disallowed rkey") 156 + Err(AtStrError::disallowed("record-key", s, &[".", ".."])) 144 157 } else if !RKEY_REGEX.is_match(s) { 145 - Err("Invalid rkey") 158 + Err(AtStrError::regex( 159 + "record-key", 160 + s, 161 + SmolStr::new_static("doesn't match 'any' schema"), 162 + )) 146 163 } else { 147 164 Ok(Self(CowStr::Owned(s.to_smolstr()))) 148 165 }
+394
crates/jacquard-common/src/types/string.rs
··· 1 + use bytes::Bytes; 2 + use miette::SourceSpan; 3 + use serde::{Deserialize, Deserializer, Serialize, Serializer}; 4 + use smol_str::{SmolStr, ToSmolStr}; 5 + use std::{collections::BTreeMap, str::FromStr, sync::Arc}; 6 + 7 + use crate::IntoStatic; 8 + pub use crate::{ 9 + CowStr, 10 + types::{ 11 + aturi::AtUri, 12 + cid::Cid, 13 + datetime::Datetime, 14 + did::Did, 15 + handle::Handle, 16 + ident::AtIdentifier, 17 + language::Language, 18 + nsid::Nsid, 19 + recordkey::{RecordKey, Rkey}, 20 + tid::Tid, 21 + uri::Uri, 22 + }, 23 + }; 24 + 25 + /// ATProto string value 26 + #[derive(Debug, Clone, PartialEq, Eq, Hash)] 27 + pub enum AtprotoStr<'s> { 28 + Datetime(Datetime), 29 + Language(Language), 30 + Tid(Tid), 31 + Nsid(Nsid<'s>), 32 + Did(Did<'s>), 33 + Handle(Handle<'s>), 34 + AtIdentifier(AtIdentifier<'s>), 35 + AtUri(AtUri<'s>), 36 + Uri(Uri<'s>), 37 + Cid(Cid<'s>), 38 + RecordKey(RecordKey<Rkey<'s>>), 39 + String(CowStr<'s>), 40 + } 41 + 42 + impl<'s> AtprotoStr<'s> { 43 + /// Borrowing constructor for bare atproto string values 44 + /// This is fairly exhaustive and potentially **slow**, prefer using anything 45 + /// that narrows down the search field quicker. 46 + /// 47 + /// Note: We don't construct record keys from bare strings in this because 48 + /// the type is too permissive and too many things would be classified as rkeys. 49 + /// 50 + /// Value object deserialization checks against the field names for common 51 + /// names (uri, cid, did, handle, createdAt, indexedAt, etc.) to improve 52 + /// performance of the happy path. 53 + pub fn new(string: &'s str) -> Self { 54 + // TODO: do some quick prefix checks like in Uri to drop through faster 55 + if let Ok(datetime) = Datetime::from_str(string) { 56 + Self::Datetime(datetime) 57 + } else if let Ok(lang) = Language::new(string) { 58 + Self::Language(lang) 59 + } else if let Ok(tid) = Tid::from_str(string) { 60 + Self::Tid(tid) 61 + } else if let Ok(did) = Did::new(string) { 62 + Self::Did(did) 63 + } else if let Ok(handle) = Handle::new(string) { 64 + Self::Handle(handle) 65 + } else if let Ok(atid) = AtIdentifier::new(string) { 66 + Self::AtIdentifier(atid) 67 + } else if let Ok(nsid) = Nsid::new(string) { 68 + Self::Nsid(nsid) 69 + } else if let Ok(aturi) = AtUri::new(string) { 70 + Self::AtUri(aturi) 71 + } else if let Ok(uri) = Uri::new(string) { 72 + Self::Uri(uri) 73 + } else if let Ok(cid) = Cid::new(string.as_bytes()) { 74 + Self::Cid(cid) 75 + } else { 76 + // We don't construct record keys from bare strings because the type is too permissive 77 + Self::String(CowStr::Borrowed(string)) 78 + } 79 + } 80 + 81 + pub fn as_str(&self) -> &str { 82 + match self { 83 + Self::Datetime(datetime) => datetime.as_str(), 84 + Self::Language(lang) => lang.as_str(), 85 + Self::Handle(handle) => handle.as_str(), 86 + Self::AtIdentifier(atid) => atid.as_str(), 87 + Self::Nsid(nsid) => nsid.as_str(), 88 + Self::AtUri(aturi) => aturi.as_str(), 89 + Self::Uri(uri) => uri.as_str(), 90 + Self::Cid(cid) => cid.as_str(), 91 + Self::Tid(tid) => tid.as_str(), 92 + Self::Did(did) => did.as_str(), 93 + Self::RecordKey(rkey) => rkey.as_ref(), 94 + Self::String(string) => string.as_ref(), 95 + } 96 + } 97 + } 98 + 99 + impl AtprotoStr<'static> { 100 + /// Owned constructor for bare atproto string values 101 + /// This is fairly exhaustive and potentially **slow**, prefer using anything 102 + /// that narrows down the search field quicker. 103 + /// 104 + /// Note: We don't construct record keys from bare strings in this because 105 + /// the type is too permissive and too many things would be classified as rkeys. 106 + /// 107 + /// Value object deserialization checks against the field names for common 108 + /// names (uri, cid, did, handle, createdAt, indexedAt, etc.) to improve 109 + /// performance of the happy path. 110 + pub fn new_owned(string: impl AsRef<str>) -> AtprotoStr<'static> { 111 + let string = string.as_ref(); 112 + // TODO: do some quick prefix checks like in Uri to drop through faster 113 + if let Ok(datetime) = Datetime::from_str(string) { 114 + Self::Datetime(datetime) 115 + } else if let Ok(lang) = Language::new(string) { 116 + Self::Language(lang) 117 + } else if let Ok(tid) = Tid::from_str(string) { 118 + Self::Tid(tid) 119 + } else if let Ok(did) = Did::new_owned(string) { 120 + Self::Did(did) 121 + } else if let Ok(handle) = Handle::new_owned(string) { 122 + Self::Handle(handle) 123 + } else if let Ok(atid) = AtIdentifier::new_owned(string) { 124 + Self::AtIdentifier(atid) 125 + } else if let Ok(nsid) = Nsid::new_owned(string) { 126 + Self::Nsid(nsid) 127 + } else if let Ok(aturi) = AtUri::new_owned(string) { 128 + Self::AtUri(aturi) 129 + } else if let Ok(uri) = Uri::new_owned(string) { 130 + Self::Uri(uri) 131 + } else if let Ok(cid) = Cid::new_owned(string.as_bytes()) { 132 + Self::Cid(cid) 133 + } else { 134 + // We don't construct record keys from bare strings because the type is too permissive 135 + Self::String(CowStr::Owned(string.to_smolstr())) 136 + } 137 + } 138 + } 139 + 140 + impl<'s> AsRef<str> for AtprotoStr<'s> { 141 + fn as_ref(&self) -> &str { 142 + match self { 143 + Self::Datetime(datetime) => datetime.as_str(), 144 + Self::Language(lang) => lang.as_ref(), 145 + Self::Tid(tid) => tid.as_ref(), 146 + Self::Did(did) => did.as_ref(), 147 + Self::Handle(handle) => handle.as_ref(), 148 + Self::AtIdentifier(atid) => atid.as_ref(), 149 + Self::Nsid(nsid) => nsid.as_ref(), 150 + Self::AtUri(aturi) => aturi.as_ref(), 151 + Self::Uri(uri) => uri.as_str(), 152 + Self::Cid(cid) => cid.as_ref(), 153 + Self::RecordKey(rkey) => rkey.as_ref(), 154 + Self::String(string) => string.as_ref(), 155 + } 156 + } 157 + } 158 + 159 + impl Serialize for AtprotoStr<'_> { 160 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 161 + where 162 + S: Serializer, 163 + { 164 + serializer.serialize_str(self.as_ref()) 165 + } 166 + } 167 + 168 + impl<'de> Deserialize<'de> for AtprotoStr<'de> { 169 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 170 + where 171 + D: Deserializer<'de>, 172 + { 173 + let value = Deserialize::deserialize(deserializer)?; 174 + Ok(Self::new(value)) 175 + } 176 + } 177 + 178 + impl IntoStatic for AtprotoStr<'_> { 179 + type Output = AtprotoStr<'static>; 180 + 181 + fn into_static(self) -> Self::Output { 182 + match self { 183 + AtprotoStr::Datetime(datetime) => AtprotoStr::Datetime(datetime), 184 + AtprotoStr::Language(language) => AtprotoStr::Language(language), 185 + AtprotoStr::Tid(tid) => AtprotoStr::Tid(tid), 186 + AtprotoStr::Nsid(nsid) => AtprotoStr::Nsid(nsid.into_static()), 187 + AtprotoStr::Did(did) => AtprotoStr::Did(did.into_static()), 188 + AtprotoStr::Handle(handle) => AtprotoStr::Handle(handle.into_static()), 189 + AtprotoStr::AtIdentifier(ident) => AtprotoStr::AtIdentifier(ident.into_static()), 190 + AtprotoStr::AtUri(at_uri) => { 191 + AtprotoStr::AtUri(AtUri::new_owned(at_uri.as_str()).unwrap()) 192 + } 193 + AtprotoStr::Uri(uri) => AtprotoStr::Uri(uri.into_static()), 194 + AtprotoStr::Cid(cid) => AtprotoStr::Cid(cid.into_static()), 195 + AtprotoStr::RecordKey(record_key) => AtprotoStr::RecordKey(record_key.into_static()), 196 + AtprotoStr::String(cow_str) => AtprotoStr::String(cow_str.into_static()), 197 + } 198 + } 199 + } 200 + 201 + /// Parsing Error for atproto string types which don't have third-party specs 202 + /// (e.g. datetime, CIDs, language tags). 203 + /// 204 + /// `spec` refers to the final url path segment on atproto.com/specs, 205 + /// detailing the specification for the type 206 + /// `source` is the source string, or part of it 207 + /// `kind` is the type of parsing error: `[StrParseKind]` 208 + #[derive(Debug, thiserror::Error, miette::Diagnostic)] 209 + #[error("error in `{source}`: {kind}")] 210 + #[diagnostic( 211 + url("https://atproto.com/specs/{spec}"), 212 + help("if something doesn't match the spec, contact the crate author") 213 + )] 214 + pub struct AtStrError { 215 + pub spec: SmolStr, 216 + #[source_code] 217 + source: String, 218 + #[source] 219 + #[diagnostic_source] 220 + pub kind: StrParseKind, 221 + } 222 + 223 + impl AtStrError { 224 + pub fn new(spec: &'static str, source: String, kind: StrParseKind) -> Self { 225 + Self { 226 + spec: SmolStr::new_static(spec), 227 + source, 228 + kind, 229 + } 230 + } 231 + 232 + pub fn wrap(spec: &'static str, source: String, error: AtStrError) -> Self { 233 + if let Some(span) = match &error.kind { 234 + StrParseKind::Disallowed { problem, .. } => problem, 235 + StrParseKind::MissingComponent { span, .. } => span, 236 + _ => &None, 237 + } { 238 + Self { 239 + spec: SmolStr::new_static(spec), 240 + source, 241 + kind: StrParseKind::Wrap { 242 + span: Some(*span), 243 + err: Arc::new(error), 244 + }, 245 + } 246 + } else { 247 + let span = source 248 + .find(&error.source) 249 + .map(|start| (start, error.source.len()).into()); 250 + Self { 251 + spec: SmolStr::new_static(spec), 252 + source, 253 + kind: StrParseKind::Wrap { 254 + span, 255 + err: Arc::new(error), 256 + }, 257 + } 258 + } 259 + } 260 + 261 + /// something on the provided disallowed list was found in the source string 262 + /// does a substring search for any of the offending strings to get the span 263 + pub fn disallowed(spec: &'static str, source: &str, disallowed: &[&str]) -> Self { 264 + for item in disallowed { 265 + if let Some(loc) = source.find(item) { 266 + return Self { 267 + spec: SmolStr::new_static(spec), 268 + source: source.to_string(), 269 + kind: StrParseKind::Disallowed { 270 + problem: Some(SourceSpan::new(loc.into(), item.len())), 271 + message: smol_str::format_smolstr!("`{item}`"), 272 + }, 273 + }; 274 + } 275 + } 276 + Self { 277 + spec: SmolStr::new_static(spec), 278 + source: source.to_string(), 279 + kind: StrParseKind::Disallowed { 280 + problem: None, 281 + message: SmolStr::new_static(""), 282 + }, 283 + } 284 + } 285 + 286 + pub fn too_long(spec: &'static str, source: &str, max: usize, actual: usize) -> Self { 287 + Self { 288 + spec: SmolStr::new_static(spec), 289 + source: source.to_string(), 290 + kind: StrParseKind::TooLong { max, actual }, 291 + } 292 + } 293 + 294 + pub fn too_short(spec: &'static str, source: &str, min: usize, actual: usize) -> Self { 295 + Self { 296 + spec: SmolStr::new_static(spec), 297 + source: source.to_string(), 298 + kind: StrParseKind::TooShort { min, actual }, 299 + } 300 + } 301 + 302 + /// missing component, with what was expected to be found 303 + pub fn missing(spec: &'static str, source: &str, expected: &str) -> Self { 304 + if let Some(loc) = source.find(expected) { 305 + return Self { 306 + spec: SmolStr::new_static(spec), 307 + source: source.to_string(), 308 + kind: StrParseKind::MissingComponent { 309 + span: Some(SourceSpan::new(loc.into(), expected.len())), 310 + message: smol_str::format_smolstr!("`{expected}` found incorrectly here"), 311 + }, 312 + }; 313 + } 314 + Self { 315 + spec: SmolStr::new_static(spec), 316 + source: source.to_string(), 317 + kind: StrParseKind::MissingComponent { 318 + span: None, 319 + message: SmolStr::new(expected), 320 + }, 321 + } 322 + } 323 + 324 + /// missing component, with the span where it was expected to be founf 325 + pub fn missing_from( 326 + spec: &'static str, 327 + source: &str, 328 + expected: &str, 329 + span: (usize, usize), 330 + ) -> Self { 331 + Self { 332 + spec: SmolStr::new_static(spec), 333 + source: source.to_string(), 334 + kind: StrParseKind::MissingComponent { 335 + span: Some(span.into()), 336 + message: SmolStr::new(expected), 337 + }, 338 + } 339 + } 340 + 341 + pub fn regex(spec: &'static str, source: &str, message: SmolStr) -> Self { 342 + Self { 343 + spec: SmolStr::new_static(spec), 344 + source: source.to_string(), 345 + kind: StrParseKind::RegexFail { 346 + span: None, 347 + message, 348 + }, 349 + } 350 + } 351 + } 352 + 353 + #[derive(Debug, thiserror::Error, miette::Diagnostic)] 354 + pub enum StrParseKind { 355 + #[error("regex failure - {message}")] 356 + #[diagnostic(code(jacquard::types::string::regex_fail))] 357 + RegexFail { 358 + #[label] 359 + span: Option<SourceSpan>, 360 + #[help] 361 + message: SmolStr, 362 + }, 363 + #[error("string too long (allowed: {max}, actual: {actual})")] 364 + #[diagnostic(code(jacquard::types::string::wrong_length))] 365 + TooLong { max: usize, actual: usize }, 366 + 367 + #[error("string too short (allowed: {min}, actual: {actual})")] 368 + #[diagnostic(code(jacquard::types::string::wrong_length))] 369 + TooShort { min: usize, actual: usize }, 370 + #[error("disallowed - {message}")] 371 + #[diagnostic(code(jacquard::types::string::disallowed))] 372 + Disallowed { 373 + #[label] 374 + problem: Option<SourceSpan>, 375 + #[help] 376 + message: SmolStr, 377 + }, 378 + #[error("missing - {message}")] 379 + #[diagnostic(code(jacquard::atstr::missing_component))] 380 + MissingComponent { 381 + #[label] 382 + span: Option<SourceSpan>, 383 + #[help] 384 + message: SmolStr, 385 + }, 386 + #[error("{err:?}")] 387 + #[diagnostic(code(jacquard::atstr::inner))] 388 + Wrap { 389 + #[label] 390 + span: Option<SourceSpan>, 391 + #[source] 392 + err: Arc<AtStrError>, 393 + }, 394 + }
+20 -4
crates/jacquard-common/src/types/tid.rs
··· 6 6 7 7 use crate::CowStr; 8 8 use crate::types::integer::LimitedU32; 9 + use crate::types::string::{AtStrError, StrParseKind}; 9 10 use regex::Regex; 10 11 11 12 fn s32_encode(mut i: u64) -> SmolStr { ··· 40 41 41 42 impl Tid { 42 43 /// Parses a `TID` from the given string. 43 - pub fn new(tid: impl AsRef<str>) -> Result<Self, &'static str> { 44 + pub fn new(tid: impl AsRef<str>) -> Result<Self, AtStrError> { 44 45 let tid = tid.as_ref(); 45 46 if tid.len() != 13 { 46 - Err("TID must be 13 characters") 47 + let kind = if tid.len() > 13 { 48 + StrParseKind::TooLong { 49 + max: 13, 50 + actual: tid.len(), 51 + } 52 + } else { 53 + StrParseKind::TooShort { 54 + min: 13, 55 + actual: tid.len(), 56 + } 57 + }; 58 + Err(AtStrError::new("tid", tid.to_string(), kind)) 47 59 } else if !TID_REGEX.is_match(&tid.as_ref()) { 48 - Err("Invalid TID") 60 + let kind = StrParseKind::RegexFail { 61 + span: None, 62 + message: SmolStr::new_static("didn't match schema"), 63 + }; 64 + Err(AtStrError::new("tid", tid.to_string(), kind)) 49 65 } else { 50 66 Ok(Self(SmolStr::new_inline(&tid))) 51 67 } ··· 117 133 } 118 134 119 135 impl FromStr for Tid { 120 - type Err = &'static str; 136 + type Err = AtStrError; 121 137 122 138 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 123 139 /// Prefer `Did::new()` or `Did::raw` if you want to borrow.
+124
crates/jacquard-common/src/types/uri.rs
··· 1 + use serde::{Deserialize, Deserializer, Serialize, Serializer}; 2 + use smol_str::ToSmolStr; 3 + use url::Url; 4 + 5 + use crate::{ 6 + CowStr, IntoStatic, 7 + types::{aturi::AtUri, cid::Cid, did::Did, string::AtStrError}, 8 + }; 9 + 10 + /// URI with best-available contextual type 11 + /// TODO: figure out wtf a DNS uri should look like 12 + #[derive(Debug, Clone, PartialEq, Eq, Hash)] 13 + pub enum Uri<'u> { 14 + Did(Did<'u>), 15 + At(AtUri<'u>), 16 + Https(Url), 17 + Wss(Url), 18 + Cid(Cid<'u>), 19 + Any(CowStr<'u>), 20 + } 21 + 22 + #[derive(Debug, thiserror::Error, miette::Diagnostic)] 23 + pub enum UriParseError { 24 + #[error("Invalid atproto string: {0}")] 25 + At(#[from] AtStrError), 26 + #[error(transparent)] 27 + Url(#[from] url::ParseError), 28 + #[error(transparent)] 29 + Cid(#[from] crate::types::cid::Error), 30 + } 31 + 32 + impl<'u> Uri<'u> { 33 + pub fn new(uri: &'u str) -> Result<Self, UriParseError> { 34 + if uri.starts_with("did:") { 35 + Ok(Uri::Did(Did::new(uri)?)) 36 + } else if uri.starts_with("at://") { 37 + Ok(Uri::At(AtUri::new(uri)?)) 38 + } else if uri.starts_with("https://") { 39 + Ok(Uri::Https(Url::parse(uri)?)) 40 + } else if uri.starts_with("wss://") { 41 + Ok(Uri::Https(Url::parse(uri)?)) 42 + } else if uri.starts_with("ipld://") { 43 + Ok(Uri::Cid(Cid::new(uri.as_bytes())?)) 44 + } else { 45 + Ok(Uri::Any(CowStr::Borrowed(uri))) 46 + } 47 + } 48 + 49 + pub fn new_owned(uri: impl AsRef<str>) -> Result<Uri<'static>, UriParseError> { 50 + let uri = uri.as_ref(); 51 + if uri.starts_with("did:") { 52 + Ok(Uri::Did(Did::new_owned(uri)?)) 53 + } else if uri.starts_with("at://") { 54 + Ok(Uri::At(AtUri::new_owned(uri)?)) 55 + } else if uri.starts_with("https://") { 56 + Ok(Uri::Https(Url::parse(uri)?)) 57 + } else if uri.starts_with("wss://") { 58 + Ok(Uri::Https(Url::parse(uri)?)) 59 + } else if uri.starts_with("ipld://") { 60 + Ok(Uri::Cid(Cid::new_owned(uri.as_bytes())?)) 61 + } else { 62 + Ok(Uri::Any(CowStr::Owned(uri.to_smolstr()))) 63 + } 64 + } 65 + 66 + pub fn as_str(&self) -> &str { 67 + match self { 68 + Uri::Did(did) => did.as_str(), 69 + Uri::At(at_uri) => at_uri.as_str(), 70 + Uri::Https(url) => url.as_str(), 71 + Uri::Wss(url) => url.as_str(), 72 + Uri::Cid(cid) => cid.as_str(), 73 + Uri::Any(s) => s.as_ref(), 74 + } 75 + } 76 + } 77 + 78 + impl Serialize for Uri<'_> { 79 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 80 + where 81 + S: Serializer, 82 + { 83 + serializer.serialize_str(self.as_str()) 84 + } 85 + } 86 + 87 + impl<'de> Deserialize<'de> for Uri<'de> { 88 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 89 + where 90 + D: Deserializer<'de>, 91 + { 92 + use serde::de::Error; 93 + let value = Deserialize::deserialize(deserializer)?; 94 + Self::new(value).map_err(D::Error::custom) 95 + } 96 + } 97 + 98 + impl<'s> AsRef<str> for Uri<'s> { 99 + fn as_ref(&self) -> &str { 100 + match self { 101 + Uri::Did(did) => did.as_str(), 102 + Uri::At(at_uri) => at_uri.as_str(), 103 + Uri::Https(url) => url.as_str(), 104 + Uri::Wss(url) => url.as_str(), 105 + Uri::Cid(cid) => cid.as_str(), 106 + Uri::Any(s) => s.as_ref(), 107 + } 108 + } 109 + } 110 + 111 + impl IntoStatic for Uri<'_> { 112 + type Output = Uri<'static>; 113 + 114 + fn into_static(self) -> Self::Output { 115 + match self { 116 + Uri::Did(did) => Uri::Did(did.into_static()), 117 + Uri::At(at_uri) => Uri::At(AtUri::new_owned(at_uri.as_str()).unwrap()), 118 + Uri::Https(url) => Uri::Https(url), 119 + Uri::Wss(url) => Uri::Wss(url), 120 + Uri::Cid(cid) => Uri::Cid(cid.into_static()), 121 + Uri::Any(s) => Uri::Any(s.into_static()), 122 + } 123 + } 124 + }
+25
crates/jacquard-common/src/types/value.rs
··· 1 + use bytes::Bytes; 2 + use serde::{Deserialize, Deserializer, Serialize, Serializer}; 3 + use smol_str::SmolStr; 4 + use std::collections::BTreeMap; 5 + 6 + use crate::types::{blob::Blob, string::*}; 7 + 8 + #[derive(Debug, Clone, PartialEq, Eq)] 9 + pub enum Data<'s> { 10 + Null, 11 + Boolean(bool), 12 + Integer(i64), 13 + String(AtprotoStr<'s>), 14 + Bytes(Bytes), // maybe need custom type for serialization 15 + CidLink(Cid<'s>), // maybe need custom type for serialization 16 + Array(Array<'s>), 17 + Object(Object<'s>), 18 + Blob(Blob<'s>), 19 + } 20 + 21 + #[derive(Debug, Clone, PartialEq, Eq)] 22 + pub struct Array<'s>(pub Vec<Data<'s>>); 23 + 24 + #[derive(Debug, Clone, PartialEq, Eq)] 25 + pub struct Object<'s>(pub BTreeMap<SmolStr, Data<'s>>);
+14
crates/jacquard-lexicon/Cargo.toml
··· 1 + [package] 2 + name = "jacquard-lexicon" 3 + edition.workspace = true 4 + version.workspace = true 5 + authors.workspace = true 6 + repository.workspace = true 7 + keywords.workspace = true 8 + categories.workspace = true 9 + readme.workspace = true 10 + documentation.workspace = true 11 + exclude.workspace = true 12 + description.workspace = true 13 + 14 + [dependencies]
+14
crates/jacquard-lexicon/src/lib.rs
··· 1 + pub fn add(left: u64, right: u64) -> u64 { 2 + left + right 3 + } 4 + 5 + #[cfg(test)] 6 + mod tests { 7 + use super::*; 8 + 9 + #[test] 10 + fn it_works() { 11 + let result = add(2, 2); 12 + assert_eq!(result, 4); 13 + } 14 + }