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 83 checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 84 84 85 85 [[package]] 86 - name = "bumpalo" 87 - version = "3.19.0" 86 + name = "borsh" 87 + version = "1.5.7" 88 88 source = "registry+https://github.com/rust-lang/crates.io-index" 89 - checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 89 + checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" 90 + dependencies = [ 91 + "cfg_aliases", 92 + ] 90 93 91 94 [[package]] 92 - name = "castaway" 93 - version = "0.2.4" 95 + name = "bumpalo" 96 + version = "3.19.0" 94 97 source = "registry+https://github.com/rust-lang/crates.io-index" 95 - checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" 96 - dependencies = [ 97 - "rustversion", 98 - ] 98 + checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 99 99 100 100 [[package]] 101 101 name = "cc" ··· 112 112 version = "1.0.3" 113 113 source = "registry+https://github.com/rust-lang/crates.io-index" 114 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" 115 121 116 122 [[package]] 117 123 name = "chrono" ··· 187 193 checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 188 194 189 195 [[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 196 name = "core-foundation-sys" 205 197 version = "0.8.7" 206 198 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 334 326 dependencies = [ 335 327 "chrono", 336 328 "cid", 337 - "compact_str", 338 329 "miette", 339 330 "multibase", 340 331 "multihash", ··· 342 333 "serde", 343 334 "serde_html_form", 344 335 "serde_json", 336 + "smol_str", 345 337 "thiserror", 346 338 ] 347 339 ··· 575 567 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 576 568 577 569 [[package]] 578 - name = "static_assertions" 579 - version = "1.1.0" 570 + name = "smol_str" 571 + version = "0.3.2" 580 572 source = "registry+https://github.com/rust-lang/crates.io-index" 581 - checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 573 + checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d" 574 + dependencies = [ 575 + "borsh", 576 + "serde", 577 + ] 582 578 583 579 [[package]] 584 580 name = "strsim"
+7
Cargo.toml
··· 7 7 edition = "2024" 8 8 version = "0.1.0" 9 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 + 10 17 11 18 description = "A simple Rust project using Nix" 12 19
+26
README.md
··· 29 29 ``` 30 30 31 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 8 [dependencies] 9 9 chrono = "0.4.42" 10 10 cid = { version = "0.11.1", features = ["serde", "std"] } 11 - compact_str = "0.9.0" 12 11 miette = "7.6.0" 13 12 multibase = "0.9.1" 14 13 multihash = "0.19.3" ··· 16 15 serde = { version = "1.0.227", features = ["derive"] } 17 16 serde_html_form = "0.2.8" 18 17 serde_json = "1.0.145" 18 + smol_str = { version = "0.3.2", features = ["serde"] } 19 19 thiserror = "2.0.16"
+13 -9
crates/jacquard-common/src/cowstr.rs
··· 1 - use compact_str::CompactString; 2 1 use serde::{Deserialize, Serialize}; 2 + use smol_str::SmolStr; 3 3 use std::{ 4 4 borrow::Cow, 5 5 fmt, ··· 10 10 use crate::IntoStatic; 11 11 12 12 /// Shamelessly copied from https://github.com/bearcove/merde 13 - /// A copy-on-write string type that uses [`CompactString`] for 13 + /// A copy-on-write immutable string type that uses [`SmolStr`] for 14 14 /// the "owned" variant. 15 15 /// 16 16 /// The standard [`Cow`] type cannot be used, since 17 - /// `<str as ToOwned>::Owned` is `String`, and not `CompactString`. 17 + /// `<str as ToOwned>::Owned` is `String`, and not `SmolStr`. 18 18 #[derive(Clone)] 19 19 pub enum CowStr<'s> { 20 20 Borrowed(&'s str), 21 - Owned(CompactString), 21 + Owned(SmolStr), 22 22 } 23 23 24 24 impl CowStr<'static> { ··· 26 26 /// if the `compact_str` feature is disabled, or if the string is longer 27 27 /// than `MAX_INLINE_SIZE`. 28 28 pub fn copy_from_str(s: &str) -> Self { 29 - Self::Owned(CompactString::from(s)) 29 + Self::Owned(SmolStr::from(s)) 30 + } 31 + 32 + pub fn new_static(s: &'static str) -> Self { 33 + Self::Owned(SmolStr::new_static(s)) 30 34 } 31 35 } 32 36 ··· 38 42 39 43 #[inline] 40 44 pub fn from_utf8_owned(s: Vec<u8>) -> Result<Self, std::str::Utf8Error> { 41 - Ok(Self::Owned(CompactString::from_utf8(s)?)) 45 + Ok(Self::Owned(SmolStr::new(std::str::from_utf8(&s)?))) 42 46 } 43 47 44 48 #[inline] 45 49 pub fn from_utf8_lossy(s: &'s [u8]) -> Self { 46 - Self::Owned(CompactString::from_utf8_lossy(s)) 50 + Self::Owned(String::from_utf8_lossy(&s).into()) 47 51 } 48 52 49 53 /// # Safety ··· 51 55 /// This function is unsafe because it does not check that the bytes are valid UTF-8. 52 56 #[inline] 53 57 pub unsafe fn from_utf8_unchecked(s: &'s [u8]) -> Self { 54 - unsafe { Self::Owned(CompactString::from_utf8_unchecked(s)) } 58 + unsafe { Self::Owned(SmolStr::new(std::str::from_utf8_unchecked(s))) } 55 59 } 56 60 } 57 61 ··· 133 137 fn from(s: CowStr<'_>) -> Self { 134 138 match s { 135 139 CowStr::Borrowed(s) => s.into(), 136 - CowStr::Owned(s) => s.into(), 140 + CowStr::Owned(s) => String::from(s).into_boxed_str(), 137 141 } 138 142 } 139 143 }
+8
crates/jacquard-common/src/types.rs
··· 1 1 pub mod aturi; 2 2 pub mod blob; 3 3 pub mod cid; 4 + pub mod collection; 4 5 pub mod datetime; 5 6 pub mod did; 6 7 pub mod handle; ··· 8 9 pub mod integer; 9 10 pub mod link; 10 11 pub mod nsid; 12 + pub mod recordkey; 11 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 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; 1 9 use std::fmt; 2 10 use std::sync::LazyLock; 3 11 use std::{ops::Deref, str::FromStr}; 4 12 5 - use compact_str::ToCompactString; 6 - use serde::{Deserialize, Deserializer, Serialize, de::Error}; 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 + } 7 28 8 - use crate::{CowStr, IntoStatic}; 9 - use regex::Regex; 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 + } 10 35 11 - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Hash)] 12 - #[serde(transparent)] 13 - pub struct AtUri<'a>(CowStr<'a>); 36 + pub type UriPathBuf = UriPath<'static>; 14 37 15 - pub static AT_URI_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^$").unwrap()); 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 + }); 16 41 17 - impl<'a> AtUri<'a> { 42 + impl<'u> AtUri<'u> { 18 43 /// 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") 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 + } 24 73 } else { 25 - Ok(Self(CowStr::Borrowed(uri))) 74 + Err("Invalid at:// URI via regex") 26 75 } 27 76 } 28 77 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") 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 + } 35 108 } else { 36 - Ok(Self(uri.into_static())) 109 + Err("Invalid at:// URI via regex") 37 110 } 38 111 } 39 112 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") 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 + } 49 143 } else { 50 - Self(CowStr::Borrowed(uri)) 144 + Err("Invalid at:// URI via regex") 51 145 } 52 146 } 53 147 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)) 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 + } 58 190 } 59 191 60 192 pub fn as_str(&self) -> &str { 61 193 { 62 - let this = &self.0; 194 + let this = &self.uri; 63 195 this 64 196 } 65 197 } ··· 69 201 type Err = &'static str; 70 202 71 203 /// 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. 204 + /// Prefer `AtUri::new()` or `AtUri::raw()` if you want to borrow. 73 205 fn from_str(s: &str) -> Result<Self, Self::Err> { 74 - Self::from_cowstr(CowStr::Owned(s.to_compact_string())) 206 + Self::new_owned(s) 75 207 } 76 208 } 77 209 78 - impl<'ae> Deserialize<'ae> for AtUri<'ae> { 210 + impl<'de> Deserialize<'de> for AtUri<'de> { 79 211 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 80 212 where 81 - D: Deserializer<'ae>, 213 + D: Deserializer<'de>, 82 214 { 83 215 let value = Deserialize::deserialize(deserializer)?; 84 216 Self::new(value).map_err(D::Error::custom) 85 217 } 86 218 } 87 219 88 - impl fmt::Display for AtUri<'_> { 89 - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 90 - f.write_str(&self.0) 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) 91 226 } 92 227 } 93 228 94 - impl<'a> From<AtUri<'a>> for String { 95 - fn from(value: AtUri<'a>) -> Self { 96 - value.0.to_string() 229 + impl fmt::Display for AtUri<'_> { 230 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 231 + f.write_str(&self.uri) 97 232 } 98 233 } 99 234 100 - impl<'s> From<&'s AtUri<'_>> for &'s str { 101 - fn from(value: &'s AtUri<'_>) -> Self { 102 - value.0.as_ref() 235 + impl<'d> From<AtUri<'d>> for String { 236 + fn from(value: AtUri<'d>) -> Self { 237 + value.uri.to_string() 103 238 } 104 239 } 105 240 106 - impl<'a> From<AtUri<'a>> for CowStr<'a> { 107 - fn from(value: AtUri<'a>) -> Self { 108 - value.0 241 + impl<'d> From<AtUri<'d>> for CowStr<'d> { 242 + fn from(value: AtUri<'d>) -> Self { 243 + value.uri 109 244 } 110 245 } 111 246 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 - } 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) 121 252 } 122 253 } 123 254 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 - } 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) 133 260 } 134 261 } 135 262 136 263 impl AsRef<str> for AtUri<'_> { 137 264 fn as_ref(&self) -> &str { 138 - self.as_str() 265 + &self.uri.as_ref() 139 266 } 140 267 } 141 268 ··· 143 270 type Target = str; 144 271 145 272 fn deref(&self) -> &Self::Target { 146 - self.as_str() 273 + self.uri.as_ref() 147 274 } 148 275 }
+23 -5
crates/jacquard-common/src/types/blob.rs
··· 1 - use crate::{CowStr, types::cid::Cid}; 2 - use compact_str::ToCompactString; 1 + use crate::{CowStr, IntoStatic, types::cid::Cid}; 3 2 #[allow(unused)] 4 3 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error}; 4 + use smol_str::ToSmolStr; 5 + use std::convert::Infallible; 5 6 #[allow(unused)] 6 7 use std::{ 7 8 borrow::Cow, ··· 39 40 /// Wrapper for file type 40 41 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] 41 42 #[serde(transparent)] 43 + #[repr(transparent)] 42 44 pub struct MimeType<'m>(pub CowStr<'m>); 43 45 44 46 impl<'m> MimeType<'m> { 45 47 /// Fallible constructor, validates, borrows from input 46 48 pub fn new(mime_type: &'m str) -> Result<MimeType<'m>, &'static str> { 47 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)) 48 58 } 49 59 50 60 /// Fallible constructor from an existing CowStr, borrows ··· 66 76 } 67 77 68 78 impl FromStr for MimeType<'_> { 69 - type Err = &'static str; 79 + type Err = Infallible; 70 80 71 81 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 72 82 fn from_str(s: &str) -> Result<Self, Self::Err> { 73 - Self::from_cowstr(CowStr::Owned(s.to_compact_string())) 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()) 74 92 } 75 93 } 76 94 ··· 107 125 108 126 impl From<String> for MimeType<'static> { 109 127 fn from(value: String) -> Self { 110 - Self(CowStr::Owned(value.to_compact_string())) 128 + Self(CowStr::Owned(value.to_smolstr())) 111 129 } 112 130 } 113 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 - 1 + use crate::{CowStr, IntoStatic}; 6 2 pub use cid::Cid as IpldCid; 7 - 8 - use crate::CowStr; 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}; 9 6 10 7 /// raw 11 8 pub const ATP_CID_CODEC: u64 = 0x55; ··· 47 44 let s = CowStr::Owned( 48 45 cid.to_string_of_base(ATP_CID_BASE) 49 46 .unwrap_or_default() 50 - .to_compact_string(), 47 + .to_smolstr(), 51 48 ); 52 49 Self::Ipld { cid, s } 53 50 } ··· 89 86 90 87 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 91 88 fn from_str(s: &str) -> Result<Self, Self::Err> { 92 - Ok(Cid::Str(CowStr::Owned(s.to_compact_string()))) 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 + } 93 104 } 94 105 } 95 106 ··· 164 175 165 176 impl From<String> for Cid<'_> { 166 177 fn from(value: String) -> Self { 167 - Cid::Str(CowStr::Owned(value.to_compact_string())) 178 + Cid::Str(CowStr::Owned(value.to_smolstr())) 168 179 } 169 180 } 170 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 1 use chrono::DurationRound; 5 - use compact_str::ToCompactString; 6 2 use serde::Serializer; 7 3 use serde::{Deserialize, Deserializer, Serialize, de::Error}; 4 + use smol_str::ToSmolStr; 5 + use std::sync::LazyLock; 6 + use std::{cmp, str::FromStr}; 8 7 9 8 use crate::{CowStr, IntoStatic}; 10 9 use regex::Regex; ··· 58 57 // This serialization format is compatible with ISO 8601. 59 58 let serialized = CowStr::Owned( 60 59 dt.to_rfc3339_opts(chrono::SecondsFormat::Micros, true) 61 - .to_compact_string(), 60 + .to_smolstr(), 62 61 ); 63 62 Self { serialized, dt } 64 63 } ··· 139 138 if ISO8601_REGEX.is_match(&value) { 140 139 let dt = chrono::DateTime::parse_from_rfc3339(&value)?; 141 140 Ok(Self { 142 - serialized: CowStr::Owned(value.to_compact_string()), 141 + serialized: CowStr::Owned(value.to_smolstr()), 143 142 dt, 144 143 }) 145 144 } else {
+53 -14
crates/jacquard-common/src/types/did.rs
··· 1 + use crate::{CowStr, IntoStatic}; 2 + use regex::Regex; 3 + use serde::{Deserialize, Deserializer, Serialize, de::Error}; 4 + use smol_str::ToSmolStr; 1 5 use std::fmt; 2 6 use std::sync::LazyLock; 3 7 use std::{ops::Deref, str::FromStr}; 4 8 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)] 9 + #[derive(Clone, PartialEq, Eq, Serialize, Hash)] 12 10 #[serde(transparent)] 11 + #[repr(transparent)] 13 12 pub struct Did<'d>(CowStr<'d>); 14 13 15 14 pub static DID_REGEX: LazyLock<Regex> = ··· 18 17 impl<'d> Did<'d> { 19 18 /// Fallible constructor, validates, borrows from input 20 19 pub fn new(did: &'d str) -> Result<Self, &'static str> { 20 + let did = did.strip_prefix("at://").unwrap_or(did); 21 21 if did.len() > 2048 { 22 22 Err("DID too long") 23 23 } else if !DID_REGEX.is_match(did) { ··· 27 27 } 28 28 } 29 29 30 - /// Fallible constructor from an existing CowStr, takes ownership 31 - pub fn from_cowstr(did: CowStr<'d>) -> Result<Did<'d>, &'static str> { 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); 32 34 if did.len() > 2048 { 33 35 Err("DID too long") 34 - } else if !DID_REGEX.is_match(&did) { 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) { 35 49 Err("Invalid DID") 36 50 } else { 37 - Ok(Self(did.into_static())) 51 + Ok(Self(CowStr::new_static(did))) 38 52 } 39 53 } 40 54 ··· 43 57 /// or API values you know are valid (rather than using serde), this is the one to use. 44 58 /// The From<String> and From<CowStr> impls use the same logic. 45 59 pub fn raw(did: &'d str) -> Self { 60 + let did = did.strip_prefix("at://").unwrap_or(did); 46 61 if did.len() > 2048 { 47 62 panic!("DID too long") 48 63 } else if !DID_REGEX.is_match(did) { ··· 72 87 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 73 88 /// Prefer `Did::new()` or `Did::raw` if you want to borrow. 74 89 fn from_str(s: &str) -> Result<Self, Self::Err> { 75 - Self::from_cowstr(CowStr::Borrowed(s).into_static()) 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()) 76 99 } 77 100 } 78 101 ··· 92 115 } 93 116 } 94 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 + 95 124 impl<'d> From<Did<'d>> for String { 96 125 fn from(value: Did<'d>) -> Self { 97 126 value.0.to_string() ··· 106 135 107 136 impl From<String> for Did<'static> { 108 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 + }; 109 143 if value.len() > 2048 { 110 144 panic!("DID too long") 111 145 } else if !DID_REGEX.is_match(&value) { 112 146 panic!("Invalid DID") 113 147 } else { 114 - Self(CowStr::Owned(value.to_compact_string())) 148 + Self(value.into_static()) 115 149 } 116 150 } 117 151 } 118 152 119 153 impl<'d> From<CowStr<'d>> for Did<'d> { 120 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 + }; 121 160 if value.len() > 2048 { 122 161 panic!("DID too long") 123 162 } else if !DID_REGEX.is_match(&value) { 124 163 panic!("Invalid DID") 125 164 } else { 126 - Self(value) 165 + Self(value.into_static()) 127 166 } 128 167 } 129 168 }
+88 -30
crates/jacquard-common/src/types/handle.rs
··· 1 + use crate::{CowStr, IntoStatic}; 2 + use regex::Regex; 3 + use serde::{Deserialize, Deserializer, Serialize, de::Error}; 4 + use smol_str::ToSmolStr; 1 5 use std::fmt; 2 6 use std::sync::LazyLock; 3 7 use std::{ops::Deref, str::FromStr}; 4 8 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)] 9 + #[derive(Clone, PartialEq, Eq, Serialize, Hash)] 12 10 #[serde(transparent)] 11 + #[repr(transparent)] 13 12 pub struct Handle<'h>(CowStr<'h>); 14 13 15 14 pub static HANDLE_REGEX: LazyLock<Regex> = LazyLock::new(|| { 16 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() 17 16 }); 18 17 18 + /// AT Protocol handle 19 19 impl<'h> Handle<'h> { 20 20 /// Fallible constructor, validates, borrows from input 21 21 /// 22 22 /// Accepts (and strips) preceding '@' if present 23 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 { 24 + let handle = handle 25 + .strip_prefix("at://") 26 + .unwrap_or(handle) 27 + .strip_prefix('@') 28 + .unwrap_or(handle); 29 + if handle.len() > 253 { 26 30 Err("handle too long") 27 31 } else if !HANDLE_REGEX.is_match(handle) { 28 32 Err("Invalid handle") ··· 31 35 } 32 36 } 33 37 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 { 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 { 44 47 Err("handle too long") 45 - } else if !HANDLE_REGEX.is_match(&handle) { 48 + } else if !HANDLE_REGEX.is_match(handle) { 46 49 Err("Invalid handle") 47 50 } else { 48 - Ok(Self(handle.into_static())) 51 + Ok(Self(CowStr::Owned(handle.to_smolstr()))) 49 52 } 50 53 } 51 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 + } 52 70 /// Infallible constructor for when you *know* the string is a valid handle. 53 71 /// Will panic on invalid handles. If you're manually decoding atproto records 54 72 /// or API values you know are valid (rather than using serde), this is the one to use. ··· 56 74 /// 57 75 /// Accepts (and strips) preceding '@' if present 58 76 pub fn raw(handle: &'h str) -> Self { 59 - let handle = handle.strip_prefix('@').unwrap_or(handle); 60 - if handle.len() > 2048 { 77 + let handle = handle 78 + .strip_prefix("at://") 79 + .unwrap_or(handle) 80 + .strip_prefix('@') 81 + .unwrap_or(handle); 82 + if handle.len() > 253 { 61 83 panic!("handle too long") 62 84 } else if !HANDLE_REGEX.is_match(handle) { 63 85 panic!("Invalid handle") ··· 71 93 /// 72 94 /// Accepts (and strips) preceding '@' if present 73 95 pub unsafe fn unchecked(handle: &'h str) -> Self { 74 - let handle = handle.strip_prefix('@').unwrap_or(handle); 96 + let handle = handle 97 + .strip_prefix("at://") 98 + .unwrap_or(handle) 99 + .strip_prefix('@') 100 + .unwrap_or(handle); 75 101 Self(CowStr::Borrowed(handle)) 76 102 } 77 103 ··· 89 115 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 90 116 /// Prefer `Handle::new()` or `Handle::raw` if you want to borrow. 91 117 fn from_str(s: &str) -> Result<Self, Self::Err> { 92 - Self::from_cowstr(CowStr::Borrowed(s).into_static()) 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()) 93 127 } 94 128 } 95 129 ··· 105 139 106 140 impl fmt::Display for Handle<'_> { 107 141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 108 - write!(f, "@{}", self.0) 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) 109 149 } 110 150 } 111 151 ··· 123 163 124 164 impl From<String> for Handle<'static> { 125 165 fn from(value: String) -> Self { 126 - if value.len() > 2048 { 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 { 127 176 panic!("handle too long") 128 177 } else if !HANDLE_REGEX.is_match(&value) { 129 178 panic!("Invalid handle") 130 179 } else { 131 - Self(CowStr::Owned(value.to_compact_string())) 180 + Self(value.into_static()) 132 181 } 133 182 } 134 183 } 135 184 136 185 impl<'h> From<CowStr<'h>> for Handle<'h> { 137 186 fn from(value: CowStr<'h>) -> Self { 138 - if value.len() > 2048 { 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 { 139 197 panic!("handle too long") 140 198 } else if !HANDLE_REGEX.is_match(&value) { 141 199 panic!("Invalid handle") 142 200 } else { 143 - Self(value) 201 + Self(value.into_static()) 144 202 } 145 203 } 146 204 }
+26 -5
crates/jacquard-common/src/types/ident.rs
··· 1 - use crate::types::did::Did; 2 1 use crate::types::handle::Handle; 2 + use crate::{IntoStatic, types::did::Did}; 3 3 use std::fmt; 4 4 use std::str::FromStr; 5 5 ··· 26 26 } 27 27 } 28 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() { 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) { 32 33 Ok(AtIdentifier::Did(did)) 33 34 } else { 34 - ident.parse().map(AtIdentifier::Handle) 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)?)) 35 45 } 36 46 } 37 47 ··· 90 100 Ok(AtIdentifier::Did(did)) 91 101 } else { 92 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()), 93 114 } 94 115 } 95 116 }
+209
crates/jacquard-common/src/types/nsid.rs
··· 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}; 1 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 serde::{Deserialize, Deserializer, Serialize, de::Error}; 2 + use smol_str::{SmolStr, SmolStrBuilder}; 1 3 use std::fmt; 2 4 use std::sync::LazyLock; 3 5 use std::{ops::Deref, str::FromStr}; 4 6 5 - use compact_str::{CompactString, ToCompactString}; 6 - use serde::{Deserialize, Deserializer, Serialize, de::Error}; 7 - 7 + use crate::CowStr; 8 8 use crate::types::integer::LimitedU32; 9 - use crate::{CowStr, IntoStatic}; 10 9 use regex::Regex; 11 10 12 - fn s32_encode(mut i: u64) -> CowStr<'static> { 11 + fn s32_encode(mut i: u64) -> SmolStr { 13 12 const S32_CHAR: &[u8] = b"234567abcdefghijklmnopqrstuvwxyz"; 14 13 15 - let mut s = CompactString::with_capacity(13); 14 + let mut s = SmolStrBuilder::new(); 16 15 for _ in 0..13 { 17 16 let c = i & 0x1F; 18 17 s.push(S32_CHAR[c as usize] as char); ··· 20 19 i >>= 5; 21 20 } 22 21 23 - // Reverse the string to convert it to big-endian format. 24 - CowStr::Owned(s.chars().rev().collect()) 22 + let mut builder = SmolStrBuilder::new(); 23 + for c in s.finish().chars().rev() { 24 + builder.push(c); 25 + } 26 + builder.finish() 25 27 } 26 28 27 29 static TID_REGEX: LazyLock<Regex> = LazyLock::new(|| { ··· 33 35 /// [Timestamp Identifier]: https://atproto.com/specs/tid 34 36 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)] 35 37 #[serde(transparent)] 36 - pub struct Tid<'t>(CowStr<'t>); 38 + #[repr(transparent)] 39 + pub struct Tid(SmolStr); 37 40 38 - impl<'t> Tid<'t> { 41 + impl Tid { 39 42 /// 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> { 43 + pub fn new(tid: impl AsRef<str>) -> Result<Self, &'static str> { 44 + let tid = tid.as_ref(); 52 45 if tid.len() != 13 { 53 46 Err("TID must be 13 characters") 54 - } else if !TID_REGEX.is_match(&tid) { 47 + } else if !TID_REGEX.is_match(&tid.as_ref()) { 55 48 Err("Invalid TID") 56 49 } else { 57 - Ok(Self(tid.into_static())) 50 + Ok(Self(SmolStr::new_inline(&tid))) 58 51 } 59 52 } 60 53 ··· 62 55 /// Will panic on invalid TID. If you're manually decoding atproto records 63 56 /// or API values you know are valid (rather than using serde), this is the one to use. 64 57 /// The From<String> and From<CowStr> impls use the same logic. 65 - pub fn raw(tid: &'t str) -> Self { 58 + pub fn raw(tid: impl AsRef<str>) -> Self { 59 + let tid = tid.as_ref(); 66 60 if tid.len() != 13 { 67 61 panic!("TID must be 13 characters") 68 62 } else if !TID_REGEX.is_match(&tid) { 69 63 panic!("Invalid TID") 70 64 } else { 71 - Self(CowStr::Borrowed(tid)) 65 + Self(SmolStr::new_inline(tid)) 72 66 } 73 67 } 74 68 75 69 /// Infallible constructor for when you *know* the string is a valid TID. 76 70 /// 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)) 71 + pub unsafe fn unchecked(tid: impl AsRef<str>) -> Self { 72 + let tid = tid.as_ref(); 73 + Self(SmolStr::new_inline(tid)) 79 74 } 80 75 81 76 /// Construct a new timestamp with the specified clock ID. ··· 121 116 } 122 117 } 123 118 124 - impl FromStr for Tid<'_> { 119 + impl FromStr for Tid { 125 120 type Err = &'static str; 126 121 127 122 /// Has to take ownership due to the lifetime constraints of the FromStr trait. 128 123 /// Prefer `Did::new()` or `Did::raw` if you want to borrow. 129 124 fn from_str(s: &str) -> Result<Self, Self::Err> { 130 - Self::from_cowstr(CowStr::Borrowed(s).into_static()) 125 + Self::new(s) 131 126 } 132 127 } 133 128 134 - impl<'de> Deserialize<'de> for Tid<'de> { 129 + impl<'de> Deserialize<'de> for Tid { 135 130 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 136 131 where 137 132 D: Deserializer<'de>, 138 133 { 139 - let value = Deserialize::deserialize(deserializer)?; 134 + let value: &str = Deserialize::deserialize(deserializer)?; 140 135 Self::new(value).map_err(D::Error::custom) 141 136 } 142 137 } 143 138 144 - impl fmt::Display for Tid<'_> { 139 + impl fmt::Display for Tid { 145 140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 146 141 f.write_str(&self.0) 147 142 } 148 143 } 149 144 150 - impl<'t> From<Tid<'t>> for String { 151 - fn from(value: Tid<'t>) -> Self { 145 + impl From<Tid> for String { 146 + fn from(value: Tid) -> Self { 152 147 value.0.to_string() 153 148 } 154 149 } 155 150 156 - impl<'t> From<Tid<'t>> for CowStr<'t> { 157 - fn from(value: Tid<'t>) -> Self { 151 + impl From<Tid> for SmolStr { 152 + fn from(value: Tid) -> Self { 158 153 value.0 159 154 } 160 155 } 161 156 162 - impl From<String> for Tid<'static> { 157 + impl From<String> for Tid { 163 158 fn from(value: String) -> Self { 164 159 if value.len() != 13 { 165 160 panic!("TID must be 13 characters") 166 161 } else if !TID_REGEX.is_match(&value) { 167 162 panic!("Invalid TID") 168 163 } else { 169 - Self(CowStr::Owned(value.to_compact_string())) 164 + Self(SmolStr::new_inline(&value)) 170 165 } 171 166 } 172 167 } 173 168 174 - impl<'t> From<CowStr<'t>> for Tid<'t> { 169 + impl<'t> From<CowStr<'t>> for Tid { 175 170 fn from(value: CowStr<'t>) -> Self { 176 171 if value.len() != 13 { 177 172 panic!("TID must be 13 characters") 178 173 } else if !TID_REGEX.is_match(&value) { 179 174 panic!("Invalid TID") 180 175 } else { 181 - Self(value) 176 + Self(SmolStr::new_inline(&value)) 182 177 } 183 178 } 184 179 } 185 180 186 - impl AsRef<str> for Tid<'_> { 181 + impl AsRef<str> for Tid { 187 182 fn as_ref(&self) -> &str { 188 183 self.as_str() 189 184 } 190 185 } 191 186 192 - impl Deref for Tid<'_> { 187 + impl Deref for Tid { 193 188 type Target = str; 194 189 195 190 fn deref(&self) -> &Self::Target {