A better Rust ATProto crate
103
fork

Configure Feed

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

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

Orual d950286c a9a67752

+1487 -64
+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 12 ] 13 13 14 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]] 15 24 name = "anstream" 16 25 version = "0.6.20" 17 26 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 62 71 ] 63 72 64 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]] 65 80 name = "base-x" 66 81 version = "0.2.11" 67 82 source = "registry+https://github.com/rust-lang/crates.io-index" 68 83 checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 69 84 70 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]] 71 92 name = "castaway" 72 93 version = "0.2.4" 73 94 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 77 98 ] 78 99 79 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]] 80 111 name = "cfg-if" 81 112 version = "1.0.3" 82 113 source = "registry+https://github.com/rust-lang/crates.io-index" 83 114 checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 84 115 85 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]] 86 130 name = "cid" 87 131 version = "0.11.1" 88 132 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 157 201 ] 158 202 159 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]] 160 210 name = "core2" 161 211 version = "0.4.0" 162 212 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 192 242 ] 193 243 194 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]] 195 272 name = "heck" 196 273 version = "0.5.0" 197 274 source = "registry+https://github.com/rust-lang/crates.io-index" 198 275 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 199 276 200 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]] 201 312 name = "is_terminal_polyfill" 202 313 version = "1.70.1" 203 314 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 214 325 version = "0.1.0" 215 326 dependencies = [ 216 327 "clap", 328 + "jacquard-common", 217 329 ] 218 330 219 331 [[package]] 220 332 name = "jacquard-common" 221 333 version = "0.1.0" 222 334 dependencies = [ 335 + "chrono", 223 336 "cid", 224 337 "compact_str", 225 338 "miette", ··· 227 340 "multihash", 228 341 "regex", 229 342 "serde", 343 + "serde_html_form", 344 + "serde_json", 230 345 "thiserror", 231 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" 232 369 233 370 [[package]] 234 371 name = "memchr" ··· 281 418 ] 282 419 283 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]] 284 436 name = "once_cell_polyfill" 285 437 version = "1.70.1" 286 438 source = "registry+https://github.com/rust-lang/crates.io-index" 287 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" 288 446 289 447 [[package]] 290 448 name = "proc-macro2" ··· 385 543 ] 386 544 387 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]] 388 578 name = "static_assertions" 389 579 version = "1.1.0" 390 580 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 452 642 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 453 643 454 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]] 455 739 name = "windows-link" 456 740 version = "0.2.0" 457 741 source = "registry+https://github.com/rust-lang/crates.io-index" 458 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 + ] 459 761 460 762 [[package]] 461 763 name = "windows-sys"
+3
crates/jacquard-common/Cargo.toml
··· 6 6 description.workspace = true 7 7 8 8 [dependencies] 9 + chrono = "0.4.42" 9 10 cid = { version = "0.11.1", features = ["serde", "std"] } 10 11 compact_str = "0.9.0" 11 12 miette = "7.6.0" ··· 13 14 multihash = "0.19.3" 14 15 regex = "1.11.3" 15 16 serde = { version = "1.0.227", features = ["derive"] } 17 + serde_html_form = "0.2.8" 18 + serde_json = "1.0.145" 16 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}; 1 + use crate::{CowStr, types::cid::Cid}; 2 2 use compact_str::ToCompactString; 3 3 #[allow(unused)] 4 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 1 use compact_str::CompactString; 2 - use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error}; 2 + use serde::{Deserialize, Serialize}; 3 3 use std::{ 4 4 borrow::Cow, 5 5 fmt, 6 6 hash::{Hash, Hasher}, 7 7 ops::Deref, 8 - str::FromStr, 9 8 }; 10 9 11 10 use crate::IntoStatic; ··· 207 206 CowStr::Owned(s) => CowStr::Owned(s), 208 207 } 209 208 } 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 209 } 261 210 262 211 impl Serialize for CowStr<'_> {
+2 -2
crates/jacquard-common/src/did.rs crates/jacquard-common/src/types/did.rs
··· 27 27 } 28 28 } 29 29 30 - /// Fallible constructor from an existing CowStr, clones and takes 30 + /// Fallible constructor from an existing CowStr, takes ownership 31 31 pub fn from_cowstr(did: CowStr<'d>) -> Result<Did<'d>, &'static str> { 32 32 if did.len() > 2048 { 33 33 Err("DID too long") ··· 72 72 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 73 73 /// Prefer `Did::new()` or `Did::raw` if you want to borrow. 74 74 fn from_str(s: &str) -> Result<Self, Self::Err> { 75 - Self::from_cowstr(CowStr::Owned(s.to_compact_string())) 75 + Self::from_cowstr(CowStr::Borrowed(s).into_static()) 76 76 } 77 77 } 78 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 1 #[macro_use] 3 2 pub mod cowstr; 4 3 #[macro_use] 5 - pub mod blob; 6 - pub mod cid; 4 + pub mod into_static; 7 5 8 - pub mod did; 9 - pub mod handle; 10 - #[macro_use] 11 - pub mod into_static; 12 - pub mod link; 13 - pub mod nsid; 6 + pub mod types; 14 7 15 8 pub use cowstr::CowStr; 16 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 10 11 11 [dependencies] 12 12 clap = { workspace = true } 13 + jacquard-common = { path = "../jacquard-common" }