A better Rust ATProto crate

at-uri implementation reworked string types a bit to use SmolStr when useful, collection and literal traits, rkey types and traits

Orual c106f152 17b4b461

+23 -27
Cargo.lock
··· 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" 95 - checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" 96 - dependencies = [ 97 - "rustversion", 98 - ] 99 100 [[package]] 101 name = "cc" ··· 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" ··· 187 checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 188 189 [[package]] 190 - name = "compact_str" 191 - version = "0.9.0" 192 - source = "registry+https://github.com/rust-lang/crates.io-index" 193 - checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" 194 - dependencies = [ 195 - "castaway", 196 - "cfg-if", 197 - "itoa", 198 - "rustversion", 199 - "ryu", 200 - "static_assertions", 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" ··· 334 dependencies = [ 335 "chrono", 336 "cid", 337 - "compact_str", 338 "miette", 339 "multibase", 340 "multihash", ··· 342 "serde", 343 "serde_html_form", 344 "serde_json", 345 "thiserror", 346 ] 347 ··· 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" 581 - checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 582 583 [[package]] 584 name = "strsim"
··· 83 checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 84 85 [[package]] 86 + name = "borsh" 87 + version = "1.5.7" 88 source = "registry+https://github.com/rust-lang/crates.io-index" 89 + checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" 90 + dependencies = [ 91 + "cfg_aliases", 92 + ] 93 94 [[package]] 95 + name = "bumpalo" 96 + version = "3.19.0" 97 source = "registry+https://github.com/rust-lang/crates.io-index" 98 + checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 99 100 [[package]] 101 name = "cc" ··· 112 version = "1.0.3" 113 source = "registry+https://github.com/rust-lang/crates.io-index" 114 checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 115 + 116 + [[package]] 117 + name = "cfg_aliases" 118 + version = "0.2.1" 119 + source = "registry+https://github.com/rust-lang/crates.io-index" 120 + checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 121 122 [[package]] 123 name = "chrono" ··· 193 checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 194 195 [[package]] 196 name = "core-foundation-sys" 197 version = "0.8.7" 198 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 326 dependencies = [ 327 "chrono", 328 "cid", 329 "miette", 330 "multibase", 331 "multihash", ··· 333 "serde", 334 "serde_html_form", 335 "serde_json", 336 + "smol_str", 337 "thiserror", 338 ] 339 ··· 567 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 568 569 [[package]] 570 + name = "smol_str" 571 + version = "0.3.2" 572 source = "registry+https://github.com/rust-lang/crates.io-index" 573 + checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d" 574 + dependencies = [ 575 + "borsh", 576 + "serde", 577 + ] 578 579 [[package]] 580 name = "strsim"
+7
Cargo.toml
··· 7 edition = "2024" 8 version = "0.1.0" 9 authors = ["Orual <orual@nonbinary.computer>"] 10 11 description = "A simple Rust project using Nix" 12
··· 7 edition = "2024" 8 version = "0.1.0" 9 authors = ["Orual <orual@nonbinary.computer>"] 10 + repository = "https://tangled.org/@nonbinary.computer/jacquard" 11 + keywords = ["atproto", "at protocol", "bluesky", "api", "client"] 12 + categories = ["api-bindings", "web-programming::http-client"] 13 + readme = "README.md" 14 + documentation = "https://docs.rs/jacquard" 15 + exclude = [".direnv"] 16 + 17 18 description = "A simple Rust project using Nix" 19
+26
README.md
··· 29 ``` 30 31 There's also a [`justfile`](https://just.systems/) for Makefile-esque commands to be run inside of the devShell, and you can generally `cargo ...` or `just ...` whatever just fine if you don't want to use Nix and have the prerequisites installed.
··· 29 ``` 30 31 There's also a [`justfile`](https://just.systems/) for Makefile-esque commands to be run inside of the devShell, and you can generally `cargo ...` or `just ...` whatever just fine if you don't want to use Nix and have the prerequisites installed. 32 + 33 + 34 + 35 + ### String types 36 + Something of a note to self. Developing a pattern with the string types (may macro-ify at some point). Each needs: 37 + - new(): constructing from a string slice with the right lifetime that borrows 38 + - new_owned(): constructing from an impl AsRef<str>, taking ownership 39 + - new_static(): construction from a &'static str, using SmolStr's/CowStr's new_static() constructor to not allocate 40 + - raw(): same as new() but panics instead of erroring 41 + - unchecked(): same as new() but doesn't validate. marked unsafe. 42 + - as_str(): does what it says on the tin 43 + #### Traits: 44 + - Serialize + Deserialize (custom impl for latter, sometimes for former) 45 + - FromStr 46 + - Display 47 + - Debug, PartialEq, Eq, Hash, Clone 48 + - From<T> for String, CowStr, SmolStr, 49 + - From<String>, From<CowStr>, From<SmolStr>, or TryFrom if likely enough to fail in practice to make panics common 50 + - AsRef<str> 51 + - Deref with Target = str (usually) 52 + 53 + Use `#[repr(transparent)]` as much as possible. Main exception is at-uri type and components. 54 + Use SmolStr directly as the inner type if most or all of the instances will be under 24 bytes, save lifetime headaches. 55 + Use CowStr for longer to allow for borrowing from input. 56 + 57 + TODO: impl IntoStatic trait to take ownership of string types
+1 -1
crates/jacquard-common/Cargo.toml
··· 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" 13 multibase = "0.9.1" 14 multihash = "0.19.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"
··· 8 [dependencies] 9 chrono = "0.4.42" 10 cid = { version = "0.11.1", features = ["serde", "std"] } 11 miette = "7.6.0" 12 multibase = "0.9.1" 13 multihash = "0.19.3" ··· 15 serde = { version = "1.0.227", features = ["derive"] } 16 serde_html_form = "0.2.8" 17 serde_json = "1.0.145" 18 + smol_str = { version = "0.3.2", features = ["serde"] } 19 thiserror = "2.0.16"
+13 -9
crates/jacquard-common/src/cowstr.rs
··· 1 - use compact_str::CompactString; 2 use serde::{Deserialize, Serialize}; 3 use std::{ 4 borrow::Cow, 5 fmt, ··· 10 use crate::IntoStatic; 11 12 /// Shamelessly copied from https://github.com/bearcove/merde 13 - /// A copy-on-write string type that uses [`CompactString`] for 14 /// the "owned" variant. 15 /// 16 /// The standard [`Cow`] type cannot be used, since 17 - /// `<str as ToOwned>::Owned` is `String`, and not `CompactString`. 18 #[derive(Clone)] 19 pub enum CowStr<'s> { 20 Borrowed(&'s str), 21 - Owned(CompactString), 22 } 23 24 impl CowStr<'static> { ··· 26 /// if the `compact_str` feature is disabled, or if the string is longer 27 /// than `MAX_INLINE_SIZE`. 28 pub fn copy_from_str(s: &str) -> Self { 29 - Self::Owned(CompactString::from(s)) 30 } 31 } 32 ··· 38 39 #[inline] 40 pub fn from_utf8_owned(s: Vec<u8>) -> Result<Self, std::str::Utf8Error> { 41 - Ok(Self::Owned(CompactString::from_utf8(s)?)) 42 } 43 44 #[inline] 45 pub fn from_utf8_lossy(s: &'s [u8]) -> Self { 46 - Self::Owned(CompactString::from_utf8_lossy(s)) 47 } 48 49 /// # Safety ··· 51 /// This function is unsafe because it does not check that the bytes are valid UTF-8. 52 #[inline] 53 pub unsafe fn from_utf8_unchecked(s: &'s [u8]) -> Self { 54 - unsafe { Self::Owned(CompactString::from_utf8_unchecked(s)) } 55 } 56 } 57 ··· 133 fn from(s: CowStr<'_>) -> Self { 134 match s { 135 CowStr::Borrowed(s) => s.into(), 136 - CowStr::Owned(s) => s.into(), 137 } 138 } 139 }
··· 1 use serde::{Deserialize, Serialize}; 2 + use smol_str::SmolStr; 3 use std::{ 4 borrow::Cow, 5 fmt, ··· 10 use crate::IntoStatic; 11 12 /// Shamelessly copied from https://github.com/bearcove/merde 13 + /// A copy-on-write immutable string type that uses [`SmolStr`] for 14 /// the "owned" variant. 15 /// 16 /// The standard [`Cow`] type cannot be used, since 17 + /// `<str as ToOwned>::Owned` is `String`, and not `SmolStr`. 18 #[derive(Clone)] 19 pub enum CowStr<'s> { 20 Borrowed(&'s str), 21 + Owned(SmolStr), 22 } 23 24 impl CowStr<'static> { ··· 26 /// if the `compact_str` feature is disabled, or if the string is longer 27 /// than `MAX_INLINE_SIZE`. 28 pub fn copy_from_str(s: &str) -> Self { 29 + Self::Owned(SmolStr::from(s)) 30 + } 31 + 32 + pub fn new_static(s: &'static str) -> Self { 33 + Self::Owned(SmolStr::new_static(s)) 34 } 35 } 36 ··· 42 43 #[inline] 44 pub fn from_utf8_owned(s: Vec<u8>) -> Result<Self, std::str::Utf8Error> { 45 + Ok(Self::Owned(SmolStr::new(std::str::from_utf8(&s)?))) 46 } 47 48 #[inline] 49 pub fn from_utf8_lossy(s: &'s [u8]) -> Self { 50 + Self::Owned(String::from_utf8_lossy(&s).into()) 51 } 52 53 /// # Safety ··· 55 /// This function is unsafe because it does not check that the bytes are valid UTF-8. 56 #[inline] 57 pub unsafe fn from_utf8_unchecked(s: &'s [u8]) -> Self { 58 + unsafe { Self::Owned(SmolStr::new(std::str::from_utf8_unchecked(s))) } 59 } 60 } 61 ··· 137 fn from(s: CowStr<'_>) -> Self { 138 match s { 139 CowStr::Borrowed(s) => s.into(), 140 + CowStr::Owned(s) => String::from(s).into_boxed_str(), 141 } 142 } 143 }
+8
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; ··· 8 pub mod integer; 9 pub mod link; 10 pub mod nsid; 11 pub mod tid;
··· 1 pub mod aturi; 2 pub mod blob; 3 pub mod cid; 4 + pub mod collection; 5 pub mod datetime; 6 pub mod did; 7 pub mod handle; ··· 9 pub mod integer; 10 pub mod link; 11 pub mod nsid; 12 + pub mod recordkey; 13 pub mod tid; 14 + 15 + /// Trait for a constant string literal type 16 + pub trait Literal: Clone + Copy + PartialEq + Eq + Send + Sync + 'static { 17 + /// The string literal 18 + const LITERAL: &'static str; 19 + }
+200 -73
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 } ··· 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 ··· 143 type Target = str; 144 145 fn deref(&self) -> &Self::Target { 146 - self.as_str() 147 } 148 }
··· 1 + use crate::CowStr; 2 + use crate::types::ident::AtIdentifier; 3 + use crate::types::nsid::Nsid; 4 + use crate::types::recordkey::{RecordKey, Rkey}; 5 + use regex::Regex; 6 + use serde::Serializer; 7 + use serde::{Deserialize, Deserializer, Serialize, de::Error}; 8 + use smol_str::ToSmolStr; 9 use std::fmt; 10 use std::sync::LazyLock; 11 use std::{ops::Deref, str::FromStr}; 12 13 + /// at:// URI type 14 + /// 15 + /// based on the regex here: https://github.com/bluesky-social/atproto/blob/main/packages/syntax/src/aturi_validation.ts 16 + /// 17 + /// Doesn't support the query segment, but then neither does the Typescript SDK 18 + /// 19 + /// TODO: support IntoStatic on string types. For composites like this where all borrow from (present) input, 20 + /// perhaps use some careful unsafe to launder the lifetimes. 21 + #[derive(Clone, PartialEq, Eq, Hash, Debug)] 22 + pub struct AtUri<'u> { 23 + uri: CowStr<'u>, 24 + pub authority: AtIdentifier<'u>, 25 + pub path: Option<UriPath<'u>>, 26 + pub fragment: Option<CowStr<'u>>, 27 + } 28 29 + /// at:// URI path component (current subset) 30 + #[derive(Clone, PartialEq, Eq, Hash, Debug)] 31 + pub struct UriPath<'u> { 32 + pub collection: Nsid<'u>, 33 + pub rkey: Option<RecordKey<Rkey<'u>>>, 34 + } 35 36 + pub type UriPathBuf = UriPath<'static>; 37 38 + pub static ATURI_REGEX: LazyLock<Regex> = LazyLock::new(|| { 39 + Regex::new(r##"^at://(?<authority>[a-zA-Z0-9._:%-]+)(/(?<collection>[a-zA-Z0-9-.]+)(/(?<rkey>[a-zA-Z0-9._~:@!$&%')(*+,;=-]+))?)?(#(?<fragment>/[a-zA-Z0-9._~:@!$&%')(*+,;=-[]/\]*))?$"##).unwrap() 40 + }); 41 42 + impl<'u> AtUri<'u> { 43 /// Fallible constructor, validates, borrows from input 44 + pub fn new(uri: &'u str) -> Result<Self, &'static str> { 45 + if let Some(parts) = ATURI_REGEX.captures(uri) { 46 + if let Some(authority) = parts.name("authority") { 47 + let authority = AtIdentifier::new(authority.as_str())?; 48 + let path = if let Some(collection) = parts.name("collection") { 49 + let collection = Nsid::new(collection.as_str())?; 50 + let rkey = if let Some(rkey) = parts.name("rkey") { 51 + let rkey = RecordKey::from(Rkey::new(rkey.as_str())?); 52 + Some(rkey) 53 + } else { 54 + None 55 + }; 56 + Some(UriPath { collection, rkey }) 57 + } else { 58 + None 59 + }; 60 + let fragment = parts.name("fragment").map(|fragment| { 61 + let fragment = CowStr::Borrowed(fragment.as_str()); 62 + fragment 63 + }); 64 + Ok(AtUri { 65 + uri: CowStr::Borrowed(uri), 66 + authority, 67 + path, 68 + fragment, 69 + }) 70 + } else { 71 + Err("at:// URI missing authority") 72 + } 73 } else { 74 + Err("Invalid at:// URI via regex") 75 } 76 } 77 78 + pub fn new_owned(uri: impl AsRef<str>) -> Result<Self, &'static str> { 79 + let uri = uri.as_ref(); 80 + if let Some(parts) = ATURI_REGEX.captures(uri) { 81 + if let Some(authority) = parts.name("authority") { 82 + let authority = AtIdentifier::new_owned(authority.as_str())?; 83 + let path = if let Some(collection) = parts.name("collection") { 84 + let collection = Nsid::new_owned(collection.as_str())?; 85 + let rkey = if let Some(rkey) = parts.name("rkey") { 86 + let rkey = RecordKey::from(Rkey::new_owned(rkey.as_str())?); 87 + Some(rkey) 88 + } else { 89 + None 90 + }; 91 + Some(UriPath { collection, rkey }) 92 + } else { 93 + None 94 + }; 95 + let fragment = parts.name("fragment").map(|fragment| { 96 + let fragment = CowStr::Owned(fragment.as_str().to_smolstr()); 97 + fragment 98 + }); 99 + Ok(AtUri { 100 + uri: CowStr::Owned(uri.to_smolstr()), 101 + authority, 102 + path, 103 + fragment, 104 + }) 105 + } else { 106 + Err("at:// URI missing authority") 107 + } 108 } else { 109 + Err("Invalid at:// URI via regex") 110 } 111 } 112 113 + pub fn new_static(uri: &'static str) -> Result<AtUri<'static>, &'static str> { 114 + let uri = uri.as_ref(); 115 + if let Some(parts) = ATURI_REGEX.captures(uri) { 116 + if let Some(authority) = parts.name("authority") { 117 + let authority = AtIdentifier::new_static(authority.as_str())?; 118 + let path = if let Some(collection) = parts.name("collection") { 119 + let collection = Nsid::new_static(collection.as_str())?; 120 + let rkey = if let Some(rkey) = parts.name("rkey") { 121 + let rkey = RecordKey::from(Rkey::new_static(rkey.as_str())?); 122 + Some(rkey) 123 + } else { 124 + None 125 + }; 126 + Some(UriPath { collection, rkey }) 127 + } else { 128 + None 129 + }; 130 + let fragment = parts.name("fragment").map(|fragment| { 131 + let fragment = CowStr::new_static(fragment.as_str()); 132 + fragment 133 + }); 134 + Ok(AtUri { 135 + uri: CowStr::new_static(uri), 136 + authority, 137 + path, 138 + fragment, 139 + }) 140 + } else { 141 + Err("at:// URI missing authority") 142 + } 143 } else { 144 + Err("Invalid at:// URI via regex") 145 } 146 } 147 148 + pub unsafe fn unchecked(uri: &'u str) -> Self { 149 + if let Some(parts) = ATURI_REGEX.captures(uri) { 150 + if let Some(authority) = parts.name("authority") { 151 + let authority = unsafe { AtIdentifier::unchecked(authority.as_str()) }; 152 + let path = if let Some(collection) = parts.name("collection") { 153 + let collection = unsafe { Nsid::unchecked(collection.as_str()) }; 154 + let rkey = if let Some(rkey) = parts.name("rkey") { 155 + let rkey = RecordKey::from(unsafe { Rkey::unchecked(rkey.as_str()) }); 156 + Some(rkey) 157 + } else { 158 + None 159 + }; 160 + Some(UriPath { collection, rkey }) 161 + } else { 162 + None 163 + }; 164 + let fragment = parts.name("fragment").map(|fragment| { 165 + let fragment = CowStr::Borrowed(fragment.as_str()); 166 + fragment 167 + }); 168 + AtUri { 169 + uri: CowStr::Borrowed(uri), 170 + authority, 171 + path, 172 + fragment, 173 + } 174 + } else { 175 + Self { 176 + uri: CowStr::Borrowed(uri), 177 + authority: unsafe { AtIdentifier::unchecked(uri) }, 178 + path: None, 179 + fragment: None, 180 + } 181 + } 182 + } else { 183 + Self { 184 + uri: CowStr::Borrowed(uri), 185 + authority: unsafe { AtIdentifier::unchecked(uri) }, 186 + path: None, 187 + fragment: None, 188 + } 189 + } 190 } 191 192 pub fn as_str(&self) -> &str { 193 { 194 + let this = &self.uri; 195 this 196 } 197 } ··· 201 type Err = &'static str; 202 203 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 204 + /// Prefer `AtUri::new()` or `AtUri::raw()` if you want to borrow. 205 fn from_str(s: &str) -> Result<Self, Self::Err> { 206 + Self::new_owned(s) 207 } 208 } 209 210 + impl<'de> Deserialize<'de> for AtUri<'de> { 211 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 212 where 213 + D: Deserializer<'de>, 214 { 215 let value = Deserialize::deserialize(deserializer)?; 216 Self::new(value).map_err(D::Error::custom) 217 } 218 } 219 220 + impl Serialize for AtUri<'_> { 221 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 222 + where 223 + S: Serializer, 224 + { 225 + serializer.serialize_str(&self.uri) 226 } 227 } 228 229 + impl fmt::Display for AtUri<'_> { 230 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 231 + f.write_str(&self.uri) 232 } 233 } 234 235 + impl<'d> From<AtUri<'d>> for String { 236 + fn from(value: AtUri<'d>) -> Self { 237 + value.uri.to_string() 238 } 239 } 240 241 + impl<'d> From<AtUri<'d>> for CowStr<'d> { 242 + fn from(value: AtUri<'d>) -> Self { 243 + value.uri 244 } 245 } 246 247 + impl TryFrom<String> for AtUri<'static> { 248 + type Error = &'static str; 249 + 250 + fn try_from(value: String) -> Result<Self, Self::Error> { 251 + Self::new_owned(&value) 252 } 253 } 254 255 + impl<'d> TryFrom<CowStr<'d>> for AtUri<'d> { 256 + type Error = &'static str; 257 + /// TODO: rewrite to avoid taking ownership/cloning 258 + fn try_from(value: CowStr<'d>) -> Result<Self, Self::Error> { 259 + Self::new_owned(value) 260 } 261 } 262 263 impl AsRef<str> for AtUri<'_> { 264 fn as_ref(&self) -> &str { 265 + &self.uri.as_ref() 266 } 267 } 268 ··· 270 type Target = str; 271 272 fn deref(&self) -> &Self::Target { 273 + self.uri.as_ref() 274 } 275 }
+23 -5
crates/jacquard-common/src/types/blob.rs
··· 1 - use crate::{CowStr, types::cid::Cid}; 2 - use compact_str::ToCompactString; 3 #[allow(unused)] 4 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error}; 5 #[allow(unused)] 6 use std::{ 7 borrow::Cow, ··· 39 /// Wrapper for file type 40 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] 41 #[serde(transparent)] 42 pub struct MimeType<'m>(pub CowStr<'m>); 43 44 impl<'m> MimeType<'m> { 45 /// Fallible constructor, validates, borrows from input 46 pub fn new(mime_type: &'m str) -> Result<MimeType<'m>, &'static str> { 47 Ok(Self(CowStr::Borrowed(mime_type))) 48 } 49 50 /// Fallible constructor from an existing CowStr, borrows ··· 66 } 67 68 impl FromStr for MimeType<'_> { 69 - type Err = &'static str; 70 71 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 72 fn from_str(s: &str) -> Result<Self, Self::Err> { 73 - Self::from_cowstr(CowStr::Owned(s.to_compact_string())) 74 } 75 } 76 ··· 107 108 impl From<String> for MimeType<'static> { 109 fn from(value: String) -> Self { 110 - Self(CowStr::Owned(value.to_compact_string())) 111 } 112 } 113
··· 1 + use crate::{CowStr, IntoStatic, types::cid::Cid}; 2 #[allow(unused)] 3 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error}; 4 + use smol_str::ToSmolStr; 5 + use std::convert::Infallible; 6 #[allow(unused)] 7 use std::{ 8 borrow::Cow, ··· 40 /// Wrapper for file type 41 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] 42 #[serde(transparent)] 43 + #[repr(transparent)] 44 pub struct MimeType<'m>(pub CowStr<'m>); 45 46 impl<'m> MimeType<'m> { 47 /// Fallible constructor, validates, borrows from input 48 pub fn new(mime_type: &'m str) -> Result<MimeType<'m>, &'static str> { 49 Ok(Self(CowStr::Borrowed(mime_type))) 50 + } 51 + 52 + pub fn new_owned(mime_type: impl AsRef<str>) -> Self { 53 + Self(CowStr::Owned(mime_type.as_ref().to_smolstr())) 54 + } 55 + 56 + pub fn new_static(mime_type: &'static str) -> Self { 57 + Self(CowStr::new_static(mime_type)) 58 } 59 60 /// Fallible constructor from an existing CowStr, borrows ··· 76 } 77 78 impl FromStr for MimeType<'_> { 79 + type Err = Infallible; 80 81 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 82 fn from_str(s: &str) -> Result<Self, Self::Err> { 83 + Ok(Self::new_owned(s)) 84 + } 85 + } 86 + 87 + impl IntoStatic for MimeType<'_> { 88 + type Output = MimeType<'static>; 89 + 90 + fn into_static(self) -> Self::Output { 91 + MimeType(self.0.into_static()) 92 } 93 } 94 ··· 125 126 impl From<String> for MimeType<'static> { 127 fn from(value: String) -> Self { 128 + Self(CowStr::Owned(value.to_smolstr())) 129 } 130 } 131
+21 -10
crates/jacquard-common/src/types/cid.rs
··· 1 - use std::{convert::Infallible, fmt, marker::PhantomData, ops::Deref, str::FromStr}; 2 - 3 - use compact_str::ToCompactString; 4 - use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Visitor}; 5 - 6 pub use cid::Cid as IpldCid; 7 - 8 - use crate::CowStr; 9 10 /// raw 11 pub const ATP_CID_CODEC: u64 = 0x55; ··· 47 let s = CowStr::Owned( 48 cid.to_string_of_base(ATP_CID_BASE) 49 .unwrap_or_default() 50 - .to_compact_string(), 51 ); 52 Self::Ipld { cid, s } 53 } ··· 89 90 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 91 fn from_str(s: &str) -> Result<Self, Self::Err> { 92 - Ok(Cid::Str(CowStr::Owned(s.to_compact_string()))) 93 } 94 } 95 ··· 164 165 impl From<String> for Cid<'_> { 166 fn from(value: String) -> Self { 167 - Cid::Str(CowStr::Owned(value.to_compact_string())) 168 } 169 } 170
··· 1 + use crate::{CowStr, IntoStatic}; 2 pub use cid::Cid as IpldCid; 3 + use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Visitor}; 4 + use smol_str::ToSmolStr; 5 + use std::{convert::Infallible, fmt, marker::PhantomData, ops::Deref, str::FromStr}; 6 7 /// raw 8 pub const ATP_CID_CODEC: u64 = 0x55; ··· 44 let s = CowStr::Owned( 45 cid.to_string_of_base(ATP_CID_BASE) 46 .unwrap_or_default() 47 + .to_smolstr(), 48 ); 49 Self::Ipld { cid, s } 50 } ··· 86 87 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 88 fn from_str(s: &str) -> Result<Self, Self::Err> { 89 + Ok(Cid::Str(CowStr::Owned(s.to_smolstr()))) 90 + } 91 + } 92 + 93 + impl IntoStatic for Cid<'_> { 94 + type Output = Cid<'static>; 95 + 96 + fn into_static(self) -> Self::Output { 97 + match self { 98 + Cid::Ipld { cid, s } => Cid::Ipld { 99 + cid, 100 + s: s.into_static(), 101 + }, 102 + Cid::Str(cow_str) => Cid::Str(cow_str.into_static()), 103 + } 104 } 105 } 106 ··· 175 176 impl From<String> for Cid<'_> { 177 fn from(value: String) -> Self { 178 + Cid::Str(CowStr::Owned(value.to_smolstr())) 179 } 180 } 181
+52
crates/jacquard-common/src/types/collection.rs
···
··· 1 + use core::fmt; 2 + 3 + use serde::{Serialize, de}; 4 + 5 + use crate::types::{ 6 + aturi::UriPath, 7 + nsid::Nsid, 8 + recordkey::{RecordKey, RecordKeyType, Rkey}, 9 + }; 10 + 11 + /// Trait for a collection of records that can be stored in a repository. 12 + /// 13 + /// The records all have the same Lexicon schema. 14 + pub trait Collection: fmt::Debug { 15 + /// The NSID for the Lexicon that defines the schema of records in this collection. 16 + const NSID: &'static str; 17 + 18 + /// This collection's record type. 19 + type Record: fmt::Debug + de::DeserializeOwned + Serialize; 20 + 21 + /// Returns the [`Nsid`] for the Lexicon that defines the schema of records in this 22 + /// collection. 23 + /// 24 + /// This is a convenience method that parses [`Self::NSID`]. 25 + /// 26 + /// # Panics 27 + /// 28 + /// Panics if [`Self::NSID`] is not a valid NSID. 29 + /// 30 + /// [`Nsid`]: string::Nsid 31 + fn nsid() -> crate::types::nsid::Nsid<'static> { 32 + Nsid::new_static(Self::NSID).expect("should be valid NSID") 33 + } 34 + 35 + /// Returns the repo path for a record in this collection with the given record key. 36 + /// 37 + /// Per the [Repo Data Structure v3] specification: 38 + /// > Repo paths currently have a fixed structure of `<collection>/<record-key>`. This 39 + /// > means a valid, normalized [`Nsid`], followed by a `/`, followed by a valid 40 + /// > [`RecordKey`]. 41 + /// 42 + /// [Repo Data Structure v3]: https://atproto.com/specs/repository#repo-data-structure-v3 43 + /// [`Nsid`]: string::Nsid 44 + fn repo_path<'u, T: RecordKeyType>( 45 + rkey: &'u crate::types::recordkey::RecordKey<T>, 46 + ) -> UriPath<'u> { 47 + UriPath { 48 + collection: Self::nsid(), 49 + rkey: Some(RecordKey::from(Rkey::raw(rkey.as_ref()))), 50 + } 51 + } 52 + }
+5 -6
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; ··· 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 } ··· 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 {
··· 1 use chrono::DurationRound; 2 use serde::Serializer; 3 use serde::{Deserialize, Deserializer, Serialize, de::Error}; 4 + use smol_str::ToSmolStr; 5 + use std::sync::LazyLock; 6 + use std::{cmp, str::FromStr}; 7 8 use crate::{CowStr, IntoStatic}; 9 use regex::Regex; ··· 57 // This serialization format is compatible with ISO 8601. 58 let serialized = CowStr::Owned( 59 dt.to_rfc3339_opts(chrono::SecondsFormat::Micros, true) 60 + .to_smolstr(), 61 ); 62 Self { serialized, dt } 63 } ··· 138 if ISO8601_REGEX.is_match(&value) { 139 let dt = chrono::DateTime::parse_from_rfc3339(&value)?; 140 Ok(Self { 141 + serialized: CowStr::Owned(value.to_smolstr()), 142 dt, 143 }) 144 } else {
+53 -14
crates/jacquard-common/src/types/did.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 Did<'d>(CowStr<'d>); 14 15 pub static DID_REGEX: LazyLock<Regex> = ··· 18 impl<'d> Did<'d> { 19 /// Fallible constructor, validates, borrows from input 20 pub fn new(did: &'d str) -> Result<Self, &'static str> { 21 if did.len() > 2048 { 22 Err("DID too long") 23 } else if !DID_REGEX.is_match(did) { ··· 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") 34 - } else if !DID_REGEX.is_match(&did) { 35 Err("Invalid DID") 36 } else { 37 - Ok(Self(did.into_static())) 38 } 39 } 40 ··· 43 /// or API values you know are valid (rather than using serde), this is the one to use. 44 /// The From<String> and From<CowStr> impls use the same logic. 45 pub fn raw(did: &'d str) -> Self { 46 if did.len() > 2048 { 47 panic!("DID too long") 48 } else if !DID_REGEX.is_match(did) { ··· 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 ··· 92 } 93 } 94 95 impl<'d> From<Did<'d>> for String { 96 fn from(value: Did<'d>) -> Self { 97 value.0.to_string() ··· 106 107 impl From<String> for Did<'static> { 108 fn from(value: String) -> Self { 109 if value.len() > 2048 { 110 panic!("DID too long") 111 } else if !DID_REGEX.is_match(&value) { 112 panic!("Invalid DID") 113 } else { 114 - Self(CowStr::Owned(value.to_compact_string())) 115 } 116 } 117 } 118 119 impl<'d> From<CowStr<'d>> for Did<'d> { 120 fn from(value: CowStr<'d>) -> Self { 121 if value.len() > 2048 { 122 panic!("DID too long") 123 } else if !DID_REGEX.is_match(&value) { 124 panic!("Invalid DID") 125 } else { 126 - Self(value) 127 } 128 } 129 }
··· 1 + use crate::{CowStr, IntoStatic}; 2 + use regex::Regex; 3 + use serde::{Deserialize, Deserializer, Serialize, de::Error}; 4 + use smol_str::ToSmolStr; 5 use std::fmt; 6 use std::sync::LazyLock; 7 use std::{ops::Deref, str::FromStr}; 8 9 + #[derive(Clone, PartialEq, Eq, Serialize, Hash)] 10 #[serde(transparent)] 11 + #[repr(transparent)] 12 pub struct Did<'d>(CowStr<'d>); 13 14 pub static DID_REGEX: LazyLock<Regex> = ··· 17 impl<'d> Did<'d> { 18 /// Fallible constructor, validates, borrows from input 19 pub fn new(did: &'d str) -> Result<Self, &'static str> { 20 + let did = did.strip_prefix("at://").unwrap_or(did); 21 if did.len() > 2048 { 22 Err("DID too long") 23 } else if !DID_REGEX.is_match(did) { ··· 27 } 28 } 29 30 + /// Fallible constructor, validates, takes ownership 31 + pub fn new_owned(did: impl AsRef<str>) -> Result<Self, &'static str> { 32 + let did = did.as_ref(); 33 + let did = did.strip_prefix("at://").unwrap_or(did); 34 if did.len() > 2048 { 35 Err("DID too long") 36 + } else if !DID_REGEX.is_match(did) { 37 + Err("Invalid DID") 38 + } else { 39 + Ok(Self(CowStr::Owned(did.to_smolstr()))) 40 + } 41 + } 42 + 43 + /// Fallible constructor, validates, doesn't allocate 44 + pub fn new_static(did: &'static str) -> Result<Self, &'static str> { 45 + let did = did.strip_prefix("at://").unwrap_or(did); 46 + if did.len() > 2048 { 47 + Err("DID too long") 48 + } else if !DID_REGEX.is_match(did) { 49 Err("Invalid DID") 50 } else { 51 + Ok(Self(CowStr::new_static(did))) 52 } 53 } 54 ··· 57 /// or API values you know are valid (rather than using serde), this is the one to use. 58 /// The From<String> and From<CowStr> impls use the same logic. 59 pub fn raw(did: &'d str) -> Self { 60 + let did = did.strip_prefix("at://").unwrap_or(did); 61 if did.len() > 2048 { 62 panic!("DID too long") 63 } else if !DID_REGEX.is_match(did) { ··· 87 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 88 /// Prefer `Did::new()` or `Did::raw` if you want to borrow. 89 fn from_str(s: &str) -> Result<Self, Self::Err> { 90 + Self::new_owned(s) 91 + } 92 + } 93 + 94 + impl IntoStatic for Did<'_> { 95 + type Output = Did<'static>; 96 + 97 + fn into_static(self) -> Self::Output { 98 + Did(self.0.into_static()) 99 } 100 } 101 ··· 115 } 116 } 117 118 + impl fmt::Debug for Did<'_> { 119 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 120 + write!(f, "at://{}", self.0) 121 + } 122 + } 123 + 124 impl<'d> From<Did<'d>> for String { 125 fn from(value: Did<'d>) -> Self { 126 value.0.to_string() ··· 135 136 impl From<String> for Did<'static> { 137 fn from(value: String) -> Self { 138 + let value = if let Some(did) = value.strip_prefix("at://") { 139 + CowStr::Borrowed(did) 140 + } else { 141 + value.into() 142 + }; 143 if value.len() > 2048 { 144 panic!("DID too long") 145 } else if !DID_REGEX.is_match(&value) { 146 panic!("Invalid DID") 147 } else { 148 + Self(value.into_static()) 149 } 150 } 151 } 152 153 impl<'d> From<CowStr<'d>> for Did<'d> { 154 fn from(value: CowStr<'d>) -> Self { 155 + let value = if let Some(did) = value.strip_prefix("at://") { 156 + CowStr::Borrowed(did) 157 + } else { 158 + value 159 + }; 160 if value.len() > 2048 { 161 panic!("DID too long") 162 } else if !DID_REGEX.is_match(&value) { 163 panic!("Invalid DID") 164 } else { 165 + Self(value.into_static()) 166 } 167 } 168 }
+88 -30
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") ··· 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. ··· 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") ··· 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 ··· 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 ··· 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 ··· 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 }
··· 1 + use crate::{CowStr, IntoStatic}; 2 + use regex::Regex; 3 + use serde::{Deserialize, Deserializer, Serialize, de::Error}; 4 + use smol_str::ToSmolStr; 5 use std::fmt; 6 use std::sync::LazyLock; 7 use std::{ops::Deref, str::FromStr}; 8 9 + #[derive(Clone, PartialEq, Eq, Serialize, Hash)] 10 #[serde(transparent)] 11 + #[repr(transparent)] 12 pub struct Handle<'h>(CowStr<'h>); 13 14 pub static HANDLE_REGEX: LazyLock<Regex> = LazyLock::new(|| { 15 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() 16 }); 17 18 + /// AT Protocol handle 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 25 + .strip_prefix("at://") 26 + .unwrap_or(handle) 27 + .strip_prefix('@') 28 + .unwrap_or(handle); 29 + if handle.len() > 253 { 30 Err("handle too long") 31 } else if !HANDLE_REGEX.is_match(handle) { 32 Err("Invalid handle") ··· 35 } 36 } 37 38 + /// Fallible constructor, validates, takes ownership 39 + pub fn new_owned(handle: impl AsRef<str>) -> Result<Self, &'static str> { 40 + let handle = handle.as_ref(); 41 + let handle = handle 42 + .strip_prefix("at://") 43 + .unwrap_or(handle) 44 + .strip_prefix('@') 45 + .unwrap_or(handle); 46 + if handle.len() > 253 { 47 Err("handle too long") 48 + } else if !HANDLE_REGEX.is_match(handle) { 49 Err("Invalid handle") 50 } else { 51 + Ok(Self(CowStr::Owned(handle.to_smolstr()))) 52 } 53 } 54 55 + /// Fallible constructor, validates, doesn't allocate 56 + pub fn new_static(handle: &'static str) -> Result<Self, &'static str> { 57 + let handle = handle 58 + .strip_prefix("at://") 59 + .unwrap_or(handle) 60 + .strip_prefix('@') 61 + .unwrap_or(handle); 62 + if handle.len() > 253 { 63 + Err("handle too long") 64 + } else if !HANDLE_REGEX.is_match(handle) { 65 + Err("Invalid handle") 66 + } else { 67 + Ok(Self(CowStr::new_static(handle))) 68 + } 69 + } 70 /// Infallible constructor for when you *know* the string is a valid handle. 71 /// Will panic on invalid handles. If you're manually decoding atproto records 72 /// or API values you know are valid (rather than using serde), this is the one to use. ··· 74 /// 75 /// Accepts (and strips) preceding '@' if present 76 pub fn raw(handle: &'h str) -> Self { 77 + let handle = handle 78 + .strip_prefix("at://") 79 + .unwrap_or(handle) 80 + .strip_prefix('@') 81 + .unwrap_or(handle); 82 + if handle.len() > 253 { 83 panic!("handle too long") 84 } else if !HANDLE_REGEX.is_match(handle) { 85 panic!("Invalid handle") ··· 93 /// 94 /// Accepts (and strips) preceding '@' if present 95 pub unsafe fn unchecked(handle: &'h str) -> Self { 96 + let handle = handle 97 + .strip_prefix("at://") 98 + .unwrap_or(handle) 99 + .strip_prefix('@') 100 + .unwrap_or(handle); 101 Self(CowStr::Borrowed(handle)) 102 } 103 ··· 115 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 116 /// Prefer `Handle::new()` or `Handle::raw` if you want to borrow. 117 fn from_str(s: &str) -> Result<Self, Self::Err> { 118 + Self::new_owned(s) 119 + } 120 + } 121 + 122 + impl IntoStatic for Handle<'_> { 123 + type Output = Handle<'static>; 124 + 125 + fn into_static(self) -> Self::Output { 126 + Handle(self.0.into_static()) 127 } 128 } 129 ··· 139 140 impl fmt::Display for Handle<'_> { 141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 142 + write!(f, "{}", self.0) 143 + } 144 + } 145 + 146 + impl fmt::Debug for Handle<'_> { 147 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 148 + write!(f, "at://{}", self.0) 149 } 150 } 151 ··· 163 164 impl From<String> for Handle<'static> { 165 fn from(value: String) -> Self { 166 + let value = if let Some(handle) = value 167 + .strip_prefix("at://") 168 + .unwrap_or(&value) 169 + .strip_prefix('@') 170 + { 171 + CowStr::Borrowed(handle) 172 + } else { 173 + value.into() 174 + }; 175 + if value.len() > 253 { 176 panic!("handle too long") 177 } else if !HANDLE_REGEX.is_match(&value) { 178 panic!("Invalid handle") 179 } else { 180 + Self(value.into_static()) 181 } 182 } 183 } 184 185 impl<'h> From<CowStr<'h>> for Handle<'h> { 186 fn from(value: CowStr<'h>) -> Self { 187 + let value = if let Some(handle) = value 188 + .strip_prefix("at://") 189 + .unwrap_or(&value) 190 + .strip_prefix('@') 191 + { 192 + CowStr::Borrowed(handle) 193 + } else { 194 + value 195 + }; 196 + if value.len() > 253 { 197 panic!("handle too long") 198 } else if !HANDLE_REGEX.is_match(&value) { 199 panic!("Invalid handle") 200 } else { 201 + Self(value.into_static()) 202 } 203 } 204 }
+26 -5
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 ··· 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 ··· 90 Ok(AtIdentifier::Did(did)) 91 } else { 92 s.parse().map(AtIdentifier::Handle) 93 } 94 } 95 }
··· 1 use crate::types::handle::Handle; 2 + use crate::{IntoStatic, types::did::Did}; 3 use std::fmt; 4 use std::str::FromStr; 5 ··· 26 } 27 } 28 29 + /// Fallible constructor, validates, takes ownership 30 + pub fn new_owned(ident: impl AsRef<str>) -> Result<Self, &'static str> { 31 + let ident = ident.as_ref(); 32 + if let Ok(did) = Did::new_owned(ident) { 33 Ok(AtIdentifier::Did(did)) 34 } else { 35 + Ok(AtIdentifier::Handle(Handle::new_owned(ident)?)) 36 + } 37 + } 38 + 39 + /// Fallible constructor, validates, doesn't allocate 40 + pub fn new_static(ident: &'static str) -> Result<AtIdentifier<'static>, &'static str> { 41 + if let Ok(did) = Did::new_static(ident) { 42 + Ok(AtIdentifier::Did(did)) 43 + } else { 44 + Ok(AtIdentifier::Handle(Handle::new_static(ident)?)) 45 } 46 } 47 ··· 100 Ok(AtIdentifier::Did(did)) 101 } else { 102 s.parse().map(AtIdentifier::Handle) 103 + } 104 + } 105 + } 106 + 107 + impl IntoStatic for AtIdentifier<'_> { 108 + type Output = AtIdentifier<'static>; 109 + 110 + fn into_static(self) -> Self::Output { 111 + match self { 112 + AtIdentifier::Did(did) => AtIdentifier::Did(did.into_static()), 113 + AtIdentifier::Handle(handle) => AtIdentifier::Handle(handle.into_static()), 114 } 115 } 116 }
+209
crates/jacquard-common/src/types/nsid.rs
··· 1
··· 1 + use crate::types::recordkey::RecordKeyType; 2 + use crate::{CowStr, IntoStatic}; 3 + use regex::Regex; 4 + use serde::{Deserialize, Deserializer, Serialize, de::Error}; 5 + use smol_str::{SmolStr, ToSmolStr}; 6 + use std::fmt; 7 + use std::sync::LazyLock; 8 + use std::{ops::Deref, str::FromStr}; 9 10 + /// Namespaced Identifier (NSID) 11 + /// 12 + /// Stored as SmolStr to ease lifetime issues and because, despite the fact that NSIDs *can* be 317 characters, most are quite short 13 + /// TODO: consider if this should go back to CowStr, or be broken up into segments 14 + #[derive(Clone, PartialEq, Eq, Serialize, Hash)] 15 + #[serde(transparent)] 16 + #[repr(transparent)] 17 + pub struct Nsid<'n>(CowStr<'n>); 18 + 19 + pub static NSID_REGEX: LazyLock<Regex> = LazyLock::new(|| { 20 + Regex::new(r"^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z][a-zA-Z0-9]{0,62})$").unwrap() 21 + }); 22 + 23 + impl<'n> Nsid<'n> { 24 + /// Fallible constructor, validates, borrows from input 25 + pub fn new(nsid: &'n str) -> Result<Self, &'static str> { 26 + if nsid.len() > 317 { 27 + Err("NSID too long") 28 + } else if !NSID_REGEX.is_match(nsid) { 29 + Err("Invalid NSID") 30 + } else { 31 + Ok(Self(CowStr::Borrowed(nsid))) 32 + } 33 + } 34 + 35 + /// Fallible constructor, validates, borrows from input 36 + pub fn new_owned(nsid: impl AsRef<str>) -> Result<Self, &'static str> { 37 + let nsid = nsid.as_ref(); 38 + if nsid.len() > 317 { 39 + Err("NSID too long") 40 + } else if !NSID_REGEX.is_match(nsid) { 41 + Err("Invalid NSID") 42 + } else { 43 + Ok(Self(CowStr::Owned(nsid.to_smolstr()))) 44 + } 45 + } 46 + 47 + /// Fallible constructor, validates, doesn't allocate 48 + pub fn new_static(nsid: &'static str) -> Result<Self, &'static str> { 49 + if nsid.len() > 317 { 50 + Err("NSID too long") 51 + } else if !NSID_REGEX.is_match(nsid) { 52 + Err("Invalid NSID") 53 + } else { 54 + Ok(Self(CowStr::new_static(nsid))) 55 + } 56 + } 57 + 58 + /// Infallible constructor for when you *know* the string is a valid NSID. 59 + /// Will panic on invalid NSIDs. If you're manually decoding atproto records 60 + /// or API values you know are valid (rather than using serde), this is the one to use. 61 + /// The From<String> and From<CowStr> impls use the same logic. 62 + pub fn raw(nsid: &'n str) -> Self { 63 + if nsid.len() > 317 { 64 + panic!("NSID too long") 65 + } else if !NSID_REGEX.is_match(nsid) { 66 + panic!("Invalid NSID") 67 + } else { 68 + Self(CowStr::Borrowed(nsid)) 69 + } 70 + } 71 + 72 + /// Infallible constructor for when you *know* the string is a valid NSID. 73 + /// Marked unsafe because responsibility for upholding the invariant is on the developer. 74 + pub unsafe fn unchecked(nsid: &'n str) -> Self { 75 + Self(CowStr::Borrowed(nsid)) 76 + } 77 + 78 + /// Returns the domain authority part of the NSID. 79 + pub fn domain_authority(&self) -> &str { 80 + let split = self.0.rfind('.').expect("enforced by constructor"); 81 + &self.0[..split] 82 + } 83 + 84 + /// Returns the name segment of the NSID. 85 + pub fn name(&self) -> &str { 86 + let split = self.0.rfind('.').expect("enforced by constructor"); 87 + &self.0[split + 1..] 88 + } 89 + 90 + pub fn as_str(&self) -> &str { 91 + { 92 + let this = &self.0; 93 + this 94 + } 95 + } 96 + } 97 + 98 + impl<'n> FromStr for Nsid<'n> { 99 + type Err = &'static str; 100 + 101 + /// Has to take ownership due to the lifetime constraints of the FromStr trait. 102 + /// Prefer `Nsid::new()` or `Nsid::raw` if you want to borrow. 103 + fn from_str(s: &str) -> Result<Self, Self::Err> { 104 + Self::new_owned(s) 105 + } 106 + } 107 + 108 + impl IntoStatic for Nsid<'_> { 109 + type Output = Nsid<'static>; 110 + 111 + fn into_static(self) -> Self::Output { 112 + Nsid(self.0.into_static()) 113 + } 114 + } 115 + 116 + impl<'de> Deserialize<'de> for Nsid<'de> { 117 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 118 + where 119 + D: Deserializer<'de>, 120 + { 121 + let value: &str = Deserialize::deserialize(deserializer)?; 122 + Self::new(value).map_err(D::Error::custom) 123 + } 124 + } 125 + 126 + impl fmt::Display for Nsid<'_> { 127 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 128 + f.write_str(&self.0) 129 + } 130 + } 131 + 132 + impl fmt::Debug for Nsid<'_> { 133 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 134 + write!(f, "at://{}", self.0) 135 + } 136 + } 137 + 138 + impl<'n> From<Nsid<'n>> for String { 139 + fn from(value: Nsid) -> Self { 140 + value.0.to_string() 141 + } 142 + } 143 + 144 + impl<'n> From<Nsid<'n>> for CowStr<'n> { 145 + fn from(value: Nsid<'n>) -> Self { 146 + value.0 147 + } 148 + } 149 + 150 + impl From<Nsid<'_>> for SmolStr { 151 + fn from(value: Nsid) -> Self { 152 + value.0.to_smolstr() 153 + } 154 + } 155 + 156 + impl<'n> From<String> for Nsid<'n> { 157 + fn from(value: String) -> Self { 158 + if value.len() > 317 { 159 + panic!("NSID too long") 160 + } else if !NSID_REGEX.is_match(&value) { 161 + panic!("Invalid NSID") 162 + } else { 163 + Self(CowStr::Owned(value.to_smolstr())) 164 + } 165 + } 166 + } 167 + 168 + impl<'n> From<CowStr<'n>> for Nsid<'n> { 169 + fn from(value: CowStr<'n>) -> Self { 170 + if value.len() > 317 { 171 + panic!("NSID too long") 172 + } else if !NSID_REGEX.is_match(&value) { 173 + panic!("Invalid NSID") 174 + } else { 175 + Self(value) 176 + } 177 + } 178 + } 179 + 180 + impl From<SmolStr> for Nsid<'_> { 181 + fn from(value: SmolStr) -> Self { 182 + if value.len() > 317 { 183 + panic!("NSID too long") 184 + } else if !NSID_REGEX.is_match(&value) { 185 + panic!("Invalid NSID") 186 + } else { 187 + Self(CowStr::Owned(value)) 188 + } 189 + } 190 + } 191 + 192 + impl AsRef<str> for Nsid<'_> { 193 + fn as_ref(&self) -> &str { 194 + self.as_str() 195 + } 196 + } 197 + 198 + impl Deref for Nsid<'_> { 199 + type Target = str; 200 + 201 + fn deref(&self) -> &Self::Target { 202 + self.as_str() 203 + } 204 + } 205 + 206 + unsafe impl RecordKeyType for Nsid<'_> { 207 + fn as_str(&self) -> &str { 208 + self.as_str() 209 + } 210 + }
+402
crates/jacquard-common/src/types/recordkey.rs
···
··· 1 + use crate::types::Literal; 2 + use crate::{CowStr, IntoStatic}; 3 + use regex::Regex; 4 + use serde::{Deserialize, Deserializer, Serialize, de::Error}; 5 + use smol_str::{SmolStr, ToSmolStr}; 6 + use std::fmt; 7 + use std::marker::PhantomData; 8 + use std::sync::LazyLock; 9 + use std::{ops::Deref, str::FromStr}; 10 + 11 + /// Trait for generic typed record keys 12 + /// 13 + /// This is deliberately public (so that consumers can develop specialized record key types), 14 + /// but is marked as unsafe, because the implementer is expected to uphold the invariants 15 + /// required by this trait, namely compliance with the [spec](https://atproto.com/specs/record-key) 16 + /// as described by [`RKEY_REGEX`](RKEY_REGEX). 17 + /// 18 + /// This crate provides implementations for TID, NSID, literals, and generic strings 19 + pub unsafe trait RecordKeyType: Clone + Serialize { 20 + fn as_str(&self) -> &str; 21 + } 22 + 23 + #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Debug)] 24 + #[serde(transparent)] 25 + #[repr(transparent)] 26 + pub struct RecordKey<T: RecordKeyType>(pub T); 27 + 28 + impl<T> From<T> for RecordKey<Rkey<'_>> 29 + where 30 + T: RecordKeyType, 31 + { 32 + fn from(value: T) -> Self { 33 + RecordKey(Rkey::from_str(value.as_str()).expect("Invalid rkey")) 34 + } 35 + } 36 + 37 + impl<T> AsRef<str> for RecordKey<T> 38 + where 39 + T: RecordKeyType, 40 + { 41 + fn as_ref(&self) -> &str { 42 + self.0.as_str() 43 + } 44 + } 45 + 46 + impl<T> IntoStatic for RecordKey<T> 47 + where 48 + T: IntoStatic + RecordKeyType, 49 + T::Output: RecordKeyType, 50 + { 51 + type Output = RecordKey<T::Output>; 52 + 53 + fn into_static(self) -> Self::Output { 54 + RecordKey(self.0.into_static()) 55 + } 56 + } 57 + 58 + /// ATProto Record Key (type `any`) 59 + /// Catch-all for any string meeting the overall Record Key requirements detailed https://atproto.com/specs/record-key 60 + #[derive(Clone, PartialEq, Eq, Serialize, Hash)] 61 + #[serde(transparent)] 62 + #[repr(transparent)] 63 + pub struct Rkey<'r>(CowStr<'r>); 64 + 65 + unsafe impl<'r> RecordKeyType for Rkey<'r> { 66 + fn as_str(&self) -> &str { 67 + self.0.as_ref() 68 + } 69 + } 70 + 71 + pub static RKEY_REGEX: LazyLock<Regex> = 72 + LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9.\-_:~]{1,512}$").unwrap()); 73 + 74 + /// AT Protocol rkey 75 + impl<'r> Rkey<'r> { 76 + /// Fallible constructor, validates, borrows from input 77 + pub fn new(rkey: &'r str) -> Result<Self, &'static str> { 78 + if [".", ".."].contains(&rkey) { 79 + Err("Disallowed rkey") 80 + } else if !RKEY_REGEX.is_match(rkey) { 81 + Err("Invalid rkey") 82 + } else { 83 + Ok(Self(CowStr::Borrowed(rkey))) 84 + } 85 + } 86 + 87 + /// Fallible constructor, validates, borrows from input 88 + pub fn new_owned(rkey: impl AsRef<str>) -> Result<Self, &'static str> { 89 + let rkey = rkey.as_ref(); 90 + if [".", ".."].contains(&rkey) { 91 + Err("Disallowed rkey") 92 + } else if !RKEY_REGEX.is_match(rkey) { 93 + Err("Invalid rkey") 94 + } else { 95 + Ok(Self(CowStr::Owned(rkey.to_smolstr()))) 96 + } 97 + } 98 + 99 + /// Fallible constructor, validates, doesn't allocate 100 + pub fn new_static(rkey: &'static str) -> Result<Self, &'static str> { 101 + if [".", ".."].contains(&rkey) { 102 + Err("Disallowed rkey") 103 + } else if !RKEY_REGEX.is_match(rkey) { 104 + Err("Invalid rkey") 105 + } else { 106 + Ok(Self(CowStr::new_static(rkey))) 107 + } 108 + } 109 + 110 + /// Infallible constructor for when you *know* the string is a valid rkey. 111 + /// Will panic on invalid rkeys. If you're manually decoding atproto records 112 + /// or API values you know are valid (rather than using serde), this is the one to use. 113 + /// The From impls use the same logic. 114 + pub fn raw(rkey: &'r str) -> Self { 115 + if [".", ".."].contains(&rkey) { 116 + panic!("Disallowed rkey") 117 + } else if !RKEY_REGEX.is_match(rkey) { 118 + panic!("Invalid rkey") 119 + } else { 120 + Self(CowStr::Borrowed(rkey)) 121 + } 122 + } 123 + 124 + /// Infallible constructor for when you *know* the string is a valid rkey. 125 + /// Marked unsafe because responsibility for upholding the invariant is on the developer. 126 + pub unsafe fn unchecked(rkey: &'r str) -> Self { 127 + Self(CowStr::Borrowed(rkey)) 128 + } 129 + 130 + pub fn as_str(&self) -> &str { 131 + { 132 + let this = &self.0; 133 + this 134 + } 135 + } 136 + } 137 + 138 + impl<'r> FromStr for Rkey<'r> { 139 + type Err = &'static str; 140 + 141 + fn from_str(s: &str) -> Result<Self, Self::Err> { 142 + if [".", ".."].contains(&s) { 143 + Err("Disallowed rkey") 144 + } else if !RKEY_REGEX.is_match(s) { 145 + Err("Invalid rkey") 146 + } else { 147 + Ok(Self(CowStr::Owned(s.to_smolstr()))) 148 + } 149 + } 150 + } 151 + 152 + impl IntoStatic for Rkey<'_> { 153 + type Output = Rkey<'static>; 154 + 155 + fn into_static(self) -> Self::Output { 156 + Rkey(self.0.into_static()) 157 + } 158 + } 159 + 160 + impl<'de> Deserialize<'de> for Rkey<'de> { 161 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 162 + where 163 + D: Deserializer<'de>, 164 + { 165 + let value: &str = Deserialize::deserialize(deserializer)?; 166 + Self::new(value).map_err(D::Error::custom) 167 + } 168 + } 169 + 170 + impl fmt::Display for Rkey<'_> { 171 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 172 + f.write_str(&self.0) 173 + } 174 + } 175 + 176 + impl fmt::Debug for Rkey<'_> { 177 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 178 + write!(f, "record-key:{}", self.0) 179 + } 180 + } 181 + 182 + impl From<Rkey<'_>> for String { 183 + fn from(value: Rkey<'_>) -> Self { 184 + value.0.to_string() 185 + } 186 + } 187 + 188 + impl<'r> From<Rkey<'r>> for CowStr<'r> { 189 + fn from(value: Rkey<'r>) -> Self { 190 + value.0 191 + } 192 + } 193 + 194 + impl<'r> From<Rkey<'r>> for SmolStr { 195 + fn from(value: Rkey) -> Self { 196 + value.0.to_smolstr() 197 + } 198 + } 199 + 200 + impl<'r> From<String> for Rkey<'r> { 201 + fn from(value: String) -> Self { 202 + if [".", ".."].contains(&value.as_str()) { 203 + panic!("Disallowed rkey") 204 + } else if !RKEY_REGEX.is_match(&value) { 205 + panic!("Invalid rkey") 206 + } else { 207 + Self(CowStr::Owned(value.to_smolstr())) 208 + } 209 + } 210 + } 211 + 212 + impl<'r> From<CowStr<'r>> for Rkey<'r> { 213 + fn from(value: CowStr<'r>) -> Self { 214 + if [".", ".."].contains(&value.as_ref()) { 215 + panic!("Disallowed rkey") 216 + } else if !RKEY_REGEX.is_match(&value) { 217 + panic!("Invalid rkey") 218 + } else { 219 + Self(value) 220 + } 221 + } 222 + } 223 + 224 + impl AsRef<str> for Rkey<'_> { 225 + fn as_ref(&self) -> &str { 226 + self.as_str() 227 + } 228 + } 229 + 230 + impl Deref for Rkey<'_> { 231 + type Target = str; 232 + 233 + fn deref(&self) -> &Self::Target { 234 + self.0.as_ref() 235 + } 236 + } 237 + 238 + /// ATProto Record Key (type `literal:<value>`) 239 + /// Zero-sized type, literal is associated constant of type parameter 240 + /// 241 + /// TODO: macro to construct arbitrary ones of these and the associated marker struct 242 + #[derive(Clone, PartialEq, Eq, Serialize, Hash)] 243 + pub struct LiteralKey<T: Literal = SelfRecord> { 244 + literal: PhantomData<T>, 245 + } 246 + 247 + #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] 248 + pub struct SelfRecord; 249 + 250 + impl Literal for SelfRecord { 251 + const LITERAL: &'static str = "self"; 252 + } 253 + 254 + unsafe impl<T: Literal> RecordKeyType for LiteralKey<T> { 255 + fn as_str(&self) -> &str { 256 + T::LITERAL 257 + } 258 + } 259 + 260 + /// AT Protocol rkey 261 + impl<T: Literal> LiteralKey<T> { 262 + /// Fallible constructor, validates, borrows from input 263 + pub fn new(rkey: impl AsRef<str>) -> Result<Self, &'static str> { 264 + let rkey = rkey.as_ref(); 265 + if !rkey.eq_ignore_ascii_case(T::LITERAL) { 266 + Err("Invalid literal rkey - does not match literal") 267 + } else if [".", ".."].contains(&rkey) { 268 + Err("Disallowed rkey") 269 + } else if !RKEY_REGEX.is_match(rkey) { 270 + Err("Invalid rkey") 271 + } else { 272 + Ok(Self { 273 + literal: PhantomData, 274 + }) 275 + } 276 + } 277 + 278 + /// Infallible constructor for when you *know* the string is a valid rkey. 279 + /// Will panic on invalid rkeys. If you're manually decoding atproto records 280 + /// or API values you know are valid (rather than using serde), this is the one to use. 281 + /// The From<String> and From<CowStr> impls use the same logic. 282 + pub fn raw(rkey: &str) -> Self { 283 + if !rkey.eq_ignore_ascii_case(T::LITERAL) { 284 + panic!( 285 + "Invalid literal rkey - does not match literal {}", 286 + T::LITERAL 287 + ) 288 + } else if [".", ".."].contains(&rkey.as_ref()) { 289 + panic!("Disallowed rkey") 290 + } else if !RKEY_REGEX.is_match(rkey) { 291 + panic!("Invalid rkey") 292 + } else { 293 + Self { 294 + literal: PhantomData, 295 + } 296 + } 297 + } 298 + 299 + /// Infallible type constructor 300 + /// 301 + /// # Safety 302 + /// Does not validate that the literal is a valid record key 303 + pub unsafe fn t() -> Self { 304 + Self { 305 + literal: PhantomData, 306 + } 307 + } 308 + 309 + pub fn as_str(&self) -> &str { 310 + T::LITERAL 311 + } 312 + } 313 + 314 + impl<T: Literal> FromStr for LiteralKey<T> { 315 + type Err = &'static str; 316 + 317 + fn from_str(s: &str) -> Result<Self, Self::Err> { 318 + Self::new(s) 319 + } 320 + } 321 + 322 + impl<'de, T: Literal> Deserialize<'de> for LiteralKey<T> { 323 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 324 + where 325 + D: Deserializer<'de>, 326 + { 327 + let value: &str = Deserialize::deserialize(deserializer)?; 328 + Self::new(value).map_err(D::Error::custom) 329 + } 330 + } 331 + 332 + impl<T: Literal> fmt::Display for LiteralKey<T> { 333 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 334 + f.write_str(T::LITERAL) 335 + } 336 + } 337 + 338 + impl<T: Literal> fmt::Debug for LiteralKey<T> { 339 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 340 + write!(f, "literal:{}", T::LITERAL) 341 + } 342 + } 343 + 344 + impl<'r, T: Literal> From<LiteralKey<T>> for String { 345 + fn from(_value: LiteralKey<T>) -> Self { 346 + T::LITERAL.to_string() 347 + } 348 + } 349 + 350 + impl<'r, T: Literal> From<LiteralKey<T>> for CowStr<'r> { 351 + fn from(_value: LiteralKey<T>) -> Self { 352 + CowStr::Borrowed(T::LITERAL) 353 + } 354 + } 355 + 356 + impl<T: Literal> TryFrom<String> for LiteralKey<T> { 357 + type Error = &'static str; 358 + fn try_from(value: String) -> Result<Self, Self::Error> { 359 + if !value.eq_ignore_ascii_case(T::LITERAL) { 360 + Err("Invalid literal rkey - does not match literal") 361 + } else if [".", ".."].contains(&value.as_str()) { 362 + Err("Disallowed rkey") 363 + } else if !RKEY_REGEX.is_match(&value) { 364 + Err("Invalid rkey") 365 + } else { 366 + Ok(Self { 367 + literal: PhantomData, 368 + }) 369 + } 370 + } 371 + } 372 + 373 + impl<'r, T: Literal> TryFrom<CowStr<'r>> for LiteralKey<T> { 374 + type Error = &'static str; 375 + fn try_from(value: CowStr<'r>) -> Result<Self, Self::Error> { 376 + if !value.eq_ignore_ascii_case(T::LITERAL) { 377 + Err("Invalid literal rkey - does not match literal") 378 + } else if [".", ".."].contains(&value.as_ref()) { 379 + Err("Disallowed rkey") 380 + } else if !RKEY_REGEX.is_match(&value) { 381 + Err("Invalid rkey") 382 + } else { 383 + Ok(Self { 384 + literal: PhantomData, 385 + }) 386 + } 387 + } 388 + } 389 + 390 + impl<T: Literal> AsRef<str> for LiteralKey<T> { 391 + fn as_ref(&self) -> &str { 392 + self.as_str() 393 + } 394 + } 395 + 396 + impl<T: Literal> Deref for LiteralKey<T> { 397 + type Target = str; 398 + 399 + fn deref(&self) -> &Self::Target { 400 + self.as_str() 401 + } 402 + }
+38 -43
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); ··· 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(|| { ··· 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 ··· 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. ··· 121 } 122 } 123 124 - impl FromStr for Tid<'_> { 125 type Err = &'static str; 126 127 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 128 /// Prefer `Did::new()` or `Did::raw` if you want to borrow. 129 fn from_str(s: &str) -> Result<Self, Self::Err> { 130 - Self::from_cowstr(CowStr::Borrowed(s).into_static()) 131 } 132 } 133 134 - impl<'de> Deserialize<'de> for Tid<'de> { 135 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 136 where 137 D: Deserializer<'de>, 138 { 139 - let value = Deserialize::deserialize(deserializer)?; 140 Self::new(value).map_err(D::Error::custom) 141 } 142 } 143 144 - impl fmt::Display for Tid<'_> { 145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 146 f.write_str(&self.0) 147 } 148 } 149 150 - impl<'t> From<Tid<'t>> for String { 151 - fn from(value: Tid<'t>) -> Self { 152 value.0.to_string() 153 } 154 } 155 156 - impl<'t> From<Tid<'t>> for CowStr<'t> { 157 - fn from(value: Tid<'t>) -> Self { 158 value.0 159 } 160 } 161 162 - impl From<String> for Tid<'static> { 163 fn from(value: String) -> Self { 164 if value.len() != 13 { 165 panic!("TID must be 13 characters") 166 } else if !TID_REGEX.is_match(&value) { 167 panic!("Invalid TID") 168 } else { 169 - Self(CowStr::Owned(value.to_compact_string())) 170 } 171 } 172 } 173 174 - impl<'t> From<CowStr<'t>> for Tid<'t> { 175 fn from(value: CowStr<'t>) -> Self { 176 if value.len() != 13 { 177 panic!("TID must be 13 characters") 178 } else if !TID_REGEX.is_match(&value) { 179 panic!("Invalid TID") 180 } else { 181 - Self(value) 182 } 183 } 184 } 185 186 - impl AsRef<str> for Tid<'_> { 187 fn as_ref(&self) -> &str { 188 self.as_str() 189 } 190 } 191 192 - impl Deref for Tid<'_> { 193 type Target = str; 194 195 fn deref(&self) -> &Self::Target {
··· 1 + use serde::{Deserialize, Deserializer, Serialize, de::Error}; 2 + use smol_str::{SmolStr, SmolStrBuilder}; 3 use std::fmt; 4 use std::sync::LazyLock; 5 use std::{ops::Deref, str::FromStr}; 6 7 + use crate::CowStr; 8 use crate::types::integer::LimitedU32; 9 use regex::Regex; 10 11 + fn s32_encode(mut i: u64) -> SmolStr { 12 const S32_CHAR: &[u8] = b"234567abcdefghijklmnopqrstuvwxyz"; 13 14 + let mut s = SmolStrBuilder::new(); 15 for _ in 0..13 { 16 let c = i & 0x1F; 17 s.push(S32_CHAR[c as usize] as char); ··· 19 i >>= 5; 20 } 21 22 + let mut builder = SmolStrBuilder::new(); 23 + for c in s.finish().chars().rev() { 24 + builder.push(c); 25 + } 26 + builder.finish() 27 } 28 29 static TID_REGEX: LazyLock<Regex> = LazyLock::new(|| { ··· 35 /// [Timestamp Identifier]: https://atproto.com/specs/tid 36 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)] 37 #[serde(transparent)] 38 + #[repr(transparent)] 39 + pub struct Tid(SmolStr); 40 41 + impl Tid { 42 /// Parses a `TID` from the given string. 43 + pub fn new(tid: impl AsRef<str>) -> Result<Self, &'static str> { 44 + let tid = tid.as_ref(); 45 if tid.len() != 13 { 46 Err("TID must be 13 characters") 47 + } else if !TID_REGEX.is_match(&tid.as_ref()) { 48 Err("Invalid TID") 49 } else { 50 + Ok(Self(SmolStr::new_inline(&tid))) 51 } 52 } 53 ··· 55 /// Will panic on invalid TID. If you're manually decoding atproto records 56 /// or API values you know are valid (rather than using serde), this is the one to use. 57 /// The From<String> and From<CowStr> impls use the same logic. 58 + pub fn raw(tid: impl AsRef<str>) -> Self { 59 + let tid = tid.as_ref(); 60 if tid.len() != 13 { 61 panic!("TID must be 13 characters") 62 } else if !TID_REGEX.is_match(&tid) { 63 panic!("Invalid TID") 64 } else { 65 + Self(SmolStr::new_inline(tid)) 66 } 67 } 68 69 /// Infallible constructor for when you *know* the string is a valid TID. 70 /// Marked unsafe because responsibility for upholding the invariant is on the developer. 71 + pub unsafe fn unchecked(tid: impl AsRef<str>) -> Self { 72 + let tid = tid.as_ref(); 73 + Self(SmolStr::new_inline(tid)) 74 } 75 76 /// Construct a new timestamp with the specified clock ID. ··· 116 } 117 } 118 119 + impl FromStr for Tid { 120 type Err = &'static str; 121 122 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 123 /// Prefer `Did::new()` or `Did::raw` if you want to borrow. 124 fn from_str(s: &str) -> Result<Self, Self::Err> { 125 + Self::new(s) 126 } 127 } 128 129 + impl<'de> Deserialize<'de> for Tid { 130 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 131 where 132 D: Deserializer<'de>, 133 { 134 + let value: &str = Deserialize::deserialize(deserializer)?; 135 Self::new(value).map_err(D::Error::custom) 136 } 137 } 138 139 + impl fmt::Display for Tid { 140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 141 f.write_str(&self.0) 142 } 143 } 144 145 + impl From<Tid> for String { 146 + fn from(value: Tid) -> Self { 147 value.0.to_string() 148 } 149 } 150 151 + impl From<Tid> for SmolStr { 152 + fn from(value: Tid) -> Self { 153 value.0 154 } 155 } 156 157 + impl From<String> for Tid { 158 fn from(value: String) -> Self { 159 if value.len() != 13 { 160 panic!("TID must be 13 characters") 161 } else if !TID_REGEX.is_match(&value) { 162 panic!("Invalid TID") 163 } else { 164 + Self(SmolStr::new_inline(&value)) 165 } 166 } 167 } 168 169 + impl<'t> From<CowStr<'t>> for Tid { 170 fn from(value: CowStr<'t>) -> Self { 171 if value.len() != 13 { 172 panic!("TID must be 13 characters") 173 } else if !TID_REGEX.is_match(&value) { 174 panic!("Invalid TID") 175 } else { 176 + Self(SmolStr::new_inline(&value)) 177 } 178 } 179 } 180 181 + impl AsRef<str> for Tid { 182 fn as_ref(&self) -> &str { 183 self.as_str() 184 } 185 } 186 187 + impl Deref for Tid { 188 type Target = str; 189 190 fn deref(&self) -> &Self::Target {