A better Rust ATProto crate

moving types into module, tid, datetime, identifier, handle

Orual d950286c a9a67752

+14
.tangled/workflows/build.yml
···
··· 1 + when: 2 + - event: ["push", "pull_request"] 3 + branch: ["main"] 4 + 5 + engine: nixery 6 + 7 + dependencies: 8 + nixpkgs: 9 + - just 10 + 11 + steps: 12 + - name: build appview 13 + command: | 14 + nix build
+302
Cargo.lock
··· 12 ] 13 14 [[package]] 15 name = "anstream" 16 version = "0.6.20" 17 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 62 ] 63 64 [[package]] 65 name = "base-x" 66 version = "0.2.11" 67 source = "registry+https://github.com/rust-lang/crates.io-index" 68 checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 69 70 [[package]] 71 name = "castaway" 72 version = "0.2.4" 73 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 77 ] 78 79 [[package]] 80 name = "cfg-if" 81 version = "1.0.3" 82 source = "registry+https://github.com/rust-lang/crates.io-index" 83 checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 84 85 [[package]] 86 name = "cid" 87 version = "0.11.1" 88 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 157 ] 158 159 [[package]] 160 name = "core2" 161 version = "0.4.0" 162 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 192 ] 193 194 [[package]] 195 name = "heck" 196 version = "0.5.0" 197 source = "registry+https://github.com/rust-lang/crates.io-index" 198 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 199 200 [[package]] 201 name = "is_terminal_polyfill" 202 version = "1.70.1" 203 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 214 version = "0.1.0" 215 dependencies = [ 216 "clap", 217 ] 218 219 [[package]] 220 name = "jacquard-common" 221 version = "0.1.0" 222 dependencies = [ 223 "cid", 224 "compact_str", 225 "miette", ··· 227 "multihash", 228 "regex", 229 "serde", 230 "thiserror", 231 ] 232 233 [[package]] 234 name = "memchr" ··· 281 ] 282 283 [[package]] 284 name = "once_cell_polyfill" 285 version = "1.70.1" 286 source = "registry+https://github.com/rust-lang/crates.io-index" 287 checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 288 289 [[package]] 290 name = "proc-macro2" ··· 385 ] 386 387 [[package]] 388 name = "static_assertions" 389 version = "1.1.0" 390 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 452 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 453 454 [[package]] 455 name = "windows-link" 456 version = "0.2.0" 457 source = "registry+https://github.com/rust-lang/crates.io-index" 458 checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" 459 460 [[package]] 461 name = "windows-sys"
··· 12 ] 13 14 [[package]] 15 + name = "android_system_properties" 16 + version = "0.1.5" 17 + source = "registry+https://github.com/rust-lang/crates.io-index" 18 + checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 19 + dependencies = [ 20 + "libc", 21 + ] 22 + 23 + [[package]] 24 name = "anstream" 25 version = "0.6.20" 26 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 71 ] 72 73 [[package]] 74 + name = "autocfg" 75 + version = "1.5.0" 76 + source = "registry+https://github.com/rust-lang/crates.io-index" 77 + checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 78 + 79 + [[package]] 80 name = "base-x" 81 version = "0.2.11" 82 source = "registry+https://github.com/rust-lang/crates.io-index" 83 checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 84 85 [[package]] 86 + name = "bumpalo" 87 + version = "3.19.0" 88 + source = "registry+https://github.com/rust-lang/crates.io-index" 89 + checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 90 + 91 + [[package]] 92 name = "castaway" 93 version = "0.2.4" 94 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 98 ] 99 100 [[package]] 101 + name = "cc" 102 + version = "1.2.39" 103 + source = "registry+https://github.com/rust-lang/crates.io-index" 104 + checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" 105 + dependencies = [ 106 + "find-msvc-tools", 107 + "shlex", 108 + ] 109 + 110 + [[package]] 111 name = "cfg-if" 112 version = "1.0.3" 113 source = "registry+https://github.com/rust-lang/crates.io-index" 114 checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 115 116 [[package]] 117 + name = "chrono" 118 + version = "0.4.42" 119 + source = "registry+https://github.com/rust-lang/crates.io-index" 120 + checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" 121 + dependencies = [ 122 + "iana-time-zone", 123 + "js-sys", 124 + "num-traits", 125 + "wasm-bindgen", 126 + "windows-link", 127 + ] 128 + 129 + [[package]] 130 name = "cid" 131 version = "0.11.1" 132 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 201 ] 202 203 [[package]] 204 + name = "core-foundation-sys" 205 + version = "0.8.7" 206 + source = "registry+https://github.com/rust-lang/crates.io-index" 207 + checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 208 + 209 + [[package]] 210 name = "core2" 211 version = "0.4.0" 212 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 242 ] 243 244 [[package]] 245 + name = "equivalent" 246 + version = "1.0.2" 247 + source = "registry+https://github.com/rust-lang/crates.io-index" 248 + checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 249 + 250 + [[package]] 251 + name = "find-msvc-tools" 252 + version = "0.1.2" 253 + source = "registry+https://github.com/rust-lang/crates.io-index" 254 + checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" 255 + 256 + [[package]] 257 + name = "form_urlencoded" 258 + version = "1.2.2" 259 + source = "registry+https://github.com/rust-lang/crates.io-index" 260 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 261 + dependencies = [ 262 + "percent-encoding", 263 + ] 264 + 265 + [[package]] 266 + name = "hashbrown" 267 + version = "0.16.0" 268 + source = "registry+https://github.com/rust-lang/crates.io-index" 269 + checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" 270 + 271 + [[package]] 272 name = "heck" 273 version = "0.5.0" 274 source = "registry+https://github.com/rust-lang/crates.io-index" 275 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 276 277 [[package]] 278 + name = "iana-time-zone" 279 + version = "0.1.64" 280 + source = "registry+https://github.com/rust-lang/crates.io-index" 281 + checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" 282 + dependencies = [ 283 + "android_system_properties", 284 + "core-foundation-sys", 285 + "iana-time-zone-haiku", 286 + "js-sys", 287 + "log", 288 + "wasm-bindgen", 289 + "windows-core", 290 + ] 291 + 292 + [[package]] 293 + name = "iana-time-zone-haiku" 294 + version = "0.1.2" 295 + source = "registry+https://github.com/rust-lang/crates.io-index" 296 + checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 297 + dependencies = [ 298 + "cc", 299 + ] 300 + 301 + [[package]] 302 + name = "indexmap" 303 + version = "2.11.4" 304 + source = "registry+https://github.com/rust-lang/crates.io-index" 305 + checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" 306 + dependencies = [ 307 + "equivalent", 308 + "hashbrown", 309 + ] 310 + 311 + [[package]] 312 name = "is_terminal_polyfill" 313 version = "1.70.1" 314 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 325 version = "0.1.0" 326 dependencies = [ 327 "clap", 328 + "jacquard-common", 329 ] 330 331 [[package]] 332 name = "jacquard-common" 333 version = "0.1.0" 334 dependencies = [ 335 + "chrono", 336 "cid", 337 "compact_str", 338 "miette", ··· 340 "multihash", 341 "regex", 342 "serde", 343 + "serde_html_form", 344 + "serde_json", 345 "thiserror", 346 ] 347 + 348 + [[package]] 349 + name = "js-sys" 350 + version = "0.3.81" 351 + source = "registry+https://github.com/rust-lang/crates.io-index" 352 + checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" 353 + dependencies = [ 354 + "once_cell", 355 + "wasm-bindgen", 356 + ] 357 + 358 + [[package]] 359 + name = "libc" 360 + version = "0.2.176" 361 + source = "registry+https://github.com/rust-lang/crates.io-index" 362 + checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" 363 + 364 + [[package]] 365 + name = "log" 366 + version = "0.4.28" 367 + source = "registry+https://github.com/rust-lang/crates.io-index" 368 + checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 369 370 [[package]] 371 name = "memchr" ··· 418 ] 419 420 [[package]] 421 + name = "num-traits" 422 + version = "0.2.19" 423 + source = "registry+https://github.com/rust-lang/crates.io-index" 424 + checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 425 + dependencies = [ 426 + "autocfg", 427 + ] 428 + 429 + [[package]] 430 + name = "once_cell" 431 + version = "1.21.3" 432 + source = "registry+https://github.com/rust-lang/crates.io-index" 433 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 434 + 435 + [[package]] 436 name = "once_cell_polyfill" 437 version = "1.70.1" 438 source = "registry+https://github.com/rust-lang/crates.io-index" 439 checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 440 + 441 + [[package]] 442 + name = "percent-encoding" 443 + version = "2.3.2" 444 + source = "registry+https://github.com/rust-lang/crates.io-index" 445 + checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 446 447 [[package]] 448 name = "proc-macro2" ··· 543 ] 544 545 [[package]] 546 + name = "serde_html_form" 547 + version = "0.2.8" 548 + source = "registry+https://github.com/rust-lang/crates.io-index" 549 + checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" 550 + dependencies = [ 551 + "form_urlencoded", 552 + "indexmap", 553 + "itoa", 554 + "ryu", 555 + "serde_core", 556 + ] 557 + 558 + [[package]] 559 + name = "serde_json" 560 + version = "1.0.145" 561 + source = "registry+https://github.com/rust-lang/crates.io-index" 562 + checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 563 + dependencies = [ 564 + "itoa", 565 + "memchr", 566 + "ryu", 567 + "serde", 568 + "serde_core", 569 + ] 570 + 571 + [[package]] 572 + name = "shlex" 573 + version = "1.3.0" 574 + source = "registry+https://github.com/rust-lang/crates.io-index" 575 + checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 576 + 577 + [[package]] 578 name = "static_assertions" 579 version = "1.1.0" 580 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 642 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 643 644 [[package]] 645 + name = "wasm-bindgen" 646 + version = "0.2.104" 647 + source = "registry+https://github.com/rust-lang/crates.io-index" 648 + checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" 649 + dependencies = [ 650 + "cfg-if", 651 + "once_cell", 652 + "rustversion", 653 + "wasm-bindgen-macro", 654 + "wasm-bindgen-shared", 655 + ] 656 + 657 + [[package]] 658 + name = "wasm-bindgen-backend" 659 + version = "0.2.104" 660 + source = "registry+https://github.com/rust-lang/crates.io-index" 661 + checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" 662 + dependencies = [ 663 + "bumpalo", 664 + "log", 665 + "proc-macro2", 666 + "quote", 667 + "syn", 668 + "wasm-bindgen-shared", 669 + ] 670 + 671 + [[package]] 672 + name = "wasm-bindgen-macro" 673 + version = "0.2.104" 674 + source = "registry+https://github.com/rust-lang/crates.io-index" 675 + checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" 676 + dependencies = [ 677 + "quote", 678 + "wasm-bindgen-macro-support", 679 + ] 680 + 681 + [[package]] 682 + name = "wasm-bindgen-macro-support" 683 + version = "0.2.104" 684 + source = "registry+https://github.com/rust-lang/crates.io-index" 685 + checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" 686 + dependencies = [ 687 + "proc-macro2", 688 + "quote", 689 + "syn", 690 + "wasm-bindgen-backend", 691 + "wasm-bindgen-shared", 692 + ] 693 + 694 + [[package]] 695 + name = "wasm-bindgen-shared" 696 + version = "0.2.104" 697 + source = "registry+https://github.com/rust-lang/crates.io-index" 698 + checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" 699 + dependencies = [ 700 + "unicode-ident", 701 + ] 702 + 703 + [[package]] 704 + name = "windows-core" 705 + version = "0.62.1" 706 + source = "registry+https://github.com/rust-lang/crates.io-index" 707 + checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" 708 + dependencies = [ 709 + "windows-implement", 710 + "windows-interface", 711 + "windows-link", 712 + "windows-result", 713 + "windows-strings", 714 + ] 715 + 716 + [[package]] 717 + name = "windows-implement" 718 + version = "0.60.1" 719 + source = "registry+https://github.com/rust-lang/crates.io-index" 720 + checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" 721 + dependencies = [ 722 + "proc-macro2", 723 + "quote", 724 + "syn", 725 + ] 726 + 727 + [[package]] 728 + name = "windows-interface" 729 + version = "0.59.2" 730 + source = "registry+https://github.com/rust-lang/crates.io-index" 731 + checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" 732 + dependencies = [ 733 + "proc-macro2", 734 + "quote", 735 + "syn", 736 + ] 737 + 738 + [[package]] 739 name = "windows-link" 740 version = "0.2.0" 741 source = "registry+https://github.com/rust-lang/crates.io-index" 742 checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" 743 + 744 + [[package]] 745 + name = "windows-result" 746 + version = "0.4.0" 747 + source = "registry+https://github.com/rust-lang/crates.io-index" 748 + checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" 749 + dependencies = [ 750 + "windows-link", 751 + ] 752 + 753 + [[package]] 754 + name = "windows-strings" 755 + version = "0.5.0" 756 + source = "registry+https://github.com/rust-lang/crates.io-index" 757 + checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" 758 + dependencies = [ 759 + "windows-link", 760 + ] 761 762 [[package]] 763 name = "windows-sys"
+3
crates/jacquard-common/Cargo.toml
··· 6 description.workspace = true 7 8 [dependencies] 9 cid = { version = "0.11.1", features = ["serde", "std"] } 10 compact_str = "0.9.0" 11 miette = "7.6.0" ··· 13 multihash = "0.19.3" 14 regex = "1.11.3" 15 serde = { version = "1.0.227", features = ["derive"] } 16 thiserror = "2.0.16"
··· 6 description.workspace = true 7 8 [dependencies] 9 + chrono = "0.4.42" 10 cid = { version = "0.11.1", features = ["serde", "std"] } 11 compact_str = "0.9.0" 12 miette = "7.6.0" ··· 14 multihash = "0.19.3" 15 regex = "1.11.3" 16 serde = { version = "1.0.227", features = ["derive"] } 17 + serde_html_form = "0.2.8" 18 + serde_json = "1.0.145" 19 thiserror = "2.0.16"
crates/jacquard-common/src/aturi.rs

