A better Rust ATProto crate

ouroboros more compact owned aturi implementation, plus likely faster less space-efficient clone method to work around limitations of the type. also plan for additional optimization option.

Changed files
+575 -99
crates
jacquard-common
+63 -1
Cargo.lock
··· 31 31 ] 32 32 33 33 [[package]] 34 + name = "aliasable" 35 + version = "0.1.3" 36 + source = "registry+https://github.com/rust-lang/crates.io-index" 37 + checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" 38 + 39 + [[package]] 34 40 name = "android_system_properties" 35 41 version = "0.1.5" 36 42 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 275 281 source = "registry+https://github.com/rust-lang/crates.io-index" 276 282 checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" 277 283 dependencies = [ 278 - "heck", 284 + "heck 0.5.0", 279 285 "proc-macro2", 280 286 "quote", 281 287 "syn 2.0.106", ··· 504 510 505 511 [[package]] 506 512 name = "heck" 513 + version = "0.4.1" 514 + source = "registry+https://github.com/rust-lang/crates.io-index" 515 + checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 516 + 517 + [[package]] 518 + name = "heck" 507 519 version = "0.5.0" 508 520 source = "registry+https://github.com/rust-lang/crates.io-index" 509 521 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" ··· 718 730 "miette", 719 731 "multibase", 720 732 "multihash", 733 + "ouroboros", 721 734 "regex", 722 735 "serde", 723 736 "serde_html_form", ··· 865 878 checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 866 879 867 880 [[package]] 881 + name = "ouroboros" 882 + version = "0.18.5" 883 + source = "registry+https://github.com/rust-lang/crates.io-index" 884 + checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" 885 + dependencies = [ 886 + "aliasable", 887 + "ouroboros_macro", 888 + "static_assertions", 889 + ] 890 + 891 + [[package]] 892 + name = "ouroboros_macro" 893 + version = "0.18.5" 894 + source = "registry+https://github.com/rust-lang/crates.io-index" 895 + checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" 896 + dependencies = [ 897 + "heck 0.4.1", 898 + "proc-macro2", 899 + "proc-macro2-diagnostics", 900 + "quote", 901 + "syn 2.0.106", 902 + ] 903 + 904 + [[package]] 868 905 name = "percent-encoding" 869 906 version = "2.3.2" 870 907 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 916 953 checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 917 954 dependencies = [ 918 955 "unicode-ident", 956 + ] 957 + 958 + [[package]] 959 + name = "proc-macro2-diagnostics" 960 + version = "0.10.1" 961 + source = "registry+https://github.com/rust-lang/crates.io-index" 962 + checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" 963 + dependencies = [ 964 + "proc-macro2", 965 + "quote", 966 + "syn 2.0.106", 967 + "version_check", 968 + "yansi", 919 969 ] 920 970 921 971 [[package]] ··· 1179 1229 "syn 2.0.106", 1180 1230 "thiserror 1.0.69", 1181 1231 ] 1232 + 1233 + [[package]] 1234 + name = "static_assertions" 1235 + version = "1.1.0" 1236 + source = "registry+https://github.com/rust-lang/crates.io-index" 1237 + checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1182 1238 1183 1239 [[package]] 1184 1240 name = "strsim" ··· 1550 1606 version = "0.6.1" 1551 1607 source = "registry+https://github.com/rust-lang/crates.io-index" 1552 1608 checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 1609 + 1610 + [[package]] 1611 + name = "yansi" 1612 + version = "1.0.1" 1613 + source = "registry+https://github.com/rust-lang/crates.io-index" 1614 + checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 1553 1615 1554 1616 [[package]] 1555 1617 name = "yoke"
+1
crates/jacquard-common/Cargo.toml
··· 20 20 miette = "7.6.0" 21 21 multibase = "0.9.1" 22 22 multihash = "0.19.3" 23 + ouroboros = "0.18.5" 23 24 regex = "1.11.3" 24 25 serde = { version = "1.0.227", features = ["derive"] } 25 26 serde_html_form = "0.2.8"
+465 -88
crates/jacquard-common/src/types/aturi.rs
··· 1 - use crate::CowStr; 2 1 use crate::types::ident::AtIdentifier; 3 2 use crate::types::nsid::Nsid; 4 3 use crate::types::recordkey::{RecordKey, Rkey}; 5 4 use crate::types::string::AtStrError; 5 + use crate::{CowStr, IntoStatic}; 6 6 use regex::Regex; 7 7 use serde::Serializer; 8 8 use serde::{Deserialize, Deserializer, Serialize, de::Error}; 9 9 use smol_str::{SmolStr, ToSmolStr}; 10 10 use std::fmt; 11 + use std::hash::{Hash, Hasher}; 11 12 use std::sync::LazyLock; 12 13 use std::{ops::Deref, str::FromStr}; 13 14 ··· 15 16 /// 16 17 /// based on the regex here: https://github.com/bluesky-social/atproto/blob/main/packages/syntax/src/aturi_validation.ts 17 18 /// 18 - /// Doesn't support the query segment, but then neither does the Typescript SDK 19 - /// 20 - /// TODO: support IntoStatic on string types. For composites like this where all borrow from (present) input, 21 - /// perhaps use some careful unsafe to launder the lifetimes. 22 - #[derive(Clone, PartialEq, Eq, Hash, Debug)] 19 + /// Doesn't support the query segment, but then neither does the Typescript SDK. 20 + #[derive(PartialEq, Eq, Debug)] 23 21 pub struct AtUri<'u> { 22 + inner: Inner<'u>, 23 + } 24 + 25 + #[ouroboros::self_referencing] 26 + #[derive(PartialEq, Eq, Debug)] 27 + struct Inner<'u> { 24 28 uri: CowStr<'u>, 25 - pub authority: AtIdentifier<'u>, 26 - pub path: Option<UriPath<'u>>, 27 - pub fragment: Option<CowStr<'u>>, 29 + #[borrows(uri)] 30 + #[covariant] 31 + pub authority: AtIdentifier<'this>, 32 + #[borrows(uri)] 33 + #[covariant] 34 + pub path: Option<UriPath<'this>>, 35 + #[borrows(uri)] 36 + #[covariant] 37 + pub fragment: Option<CowStr<'this>>, 38 + } 39 + 40 + impl Clone for AtUri<'_> { 41 + fn clone(&self) -> Self { 42 + let uri = self.inner.borrow_uri(); 43 + 44 + Self { 45 + inner: Inner::new( 46 + CowStr::Owned(uri.as_ref().to_smolstr()), 47 + |uri| { 48 + let parts = ATURI_REGEX.captures(uri).unwrap(); 49 + unsafe { AtIdentifier::unchecked(parts.name("authority").unwrap().as_str()) } 50 + }, 51 + |uri| { 52 + let parts = ATURI_REGEX.captures(uri).unwrap(); 53 + if let Some(collection) = parts.name("collection") { 54 + let collection = unsafe { Nsid::unchecked(collection.as_str()) }; 55 + let rkey = if let Some(rkey) = parts.name("rkey") { 56 + let rkey = unsafe { RecordKey::from(Rkey::unchecked(rkey.as_str())) }; 57 + Some(rkey) 58 + } else { 59 + None 60 + }; 61 + Some(UriPath { collection, rkey }) 62 + } else { 63 + None 64 + } 65 + }, 66 + |uri| { 67 + let parts = ATURI_REGEX.captures(uri).unwrap(); 68 + parts.name("fragment").map(|fragment| { 69 + let fragment = CowStr::Borrowed(fragment.as_str()); 70 + fragment 71 + }) 72 + }, 73 + ), 74 + } 75 + } 76 + } 77 + 78 + impl Hash for AtUri<'_> { 79 + fn hash<H: Hasher>(&self, state: &mut H) { 80 + self.inner.borrow_uri().hash(state); 81 + } 28 82 } 29 83 30 84 /// at:// URI path component (current subset) ··· 32 86 pub struct UriPath<'u> { 33 87 pub collection: Nsid<'u>, 34 88 pub rkey: Option<RecordKey<Rkey<'u>>>, 89 + } 90 + 91 + impl IntoStatic for UriPath<'_> { 92 + type Output = UriPath<'static>; 93 + 94 + fn into_static(self) -> Self::Output { 95 + UriPath { 96 + collection: self.collection.into_static(), 97 + rkey: self.rkey.map(|rkey| rkey.into_static()), 98 + } 99 + } 35 100 } 36 101 37 102 pub type UriPathBuf = UriPath<'static>; ··· 68 133 fragment 69 134 }); 70 135 Ok(AtUri { 71 - uri: CowStr::Borrowed(uri), 72 - authority, 73 - path, 74 - fragment, 136 + inner: InnerBuilder { 137 + uri: CowStr::Borrowed(uri), 138 + authority_builder: |_| authority, 139 + path_builder: |_| path, 140 + fragment_builder: |_| fragment, 141 + } 142 + .build(), 75 143 }) 76 144 } else { 77 145 Err(AtStrError::missing("at-uri-scheme", uri, "authority")) ··· 106 174 fragment 107 175 }); 108 176 AtUri { 109 - uri: CowStr::Borrowed(uri), 110 - authority, 111 - path, 112 - fragment, 177 + inner: InnerBuilder { 178 + uri: CowStr::Borrowed(uri), 179 + authority_builder: |_| authority, 180 + path_builder: |_| path, 181 + fragment_builder: |_| fragment, 182 + } 183 + .build(), 113 184 } 114 185 } else { 115 186 panic!("at:// URI missing authority") ··· 119 190 } 120 191 } 121 192 122 - pub fn new_owned(uri: impl AsRef<str>) -> Result<Self, AtStrError> { 123 - let uri = uri.as_ref(); 193 + /// Unchecked borrowing constructor. This one does do some validation but if that fails will just 194 + /// dump everything in the authority field. 195 + /// 196 + /// TODO: do some fallback splitting, but really, if you use this on something invalid, you deserve it. 197 + pub unsafe fn unchecked(uri: &'u str) -> Self { 124 198 if let Some(parts) = ATURI_REGEX.captures(uri) { 125 199 if let Some(authority) = parts.name("authority") { 126 - let authority = AtIdentifier::new_owned(authority.as_str()) 127 - .map_err(|e| AtStrError::wrap("at-uri-scheme", uri.to_string(), e))?; 200 + let authority = unsafe { AtIdentifier::unchecked(authority.as_str()) }; 128 201 let path = if let Some(collection) = parts.name("collection") { 129 - let collection = Nsid::new_owned(collection.as_str()) 130 - .map_err(|e| AtStrError::wrap("at-uri-scheme", uri.to_string(), e))?; 202 + let collection = unsafe { Nsid::unchecked(collection.as_str()) }; 131 203 let rkey = if let Some(rkey) = parts.name("rkey") { 132 - let rkey = 133 - RecordKey::from(Rkey::new_owned(rkey.as_str()).map_err(|e| { 134 - AtStrError::wrap("at-uri-scheme", uri.to_string(), e) 135 - })?); 204 + let rkey = RecordKey::from(unsafe { Rkey::unchecked(rkey.as_str()) }); 136 205 Some(rkey) 137 206 } else { 138 207 None ··· 142 211 None 143 212 }; 144 213 let fragment = parts.name("fragment").map(|fragment| { 145 - let fragment = CowStr::Owned(fragment.as_str().to_smolstr()); 214 + let fragment = CowStr::Borrowed(fragment.as_str()); 146 215 fragment 147 216 }); 217 + AtUri { 218 + inner: InnerBuilder { 219 + uri: CowStr::Borrowed(uri), 220 + authority_builder: |_| authority, 221 + path_builder: |_| path, 222 + fragment_builder: |_| fragment, 223 + } 224 + .build(), 225 + } 226 + } else { 227 + // let mut uriParts = uri.split('#'); 228 + // let mut parts = uriParts.next().unwrap_or(uri).split('/'); 229 + // let auth = parts.next().unwrap_or(uri); 230 + Self { 231 + inner: InnerBuilder { 232 + uri: CowStr::Borrowed(uri), 233 + authority_builder: |_| unsafe { AtIdentifier::unchecked(uri) }, 234 + path_builder: |_| None, 235 + fragment_builder: |_| None, 236 + } 237 + .build(), 238 + } 239 + } 240 + } else { 241 + Self { 242 + inner: InnerBuilder { 243 + uri: CowStr::Borrowed(uri), 244 + authority_builder: |_| unsafe { AtIdentifier::unchecked(uri) }, 245 + path_builder: |_| None, 246 + fragment_builder: |_| None, 247 + } 248 + .build(), 249 + } 250 + } 251 + } 252 + 253 + /// Clone method that should be O(1) in terms of time 254 + /// 255 + /// Calling on a borrowed variant will turn it into an owned variant, taking a little 256 + /// more time and allocating memory for each part. Calling it on an owned variant will 257 + /// increment all the internal reference counters (or, if constructed from a `&'static str`, 258 + /// essentially do nothing). 259 + pub fn fast_clone(&self) -> AtUri<'static> { 260 + self.inner.with(move |u| { 261 + let uri = u.uri.clone().into_static(); 262 + let authority = u.authority.clone().into_static(); 263 + let path = u.path.clone().into_static(); 264 + let fragment = u.fragment.clone().into_static(); 265 + AtUri { 266 + inner: InnerBuilder { 267 + uri, 268 + authority_builder: |_| authority, 269 + path_builder: |_| path, 270 + fragment_builder: |_| fragment, 271 + } 272 + .build(), 273 + } 274 + }) 275 + } 276 + 277 + pub fn as_str(&self) -> &str { 278 + { 279 + let this = &self.inner.borrow_uri(); 280 + this 281 + } 282 + } 283 + 284 + pub fn authority(&self) -> &AtIdentifier<'_> { 285 + self.inner.borrow_authority() 286 + } 287 + 288 + pub fn path(&self) -> &Option<UriPath<'_>> { 289 + self.inner.borrow_path() 290 + } 291 + 292 + pub fn fragment(&self) -> &Option<CowStr<'_>> { 293 + self.inner.borrow_fragment() 294 + } 295 + 296 + pub fn collection(&self) -> Option<&Nsid<'_>> { 297 + self.inner.borrow_path().as_ref().map(|p| &p.collection) 298 + } 299 + 300 + pub fn rkey(&self) -> Option<&RecordKey<Rkey<'_>>> { 301 + self.inner 302 + .borrow_path() 303 + .as_ref() 304 + .and_then(|p| p.rkey.as_ref()) 305 + } 306 + } 307 + 308 + impl AtUri<'static> { 309 + /// Owned constructor 310 + /// 311 + /// Uses ouroboros self-referential tricks internally to make sure everything 312 + /// borrows efficiently from the uri `CowStr<'static>`. 313 + /// 314 + /// Performs validation up-front, but is slower than the borrowing constructor 315 + /// due to currently having to re-run the main regex, in addition to allocating. 316 + /// 317 + /// `.into_static()` and Clone implementations have similar limitations. 318 + /// 319 + /// O(1) clone mathod is AtUri::fast_clone(). 320 + /// 321 + /// Future optimization involves working out the indices borrowed and either using those 322 + /// to avoid re-computing in some places, or, for a likely fully optimal version, only storing 323 + /// the indices and constructing the borrowed components unsafely when asked. 324 + pub fn new_owned(uri: impl AsRef<str>) -> Result<Self, AtStrError> { 325 + if let Some(parts) = ATURI_REGEX.captures(uri.as_ref()) { 326 + if let Some(authority) = parts.name("authority") { 327 + let _authority = AtIdentifier::new(authority.as_str()) 328 + .map_err(|e| AtStrError::wrap("at-uri-scheme", uri.as_ref().to_string(), e))?; 329 + let path = if let Some(collection) = parts.name("collection") { 330 + let collection = Nsid::new(collection.as_str()).map_err(|e| { 331 + AtStrError::wrap("at-uri-scheme", uri.as_ref().to_string(), e) 332 + })?; 333 + let rkey = if let Some(rkey) = parts.name("rkey") { 334 + let rkey = RecordKey::from(Rkey::new(rkey.as_str()).map_err(|e| { 335 + AtStrError::wrap("at-uri-scheme", uri.as_ref().to_string(), e) 336 + })?); 337 + Some(rkey) 338 + } else { 339 + None 340 + }; 341 + Some(UriPath { collection, rkey }) 342 + } else { 343 + None 344 + }; 345 + 148 346 Ok(AtUri { 149 - uri: CowStr::Owned(uri.to_smolstr()), 150 - authority, 151 - path, 152 - fragment, 347 + inner: Inner::new( 348 + CowStr::Owned(uri.as_ref().to_smolstr()), 349 + |uri| { 350 + let parts = ATURI_REGEX.captures(uri).unwrap(); 351 + unsafe { 352 + AtIdentifier::unchecked(parts.name("authority").unwrap().as_str()) 353 + } 354 + }, 355 + |uri| { 356 + if path.is_some() { 357 + let parts = ATURI_REGEX.captures(uri).unwrap(); 358 + if let Some(collection) = parts.name("collection") { 359 + let collection = 360 + unsafe { Nsid::unchecked(collection.as_str()) }; 361 + let rkey = if let Some(rkey) = parts.name("rkey") { 362 + let rkey = unsafe { 363 + RecordKey::from(Rkey::unchecked(rkey.as_str())) 364 + }; 365 + Some(rkey) 366 + } else { 367 + None 368 + }; 369 + Some(UriPath { collection, rkey }) 370 + } else { 371 + None 372 + } 373 + } else { 374 + None 375 + } 376 + }, 377 + |uri| { 378 + let parts = ATURI_REGEX.captures(uri).unwrap(); 379 + parts.name("fragment").map(|fragment| { 380 + let fragment = CowStr::Borrowed(fragment.as_str()); 381 + fragment 382 + }) 383 + }, 384 + ), 153 385 }) 154 386 } else { 155 - Err(AtStrError::missing("at-uri-scheme", uri, "authority")) 387 + Err(AtStrError::missing( 388 + "at-uri-scheme", 389 + &uri.as_ref(), 390 + "authority", 391 + )) 156 392 } 157 393 } else { 158 394 Err(AtStrError::regex( 159 395 "at-uri-scheme", 160 - uri, 396 + &uri.as_ref(), 161 397 SmolStr::new_static("doesn't match schema"), 162 398 )) 163 399 } 164 400 } 165 401 166 - pub fn new_static(uri: &'static str) -> Result<AtUri<'static>, AtStrError> { 402 + pub fn new_static(uri: &'static str) -> Result<Self, AtStrError> { 167 403 let uri = uri.as_ref(); 168 404 if let Some(parts) = ATURI_REGEX.captures(uri) { 169 405 if let Some(authority) = parts.name("authority") { ··· 190 426 fragment 191 427 }); 192 428 Ok(AtUri { 193 - uri: CowStr::new_static(uri), 194 - authority, 195 - path, 196 - fragment, 429 + inner: InnerBuilder { 430 + uri: CowStr::new_static(uri), 431 + authority_builder: |_| authority, 432 + path_builder: |_| path, 433 + fragment_builder: |_| fragment, 434 + } 435 + .build(), 197 436 }) 198 437 } else { 199 438 Err(AtStrError::missing("at-uri-scheme", uri, "authority")) ··· 206 445 )) 207 446 } 208 447 } 448 + } 209 449 210 - pub unsafe fn unchecked(uri: &'u str) -> Self { 211 - if let Some(parts) = ATURI_REGEX.captures(uri) { 450 + impl FromStr for AtUri<'_> { 451 + type Err = AtStrError; 452 + 453 + /// Has to take ownership due to the lifetime constraints of the FromStr trait. 454 + /// Prefer `AtUri::new()` or `AtUri::raw()` if you want to borrow. 455 + fn from_str(uri: &str) -> Result<Self, Self::Err> { 456 + if let Some(parts) = ATURI_REGEX.captures(uri.as_ref()) { 212 457 if let Some(authority) = parts.name("authority") { 213 - let authority = unsafe { AtIdentifier::unchecked(authority.as_str()) }; 458 + let _authority = AtIdentifier::new(authority.as_str()) 459 + .map_err(|e| AtStrError::wrap("at-uri-scheme", uri.to_string(), e))?; 214 460 let path = if let Some(collection) = parts.name("collection") { 215 - let collection = unsafe { Nsid::unchecked(collection.as_str()) }; 461 + let collection = Nsid::new(collection.as_str()) 462 + .map_err(|e| AtStrError::wrap("at-uri-scheme", uri.to_string(), e))?; 216 463 let rkey = if let Some(rkey) = parts.name("rkey") { 217 - let rkey = RecordKey::from(unsafe { Rkey::unchecked(rkey.as_str()) }); 464 + let rkey = 465 + RecordKey::from(Rkey::new(rkey.as_str()).map_err(|e| { 466 + AtStrError::wrap("at-uri-scheme", uri.to_string(), e) 467 + })?); 218 468 Some(rkey) 219 469 } else { 220 470 None ··· 223 473 } else { 224 474 None 225 475 }; 226 - let fragment = parts.name("fragment").map(|fragment| { 227 - let fragment = CowStr::Borrowed(fragment.as_str()); 228 - fragment 229 - }); 230 - AtUri { 231 - uri: CowStr::Borrowed(uri), 232 - authority, 233 - path, 234 - fragment, 235 - } 476 + 477 + Ok(AtUri { 478 + inner: Inner::new( 479 + CowStr::Owned(uri.to_smolstr()), 480 + |uri| { 481 + let parts = ATURI_REGEX.captures(uri).unwrap(); 482 + unsafe { 483 + AtIdentifier::unchecked(parts.name("authority").unwrap().as_str()) 484 + } 485 + }, 486 + |uri| { 487 + if path.is_some() { 488 + let parts = ATURI_REGEX.captures(uri).unwrap(); 489 + if let Some(collection) = parts.name("collection") { 490 + let collection = 491 + unsafe { Nsid::unchecked(collection.as_str()) }; 492 + let rkey = if let Some(rkey) = parts.name("rkey") { 493 + let rkey = unsafe { 494 + RecordKey::from(Rkey::unchecked(rkey.as_str())) 495 + }; 496 + Some(rkey) 497 + } else { 498 + None 499 + }; 500 + Some(UriPath { collection, rkey }) 501 + } else { 502 + None 503 + } 504 + } else { 505 + None 506 + } 507 + }, 508 + |uri| { 509 + let parts = ATURI_REGEX.captures(uri).unwrap(); 510 + parts.name("fragment").map(|fragment| { 511 + let fragment = CowStr::Borrowed(fragment.as_str()); 512 + fragment 513 + }) 514 + }, 515 + ), 516 + }) 236 517 } else { 237 - Self { 238 - uri: CowStr::Borrowed(uri), 239 - authority: unsafe { AtIdentifier::unchecked(uri) }, 240 - path: None, 241 - fragment: None, 242 - } 518 + Err(AtStrError::missing( 519 + "at-uri-scheme", 520 + &uri.as_ref(), 521 + "authority", 522 + )) 243 523 } 244 524 } else { 245 - Self { 246 - uri: CowStr::Borrowed(uri), 247 - authority: unsafe { AtIdentifier::unchecked(uri) }, 248 - path: None, 249 - fragment: None, 250 - } 251 - } 252 - } 253 - 254 - pub fn as_str(&self) -> &str { 255 - { 256 - let this = &self.uri; 257 - this 525 + Err(AtStrError::regex( 526 + "at-uri-scheme", 527 + &uri.as_ref(), 528 + SmolStr::new_static("doesn't match schema"), 529 + )) 258 530 } 259 531 } 260 532 } 261 533 262 - impl FromStr for AtUri<'_> { 263 - type Err = AtStrError; 534 + impl IntoStatic for AtUri<'_> { 535 + type Output = AtUri<'static>; 264 536 265 - /// Has to take ownership due to the lifetime constraints of the FromStr trait. 266 - /// Prefer `AtUri::new()` or `AtUri::raw()` if you want to borrow. 267 - fn from_str(s: &str) -> Result<Self, Self::Err> { 268 - Self::new_owned(s) 537 + fn into_static(self) -> AtUri<'static> { 538 + AtUri { 539 + inner: Inner::new( 540 + self.inner.borrow_uri().clone().into_static(), 541 + |uri| { 542 + let parts = ATURI_REGEX.captures(uri).unwrap(); 543 + unsafe { AtIdentifier::unchecked(parts.name("authority").unwrap().as_str()) } 544 + }, 545 + |uri| { 546 + if self.inner.borrow_path().is_some() { 547 + let parts = ATURI_REGEX.captures(uri).unwrap(); 548 + if let Some(collection) = parts.name("collection") { 549 + let collection = unsafe { Nsid::unchecked(collection.as_str()) }; 550 + let rkey = if let Some(rkey) = parts.name("rkey") { 551 + let rkey = 552 + unsafe { RecordKey::from(Rkey::unchecked(rkey.as_str())) }; 553 + Some(rkey) 554 + } else { 555 + None 556 + }; 557 + Some(UriPath { collection, rkey }) 558 + } else { 559 + None 560 + } 561 + } else { 562 + None 563 + } 564 + }, 565 + |uri| { 566 + if self.inner.borrow_fragment().is_some() { 567 + let parts = ATURI_REGEX.captures(uri).unwrap(); 568 + parts.name("fragment").map(|fragment| { 569 + let fragment = CowStr::Borrowed(fragment.as_str()); 570 + fragment 571 + }) 572 + } else { 573 + None 574 + } 575 + }, 576 + ), 577 + } 269 578 } 270 579 } 271 580 ··· 284 593 where 285 594 S: Serializer, 286 595 { 287 - serializer.serialize_str(&self.uri) 596 + serializer.serialize_str(&self.inner.borrow_uri()) 288 597 } 289 598 } 290 599 291 600 impl fmt::Display for AtUri<'_> { 292 601 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 293 - f.write_str(&self.uri) 602 + f.write_str(&self.inner.borrow_uri()) 294 603 } 295 604 } 296 605 297 606 impl<'d> From<AtUri<'d>> for String { 298 607 fn from(value: AtUri<'d>) -> Self { 299 - value.uri.to_string() 608 + value.inner.borrow_uri().to_string() 300 609 } 301 610 } 302 611 303 612 impl<'d> From<AtUri<'d>> for CowStr<'d> { 304 613 fn from(value: AtUri<'d>) -> Self { 305 - value.uri 614 + value.inner.borrow_uri().clone() 306 615 } 307 616 } 308 617 ··· 316 625 317 626 impl<'d> TryFrom<CowStr<'d>> for AtUri<'d> { 318 627 type Error = AtStrError; 319 - /// TODO: rewrite to avoid taking ownership/cloning 320 - fn try_from(value: CowStr<'d>) -> Result<Self, Self::Error> { 321 - Self::new_owned(value) 628 + fn try_from(uri: CowStr<'d>) -> Result<Self, Self::Error> { 629 + if let Some(parts) = ATURI_REGEX.captures(uri.as_ref()) { 630 + if let Some(authority) = parts.name("authority") { 631 + let _authority = AtIdentifier::new(authority.as_str()) 632 + .map_err(|e| AtStrError::wrap("at-uri-scheme", uri.to_string(), e))?; 633 + let _path = if let Some(collection) = parts.name("collection") { 634 + let collection = Nsid::new(collection.as_str()) 635 + .map_err(|e| AtStrError::wrap("at-uri-scheme", uri.to_string(), e))?; 636 + let rkey = if let Some(rkey) = parts.name("rkey") { 637 + let rkey = 638 + RecordKey::from(Rkey::new(rkey.as_str()).map_err(|e| { 639 + AtStrError::wrap("at-uri-scheme", uri.to_string(), e) 640 + })?); 641 + Some(rkey) 642 + } else { 643 + None 644 + }; 645 + Some(UriPath { collection, rkey }) 646 + } else { 647 + None 648 + }; 649 + drop(parts); 650 + 651 + Ok(AtUri { 652 + inner: Inner::new( 653 + uri, 654 + |uri| { 655 + let parts = ATURI_REGEX.captures(uri).unwrap(); 656 + unsafe { 657 + AtIdentifier::unchecked(parts.name("authority").unwrap().as_str()) 658 + } 659 + }, 660 + |uri| { 661 + let parts = ATURI_REGEX.captures(uri).unwrap(); 662 + if let Some(collection) = parts.name("collection") { 663 + let collection = unsafe { Nsid::unchecked(collection.as_str()) }; 664 + let rkey = if let Some(rkey) = parts.name("rkey") { 665 + let rkey = 666 + unsafe { RecordKey::from(Rkey::unchecked(rkey.as_str())) }; 667 + Some(rkey) 668 + } else { 669 + None 670 + }; 671 + Some(UriPath { collection, rkey }) 672 + } else { 673 + None 674 + } 675 + }, 676 + |uri| { 677 + let parts = ATURI_REGEX.captures(uri).unwrap(); 678 + parts.name("fragment").map(|fragment| { 679 + let fragment = CowStr::Borrowed(fragment.as_str()); 680 + fragment 681 + }) 682 + }, 683 + ), 684 + }) 685 + } else { 686 + Err(AtStrError::missing( 687 + "at-uri-scheme", 688 + &uri.as_ref(), 689 + "authority", 690 + )) 691 + } 692 + } else { 693 + Err(AtStrError::regex( 694 + "at-uri-scheme", 695 + &uri.as_ref(), 696 + SmolStr::new_static("doesn't match schema"), 697 + )) 698 + } 322 699 } 323 700 } 324 701 325 702 impl AsRef<str> for AtUri<'_> { 326 703 fn as_ref(&self) -> &str { 327 - &self.uri.as_ref() 704 + &self.inner.borrow_uri().as_ref() 328 705 } 329 706 } 330 707 ··· 332 709 type Target = str; 333 710 334 711 fn deref(&self) -> &Self::Target { 335 - self.uri.as_ref() 712 + self.inner.borrow_uri().as_ref() 336 713 } 337 714 }
+43 -4
crates/jacquard-common/src/types/blob.rs
··· 21 21 pub size: usize, 22 22 } 23 23 24 - impl<'r> BlobRef<'r> { 25 - pub fn blob(&self) -> &Blob<'r> { 26 - match self { 27 - BlobRef::Blob(blob) => blob, 24 + impl IntoStatic for Blob<'_> { 25 + type Output = Blob<'static>; 26 + 27 + fn into_static(self) -> Self::Output { 28 + Blob { 29 + r#ref: self.r#ref.into_static(), 30 + mime_type: self.mime_type.into_static(), 31 + size: self.size, 28 32 } 29 33 } 30 34 } 31 35 32 36 /// Current, typed blob reference. 37 + /// Quite dislike this nesting, but it serves the same purpose as it did in Atrium 38 + /// Couple of helper methods and conversions to make it less annoying. 39 + /// TODO: revisit nesting and maybe hand-roll a serde impl that supports this sans nesting 33 40 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 34 41 #[serde(tag = "$type", rename_all = "lowercase")] 35 42 pub enum BlobRef<'r> { 36 43 #[serde(borrow)] 37 44 Blob(Blob<'r>), 45 + } 46 + 47 + impl<'r> BlobRef<'r> { 48 + pub fn blob(&self) -> &Blob<'r> { 49 + match self { 50 + BlobRef::Blob(blob) => blob, 51 + } 52 + } 53 + } 54 + 55 + impl<'b> From<BlobRef<'b>> for Blob<'b> { 56 + fn from(blob_ref: BlobRef<'b>) -> Self { 57 + match blob_ref { 58 + BlobRef::Blob(blob) => blob, 59 + } 60 + } 61 + } 62 + 63 + impl<'b> From<Blob<'b>> for BlobRef<'b> { 64 + fn from(blob: Blob<'b>) -> Self { 65 + BlobRef::Blob(blob) 66 + } 67 + } 68 + 69 + impl IntoStatic for BlobRef<'_> { 70 + type Output = BlobRef<'static>; 71 + 72 + fn into_static(self) -> Self::Output { 73 + match self { 74 + BlobRef::Blob(blob) => BlobRef::Blob(blob.into_static()), 75 + } 76 + } 38 77 } 39 78 40 79 /// Wrapper for file type
+3 -6
crates/jacquard-common/src/types/string.rs
··· 1 - use bytes::Bytes; 2 1 use miette::SourceSpan; 3 2 use serde::{Deserialize, Deserializer, Serialize, Serializer}; 4 3 use smol_str::{SmolStr, ToSmolStr}; 5 - use std::{collections::BTreeMap, str::FromStr, sync::Arc}; 4 + use std::{str::FromStr, sync::Arc}; 6 5 7 6 use crate::IntoStatic; 8 7 pub use crate::{ ··· 187 186 AtprotoStr::Did(did) => AtprotoStr::Did(did.into_static()), 188 187 AtprotoStr::Handle(handle) => AtprotoStr::Handle(handle.into_static()), 189 188 AtprotoStr::AtIdentifier(ident) => AtprotoStr::AtIdentifier(ident.into_static()), 190 - AtprotoStr::AtUri(at_uri) => { 191 - AtprotoStr::AtUri(AtUri::new_owned(at_uri.as_str()).unwrap()) 192 - } 189 + AtprotoStr::AtUri(at_uri) => AtprotoStr::AtUri(at_uri.into_static()), 193 190 AtprotoStr::Uri(uri) => AtprotoStr::Uri(uri.into_static()), 194 191 AtprotoStr::Cid(cid) => AtprotoStr::Cid(cid.into_static()), 195 192 AtprotoStr::RecordKey(record_key) => AtprotoStr::RecordKey(record_key.into_static()), ··· 214 211 pub struct AtStrError { 215 212 pub spec: SmolStr, 216 213 #[source_code] 217 - source: String, 214 + pub source: String, 218 215 #[source] 219 216 #[diagnostic_source] 220 217 pub kind: StrParseKind,