This is a binary file and will not be displayed.

+1 -1
crates/jacquard-common/src/blob.rs crates/jacquard-common/src/types/blob.rs
··· 1 - use crate::{CowStr, cid::Cid}; 2 use compact_str::ToCompactString; 3 #[allow(unused)] 4 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
··· 1 + use crate::{CowStr, types::cid::Cid}; 2 use compact_str::ToCompactString; 3 #[allow(unused)] 4 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
crates/jacquard-common/src/cid.rs crates/jacquard-common/src/types/cid.rs
+1 -52
crates/jacquard-common/src/cowstr.rs
··· 1 use compact_str::CompactString; 2 - use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error}; 3 use std::{ 4 borrow::Cow, 5 fmt, 6 hash::{Hash, Hasher}, 7 ops::Deref, 8 - str::FromStr, 9 }; 10 11 use crate::IntoStatic; ··· 207 CowStr::Owned(s) => CowStr::Owned(s), 208 } 209 } 210 - } 211 - 212 - /// Common trait implementations for Lexicon string formats that are newtype wrappers 213 - /// around `String`. 214 - macro_rules! string_newtype { 215 - ($name:ident) => { 216 - impl FromStr for $name<'_> { 217 - type Err = &'static str; 218 - 219 - fn from_str(s: &str) -> Result<Self, Self::Err> { 220 - Self::new(s) 221 - } 222 - } 223 - 224 - impl<'de> Deserialize<'de> for $name<'de> { 225 - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 226 - where 227 - D: Deserializer<'de>, 228 - { 229 - let value = Deserialize::deserialize(deserializer)?; 230 - Self::new(value).map_err(D::Error::custom) 231 - } 232 - } 233 - 234 - impl From<$name<'_>> for String { 235 - fn from(value: $name) -> Self { 236 - value.0.to_string() 237 - } 238 - } 239 - 240 - impl From<$name> for CowStr<'s> { 241 - fn from(value: $name) -> Self { 242 - value.0 243 - } 244 - } 245 - 246 - impl AsRef<str> for $name<'_> { 247 - fn as_ref(&self) -> &str { 248 - self.as_str() 249 - } 250 - } 251 - 252 - impl Deref for $name<'_> { 253 - type Target = str; 254 - 255 - fn deref(&self) -> &Self::Target { 256 - self.as_str() 257 - } 258 - } 259 - }; 260 } 261 262 impl Serialize for CowStr<'_> {
··· 1 use compact_str::CompactString; 2 + use serde::{Deserialize, Serialize}; 3 use std::{ 4 borrow::Cow, 5 fmt, 6 hash::{Hash, Hasher}, 7 ops::Deref, 8 }; 9 10 use crate::IntoStatic; ··· 206 CowStr::Owned(s) => CowStr::Owned(s), 207 } 208 } 209 } 210 211 impl Serialize for CowStr<'_> {
+2 -2
crates/jacquard-common/src/did.rs crates/jacquard-common/src/types/did.rs
··· 27 } 28 } 29 30 - /// Fallible constructor from an existing CowStr, clones and takes 31 pub fn from_cowstr(did: CowStr<'d>) -> Result<Did<'d>, &'static str> { 32 if did.len() > 2048 { 33 Err("DID too long") ··· 72 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 73 /// Prefer `Did::new()` or `Did::raw` if you want to borrow. 74 fn from_str(s: &str) -> Result<Self, Self::Err> { 75 - Self::from_cowstr(CowStr::Owned(s.to_compact_string())) 76 } 77 } 78
··· 27 } 28 } 29 30 + /// Fallible constructor from an existing CowStr, takes ownership 31 pub fn from_cowstr(did: CowStr<'d>) -> Result<Did<'d>, &'static str> { 32 if did.len() > 2048 { 33 Err("DID too long") ··· 72 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 73 /// Prefer `Did::new()` or `Did::raw` if you want to borrow. 74 fn from_str(s: &str) -> Result<Self, Self::Err> { 75 + Self::from_cowstr(CowStr::Borrowed(s).into_static()) 76 } 77 } 78
crates/jacquard-common/src/handle.rs

This is a binary file and will not be displayed.

+2 -9
crates/jacquard-common/src/lib.rs
··· 1 - pub mod aturi; 2 #[macro_use] 3 pub mod cowstr; 4 #[macro_use] 5 - pub mod blob; 6 - pub mod cid; 7 8 - pub mod did; 9 - pub mod handle; 10 - #[macro_use] 11 - pub mod into_static; 12 - pub mod link; 13 - pub mod nsid; 14 15 pub use cowstr::CowStr; 16 pub use into_static::IntoStatic;
··· 1 #[macro_use] 2 pub mod cowstr; 3 #[macro_use] 4 + pub mod into_static; 5 6 + pub mod types; 7 8 pub use cowstr::CowStr; 9 pub use into_static::IntoStatic;
crates/jacquard-common/src/link.rs crates/jacquard-common/src/types/link.rs
crates/jacquard-common/src/nsid.rs crates/jacquard-common/src/types/nsid.rs
+11
crates/jacquard-common/src/types.rs
···
··· 1 + pub mod aturi; 2 + pub mod blob; 3 + pub mod cid; 4 + pub mod datetime; 5 + pub mod did; 6 + pub mod handle; 7 + pub mod ident; 8 + pub mod integer; 9 + pub mod link; 10 + pub mod nsid; 11 + pub mod tid;
+148
crates/jacquard-common/src/types/aturi.rs
···
··· 1 + use std::fmt; 2 + use std::sync::LazyLock; 3 + use std::{ops::Deref, str::FromStr}; 4 + 5 + use compact_str::ToCompactString; 6 + use serde::{Deserialize, Deserializer, Serialize, de::Error}; 7 + 8 + use crate::{CowStr, IntoStatic}; 9 + use regex::Regex; 10 + 11 + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Hash)] 12 + #[serde(transparent)] 13 + pub struct AtUri<'a>(CowStr<'a>); 14 + 15 + pub static AT_URI_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^$").unwrap()); 16 + 17 + impl<'a> AtUri<'a> { 18 + /// Fallible constructor, validates, borrows from input 19 + pub fn new(uri: &'a str) -> Result<Self, &'static str> { 20 + if uri.len() > 2048 { 21 + Err("AT_URI too long") 22 + } else if !AT_URI_REGEX.is_match(uri) { 23 + Err("Invalid AT_URI") 24 + } else { 25 + Ok(Self(CowStr::Borrowed(uri))) 26 + } 27 + } 28 + 29 + /// Fallible constructor from an existing CowStr, clones and takes 30 + pub fn from_cowstr(uri: CowStr<'a>) -> Result<AtUri<'a>, &'static str> { 31 + if uri.len() > 2048 { 32 + Err("AT_URI too long") 33 + } else if !AT_URI_REGEX.is_match(&uri) { 34 + Err("Invalid AT_URI") 35 + } else { 36 + Ok(Self(uri.into_static())) 37 + } 38 + } 39 + 40 + /// Infallible constructor for when you *know* the string slice is a valid at:// uri. 41 + /// Will panic on invalid URIs. If you're manually decoding atproto records 42 + /// or API values you know are valid (rather than using serde), this is the one to use. 43 + /// The From<String> and From<CowStr> impls use the same logic. 44 + pub fn raw(uri: &'a str) -> Self { 45 + if uri.len() > 2048 { 46 + panic!("AT_URI too long") 47 + } else if !AT_URI_REGEX.is_match(uri) { 48 + panic!("Invalid AT_URI") 49 + } else { 50 + Self(CowStr::Borrowed(uri)) 51 + } 52 + } 53 + 54 + /// Infallible constructor for when you *know* the string is a valid AT_URI. 55 + /// Marked unsafe because responsibility for upholding the invariant is on the developer. 56 + pub unsafe fn unchecked(uri: &'a str) -> Self { 57 + Self(CowStr::Borrowed(uri)) 58 + } 59 + 60 + pub fn as_str(&self) -> &str { 61 + { 62 + let this = &self.0; 63 + this 64 + } 65 + } 66 + } 67 + 68 + impl FromStr for AtUri<'_> { 69 + type Err = &'static str; 70 + 71 + /// Has to take ownership due to the lifetime constraints of the FromStr trait. 72 + /// Prefer `AtUri::new()` or `AtUri::raw` if you want to borrow. 73 + fn from_str(s: &str) -> Result<Self, Self::Err> { 74 + Self::from_cowstr(CowStr::Owned(s.to_compact_string())) 75 + } 76 + } 77 + 78 + impl<'ae> Deserialize<'ae> for AtUri<'ae> { 79 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 80 + where 81 + D: Deserializer<'ae>, 82 + { 83 + let value = Deserialize::deserialize(deserializer)?; 84 + Self::new(value).map_err(D::Error::custom) 85 + } 86 + } 87 + 88 + impl fmt::Display for AtUri<'_> { 89 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 90 + f.write_str(&self.0) 91 + } 92 + } 93 + 94 + impl<'a> From<AtUri<'a>> for String { 95 + fn from(value: AtUri<'a>) -> Self { 96 + value.0.to_string() 97 + } 98 + } 99 + 100 + impl<'s> From<&'s AtUri<'_>> for &'s str { 101 + fn from(value: &'s AtUri<'_>) -> Self { 102 + value.0.as_ref() 103 + } 104 + } 105 + 106 + impl<'a> From<AtUri<'a>> for CowStr<'a> { 107 + fn from(value: AtUri<'a>) -> Self { 108 + value.0 109 + } 110 + } 111 + 112 + impl From<String> for AtUri<'static> { 113 + fn from(value: String) -> Self { 114 + if value.len() > 2048 { 115 + panic!("AT_URI too long") 116 + } else if !AT_URI_REGEX.is_match(&value) { 117 + panic!("Invalid AT_URI") 118 + } else { 119 + Self(CowStr::Owned(value.to_compact_string())) 120 + } 121 + } 122 + } 123 + 124 + impl<'a> From<CowStr<'a>> for AtUri<'a> { 125 + fn from(value: CowStr<'a>) -> Self { 126 + if value.len() > 2048 { 127 + panic!("AT_URI too long") 128 + } else if !AT_URI_REGEX.is_match(&value) { 129 + panic!("Invalid AT_URI") 130 + } else { 131 + Self(value) 132 + } 133 + } 134 + } 135 + 136 + impl AsRef<str> for AtUri<'_> { 137 + fn as_ref(&self) -> &str { 138 + self.as_str() 139 + } 140 + } 141 + 142 + impl Deref for AtUri<'_> { 143 + type Target = str; 144 + 145 + fn deref(&self) -> &Self::Target { 146 + self.as_str() 147 + } 148 + }
+166
crates/jacquard-common/src/types/datetime.rs
···
··· 1 + use std::sync::LazyLock; 2 + use std::{cmp, str::FromStr}; 3 + 4 + use chrono::DurationRound; 5 + use compact_str::ToCompactString; 6 + use serde::Serializer; 7 + use serde::{Deserialize, Deserializer, Serialize, de::Error}; 8 + 9 + use crate::{CowStr, IntoStatic}; 10 + use regex::Regex; 11 + 12 + pub static ISO8601_REGEX: LazyLock<Regex> = LazyLock::new(|| { 13 + Regex::new(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|(\+[0-9]{2}|\-[0-9][1-9]):[0-9]{2})$").unwrap() 14 + }); 15 + 16 + /// A Lexicon timestamp. 17 + #[derive(Clone, Debug, Eq)] 18 + pub struct Datetime { 19 + /// Serialized form. Preserved during parsing to ensure round-trip re-serialization. 20 + serialized: CowStr<'static>, 21 + /// Parsed form. 22 + dt: chrono::DateTime<chrono::FixedOffset>, 23 + } 24 + 25 + impl PartialEq for Datetime { 26 + fn eq(&self, other: &Self) -> bool { 27 + self.dt == other.dt 28 + } 29 + } 30 + 31 + impl Ord for Datetime { 32 + fn cmp(&self, other: &Self) -> cmp::Ordering { 33 + self.dt.cmp(&other.dt) 34 + } 35 + } 36 + 37 + impl PartialOrd for Datetime { 38 + fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { 39 + Some(self.cmp(other)) 40 + } 41 + } 42 + 43 + impl Datetime { 44 + /// Returns a `Datetime` which corresponds to the current date and time in UTC. 45 + /// 46 + /// The timestamp uses microsecond precision. 47 + pub fn now() -> Self { 48 + Self::new(chrono::Utc::now().fixed_offset()) 49 + } 50 + 51 + /// Constructs a new Lexicon timestamp. 52 + /// 53 + /// The timestamp is rounded to microsecond precision. 54 + pub fn new(dt: chrono::DateTime<chrono::FixedOffset>) -> Self { 55 + let dt = dt 56 + .duration_round(chrono::Duration::microseconds(1)) 57 + .expect("delta does not exceed limits"); 58 + // This serialization format is compatible with ISO 8601. 59 + let serialized = CowStr::Owned( 60 + dt.to_rfc3339_opts(chrono::SecondsFormat::Micros, true) 61 + .to_compact_string(), 62 + ); 63 + Self { serialized, dt } 64 + } 65 + 66 + /// Infallibly parses a new Lexicon timestamp from a compatible str reference 67 + /// 68 + /// Panics if invalid. Use the fallible trait implementations or deserialize for input 69 + /// you cannot reasonably trust to be properly formatted. 70 + pub fn raw_str(s: impl AsRef<str>) -> Self { 71 + let s = s.as_ref(); 72 + if ISO8601_REGEX.is_match(s) { 73 + let dt = chrono::DateTime::parse_from_rfc3339(s).expect("valid ISO8601 time string"); 74 + Self { 75 + serialized: CowStr::Borrowed(s).into_static(), 76 + dt, 77 + } 78 + } else { 79 + panic!("atproto datetime should be valid ISO8601") 80 + } 81 + } 82 + 83 + /// Extracts a string slice containing the entire `Datetime`. 84 + #[inline] 85 + #[must_use] 86 + pub fn as_str(&self) -> &str { 87 + self.serialized.as_ref() 88 + } 89 + } 90 + 91 + impl FromStr for Datetime { 92 + type Err = chrono::ParseError; 93 + 94 + fn from_str(s: &str) -> Result<Self, Self::Err> { 95 + // The `chrono` crate only supports RFC 3339 parsing, but Lexicon restricts 96 + // datetimes to the subset that is also valid under ISO 8601. Apply a regex that 97 + // validates enough of the relevant ISO 8601 format that the RFC 3339 parser can 98 + // do the rest. 99 + if ISO8601_REGEX.is_match(s) { 100 + let dt = chrono::DateTime::parse_from_rfc3339(s)?; 101 + Ok(Self { 102 + serialized: CowStr::Borrowed(s).into_static(), 103 + dt, 104 + }) 105 + } else { 106 + // Simulate an invalid `ParseError`. 107 + Err(chrono::DateTime::parse_from_rfc3339("invalid").expect_err("invalid")) 108 + } 109 + } 110 + } 111 + 112 + impl<'de> Deserialize<'de> for Datetime { 113 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 114 + where 115 + D: Deserializer<'de>, 116 + { 117 + let value: String = Deserialize::deserialize(deserializer)?; 118 + Self::from_str(&value).map_err(D::Error::custom) 119 + } 120 + } 121 + impl Serialize for Datetime { 122 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 123 + where 124 + S: Serializer, 125 + { 126 + serializer.serialize_str(&self.serialized) 127 + } 128 + } 129 + 130 + impl AsRef<chrono::DateTime<chrono::FixedOffset>> for Datetime { 131 + fn as_ref(&self) -> &chrono::DateTime<chrono::FixedOffset> { 132 + &self.dt 133 + } 134 + } 135 + 136 + impl TryFrom<String> for Datetime { 137 + type Error = chrono::ParseError; 138 + fn try_from(value: String) -> Result<Self, Self::Error> { 139 + if ISO8601_REGEX.is_match(&value) { 140 + let dt = chrono::DateTime::parse_from_rfc3339(&value)?; 141 + Ok(Self { 142 + serialized: CowStr::Owned(value.to_compact_string()), 143 + dt, 144 + }) 145 + } else { 146 + // Simulate an invalid `ParseError`. 147 + Err(chrono::DateTime::parse_from_rfc3339("invalid").expect_err("invalid")) 148 + } 149 + } 150 + } 151 + 152 + impl TryFrom<CowStr<'_>> for Datetime { 153 + type Error = chrono::ParseError; 154 + fn try_from(value: CowStr<'_>) -> Result<Self, Self::Error> { 155 + if ISO8601_REGEX.is_match(&value) { 156 + let dt = chrono::DateTime::parse_from_rfc3339(&value)?; 157 + Ok(Self { 158 + serialized: value.into_static(), 159 + dt, 160 + }) 161 + } else { 162 + // Simulate an invalid `ParseError`. 163 + Err(chrono::DateTime::parse_from_rfc3339("invalid").expect_err("invalid")) 164 + } 165 + } 166 + }
+160
crates/jacquard-common/src/types/handle.rs
···
··· 1 + use std::fmt; 2 + use std::sync::LazyLock; 3 + use std::{ops::Deref, str::FromStr}; 4 + 5 + use compact_str::ToCompactString; 6 + use serde::{Deserialize, Deserializer, Serialize, de::Error}; 7 + 8 + use crate::{CowStr, IntoStatic}; 9 + use regex::Regex; 10 + 11 + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Hash)] 12 + #[serde(transparent)] 13 + pub struct Handle<'h>(CowStr<'h>); 14 + 15 + pub static HANDLE_REGEX: LazyLock<Regex> = LazyLock::new(|| { 16 + Regex::new(r"^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$").unwrap() 17 + }); 18 + 19 + impl<'h> Handle<'h> { 20 + /// Fallible constructor, validates, borrows from input 21 + /// 22 + /// Accepts (and strips) preceding '@' if present 23 + pub fn new(handle: &'h str) -> Result<Self, &'static str> { 24 + let handle = handle.strip_prefix('@').unwrap_or(handle); 25 + if handle.len() > 2048 { 26 + Err("handle too long") 27 + } else if !HANDLE_REGEX.is_match(handle) { 28 + Err("Invalid handle") 29 + } else { 30 + Ok(Self(CowStr::Borrowed(handle))) 31 + } 32 + } 33 + 34 + /// Fallible constructor from an existing CowStr, takes ownership 35 + /// 36 + /// Accepts (and strips) preceding '@' if present 37 + pub fn from_cowstr(handle: CowStr<'h>) -> Result<Handle<'h>, &'static str> { 38 + let handle = if let Some(handle) = handle.strip_prefix('@') { 39 + CowStr::Borrowed(handle) 40 + } else { 41 + handle 42 + }; 43 + if handle.len() > 2048 { 44 + Err("handle too long") 45 + } else if !HANDLE_REGEX.is_match(&handle) { 46 + Err("Invalid handle") 47 + } else { 48 + Ok(Self(handle.into_static())) 49 + } 50 + } 51 + 52 + /// Infallible constructor for when you *know* the string is a valid handle. 53 + /// Will panic on invalid handles. If you're manually decoding atproto records 54 + /// or API values you know are valid (rather than using serde), this is the one to use. 55 + /// The From<String> and From<CowStr> impls use the same logic. 56 + /// 57 + /// Accepts (and strips) preceding '@' if present 58 + pub fn raw(handle: &'h str) -> Self { 59 + let handle = handle.strip_prefix('@').unwrap_or(handle); 60 + if handle.len() > 2048 { 61 + panic!("handle too long") 62 + } else if !HANDLE_REGEX.is_match(handle) { 63 + panic!("Invalid handle") 64 + } else { 65 + Self(CowStr::Borrowed(handle)) 66 + } 67 + } 68 + 69 + /// Infallible constructor for when you *know* the string is a valid handle. 70 + /// Marked unsafe because responsibility for upholding the invariant is on the developer. 71 + /// 72 + /// Accepts (and strips) preceding '@' if present 73 + pub unsafe fn unchecked(handle: &'h str) -> Self { 74 + let handle = handle.strip_prefix('@').unwrap_or(handle); 75 + Self(CowStr::Borrowed(handle)) 76 + } 77 + 78 + pub fn as_str(&self) -> &str { 79 + { 80 + let this = &self.0; 81 + this 82 + } 83 + } 84 + } 85 + 86 + impl FromStr for Handle<'_> { 87 + type Err = &'static str; 88 + 89 + /// Has to take ownership due to the lifetime constraints of the FromStr trait. 90 + /// Prefer `Handle::new()` or `Handle::raw` if you want to borrow. 91 + fn from_str(s: &str) -> Result<Self, Self::Err> { 92 + Self::from_cowstr(CowStr::Borrowed(s).into_static()) 93 + } 94 + } 95 + 96 + impl<'de> Deserialize<'de> for Handle<'de> { 97 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 98 + where 99 + D: Deserializer<'de>, 100 + { 101 + let value = Deserialize::deserialize(deserializer)?; 102 + Self::new(value).map_err(D::Error::custom) 103 + } 104 + } 105 + 106 + impl fmt::Display for Handle<'_> { 107 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 108 + write!(f, "@{}", self.0) 109 + } 110 + } 111 + 112 + impl<'h> From<Handle<'h>> for String { 113 + fn from(value: Handle<'h>) -> Self { 114 + value.0.to_string() 115 + } 116 + } 117 + 118 + impl<'h> From<Handle<'h>> for CowStr<'h> { 119 + fn from(value: Handle<'h>) -> Self { 120 + value.0 121 + } 122 + } 123 + 124 + impl From<String> for Handle<'static> { 125 + fn from(value: String) -> Self { 126 + if value.len() > 2048 { 127 + panic!("handle too long") 128 + } else if !HANDLE_REGEX.is_match(&value) { 129 + panic!("Invalid handle") 130 + } else { 131 + Self(CowStr::Owned(value.to_compact_string())) 132 + } 133 + } 134 + } 135 + 136 + impl<'h> From<CowStr<'h>> for Handle<'h> { 137 + fn from(value: CowStr<'h>) -> Self { 138 + if value.len() > 2048 { 139 + panic!("handle too long") 140 + } else if !HANDLE_REGEX.is_match(&value) { 141 + panic!("Invalid handle") 142 + } else { 143 + Self(value) 144 + } 145 + } 146 + } 147 + 148 + impl AsRef<str> for Handle<'_> { 149 + fn as_ref(&self) -> &str { 150 + self.as_str() 151 + } 152 + } 153 + 154 + impl Deref for Handle<'_> { 155 + type Target = str; 156 + 157 + fn deref(&self) -> &Self::Target { 158 + self.as_str() 159 + } 160 + }
+148
crates/jacquard-common/src/types/ident.rs
···
··· 1 + use crate::types::did::Did; 2 + use crate::types::handle::Handle; 3 + use std::fmt; 4 + use std::str::FromStr; 5 + 6 + use serde::{Deserialize, Serialize}; 7 + 8 + use crate::CowStr; 9 + 10 + /// An AT Protocol identifier. 11 + #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] 12 + #[serde(untagged)] 13 + pub enum AtIdentifier<'i> { 14 + #[serde(borrow)] 15 + Did(Did<'i>), 16 + Handle(Handle<'i>), 17 + } 18 + 19 + impl<'i> AtIdentifier<'i> { 20 + /// Fallible constructor, validates, borrows from input 21 + pub fn new(ident: &'i str) -> Result<Self, &'static str> { 22 + if let Ok(did) = ident.parse() { 23 + Ok(AtIdentifier::Did(did)) 24 + } else { 25 + ident.parse().map(AtIdentifier::Handle) 26 + } 27 + } 28 + 29 + /// Fallible constructor from an existing CowStr, borrows 30 + pub fn from_cowstr(ident: CowStr<'i>) -> Result<AtIdentifier<'i>, &'static str> { 31 + if let Ok(did) = ident.parse() { 32 + Ok(AtIdentifier::Did(did)) 33 + } else { 34 + ident.parse().map(AtIdentifier::Handle) 35 + } 36 + } 37 + 38 + /// Infallible constructor for when you *know* the string is a valid identifier. 39 + /// Will panic on invalid identifiers. If you're manually decoding atproto records 40 + /// or API values you know are valid (rather than using serde), this is the one to use. 41 + /// The From<String> and From<CowStr> impls use the same logic. 42 + pub fn raw(ident: &'i str) -> Self { 43 + if let Ok(did) = ident.parse() { 44 + AtIdentifier::Did(did) 45 + } else { 46 + ident 47 + .parse() 48 + .map(AtIdentifier::Handle) 49 + .expect("valid handle") 50 + } 51 + } 52 + 53 + /// Infallible constructor for when you *know* the string is a valid identifier. 54 + /// Marked unsafe because responsibility for upholding the invariant is on the developer. 55 + /// 56 + /// Will validate DIDs, but will treat anything else as a valid handle 57 + pub unsafe fn unchecked(ident: &'i str) -> Self { 58 + if let Ok(did) = ident.parse() { 59 + AtIdentifier::Did(did) 60 + } else { 61 + unsafe { AtIdentifier::Handle(Handle::unchecked(ident)) } 62 + } 63 + } 64 + 65 + pub fn as_str(&self) -> &str { 66 + match self { 67 + AtIdentifier::Did(did) => did.as_str(), 68 + AtIdentifier::Handle(handle) => handle.as_str(), 69 + } 70 + } 71 + } 72 + 73 + impl<'i> From<Did<'i>> for AtIdentifier<'i> { 74 + fn from(did: Did<'i>) -> Self { 75 + AtIdentifier::Did(did) 76 + } 77 + } 78 + 79 + impl<'i> From<Handle<'i>> for AtIdentifier<'i> { 80 + fn from(handle: Handle<'i>) -> Self { 81 + AtIdentifier::Handle(handle) 82 + } 83 + } 84 + 85 + impl FromStr for AtIdentifier<'_> { 86 + type Err = &'static str; 87 + 88 + fn from_str(s: &str) -> Result<Self, Self::Err> { 89 + if let Ok(did) = s.parse() { 90 + Ok(AtIdentifier::Did(did)) 91 + } else { 92 + s.parse().map(AtIdentifier::Handle) 93 + } 94 + } 95 + } 96 + 97 + impl fmt::Display for AtIdentifier<'_> { 98 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 99 + match self { 100 + AtIdentifier::Did(did) => did.fmt(f), 101 + AtIdentifier::Handle(handle) => handle.fmt(f), 102 + } 103 + } 104 + } 105 + 106 + impl From<String> for AtIdentifier<'static> { 107 + fn from(value: String) -> Self { 108 + if let Ok(did) = value.parse() { 109 + AtIdentifier::Did(did) 110 + } else { 111 + value 112 + .parse() 113 + .map(AtIdentifier::Handle) 114 + .expect("valid handle") 115 + } 116 + } 117 + } 118 + 119 + impl<'i> From<CowStr<'i>> for AtIdentifier<'i> { 120 + fn from(value: CowStr<'i>) -> Self { 121 + if let Ok(did) = value.parse() { 122 + AtIdentifier::Did(did) 123 + } else { 124 + value 125 + .parse() 126 + .map(AtIdentifier::Handle) 127 + .expect("valid handle") 128 + } 129 + } 130 + } 131 + 132 + impl<'i> From<AtIdentifier<'i>> for String { 133 + fn from(value: AtIdentifier) -> Self { 134 + match value { 135 + AtIdentifier::Did(did) => did.into(), 136 + AtIdentifier::Handle(handle) => handle.into(), 137 + } 138 + } 139 + } 140 + 141 + impl AsRef<str> for AtIdentifier<'_> { 142 + fn as_ref(&self) -> &str { 143 + match self { 144 + AtIdentifier::Did(did) => did.as_ref(), 145 + AtIdentifier::Handle(handle) => handle.as_ref(), 146 + } 147 + } 148 + }
+326
crates/jacquard-common/src/types/integer.rs
···
··· 1 + //! Lexicon integer types with minimum or maximum acceptable values. 2 + //! Copied from [atrium](https://github.com/atrium-rs/atrium/blob/main/atrium-api/src/types/integer.rs), because this they got right 3 + 4 + use std::num::{NonZeroU8, NonZeroU16, NonZeroU32, NonZeroU64}; 5 + use std::str::FromStr; 6 + 7 + use serde::{Deserialize, de::Error}; 8 + 9 + macro_rules! uint { 10 + ($primitive:ident, $nz:ident, $lim:ident, $lim_nz:ident, $bounded:ident) => { 11 + /// An unsigned integer with a maximum value of `MAX`. 12 + #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, Hash)] 13 + #[repr(transparent)] 14 + #[serde(transparent)] 15 + pub struct $lim<const MAX: $primitive>($primitive); 16 + 17 + impl<const MAX: $primitive> $lim<MAX> { 18 + /// The smallest value that can be represented by this limited integer type. 19 + pub const MIN: Self = Self(<$primitive>::MIN); 20 + 21 + /// The largest value that can be represented by this limited integer type. 22 + pub const MAX: Self = Self(MAX); 23 + 24 + fn new(value: $primitive) -> Result<Self, String> { 25 + if value > MAX { 26 + Err(format!("value is greater than {}", MAX)) 27 + } else { 28 + Ok(Self(value)) 29 + } 30 + } 31 + } 32 + 33 + impl<const MAX: $primitive> FromStr for $lim<MAX> { 34 + type Err = String; 35 + 36 + fn from_str(src: &str) -> Result<Self, Self::Err> { 37 + Self::new(src.parse::<$primitive>().map_err(|e| e.to_string())?) 38 + } 39 + } 40 + 41 + impl<const MAX: $primitive> TryFrom<$primitive> for $lim<MAX> { 42 + type Error = String; 43 + 44 + fn try_from(value: $primitive) -> Result<Self, Self::Error> { 45 + Self::new(value) 46 + } 47 + } 48 + 49 + impl<'de, const MAX: $primitive> Deserialize<'de> for $lim<MAX> { 50 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 51 + where 52 + D: serde::Deserializer<'de>, 53 + { 54 + Self::new(Deserialize::deserialize(deserializer)?).map_err(D::Error::custom) 55 + } 56 + } 57 + 58 + impl<const MAX: $primitive> From<$lim<MAX>> for $primitive { 59 + fn from(value: $lim<MAX>) -> Self { 60 + value.0 61 + } 62 + } 63 + 64 + /// An unsigned integer with a minimum value of 1 and a maximum value of `MAX`. 65 + #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, Hash)] 66 + #[repr(transparent)] 67 + #[serde(transparent)] 68 + pub struct $lim_nz<const MAX: $primitive>($nz); 69 + 70 + impl<const MAX: $primitive> $lim_nz<MAX> { 71 + /// The smallest value that can be represented by this limited non-zero 72 + /// integer type. 73 + pub const MIN: Self = Self($nz::MIN); 74 + 75 + /// The largest value that can be represented by this limited non-zero integer 76 + /// type. 77 + pub const MAX: Self = Self(unsafe { $nz::new_unchecked(MAX) }); 78 + 79 + fn new(value: $primitive) -> Result<Self, String> { 80 + if value > MAX { 81 + Err(format!("value is greater than {}", MAX)) 82 + } else if let Some(value) = $nz::new(value) { 83 + Ok(Self(value)) 84 + } else { 85 + Err("value is zero".into()) 86 + } 87 + } 88 + } 89 + 90 + impl<const MAX: $primitive> FromStr for $lim_nz<MAX> { 91 + type Err = String; 92 + 93 + fn from_str(src: &str) -> Result<Self, Self::Err> { 94 + Self::new(src.parse::<$primitive>().map_err(|e| e.to_string())?) 95 + } 96 + } 97 + 98 + impl<const MAX: $primitive> TryFrom<$primitive> for $lim_nz<MAX> { 99 + type Error = String; 100 + 101 + fn try_from(value: $primitive) -> Result<Self, Self::Error> { 102 + Self::new(value) 103 + } 104 + } 105 + 106 + impl<'de, const MAX: $primitive> Deserialize<'de> for $lim_nz<MAX> { 107 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 108 + where 109 + D: serde::Deserializer<'de>, 110 + { 111 + Self::new(Deserialize::deserialize(deserializer)?).map_err(D::Error::custom) 112 + } 113 + } 114 + 115 + impl<const MAX: $primitive> From<$lim_nz<MAX>> for $nz { 116 + fn from(value: $lim_nz<MAX>) -> Self { 117 + value.0 118 + } 119 + } 120 + 121 + impl<const MAX: $primitive> From<$lim_nz<MAX>> for $primitive { 122 + fn from(value: $lim_nz<MAX>) -> Self { 123 + value.0.into() 124 + } 125 + } 126 + 127 + /// An unsigned integer with a minimum value of `MIN` and a maximum value of `MAX`. 128 + /// 129 + /// `MIN` must be non-zero. 130 + #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, Hash)] 131 + #[repr(transparent)] 132 + #[serde(transparent)] 133 + pub struct $bounded<const MIN: $primitive, const MAX: $primitive>($nz); 134 + 135 + impl<const MIN: $primitive, const MAX: $primitive> $bounded<MIN, MAX> { 136 + /// The smallest value that can be represented by this bounded integer type. 137 + pub const MIN: Self = Self(unsafe { $nz::new_unchecked(MIN) }); 138 + 139 + /// The largest value that can be represented by this bounded integer type. 140 + pub const MAX: Self = Self(unsafe { $nz::new_unchecked(MAX) }); 141 + 142 + fn new(value: $primitive) -> Result<Self, String> { 143 + if value < MIN { 144 + Err(format!("value is less than {}", MIN)) 145 + } else if value > MAX { 146 + Err(format!("value is greater than {}", MAX)) 147 + } else if let Some(value) = $nz::new(value) { 148 + Ok(Self(value)) 149 + } else { 150 + Err("value is zero".into()) 151 + } 152 + } 153 + } 154 + 155 + impl<const MIN: $primitive, const MAX: $primitive> TryFrom<$primitive> 156 + for $bounded<MIN, MAX> 157 + { 158 + type Error = String; 159 + 160 + fn try_from(value: $primitive) -> Result<Self, Self::Error> { 161 + Self::new(value) 162 + } 163 + } 164 + 165 + impl<const MIN: $primitive, const MAX: $primitive> FromStr for $bounded<MIN, MAX> { 166 + type Err = String; 167 + 168 + fn from_str(src: &str) -> Result<Self, Self::Err> { 169 + Self::new(src.parse::<$primitive>().map_err(|e| e.to_string())?) 170 + } 171 + } 172 + 173 + impl<'de, const MIN: $primitive, const MAX: $primitive> Deserialize<'de> 174 + for $bounded<MIN, MAX> 175 + { 176 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 177 + where 178 + D: serde::Deserializer<'de>, 179 + { 180 + Self::new(Deserialize::deserialize(deserializer)?).map_err(D::Error::custom) 181 + } 182 + } 183 + 184 + impl<const MIN: $primitive, const MAX: $primitive> From<$bounded<MIN, MAX>> for $nz { 185 + fn from(value: $bounded<MIN, MAX>) -> Self { 186 + value.0 187 + } 188 + } 189 + 190 + impl<const MIN: $primitive, const MAX: $primitive> From<$bounded<MIN, MAX>> for $primitive { 191 + fn from(value: $bounded<MIN, MAX>) -> Self { 192 + value.0.into() 193 + } 194 + } 195 + }; 196 + } 197 + 198 + uint!(u8, NonZeroU8, LimitedU8, LimitedNonZeroU8, BoundedU8); 199 + uint!(u16, NonZeroU16, LimitedU16, LimitedNonZeroU16, BoundedU16); 200 + uint!(u32, NonZeroU32, LimitedU32, LimitedNonZeroU32, BoundedU32); 201 + uint!(u64, NonZeroU64, LimitedU64, LimitedNonZeroU64, BoundedU64); 202 + 203 + #[cfg(test)] 204 + mod tests { 205 + use super::*; 206 + 207 + #[test] 208 + fn u8_min_max() { 209 + assert_eq!(Ok(LimitedU8::<10>::MIN), 0.try_into()); 210 + assert_eq!(Ok(LimitedU8::<10>::MAX), 10.try_into()); 211 + assert_eq!(Ok(LimitedNonZeroU8::<10>::MIN), 1.try_into()); 212 + assert_eq!(Ok(LimitedNonZeroU8::<10>::MAX), 10.try_into()); 213 + assert_eq!(Ok(BoundedU8::<7, 10>::MIN), 7.try_into()); 214 + assert_eq!(Ok(BoundedU8::<7, 10>::MAX), 10.try_into()); 215 + } 216 + 217 + #[test] 218 + fn u8_from_str() { 219 + { 220 + type LU8 = LimitedU8<10>; 221 + assert_eq!(Ok(LU8::MIN), "0".parse()); 222 + assert_eq!(Ok(LU8::MAX), "10".parse()); 223 + assert_eq!(Err("value is greater than 10".into()), "11".parse::<LU8>()); 224 + } 225 + { 226 + type LU8 = LimitedNonZeroU8<10>; 227 + assert_eq!(Ok(LU8::MIN), "1".parse()); 228 + assert_eq!(Ok(LU8::MAX), "10".parse()); 229 + assert_eq!(Err("value is greater than 10".into()), "11".parse::<LU8>()); 230 + } 231 + { 232 + type BU8 = BoundedU8<7, 10>; 233 + assert_eq!(Err("value is less than 7".into()), "6".parse::<BU8>()); 234 + assert_eq!(Ok(BU8::MIN), "7".parse()); 235 + assert_eq!(Ok(BU8::MAX), "10".parse()); 236 + assert_eq!(Err("value is greater than 10".into()), "11".parse::<BU8>()); 237 + } 238 + } 239 + 240 + #[test] 241 + fn deserialize_u8_from_str() { 242 + { 243 + #[derive(Deserialize, Debug)] 244 + struct Foo { 245 + bar: LimitedU8<10>, 246 + } 247 + 248 + match serde_json::from_str::<Foo>(r#"{"bar": 0}"#) { 249 + Ok(foo) => assert_eq!(foo.bar, LimitedU8::<10>::MIN), 250 + Err(e) => panic!("failed to deserialize: {e}"), 251 + } 252 + match serde_json::from_str::<Foo>(r#"{"bar": "0"}"#) { 253 + Ok(_) => panic!("deserialization should fail"), 254 + Err(e) => assert!(e.to_string().contains("invalid type: string")), 255 + } 256 + match serde_html_form::from_str::<Foo>(r#"bar=0"#) { 257 + Ok(foo) => assert_eq!(foo.bar, LimitedU8::<10>::MIN), 258 + Err(e) => panic!("failed to deserialize: {e}"), 259 + } 260 + match serde_html_form::from_str::<Foo>(r#"bar=10"#) { 261 + Ok(foo) => assert_eq!(foo.bar, LimitedU8::<10>::MAX), 262 + Err(e) => panic!("failed to deserialize: {e}"), 263 + } 264 + match serde_html_form::from_str::<Foo>(r#"bar=11"#) { 265 + Ok(_) => panic!("deserialization should fail"), 266 + Err(e) => assert_eq!(e.to_string(), "value is greater than 10"), 267 + } 268 + } 269 + 270 + { 271 + #[derive(Deserialize, Debug)] 272 + struct Foo { 273 + bar: LimitedNonZeroU8<10>, 274 + } 275 + 276 + match serde_json::from_str::<Foo>(r#"{"bar": 0}"#) { 277 + Ok(_) => panic!("deserialization should fail"), 278 + Err(e) => assert_eq!(e.to_string(), "value is zero at line 1 column 10"), 279 + } 280 + match serde_json::from_str::<Foo>(r#"{"bar": "0"}"#) { 281 + Ok(_) => panic!("deserialization should fail"), 282 + Err(e) => assert!(e.to_string().contains("invalid type: string")), 283 + } 284 + match serde_html_form::from_str::<Foo>(r#"bar=0"#) { 285 + Ok(_) => panic!("deserialization should fail"), 286 + Err(e) => assert_eq!(e.to_string(), "value is zero"), 287 + } 288 + match serde_html_form::from_str::<Foo>(r#"bar=10"#) { 289 + Ok(foo) => assert_eq!(foo.bar, LimitedNonZeroU8::<10>::MAX), 290 + Err(e) => panic!("failed to deserialize: {e}"), 291 + } 292 + match serde_html_form::from_str::<Foo>(r#"bar=11"#) { 293 + Ok(_) => panic!("deserialization should fail"), 294 + Err(e) => assert_eq!(e.to_string(), "value is greater than 10"), 295 + } 296 + } 297 + 298 + { 299 + #[derive(Deserialize, Debug)] 300 + struct Foo { 301 + bar: BoundedU8<1, 10>, 302 + } 303 + 304 + match serde_json::from_str::<Foo>(r#"{"bar": 0}"#) { 305 + Ok(_) => panic!("deserialization should fail"), 306 + Err(e) => assert_eq!(e.to_string(), "value is less than 1 at line 1 column 10"), 307 + } 308 + match serde_json::from_str::<Foo>(r#"{"bar": "0"}"#) { 309 + Ok(_) => panic!("deserialization should fail"), 310 + Err(e) => assert!(e.to_string().contains("invalid type: string")), 311 + } 312 + match serde_html_form::from_str::<Foo>(r#"bar=0"#) { 313 + Ok(_) => panic!("deserialization should fail"), 314 + Err(e) => assert_eq!(e.to_string(), "value is less than 1"), 315 + } 316 + match serde_html_form::from_str::<Foo>(r#"bar=10"#) { 317 + Ok(foo) => assert_eq!(foo.bar, BoundedU8::<1, 10>::MAX), 318 + Err(e) => panic!("failed to deserialize: {e}"), 319 + } 320 + match serde_html_form::from_str::<Foo>(r#"bar=11"#) { 321 + Ok(_) => panic!("deserialization should fail"), 322 + Err(e) => assert_eq!(e.to_string(), "value is greater than 10"), 323 + } 324 + } 325 + } 326 + }
+202
crates/jacquard-common/src/types/tid.rs
···
··· 1 + use std::fmt; 2 + use std::sync::LazyLock; 3 + use std::{ops::Deref, str::FromStr}; 4 + 5 + use compact_str::{CompactString, ToCompactString}; 6 + use serde::{Deserialize, Deserializer, Serialize, de::Error}; 7 + 8 + use crate::types::integer::LimitedU32; 9 + use crate::{CowStr, IntoStatic}; 10 + use regex::Regex; 11 + 12 + fn s32_encode(mut i: u64) -> CowStr<'static> { 13 + const S32_CHAR: &[u8] = b"234567abcdefghijklmnopqrstuvwxyz"; 14 + 15 + let mut s = CompactString::with_capacity(13); 16 + for _ in 0..13 { 17 + let c = i & 0x1F; 18 + s.push(S32_CHAR[c as usize] as char); 19 + 20 + i >>= 5; 21 + } 22 + 23 + // Reverse the string to convert it to big-endian format. 24 + CowStr::Owned(s.chars().rev().collect()) 25 + } 26 + 27 + static TID_REGEX: LazyLock<Regex> = LazyLock::new(|| { 28 + Regex::new(r"^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$").unwrap() 29 + }); 30 + 31 + /// A [Timestamp Identifier]. 32 + /// 33 + /// [Timestamp Identifier]: https://atproto.com/specs/tid 34 + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)] 35 + #[serde(transparent)] 36 + pub struct Tid<'t>(CowStr<'t>); 37 + 38 + impl<'t> Tid<'t> { 39 + /// Parses a `TID` from the given string. 40 + pub fn new(tid: &'t str) -> Result<Self, &'static str> { 41 + if tid.len() != 13 { 42 + Err("TID must be 13 characters") 43 + } else if !TID_REGEX.is_match(&tid) { 44 + Err("Invalid TID") 45 + } else { 46 + Ok(Self(CowStr::Owned(tid.to_compact_string()))) 47 + } 48 + } 49 + 50 + /// Fallible constructor from an existing CowStr, takes ownership 51 + pub fn from_cowstr(tid: CowStr<'t>) -> Result<Tid<'t>, &'static str> { 52 + if tid.len() != 13 { 53 + Err("TID must be 13 characters") 54 + } else if !TID_REGEX.is_match(&tid) { 55 + Err("Invalid TID") 56 + } else { 57 + Ok(Self(tid.into_static())) 58 + } 59 + } 60 + 61 + /// Infallible constructor for when you *know* the string is a valid TID. 62 + /// Will panic on invalid TID. If you're manually decoding atproto records 63 + /// or API values you know are valid (rather than using serde), this is the one to use. 64 + /// The From<String> and From<CowStr> impls use the same logic. 65 + pub fn raw(tid: &'t str) -> Self { 66 + if tid.len() != 13 { 67 + panic!("TID must be 13 characters") 68 + } else if !TID_REGEX.is_match(&tid) { 69 + panic!("Invalid TID") 70 + } else { 71 + Self(CowStr::Borrowed(tid)) 72 + } 73 + } 74 + 75 + /// Infallible constructor for when you *know* the string is a valid TID. 76 + /// Marked unsafe because responsibility for upholding the invariant is on the developer. 77 + pub unsafe fn unchecked(tid: &'t str) -> Self { 78 + Self(CowStr::Borrowed(tid)) 79 + } 80 + 81 + /// Construct a new timestamp with the specified clock ID. 82 + /// 83 + /// If you have multiple clock sources, you can use `clkid` to distinguish between them 84 + /// and hint to other implementations that the timestamp cannot be compared with other 85 + /// timestamps from other sources. 86 + /// If you are only using a single clock source, you can just specify `0` for `clkid`. 87 + pub fn from_datetime(clkid: LimitedU32<1023>, time: chrono::DateTime<chrono::Utc>) -> Self { 88 + let time = time.timestamp_micros() as u64; 89 + 90 + // The TID is laid out as follows: 91 + // 0TTTTTTTTTTTTTTT TTTTTTTTTTTTTTTT TTTTTTTTTTTTTTTT TTTTTTCCCCCCCCCC 92 + let tid = (time << 10) & 0x7FFF_FFFF_FFFF_FC00 | (Into::<u32>::into(clkid) as u64 & 0x3FF); 93 + Self(s32_encode(tid)) 94 + } 95 + 96 + /// Construct a new [Tid] that represents the current time. 97 + /// 98 + /// If you have multiple clock sources, you can use `clkid` to distinguish between them 99 + /// and hint to other implementations that the timestamp cannot be compared with other 100 + /// timestamps from other sources. 101 + /// If you are only using a single clock source, you can just specify `0` for `clkid`. 102 + /// 103 + /// _Warning:_ It's possible that this function will return the same time more than once. 104 + /// If it's important that these values be unique, you will want to repeatedly call this 105 + /// function until a different time is returned. 106 + pub fn now(clkid: LimitedU32<1023>) -> Self { 107 + Self::from_datetime(clkid, chrono::Utc::now()) 108 + } 109 + 110 + /// Construct a new [Tid] that represents the current time with clkid 0. 111 + /// 112 + /// _Warning:_ It's possible that this function will return the same time more than once. 113 + /// If it's important that these values be unique, you will want to repeatedly call this 114 + /// function until a different time is returned. 115 + pub fn now_0() -> Self { 116 + Self::from_datetime(LimitedU32::from_str("0").unwrap(), chrono::Utc::now()) 117 + } 118 + 119 + /// Returns the TID as a string slice. 120 + pub fn as_str(&self) -> &str { 121 + { 122 + let this = &self.0; 123 + this 124 + } 125 + } 126 + } 127 + 128 + impl FromStr for Tid<'_> { 129 + type Err = &'static str; 130 + 131 + /// Has to take ownership due to the lifetime constraints of the FromStr trait. 132 + /// Prefer `Did::new()` or `Did::raw` if you want to borrow. 133 + fn from_str(s: &str) -> Result<Self, Self::Err> { 134 + Self::from_cowstr(CowStr::Borrowed(s).into_static()) 135 + } 136 + } 137 + 138 + impl<'de> Deserialize<'de> for Tid<'de> { 139 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 140 + where 141 + D: Deserializer<'de>, 142 + { 143 + let value = Deserialize::deserialize(deserializer)?; 144 + Self::new(value).map_err(D::Error::custom) 145 + } 146 + } 147 + 148 + impl fmt::Display for Tid<'_> { 149 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 150 + f.write_str(&self.0) 151 + } 152 + } 153 + 154 + impl<'t> From<Tid<'t>> for String { 155 + fn from(value: Tid<'t>) -> Self { 156 + value.0.to_string() 157 + } 158 + } 159 + 160 + impl<'t> From<Tid<'t>> for CowStr<'t> { 161 + fn from(value: Tid<'t>) -> Self { 162 + value.0 163 + } 164 + } 165 + 166 + impl From<String> for Tid<'static> { 167 + fn from(value: String) -> Self { 168 + if value.len() != 13 { 169 + panic!("TID must be 13 characters") 170 + } else if !TID_REGEX.is_match(&value) { 171 + panic!("Invalid TID") 172 + } else { 173 + Self(CowStr::Owned(value.to_compact_string())) 174 + } 175 + } 176 + } 177 + 178 + impl<'t> From<CowStr<'t>> for Tid<'t> { 179 + fn from(value: CowStr<'t>) -> Self { 180 + if value.len() != 13 { 181 + panic!("TID must be 13 characters") 182 + } else if !TID_REGEX.is_match(&value) { 183 + panic!("Invalid TID") 184 + } else { 185 + Self(value) 186 + } 187 + } 188 + } 189 + 190 + impl AsRef<str> for Tid<'_> { 191 + fn as_ref(&self) -> &str { 192 + self.as_str() 193 + } 194 + } 195 + 196 + impl Deref for Tid<'_> { 197 + type Target = str; 198 + 199 + fn deref(&self) -> &Self::Target { 200 + self.as_str() 201 + } 202 + }
+1
crates/jacquard/Cargo.toml
··· 10 11 [dependencies] 12 clap = { workspace = true }
··· 10 11 [dependencies] 12 clap = { workspace = true } 13 + jacquard-common = { path = "../jacquard-common" }