A better Rust ATProto crate
102
fork

Configure Feed

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

jacquard-identity updated plus some bits in lexgen

+313 -267
+11 -30
crates/jacquard-common/src/xrpc.rs
··· 290 290 /// ``` 291 291 pub trait XrpcExt: HttpClient { 292 292 /// Start building an XRPC call for the given base URI. 293 - fn xrpc<'a>(&'a self, base: Uri<String>) -> XrpcCall<'a, Self> 293 + fn xrpc<'a>(&'a self, base: Uri<&'a str>) -> XrpcCall<'a, Self> 294 294 where 295 295 Self: Sized, 296 296 { ··· 454 454 /// ``` 455 455 pub struct XrpcCall<'a, C: HttpClient> { 456 456 pub(crate) client: &'a C, 457 - pub(crate) base: Uri<String>, 457 + pub(crate) base: Uri<&'a str>, 458 458 pub(crate) opts: CallOptions<'a>, 459 459 } 460 460 ··· 578 578 /// 3. Builds new path: `{base_path}/xrpc/{nsid}` 579 579 /// 4. Optionally sets query from serialized parameters 580 580 /// 5. Returns the constructed URI 581 - fn xrpc_endpoint_uri( 582 - base: &Uri<String>, 583 - nsid: &str, 584 - query: Option<&str>, 585 - ) -> XrpcResult<Uri<String>> { 581 + fn xrpc_endpoint_uri(base: &Uri<&str>, nsid: &str, query: Option<&str>) -> XrpcResult<Uri<String>> { 586 582 use crate::error::ClientError; 587 583 588 584 let base_path = base.path().as_str().trim_end_matches('/'); ··· 615 611 } 616 612 617 613 Uri::parse(uri_str) 618 - .map(|u| u.to_owned()) 619 614 .map_err(|_| ClientError::invalid_request("Failed to construct XRPC endpoint URI")) 620 615 } 621 616 622 617 /// Build an HTTP request for an XRPC call given base URI and options 623 618 pub fn build_http_request<'s, R>( 624 - base: &Uri<String>, 619 + base: &Uri<&str>, 625 620 req: &R, 626 621 opts: &CallOptions<'_>, 627 622 ) -> XrpcResult<Request<Vec<u8>>> ··· 1207 1202 let opts = CallOptions::default(); 1208 1203 1209 1204 // AC1.1: Base URI without trailing slash + NSID produces correct `/xrpc/{nsid}` path 1210 - let base1 = Uri::parse("https://pds.example.com") 1211 - .expect("URI should be valid") 1212 - .to_owned(); 1205 + let base1 = Uri::parse("https://pds.example.com").expect("URI should be valid"); 1213 1206 let req1 = build_http_request(&base1, &Req, &opts).unwrap(); 1214 1207 let uri1 = req1.uri().to_string(); 1215 1208 assert!( ··· 1223 1216 ); 1224 1217 1225 1218 // AC1.2: Base URI with sub-path preserves it: `/base/xrpc/{nsid}` 1226 - let base2 = Uri::parse("https://pds.example.com/base") 1227 - .expect("URI should be valid") 1228 - .to_owned(); 1219 + let base2 = Uri::parse("https://pds.example.com/base").expect("URI should be valid"); 1229 1220 let req2 = build_http_request(&base2, &Req, &opts).unwrap(); 1230 1221 let uri2 = req2.uri().to_string(); 1231 1222 assert!( ··· 1239 1230 ); 1240 1231 1241 1232 // AC1.5: Base URI with trailing slash is normalized (slash stripped) before construction 1242 - let base_with_slash = Uri::parse("https://pds.example.com/") 1243 - .expect("URI should be valid") 1244 - .to_owned(); 1233 + let base_with_slash = Uri::parse("https://pds.example.com/").expect("URI should be valid"); 1245 1234 let req_slash = build_http_request(&base_with_slash, &Req, &opts).unwrap(); 1246 1235 let uri_slash = req_slash.uri().to_string(); 1247 1236 assert!( ··· 1285 1274 } 1286 1275 1287 1276 let opts = CallOptions::default(); 1288 - let base = Uri::parse("https://pds.example.com") 1289 - .expect("URI should be valid") 1290 - .to_owned(); 1277 + let base = Uri::parse("https://pds.example.com").expect("URI should be valid"); 1291 1278 1292 1279 // AC1.3: Query parameters from serde serialisation are set correctly 1293 1280 let req_with_params = QueryReq { ··· 1359 1346 } 1360 1347 1361 1348 let opts = CallOptions::default(); 1362 - let base = Uri::parse("https://pds.example.com") 1363 - .expect("URI should be valid") 1364 - .to_owned(); 1349 + let base = Uri::parse("https://pds.example.com").expect("URI should be valid"); 1365 1350 1366 1351 // AC1.3: Test with spaces (serde_html_form uses + for spaces per application/x-www-form-urlencoded) 1367 1352 let req_spaces = QueryReq { ··· 1447 1432 let opts = CallOptions::default(); 1448 1433 1449 1434 // Ensure no double slashes in path 1450 - let base1 = Uri::parse("https://pds") 1451 - .expect("URI should be valid") 1452 - .to_owned(); 1435 + let base1 = Uri::parse("https://pds").expect("URI should be valid"); 1453 1436 let req1 = build_http_request(&base1, &Req, &opts).unwrap(); 1454 1437 let uri1 = req1.uri().to_string(); 1455 1438 assert!( ··· 1458 1441 uri1 1459 1442 ); 1460 1443 1461 - let base2 = Uri::parse("https://pds/base") 1462 - .expect("URI should be valid") 1463 - .to_owned(); 1444 + let base2 = Uri::parse("https://pds/base").expect("URI should be valid"); 1464 1445 let req2 = build_http_request(&base2, &Req, &opts).unwrap(); 1465 1446 let uri2 = req2.uri().to_string(); 1466 1447 assert!(
+48 -52
crates/jacquard-identity/src/lexicon_resolver.rs
··· 7 7 use crate::resolver::{IdentityError, IdentityResolver}; 8 8 9 9 use jacquard_common::{ 10 - IntoStatic, 10 + bos::Bos, 11 11 deps::smol_str, 12 12 types::{cid::Cid, did::Did, string::Nsid}, 13 13 }; ··· 23 23 /// (e.g., `app.bsky.feed` → query `_lexicon.feed.bsky.app`). 24 24 /// 25 25 /// Note: No hierarchical fallback - per the spec, only exact authority match is checked. 26 - async fn resolve_lexicon_authority( 26 + async fn resolve_lexicon_authority<S: Bos<str> + AsRef<str> + Sync>( 27 27 &self, 28 - nsid: &Nsid, 29 - ) -> std::result::Result<Did<'static>, LexiconResolutionError>; 28 + nsid: &Nsid<S>, 29 + ) -> std::result::Result<Did, LexiconResolutionError>; 30 30 } 31 31 32 32 /// Resolve lexicon schemas (NSID → schema document) 33 33 #[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))] 34 34 pub trait LexiconSchemaResolver { 35 35 /// Resolve a complete lexicon schema for an NSID 36 - async fn resolve_lexicon_schema( 36 + async fn resolve_lexicon_schema<S: Bos<str> + AsRef<str> + Sync>( 37 37 &self, 38 - nsid: &Nsid, 38 + nsid: &Nsid<S>, 39 39 ) -> std::result::Result<ResolvedLexiconSchema<'static>, LexiconResolutionError>; 40 40 } 41 41 ··· 43 43 #[derive(Debug, Clone)] 44 44 pub struct ResolvedLexiconSchema<'s> { 45 45 /// The NSID of the schema 46 - pub nsid: Nsid<'s>, 46 + pub nsid: Nsid, 47 47 /// DID of the repository this schema was fetched from 48 - pub repo: Did<'s>, 48 + pub repo: Did, 49 49 /// Content ID of the record (for cache invalidation) 50 - pub cid: Cid<'s>, 50 + pub cid: Cid, 51 51 /// Parsed lexicon document 52 52 pub doc: jacquard_lexicon::lexicon::LexiconDoc<'s>, 53 53 } ··· 321 321 /// 322 322 /// Queries `_lexicon.{reversed-authority}` for a TXT record containing `did=...` 323 323 #[cfg(all(feature = "dns", not(target_family = "wasm")))] 324 - async fn resolve_lexicon_authority_dns( 324 + async fn resolve_lexicon_authority_dns<S: Bos<str> + AsRef<str> + Sync>( 325 325 &self, 326 - nsid: &Nsid<'_>, 327 - ) -> std::result::Result<Did<'static>, LexiconResolutionError> { 326 + nsid: &Nsid<S>, 327 + ) -> std::result::Result<Did, LexiconResolutionError> { 328 328 let Some(dns) = &self.dns else { 329 329 return Err(LexiconResolutionError::dns_not_configured()); 330 330 }; ··· 347 347 for data in txt.txt_data().iter() { 348 348 let text = std::str::from_utf8(data).unwrap_or(""); 349 349 if let Some(did_str) = text.strip_prefix("did=") { 350 - use jacquard_common::IntoStatic; 351 - 352 - return Did::new_owned(did_str) 353 - .map(|d| d.into_static()) 354 - .map_err(|_| { 355 - LexiconResolutionError::invalid_did(authority, did_str) 356 - .with_context(format!("resolving NSID {}", nsid)) 357 - }); 350 + return Did::new_owned(did_str).map_err(|_| { 351 + LexiconResolutionError::invalid_did(authority, did_str) 352 + .with_context(format!("resolving NSID {}", nsid)) 353 + }); 358 354 } 359 355 } 360 356 } ··· 363 359 } 364 360 } 365 361 366 - 367 362 #[cfg(all(feature = "dns", not(target_family = "wasm")))] 368 363 impl LexiconAuthorityResolver for crate::JacquardResolver { 369 - async fn resolve_lexicon_authority( 364 + async fn resolve_lexicon_authority<S: Bos<str> + AsRef<str> + Sync>( 370 365 &self, 371 - nsid: &Nsid<'_>, 372 - ) -> std::result::Result<Did<'static>, LexiconResolutionError> { 366 + nsid: &Nsid<S>, 367 + ) -> std::result::Result<Did, LexiconResolutionError> { 373 368 // Try cache first 374 369 #[cfg(feature = "cache")] 375 370 if let Some(caches) = &self.caches { ··· 404 399 405 400 #[cfg(not(all(feature = "dns", not(target_family = "wasm"))))] 406 401 impl LexiconAuthorityResolver for crate::JacquardResolver { 407 - async fn resolve_lexicon_authority( 402 + async fn resolve_lexicon_authority<S: Bos<str> + AsRef<str> + Sync>( 408 403 &self, 409 - nsid: &Nsid<'_>, 410 - ) -> std::result::Result<Did<'static>, LexiconResolutionError> { 404 + nsid: &Nsid<S>, 405 + ) -> std::result::Result<Did, LexiconResolutionError> { 411 406 // Use DNS-over-HTTPS fallback for WASM/non-DNS builds 412 407 self.resolve_lexicon_authority_doh(nsid).await 413 408 } ··· 416 411 impl crate::JacquardResolver { 417 412 /// Resolve lexicon authority via DNS-over-HTTPS (for WASM compatibility) 418 413 #[allow(dead_code)] 419 - async fn resolve_lexicon_authority_doh( 414 + async fn resolve_lexicon_authority_doh<S: Bos<str> + AsRef<str> + Sync>( 420 415 &self, 421 - nsid: &Nsid<'_>, 422 - ) -> std::result::Result<Did<'static>, LexiconResolutionError> { 416 + nsid: &Nsid<S>, 417 + ) -> std::result::Result<Did, LexiconResolutionError> { 423 418 // Try cache first 424 419 #[cfg(feature = "cache")] 425 420 if let Some(caches) = &self.caches { ··· 453 448 let txt_data = data.trim_matches('"'); 454 449 455 450 if let Some(did_str) = txt_data.strip_prefix("did=") { 456 - let result = Did::new_owned(did_str) 457 - .map(|d| d.into_static()) 458 - .map_err(|_| { 459 - LexiconResolutionError::invalid_did(authority, did_str) 460 - .with_context(format!("resolving NSID {}", nsid)) 461 - }); 451 + let result = Did::new_owned(did_str).map_err(|_| { 452 + LexiconResolutionError::invalid_did(authority, did_str) 453 + .with_context(format!("resolving NSID {}", nsid)) 454 + }); 462 455 463 456 // Cache on success 464 457 #[cfg(feature = "cache")] ··· 484 477 } 485 478 486 479 impl LexiconSchemaResolver for crate::JacquardResolver { 487 - async fn resolve_lexicon_schema( 480 + async fn resolve_lexicon_schema<S: Bos<str> + AsRef<str> + Sync>( 488 481 &self, 489 - nsid: &Nsid<'_>, 482 + nsid: &Nsid<S>, 490 483 ) -> std::result::Result<ResolvedLexiconSchema<'static>, LexiconResolutionError> { 491 484 use jacquard_common::xrpc::atproto::GetRecord; 492 485 use jacquard_common::{IntoStatic, xrpc::XrpcExt}; 493 486 487 + use jacquard_common::CowStr; 488 + 489 + let nsid_str = nsid.as_str(); 490 + let owned_nsid: Nsid = Nsid::new_owned(nsid_str).expect("already validated NSID"); 491 + 494 492 // Try cache first 495 493 #[cfg(feature = "cache")] 496 494 if let Some(caches) = &self.caches { 497 - let key = nsid.clone().into_static(); 498 - if let Some(schema) = crate::cache_impl::get(&caches.nsid_to_schema, &key) { 495 + if let Some(schema) = crate::cache_impl::get(&caches.nsid_to_schema, &owned_nsid) { 499 496 return Ok((*schema).clone()); 500 497 } 501 498 } ··· 528 525 .map_err(|_| LexiconResolutionError::invalid_collection())?; 529 526 530 527 let request = GetRecord { 531 - repo: authority_did.clone().into(), 532 - collection: collection.into_static(), 533 - rkey: nsid.clone().into(), 528 + repo: authority_did.clone().convert::<CowStr<'_>>().into(), 529 + collection: collection.convert::<CowStr<'_>>(), 530 + rkey: CowStr::from(nsid_str), 534 531 cid: None, 535 532 }; 536 533 ··· 538 535 .xrpc(pds) 539 536 .send(&request) 540 537 .await 541 - .map_err(|e| LexiconResolutionError::fetch_failed(nsid.as_str(), e))?; 538 + .map_err(|e| LexiconResolutionError::fetch_failed(nsid_str, e))?; 542 539 543 540 let output = response 544 541 .into_output() 545 - .map_err(|e| LexiconResolutionError::fetch_failed(nsid.as_str(), e))?; 542 + .map_err(|e| LexiconResolutionError::fetch_failed(nsid_str, e))?; 546 543 547 544 // 4. Parse lexicon document from value 548 545 let json_str = serde_json::to_string(&output.value) 549 - .map_err(|e| LexiconResolutionError::parse_failed(nsid.as_str(), e))?; 546 + .map_err(|e| LexiconResolutionError::parse_failed(nsid_str, e))?; 550 547 551 548 let doc: jacquard_lexicon::lexicon::LexiconDoc = serde_json::from_str(&json_str) 552 - .map_err(|e| LexiconResolutionError::parse_failed(nsid.as_str(), e))?; 549 + .map_err(|e| LexiconResolutionError::parse_failed(nsid_str, e))?; 553 550 554 551 #[cfg(feature = "tracing")] 555 552 tracing::trace!("successfully parsed lexicon schema {}", nsid); 556 553 557 554 let cid = output 558 555 .cid 559 - .ok_or_else(|| LexiconResolutionError::missing_cid(nsid.as_str()))? 560 - .into_static(); 556 + .ok_or_else(|| LexiconResolutionError::missing_cid(nsid_str))?; 561 557 562 558 Ok(ResolvedLexiconSchema { 563 - nsid: nsid.clone().into_static(), 564 - repo: authority_did.into_static(), 559 + nsid: owned_nsid.clone(), 560 + repo: authority_did, 565 561 cid, 566 562 doc: doc.into_static(), 567 563 }) ··· 576 572 if let Some(caches) = &self.caches { 577 573 crate::cache_impl::insert( 578 574 &caches.nsid_to_schema, 579 - nsid.clone().into_static(), 575 + owned_nsid, 580 576 std::sync::Arc::new(schema.clone()), 581 577 ); 582 578 }
+107 -97
crates/jacquard-identity/src/lib.rs
··· 75 75 ResolverOptions, 76 76 }; 77 77 use bytes::Bytes; 78 - use jacquard_common::xrpc::atproto::{ResolveDid, ResolveHandle}; 79 78 #[cfg(feature = "streaming")] 80 79 use jacquard_common::ByteStream; 80 + use jacquard_common::bos::Bos; 81 81 use jacquard_common::deps::fluent_uri::Uri; 82 82 use jacquard_common::deps::fluent_uri::pct_enc::{ 83 83 EString, ··· 88 88 use jacquard_common::types::did::Did; 89 89 use jacquard_common::types::did_doc::DidDocument; 90 90 use jacquard_common::types::ident::AtIdentifier; 91 + use jacquard_common::types::string::Handle; 91 92 use jacquard_common::xrpc::XrpcExt; 92 - use jacquard_common::{IntoStatic, types::string::Handle}; 93 + use jacquard_common::xrpc::atproto::{ResolveDid, ResolveHandle}; 93 94 use reqwest::StatusCode; 94 95 95 96 #[cfg(all(feature = "dns", not(target_family = "wasm")))] ··· 283 284 #[derive(Clone)] 284 285 pub struct ResolverCaches { 285 286 /// Cache mapping handles to their resolved DIDs. 286 - pub handle_to_did: cache_impl::Cache<Handle<'static>, Did<'static>>, 287 + pub handle_to_did: cache_impl::Cache<Handle, Did>, 287 288 /// Cache mapping DIDs to their full DID documents. 288 - pub did_to_doc: cache_impl::Cache<Did<'static>, Arc<DidDocResponse>>, 289 + pub did_to_doc: cache_impl::Cache<Did, Arc<DidDocResponse>>, 289 290 /// Cache mapping authority strings (e.g., PDS hosts) to DIDs. 290 - pub authority_to_did: cache_impl::Cache<SmolStr, Did<'static>>, 291 + pub authority_to_did: cache_impl::Cache<SmolStr, Did>, 291 292 /// Cache mapping NSIDs to their resolved lexicon schemas. 292 - pub nsid_to_schema: cache_impl::Cache<Nsid<'static>, Arc<ResolvedLexiconSchema<'static>>>, 293 + pub nsid_to_schema: cache_impl::Cache<Nsid, Arc<ResolvedLexiconSchema<'static>>>, 293 294 } 294 295 295 296 #[cfg(feature = "cache")] ··· 420 421 /// 421 422 /// - `did:web:example.com` → `https://example.com/.well-known/did.json` 422 423 /// - `did:web:example.com:user:alice` → `https://example.com/user/alice/did.json` 423 - fn did_web_url(&self, did: &Did<'_>) -> resolver::Result<Uri<String>> { 424 + fn did_web_url<S: Bos<str> + AsRef<str> + Sync>( 425 + &self, 426 + did: &Did<S>, 427 + ) -> resolver::Result<Uri<String>> { 424 428 // did:web:example.com[:path:segments] 425 429 let s = did.as_str(); 426 430 let rest = s ··· 555 559 Ok(results) 556 560 } 557 561 558 - fn parse_atproto_did_body(body: &str, identifier: &str) -> resolver::Result<Did<'static>> { 562 + fn parse_atproto_did_body(body: &str, identifier: &str) -> resolver::Result<Did> { 559 563 let line = body 560 564 .lines() 561 565 .find(|l| !l.trim().is_empty()) 562 566 .ok_or_else(|| IdentityError::invalid_well_known(identifier))?; 563 - let did = Did::new(line.trim()) 564 - .map_err(|e| IdentityError::invalid_well_known_with_source(identifier, e))?; 565 - Ok(did.into_static()) 567 + Did::new_owned(line.trim()) 568 + .map_err(|e| IdentityError::invalid_well_known_with_source(identifier, e)) 566 569 } 567 570 } 568 571 569 572 impl JacquardResolver { 570 573 /// Resolve handle to DID via a PDS XRPC call (stateless, unauth by default) 571 - pub async fn resolve_handle_via_pds( 574 + pub async fn resolve_handle_via_pds<S: Bos<str> + AsRef<str> + Sync>( 572 575 &self, 573 - handle: &Handle<'_>, 574 - ) -> resolver::Result<Did<'static>> { 576 + handle: &Handle<S>, 577 + ) -> resolver::Result<Did> { 575 578 let pds = match &self.opts.pds_fallback { 576 579 Some(u) => u.clone(), 577 580 None => return Err(IdentityError::no_pds_fallback()), 578 581 }; 582 + let owned_handle: Handle = 583 + Handle::new_owned(handle.as_str()).expect("already validated handle"); 579 584 let req = ResolveHandle { 580 - handle: handle.clone().into_static(), 585 + handle: owned_handle, 581 586 }; 582 - let resp = self.http.xrpc(pds).send(&req).await.map_err(|e| { 587 + let resp = self.http.xrpc(pds.borrow()).send(&req).await.map_err(|e| { 583 588 IdentityError::from(e).with_context(format!("resolving handle {}", handle)) 584 589 })?; 585 590 // Note: XrpcError<E> has GAT lifetimes that prevent boxing; use debug format 586 - let out = resp.parse().map_err(|e| { 591 + let out = resp.parse::<SmolStr>().map_err(|e| { 587 592 IdentityError::xrpc(jacquard_common::deps::smol_str::format_smolstr!("{:?}", e)) 588 593 .with_context(format!("parsing response for handle {}", handle)) 589 594 })?; 590 - Did::new_owned(out.did.as_str()) 591 - .map(|d| d.into_static()) 592 - .map_err(|e| { 593 - IdentityError::invalid_doc(jacquard_common::deps::smol_str::format_smolstr!( 594 - "PDS returned invalid DID '{}': {}", 595 - out.did, 596 - e 597 - )) 598 - }) 595 + Did::new_owned(out.did.as_str()).map_err(|e| { 596 + IdentityError::invalid_doc(jacquard_common::deps::smol_str::format_smolstr!( 597 + "PDS returned invalid DID '{}': {}", 598 + out.did, 599 + e 600 + )) 601 + }) 599 602 } 600 603 601 604 /// Fetch DID document via PDS resolveDid (returns owned DidDocument) 602 - pub async fn fetch_did_doc_via_pds_owned( 605 + pub async fn fetch_did_doc_via_pds_owned<S: Bos<str> + AsRef<str> + Sync>( 603 606 &self, 604 - did: &Did<'_>, 605 - ) -> resolver::Result<DidDocument<'static>> { 607 + did: &Did<S>, 608 + ) -> resolver::Result<DidDocument> { 606 609 let pds = match &self.opts.pds_fallback { 607 610 Some(u) => u.clone(), 608 611 None => return Err(IdentityError::no_pds_fallback()), 609 612 }; 610 - let req = ResolveDid { 611 - did: did.clone(), 612 - }; 613 - let resp = self.http.xrpc(pds).send(&req).await.map_err(|e| { 613 + let owned_did: Did = Did::new_owned(did.as_str()).expect("already validated DID"); 614 + let req = ResolveDid { did: owned_did }; 615 + let resp = self.http.xrpc(pds.borrow()).send(&req).await.map_err(|e| { 614 616 IdentityError::from(e).with_context(format!("fetching DID doc for {}", did)) 615 617 })?; 616 618 // Note: XrpcError<E> has GAT lifetimes that prevent boxing; use debug format 617 - let out = resp.parse().map_err(|e| { 619 + let out = resp.parse::<SmolStr>().map_err(|e| { 618 620 IdentityError::xrpc(jacquard_common::deps::smol_str::format_smolstr!("{:?}", e)) 619 621 .with_context(format!("parsing DID doc response for {}", did)) 620 622 })?; 621 623 let doc_json = serde_json::to_value(&out.did_doc)?; 622 624 let s = serde_json::to_string(&doc_json)?; 623 - let doc_borrowed: DidDocument<'_> = serde_json::from_str(&s)?; 624 - Ok(doc_borrowed.into_static()) 625 + let doc: DidDocument = serde_json::from_str(&s)?; 626 + Ok(doc) 625 627 } 626 628 627 629 /// Fetch a minimal DID document via a Slingshot mini-doc endpoint, if your PlcSource uses Slingshot. 628 630 /// Returns the raw response wrapper for borrowed parsing and validation. 629 - pub async fn fetch_mini_doc_via_slingshot( 631 + pub async fn fetch_mini_doc_via_slingshot<S: Bos<str> + AsRef<str> + Sync>( 630 632 &self, 631 - did: &Did<'_>, 633 + did: &Did<S>, 632 634 ) -> resolver::Result<DidDocResponse> { 633 635 let base = match &self.opts.plc_source { 634 636 PlcSource::Slingshot { base } => base.clone(), ··· 640 642 } 641 643 }; 642 644 // Build URL using string manipulation, then parse 643 - let qs = serde_html_form::to_string( 644 - &ResolveDid { 645 - did: did.clone().into_static(), 646 - }, 647 - ) 645 + let owned_did: Did = Did::new_owned(did.as_str()).expect("already validated DID"); 646 + let qs = serde_html_form::to_string(&ResolveDid { 647 + did: owned_did.clone(), 648 + }) 648 649 .unwrap_or_default(); 649 650 let url_str = if qs.is_empty() { 650 651 format!( ··· 665 666 Ok(DidDocResponse { 666 667 buffer: buf, 667 668 status, 668 - requested: Some(did.clone().into_static()), 669 + requested: Some(owned_did), 669 670 }) 670 671 } 671 672 } ··· 675 676 &self.opts 676 677 } 677 678 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(handle = %handle)))] 678 - async fn resolve_handle(&self, handle: &Handle<'_>) -> resolver::Result<Did<'static>> { 679 + async fn resolve_handle<S: Bos<str> + AsRef<str> + Sync>( 680 + &self, 681 + handle: &Handle<S>, 682 + ) -> resolver::Result<Did> { 679 683 // Try cache first 680 684 #[cfg(feature = "cache")] 681 685 if let Some(caches) = &self.caches { 682 - let key = handle.clone().into_static(); 686 + let key = Handle::new_owned(handle.as_str()).expect("already validated handle"); 683 687 if let Some(did) = cache_impl::get(&caches.handle_to_did, &key) { 684 688 return Ok(did); 685 689 } 686 690 } 687 691 688 692 let host = handle.as_str(); 689 - let mut resolved_did: Option<Did<'static>> = None; 693 + let mut resolved_did: Option<Did> = None; 690 694 691 695 'outer: for step in &self.opts.handle_order { 692 696 match step { ··· 694 698 if let Ok(txts) = self.dns_txt(host).await { 695 699 for txt in txts { 696 700 if let Some(did_str) = txt.strip_prefix("did=") { 697 - if let Ok(did) = Did::new(did_str) { 698 - resolved_did = Some(did.into_static()); 701 + if let Ok(did) = Did::new_owned(did_str) { 702 + resolved_did = Some(did); 699 703 break 'outer; 700 704 } 701 705 } ··· 722 726 } 723 727 // Public unauth fallback 724 728 if self.opts.public_fallback_for_handle { 725 - if let Ok(qs) = serde_html_form::to_string( 726 - &ResolveHandle { 727 - handle: (*handle).clone(), 728 - }, 729 - ) { 729 + let owned_handle: Handle = 730 + Handle::new_owned(handle.as_str()).expect("already validated handle"); 731 + if let Ok(qs) = serde_html_form::to_string(&ResolveHandle { 732 + handle: owned_handle, 733 + }) { 730 734 let url_str = format!( 731 735 "https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?{}", 732 736 qs ··· 744 748 val.get("did").and_then(|v| v.as_str()) 745 749 { 746 750 if let Ok(did) = Did::new_owned(did_str) { 747 - resolved_did = Some(did.into_static()); 751 + resolved_did = Some(did); 748 752 break 'outer; 749 753 } 750 754 } ··· 758 762 } 759 763 // Non-auth path: if PlcSource is Slingshot, use its resolveHandle endpoint. 760 764 if let PlcSource::Slingshot { base } = &self.opts.plc_source { 761 - let qs = serde_html_form::to_string( 762 - &ResolveHandle { 763 - handle: (*handle).clone(), 764 - }, 765 - ) 765 + let owned_handle: Handle = 766 + Handle::new_owned(handle.as_str()).expect("already validated handle"); 767 + let qs = serde_html_form::to_string(&ResolveHandle { 768 + handle: owned_handle, 769 + }) 766 770 .unwrap_or_default(); 767 771 let url_str = if qs.is_empty() { 768 772 format!( ··· 790 794 val.get("did").and_then(|v| v.as_str()) 791 795 { 792 796 if let Ok(did) = Did::new_owned(did_str) { 793 - resolved_did = Some(did.into_static()); 797 + resolved_did = Some(did); 794 798 break 'outer; 795 799 } 796 800 } ··· 810 814 if let Some(caches) = &self.caches { 811 815 cache_impl::insert( 812 816 &caches.handle_to_did, 813 - handle.clone().into_static(), 817 + Handle::new_owned(handle.as_str()).expect("already validated handle"), 814 818 did.clone(), 815 819 ); 816 820 } ··· 825 829 } 826 830 827 831 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(did = %did)))] 828 - async fn resolve_did_doc(&self, did: &Did<'_>) -> resolver::Result<DidDocResponse> { 832 + async fn resolve_did_doc<S: Bos<str> + AsRef<str> + Sync>( 833 + &self, 834 + did: &Did<S>, 835 + ) -> resolver::Result<DidDocResponse> { 836 + let owned_did: Did = Did::new_owned(did.as_str()).expect("already validated DID"); 837 + 829 838 // Try cache first 830 839 #[cfg(feature = "cache")] 831 840 if let Some(caches) = &self.caches { 832 - let key = did.clone().into_static(); 833 - if let Some(doc_resp) = cache_impl::get(&caches.did_to_doc, &key) { 841 + if let Some(doc_resp) = cache_impl::get(&caches.did_to_doc, &owned_did) { 834 842 return Ok((*doc_resp).clone()); 835 843 } 836 844 } ··· 846 854 resolved_doc = Some(DidDocResponse { 847 855 buffer: buf, 848 856 status, 849 - requested: Some(did.clone().into_static()), 857 + requested: Some(owned_did.clone()), 850 858 }); 851 859 break 'outer; 852 860 } ··· 869 877 resolved_doc = Some(DidDocResponse { 870 878 buffer: buf, 871 879 status, 872 - requested: Some(did.clone().into_static()), 880 + requested: Some(owned_did.clone()), 873 881 }); 874 882 break 'outer; 875 883 } ··· 882 890 resolved_doc = Some(DidDocResponse { 883 891 buffer: Bytes::from(buf), 884 892 status: StatusCode::OK, 885 - requested: Some(did.clone().into_static()), 893 + requested: Some(owned_did.clone()), 886 894 }); 887 895 break 'outer; 888 896 } ··· 893 901 resolved_doc = Some(DidDocResponse { 894 902 buffer: buf, 895 903 status, 896 - requested: Some(did.clone().into_static()), 904 + requested: Some(owned_did.clone()), 897 905 }); 898 906 break 'outer; 899 907 } ··· 907 915 // Cache successful resolution 908 916 #[cfg(feature = "cache")] 909 917 if let Some(caches) = &self.caches { 910 - cache_impl::insert( 911 - &caches.did_to_doc, 912 - did.clone().into_static(), 913 - Arc::new(doc_resp.clone()), 914 - ); 918 + cache_impl::insert(&caches.did_to_doc, owned_did, Arc::new(doc_resp.clone())); 915 919 } 916 920 Ok(doc_resp) 917 921 } else { ··· 1029 1033 /// The DID doc did not contain the expected handle alias under alsoKnownAs 1030 1034 HandleAliasMismatch { 1031 1035 #[allow(missing_docs)] 1032 - expected: Handle<'static>, 1036 + expected: Handle, 1033 1037 }, 1034 1038 } 1035 1039 1036 1040 impl JacquardResolver { 1037 1041 /// Resolve a handle to its DID, fetch the DID document, and return doc plus any warnings. 1038 1042 /// This applies the default equality check on the document id (error with doc if mismatch). 1039 - pub async fn resolve_handle_and_doc( 1043 + pub async fn resolve_handle_and_doc<S: Bos<str> + AsRef<str> + Sync>( 1040 1044 &self, 1041 - handle: &Handle<'_>, 1042 - ) -> resolver::Result<(Did<'static>, DidDocResponse, Vec<IdentityWarning>)> { 1045 + handle: &Handle<S>, 1046 + ) -> resolver::Result<(Did, DidDocResponse, Vec<IdentityWarning>)> { 1043 1047 let did = self.resolve_handle(handle).await?; 1044 1048 let resp = self.resolve_did_doc(&did).await?; 1045 - let resp_for_parse = resp.clone(); 1046 - let doc_borrowed = resp_for_parse.parse()?; 1049 + let doc_borrowed = resp.parse()?; 1047 1050 if self.opts.validate_doc_id && doc_borrowed.id.as_str() != did.as_str() { 1048 - return Err(IdentityError::doc_id_mismatch( 1049 - did.clone().into_static(), 1050 - doc_borrowed.clone().into_static(), 1051 - )); 1051 + let owned_doc = resp.clone().into_owned().unwrap_or_else(|_| DidDocument { 1052 + context: jacquard_common::types::did_doc::default_context(), 1053 + id: Did::new_owned(doc_borrowed.id.as_str()).expect("already validated DID"), 1054 + also_known_as: None, 1055 + verification_method: None, 1056 + service: None, 1057 + extra_data: std::collections::BTreeMap::new(), 1058 + }); 1059 + return Err(IdentityError::doc_id_mismatch(did.clone(), owned_doc)); 1052 1060 } 1053 1061 let mut warnings = Vec::new(); 1054 1062 // Check handle alias presence (soft warning) ··· 1057 1065 .as_ref() 1058 1066 .map(|v| { 1059 1067 v.iter().any(|s| { 1060 - let s = s.strip_prefix("at://").unwrap_or(s); 1068 + let s = s.as_ref().strip_prefix("at://").unwrap_or(s.as_ref()); 1061 1069 s == handle.as_str() 1062 1070 }) 1063 1071 }) 1064 1072 .unwrap_or(false); 1065 1073 if !has_alias { 1066 1074 warnings.push(IdentityWarning::HandleAliasMismatch { 1067 - expected: handle.clone().into_static(), 1075 + expected: Handle::new_owned(handle.as_str()).expect("already validated handle"), 1068 1076 }); 1069 1077 } 1070 1078 Ok((did, resp, warnings)) ··· 1091 1099 } 1092 1100 1093 1101 #[cfg(feature = "cache")] 1094 - async fn invalidate_handle_chain(&self, handle: &Handle<'_>) { 1102 + async fn invalidate_handle_chain<S: Bos<str> + AsRef<str> + Sync>(&self, handle: &Handle<S>) { 1095 1103 if let Some(caches) = &self.caches { 1096 - let key = handle.clone().into_static(); 1104 + let key = Handle::new_owned(handle.as_str()).expect("already validated handle"); 1097 1105 cache_impl::invalidate(&caches.handle_to_did, &key); 1098 1106 } 1099 1107 } 1100 1108 1101 1109 #[cfg(feature = "cache")] 1102 - async fn invalidate_did_chain(&self, did: &Did<'_>) { 1110 + async fn invalidate_did_chain<S: Bos<str> + AsRef<str> + Sync>(&self, did: &Did<S>) { 1103 1111 if let Some(caches) = &self.caches { 1104 - let did_key = did.clone().into_static(); 1112 + let did_key = Did::new_owned(did.as_str()).expect("already validated DID"); 1105 1113 // Get doc before evicting to extract handles 1106 1114 if let Some(doc_resp) = cache_impl::get(&caches.did_to_doc, &did_key) { 1107 1115 let doc_resp_clone = (*doc_resp).clone(); ··· 1109 1117 if let Some(aliases) = &doc.also_known_as { 1110 1118 for alias in aliases { 1111 1119 if let Some(handle_str) = alias.as_ref().strip_prefix("at://") { 1112 - if let Ok(handle) = Handle::new(handle_str) { 1113 - let handle_key = handle.into_static(); 1114 - cache_impl::invalidate(&caches.handle_to_did, &handle_key); 1120 + if let Ok(handle) = Handle::new_owned(handle_str) { 1121 + cache_impl::invalidate(&caches.handle_to_did, &handle); 1115 1122 } 1116 1123 } 1117 1124 } ··· 1131 1138 } 1132 1139 1133 1140 #[cfg(feature = "cache")] 1134 - async fn invalidate_lexicon_chain(&self, nsid: &jacquard_common::types::string::Nsid<'_>) { 1141 + async fn invalidate_lexicon_chain<S: Bos<str> + AsRef<str> + Sync>( 1142 + &self, 1143 + nsid: &jacquard_common::types::string::Nsid<S>, 1144 + ) { 1135 1145 if let Some(caches) = &self.caches { 1136 - let nsid_key = nsid.clone().into_static(); 1146 + let nsid_key = Nsid::new_owned(nsid.as_str()).expect("already validated NSID"); 1137 1147 if let Some(schema) = cache_impl::get(&caches.nsid_to_schema, &nsid_key) { 1138 1148 let authority = SmolStr::from(nsid.domain_authority()); 1139 1149 cache_impl::invalidate(&caches.authority_to_did, &authority); ··· 1144 1154 } 1145 1155 1146 1156 /// Fetch a minimal DID document via Slingshot's mini-doc endpoint using a generic at-identifier 1147 - pub async fn fetch_mini_doc_via_slingshot_identifier( 1157 + pub async fn fetch_mini_doc_via_slingshot_identifier<S: Bos<str> + AsRef<str> + Sync>( 1148 1158 &self, 1149 - identifier: &AtIdentifier<'_>, 1159 + identifier: &AtIdentifier<S>, 1150 1160 ) -> resolver::Result<MiniDocResponse> { 1151 1161 let base = match &self.opts.plc_source { 1152 1162 PlcSource::Slingshot { base } => base.clone(),
+128 -75
crates/jacquard-identity/src/resolver.rs
··· 12 12 use bon::Builder; 13 13 use bytes::Bytes; 14 14 use http::StatusCode; 15 + use jacquard_common::bos::Bos; 15 16 use jacquard_common::deps::fluent_uri::Uri; 16 17 use jacquard_common::error::BoxError; 17 18 use jacquard_common::types::did::Did; ··· 20 21 use jacquard_common::types::string::{AtprotoStr, Handle}; 21 22 use jacquard_common::types::uri::UriValue; 22 23 use jacquard_common::types::value::{AtDataError, Data}; 23 - use jacquard_common::{CowStr, IntoStatic, deps::smol_str}; 24 + use jacquard_common::{CowStr, deps::smol_str}; 24 25 use n0_future::time::Duration; 25 26 use smol_str::SmolStr; 26 27 use std::collections::BTreeMap; ··· 81 82 #[allow(missing_docs)] 82 83 pub status: StatusCode, 83 84 /// Optional DID we intended to resolve; used for validation helpers 84 - pub requested: Option<Did<'static>>, 85 + pub requested: Option<Did>, 85 86 } 86 87 87 88 impl DidDocResponse { 88 - /// Parse as borrowed DidDocument<'_> 89 - pub fn parse<'b>(&'b self) -> Result<DidDocument<'b>> { 89 + /// Parse as borrowed DidDocument 90 + pub fn parse<'b>(&'b self) -> Result<DidDocument<CowStr<'b>>> { 90 91 if self.status.is_success() { 91 - if let Ok(doc) = serde_json::from_slice::<DidDocument<'b>>(&self.buffer) { 92 + if let Ok(doc) = serde_json::from_slice::<DidDocument<CowStr<'b>>>(&self.buffer) { 92 93 Ok(doc) 93 94 } else if let Ok(mini_doc) = serde_json::from_slice::<MiniDoc<'b>>(&self.buffer) { 94 95 let pds_uri = Uri::parse(mini_doc.pds.as_ref()) ··· 97 98 Ok(DidDocument { 98 99 context: default_context(), 99 100 id: mini_doc.did, 100 - also_known_as: Some(vec![CowStr::from(mini_doc.handle)]), 101 + also_known_as: Some(vec![CowStr::Owned(SmolStr::from( 102 + mini_doc.handle.as_str(), 103 + ))]), 101 104 verification_method: None, 102 105 service: Some(vec![Service { 103 106 id: CowStr::new_static("#atproto_pds"), ··· 131 134 /// Parse and validate that the DID in the document matches the requested DID if present. 132 135 /// 133 136 /// On mismatch, returns an error that contains the owned document for inspection. 134 - pub fn parse_validated<'b>(&'b self) -> Result<DidDocument<'b>> { 137 + pub fn parse_validated<'b>(&'b self) -> Result<DidDocument<CowStr<'b>>> { 135 138 let doc = self.parse()?; 136 139 if let Some(expected) = &self.requested { 137 140 if doc.id.as_str() != expected.as_str() { 138 - return Err(IdentityError::doc_id_mismatch( 139 - expected.clone(), 140 - doc.clone().into_static(), 141 - )); 141 + // Re-parse as owned for the error payload. 142 + let owned_doc = 143 + serde_json::from_slice::<DidDocument>(&self.buffer).unwrap_or_else(|_| { 144 + // Fallback: construct minimal doc for error reporting. 145 + DidDocument { 146 + context: default_context(), 147 + id: Did::new_owned(doc.id.as_str()).expect("already validated DID"), 148 + also_known_as: None, 149 + verification_method: None, 150 + service: None, 151 + extra_data: BTreeMap::new(), 152 + } 153 + }); 154 + return Err(IdentityError::doc_id_mismatch(expected.clone(), owned_doc)); 142 155 } 143 156 } 144 157 Ok(doc) 145 158 } 146 159 147 - /// Parse as owned DidDocument<'static> 148 - pub fn into_owned(self) -> Result<DidDocument<'static>> { 160 + /// Parse as owned DidDocument 161 + pub fn into_owned(self) -> Result<DidDocument> { 149 162 let did_str = self 150 163 .requested 151 164 .as_ref() ··· 153 166 .unwrap_or_else(|| SmolStr::new_static("unknown")); 154 167 155 168 if self.status.is_success() { 156 - if let Ok(doc) = serde_json::from_slice::<DidDocument<'_>>(&self.buffer) { 157 - Ok(doc.into_static()) 169 + if let Ok(doc) = serde_json::from_slice::<DidDocument>(&self.buffer) { 170 + Ok(doc) 158 171 } else if let Ok(mini_doc) = serde_json::from_slice::<MiniDoc<'_>>(&self.buffer) { 159 172 let pds_uri = Uri::parse(mini_doc.pds.as_ref()) 160 173 .map_err(|e| IdentityError::url(e))? 161 174 .to_owned(); 162 175 Ok(DidDocument { 163 176 context: default_context(), 164 - id: mini_doc.did, 165 - also_known_as: Some(vec![CowStr::from(mini_doc.handle)]), 177 + id: Did::new_owned(mini_doc.did.as_str()).expect("already validated DID"), 178 + also_known_as: Some(vec![SmolStr::from(mini_doc.handle.as_str())]), 166 179 verification_method: None, 167 180 service: Some(vec![Service { 168 - id: CowStr::new_static("#atproto_pds"), 169 - r#type: CowStr::new_static("AtprotoPersonalDataServer"), 181 + id: SmolStr::new_static("#atproto_pds"), 182 + r#type: SmolStr::new_static("AtprotoPersonalDataServer"), 170 183 service_endpoint: Some(Data::String(AtprotoStr::Uri(UriValue::Https( 171 184 pds_uri, 172 185 )))), 173 186 extra_data: BTreeMap::new(), 174 187 }]), 175 188 extra_data: BTreeMap::new(), 176 - } 177 - .into_static()) 189 + }) 178 190 } else { 179 191 Err(IdentityError::missing_pds_endpoint(did_str)) 180 192 } ··· 191 203 #[allow(missing_docs)] 192 204 pub struct MiniDoc<'a> { 193 205 #[serde(borrow)] 194 - pub did: Did<'a>, 206 + pub did: Did<CowStr<'a>>, 195 207 #[serde(borrow)] 196 - pub handle: Handle<'a>, 208 + pub handle: Handle<CowStr<'a>>, 197 209 #[serde(borrow)] 198 210 pub pds: CowStr<'a>, 199 211 #[serde(borrow, rename = "signingKey", alias = "signing_key")] ··· 303 315 304 316 /// Resolve handle 305 317 #[cfg(not(target_arch = "wasm32"))] 306 - fn resolve_handle(&self, handle: &Handle<'_>) -> impl Future<Output = Result<Did<'static>>> 318 + fn resolve_handle<S: Bos<str> + AsRef<str> + Sync>( 319 + &self, 320 + handle: &Handle<S>, 321 + ) -> impl Future<Output = Result<Did>> 307 322 where 308 323 Self: Sync; 309 324 310 325 /// Resolve handle 311 326 #[cfg(target_arch = "wasm32")] 312 - fn resolve_handle(&self, handle: &Handle<'_>) -> impl Future<Output = Result<Did<'static>>>; 327 + fn resolve_handle<S: Bos<str> + AsRef<str> + Sync>( 328 + &self, 329 + handle: &Handle<S>, 330 + ) -> impl Future<Output = Result<Did>>; 313 331 314 332 /// Resolve DID document 315 333 #[cfg(not(target_arch = "wasm32"))] 316 - fn resolve_did_doc(&self, did: &Did<'_>) -> impl Future<Output = Result<DidDocResponse>> 334 + fn resolve_did_doc<S: Bos<str> + AsRef<str> + Sync>( 335 + &self, 336 + did: &Did<S>, 337 + ) -> impl Future<Output = Result<DidDocResponse>> 317 338 where 318 339 Self: Sync; 319 340 320 341 /// Resolve DID document 321 342 #[cfg(target_arch = "wasm32")] 322 - fn resolve_did_doc(&self, did: &Did<'_>) -> impl Future<Output = Result<DidDocResponse>>; 343 + fn resolve_did_doc<S: Bos<str> + AsRef<str> + Sync>( 344 + &self, 345 + did: &Did<S>, 346 + ) -> impl Future<Output = Result<DidDocResponse>>; 323 347 324 348 /// Resolve DID doc from an identifier 325 349 #[cfg(not(target_arch = "wasm32"))] 326 - fn resolve_ident( 350 + fn resolve_ident<S: Bos<str> + AsRef<str> + Sync>( 327 351 &self, 328 - actor: &AtIdentifier<'_>, 352 + actor: &AtIdentifier<S>, 329 353 ) -> impl Future<Output = Result<DidDocResponse>> 330 354 where 331 355 Self: Sync, 332 356 { 333 357 async move { 334 358 match actor { 335 - AtIdentifier::Did(did) => self.resolve_did_doc(&did).await, 359 + AtIdentifier::Did(did) => self.resolve_did_doc(did).await, 336 360 AtIdentifier::Handle(handle) => { 337 - let did = self.resolve_handle(&handle).await?; 361 + let did = self.resolve_handle(handle).await?; 338 362 self.resolve_did_doc(&did).await 339 363 } 340 364 } ··· 343 367 344 368 /// Resolve DID doc from an identifier 345 369 #[cfg(target_arch = "wasm32")] 346 - fn resolve_ident( 370 + fn resolve_ident<S: Bos<str> + AsRef<str> + Sync>( 347 371 &self, 348 - actor: &AtIdentifier<'_>, 372 + actor: &AtIdentifier<S>, 349 373 ) -> impl Future<Output = Result<DidDocResponse>> { 350 374 async move { 351 375 match actor { 352 - AtIdentifier::Did(did) => self.resolve_did_doc(&did).await, 376 + AtIdentifier::Did(did) => self.resolve_did_doc(did).await, 353 377 AtIdentifier::Handle(handle) => { 354 - let did = self.resolve_handle(&handle).await?; 378 + let did = self.resolve_handle(handle).await?; 355 379 self.resolve_did_doc(&did).await 356 380 } 357 381 } ··· 360 384 361 385 /// Resolve DID doc from an identifier 362 386 #[cfg(not(target_arch = "wasm32"))] 363 - fn resolve_ident_owned( 387 + fn resolve_ident_owned<S: Bos<str> + AsRef<str> + Sync>( 364 388 &self, 365 - actor: &AtIdentifier<'_>, 366 - ) -> impl Future<Output = Result<DidDocument<'static>>> 389 + actor: &AtIdentifier<S>, 390 + ) -> impl Future<Output = Result<DidDocument>> 367 391 where 368 392 Self: Sync, 369 393 { 370 394 async move { 371 395 match actor { 372 - AtIdentifier::Did(did) => self.resolve_did_doc_owned(&did).await, 396 + AtIdentifier::Did(did) => self.resolve_did_doc_owned(did).await, 373 397 AtIdentifier::Handle(handle) => { 374 - let did = self.resolve_handle(&handle).await?; 398 + let did = self.resolve_handle(handle).await?; 375 399 self.resolve_did_doc_owned(&did).await 376 400 } 377 401 } ··· 380 404 381 405 /// Resolve DID doc from an identifier 382 406 #[cfg(target_arch = "wasm32")] 383 - fn resolve_ident_owned( 407 + fn resolve_ident_owned<S: Bos<str> + AsRef<str> + Sync>( 384 408 &self, 385 - actor: &AtIdentifier<'_>, 386 - ) -> impl Future<Output = Result<DidDocument<'static>>> { 409 + actor: &AtIdentifier<S>, 410 + ) -> impl Future<Output = Result<DidDocument>> { 387 411 async move { 388 412 match actor { 389 - AtIdentifier::Did(did) => self.resolve_did_doc_owned(&did).await, 413 + AtIdentifier::Did(did) => self.resolve_did_doc_owned(did).await, 390 414 AtIdentifier::Handle(handle) => { 391 - let did = self.resolve_handle(&handle).await?; 415 + let did = self.resolve_handle(handle).await?; 392 416 self.resolve_did_doc_owned(&did).await 393 417 } 394 418 } ··· 397 421 398 422 /// Resolve the DID document and return an owned version 399 423 #[cfg(not(target_arch = "wasm32"))] 400 - fn resolve_did_doc_owned( 424 + fn resolve_did_doc_owned<S: Bos<str> + AsRef<str> + Sync>( 401 425 &self, 402 - did: &Did<'_>, 403 - ) -> impl Future<Output = Result<DidDocument<'static>>> 426 + did: &Did<S>, 427 + ) -> impl Future<Output = Result<DidDocument>> 404 428 where 405 429 Self: Sync, 406 430 { ··· 409 433 410 434 /// Resolve the DID document and return an owned version 411 435 #[cfg(target_arch = "wasm32")] 412 - fn resolve_did_doc_owned( 436 + fn resolve_did_doc_owned<S: Bos<str> + AsRef<str> + Sync>( 413 437 &self, 414 - did: &Did<'_>, 415 - ) -> impl Future<Output = Result<DidDocument<'static>>> { 438 + did: &Did<S>, 439 + ) -> impl Future<Output = Result<DidDocument>> { 416 440 async { self.resolve_did_doc(did).await?.into_owned() } 417 441 } 418 442 419 443 /// Return the PDS url for a DID 420 444 #[cfg(not(target_arch = "wasm32"))] 421 - fn pds_for_did( 445 + fn pds_for_did<S: Bos<str> + AsRef<str> + Sync>( 422 446 &self, 423 - did: &Did<'_>, 447 + did: &Did<S>, 424 448 ) -> impl Future<Output = Result<jacquard_common::deps::fluent_uri::Uri<String>>> 425 449 where 426 450 Self: Sync, ··· 431 455 // Default-on doc id equality check 432 456 if self.options().validate_doc_id { 433 457 if doc.id.as_str() != did.as_str() { 458 + let owned_doc = resp.clone().into_owned().unwrap_or_else(|_| DidDocument { 459 + context: default_context(), 460 + id: Did::new_owned(doc.id.as_str()).expect("already validated DID"), 461 + also_known_as: None, 462 + verification_method: None, 463 + service: None, 464 + extra_data: BTreeMap::new(), 465 + }); 434 466 return Err(IdentityError::doc_id_mismatch( 435 - did.clone().into_static(), 436 - doc.clone().into_static(), 467 + Did::new_owned(did.as_str()).expect("already validated DID"), 468 + owned_doc, 437 469 )); 438 470 } 439 471 } 440 472 doc.pds_endpoint() 473 + .map(|u| u.to_owned()) 441 474 .ok_or_else(|| IdentityError::missing_pds_endpoint(did.as_str())) 442 475 } 443 476 } 444 477 445 478 /// Return the PDS url for a DID 446 479 #[cfg(target_arch = "wasm32")] 447 - fn pds_for_did( 480 + fn pds_for_did<S: Bos<str> + AsRef<str> + Sync>( 448 481 &self, 449 - did: &Did<'_>, 482 + did: &Did<S>, 450 483 ) -> impl Future<Output = Result<jacquard_common::deps::fluent_uri::Uri<String>>> { 451 484 async { 452 485 let resp = self.resolve_did_doc(did).await?; ··· 454 487 // Default-on doc id equality check 455 488 if self.options().validate_doc_id { 456 489 if doc.id.as_str() != did.as_str() { 490 + let owned_doc = resp.clone().into_owned().unwrap_or_else(|_| DidDocument { 491 + context: default_context(), 492 + id: Did::new_owned(doc.id.as_str()).expect("already validated DID"), 493 + also_known_as: None, 494 + verification_method: None, 495 + service: None, 496 + extra_data: BTreeMap::new(), 497 + }); 457 498 return Err(IdentityError::doc_id_mismatch( 458 - did.clone().into_static(), 459 - doc.clone().into_static(), 499 + Did::new_owned(did.as_str()).expect("already validated DID"), 500 + owned_doc, 460 501 )); 461 502 } 462 503 } 463 504 doc.pds_endpoint() 505 + .map(|u| u.to_owned()) 464 506 .ok_or_else(|| IdentityError::missing_pds_endpoint(did.as_str())) 465 507 } 466 508 } 467 509 468 - /// Return the DIS and PDS url for a handle 510 + /// Return the DID and PDS url for a handle 469 511 #[cfg(not(target_arch = "wasm32"))] 470 - fn pds_for_handle( 512 + fn pds_for_handle<S: Bos<str> + AsRef<str> + Sync>( 471 513 &self, 472 - handle: &Handle<'_>, 473 - ) -> impl Future<Output = Result<(Did<'static>, jacquard_common::deps::fluent_uri::Uri<String>)>> 514 + handle: &Handle<S>, 515 + ) -> impl Future<Output = Result<(Did, jacquard_common::deps::fluent_uri::Uri<String>)>> 474 516 where 475 517 Self: Sync, 476 518 { ··· 481 523 } 482 524 } 483 525 484 - /// Return the DIS and PDS url for a handle 526 + /// Return the DID and PDS url for a handle 485 527 #[cfg(target_arch = "wasm32")] 486 - fn pds_for_handle( 528 + fn pds_for_handle<S: Bos<str> + AsRef<str> + Sync>( 487 529 &self, 488 - handle: &Handle<'_>, 489 - ) -> impl Future<Output = Result<(Did<'static>, jacquard_common::deps::fluent_uri::Uri<String>)>> 490 - { 530 + handle: &Handle<S>, 531 + ) -> impl Future<Output = Result<(Did, jacquard_common::deps::fluent_uri::Uri<String>)>> { 491 532 async { 492 533 let did = self.resolve_handle(handle).await?; 493 534 let pds = self.pds_for_did(&did).await?; ··· 503 544 } 504 545 505 546 /// Resolve handle 506 - async fn resolve_handle(&self, handle: &Handle<'_>) -> Result<Did<'static>> { 547 + async fn resolve_handle<S: Bos<str> + AsRef<str> + Sync>( 548 + &self, 549 + handle: &Handle<S>, 550 + ) -> Result<Did> { 507 551 self.as_ref().resolve_handle(handle).await 508 552 } 509 553 510 554 /// Resolve DID document 511 - async fn resolve_did_doc(&self, did: &Did<'_>) -> Result<DidDocResponse> { 555 + async fn resolve_did_doc<S: Bos<str> + AsRef<str> + Sync>( 556 + &self, 557 + did: &Did<S>, 558 + ) -> Result<DidDocResponse> { 512 559 self.as_ref().resolve_did_doc(did).await 513 560 } 514 561 } ··· 520 567 } 521 568 522 569 /// Resolve handle 523 - async fn resolve_handle(&self, handle: &Handle<'_>) -> Result<Did<'static>> { 570 + async fn resolve_handle<S: Bos<str> + AsRef<str> + Sync>( 571 + &self, 572 + handle: &Handle<S>, 573 + ) -> Result<Did> { 524 574 self.as_ref().resolve_handle(handle).await 525 575 } 526 576 527 577 /// Resolve DID document 528 - async fn resolve_did_doc(&self, did: &Did<'_>) -> Result<DidDocResponse> { 578 + async fn resolve_did_doc<S: Bos<str> + AsRef<str> + Sync>( 579 + &self, 580 + did: &Did<S>, 581 + ) -> Result<DidDocResponse> { 529 582 self.as_ref().resolve_did_doc(did).await 530 583 } 531 584 } ··· 662 715 )] 663 716 DocIdMismatch { 664 717 /// The DID that was requested and expected to appear as the document `id`. 665 - expected: Did<'static>, 718 + expected: Did, 666 719 /// The DID document we *actually* got 667 - doc: DidDocument<'static>, 720 + doc: DidDocument, 668 721 }, 669 722 } 670 723 ··· 791 844 } 792 845 793 846 /// Create a doc id mismatch error 794 - pub fn doc_id_mismatch(expected: Did<'static>, doc: DidDocument<'static>) -> Self { 847 + pub fn doc_id_mismatch(expected: Did, doc: DidDocument) -> Self { 795 848 Self::new(IdentityErrorKind::DocIdMismatch { expected, doc }, None) 796 849 } 797 850 }
+13 -9
crates/jacquard-lexgen/src/fetch/sources/atproto.rs
··· 1 1 use super::LexiconSource; 2 - use jacquard_common::xrpc::atproto::{ListRecords, ListRecordsRecord}; 2 + use jacquard_common::deps::smol_str::SmolStr; 3 3 use jacquard_common::types::ident::AtIdentifier; 4 4 use jacquard_common::types::string::Nsid; 5 5 use jacquard_common::xrpc::XrpcExt; 6 - use jacquard_common::{CowStr, IntoStatic}; 6 + use jacquard_common::xrpc::atproto::{ListRecords, ListRecordsRecord}; 7 + use jacquard_common::{Bos, IntoStatic}; 7 8 use jacquard_identity::JacquardResolver; 8 9 use jacquard_identity::lexicon_resolver::LexiconSchemaResolver; 9 10 use jacquard_identity::resolver::{IdentityResolver, ResolverOptions}; 10 11 use jacquard_lexicon::lexicon::LexiconDoc; 11 12 use miette::{Result, miette}; 13 + use serde::Serialize; 12 14 use std::collections::HashMap; 13 15 14 16 #[derive(Debug, Clone)] ··· 19 21 20 22 impl AtProtoSource { 21 23 /// Fetch a single lexicon schema by NSID using DNS + XRPC resolution 22 - async fn fetch_single_lexicon( 24 + async fn fetch_single_lexicon<S: Bos<str> + AsRef<str> + Sync>( 23 25 &self, 24 26 resolver: &JacquardResolver, 25 - nsid: &Nsid<'_>, 27 + nsid: &Nsid<S>, 26 28 ) -> Result<HashMap<String, LexiconDoc<'_>>> { 27 29 let schema = resolver 28 30 .resolve_lexicon_schema(nsid) ··· 35 37 Ok(lexicons) 36 38 } 37 39 38 - fn parse_lexicon_record(record_data: &ListRecordsRecord<'_>) -> Option<LexiconDoc<'static>> { 40 + fn parse_lexicon_record<S: Bos<str> + AsRef<str> + Sync + Serialize>( 41 + record_data: &ListRecordsRecord<S>, 42 + ) -> Option<LexiconDoc<'static>> { 39 43 // // Extract the 'value' field from the record 40 44 // let value = match record_data { 41 45 // jacquard_common::types::value::Data::Object(map) => map.0.get("value")?, ··· 67 71 let resolver = JacquardResolver::new_dns(http.clone(), ResolverOptions::default()); 68 72 69 73 // Try parsing as NSID first (for single lexicon fetch) 70 - if let Ok(nsid) = Nsid::new(&self.endpoint) { 74 + if let Ok(nsid) = Nsid::new(self.endpoint.as_ref()) { 71 75 return self.fetch_single_lexicon(&resolver, &nsid).await; 72 76 } 73 77 74 78 // Otherwise parse as at-identifier (handle or DID) for bulk fetch 75 - let identifier = AtIdentifier::new(&self.endpoint) 79 + let identifier = AtIdentifier::new(self.endpoint.as_ref()) 76 80 .map_err(|e| miette!("Invalid endpoint '{}': {}", self.endpoint, e))?; 77 81 78 82 // Resolve to get PDS endpoint ··· 91 95 92 96 // Determine repo - use slice if provided, otherwise use the resolved DID 93 97 let repo = if let Some(ref slice) = self.slice { 94 - AtIdentifier::new(slice) 98 + AtIdentifier::new(slice.as_ref()) 95 99 .map_err(|e| miette!("Invalid slice '{}': {}", slice, e))? 96 100 .into_static() 97 101 } else { ··· 129 133 eprintln!("Warning: Batch decode failed from {}: {}", self.endpoint, e); 130 134 eprintln!("Retrying with limit=1 to skip invalid records..."); 131 135 132 - let mut cursor: Option<CowStr> = None; 136 + let mut cursor: Option<SmolStr> = None; 133 137 loop { 134 138 let req = ListRecords { 135 139 repo: repo.clone().into_static().into(),
+6 -4
crates/jacquard-lexgen/src/fetch/sources/jsonfile.rs
··· 1 1 use super::LexiconSource; 2 - use jacquard_common::IntoStatic; 3 2 use jacquard_common::types::value::Data; 3 + use jacquard_common::{Bos, IntoStatic}; 4 4 use jacquard_lexicon::lexicon::LexiconDoc; 5 5 use miette::{IntoDiagnostic, Result}; 6 - use serde::Deserialize; 6 + use serde::{Deserialize, Serialize}; 7 7 use std::collections::HashMap; 8 8 use std::path::PathBuf; 9 9 ··· 15 15 #[derive(Deserialize)] 16 16 struct RecordsFile<'a> { 17 17 #[serde(borrow)] 18 - records: Vec<Data<'a>>, 18 + records: Vec<Data<&'a str>>, 19 19 } 20 20 21 21 impl LexiconSource for JsonFileSource { ··· 37 37 } 38 38 39 39 impl JsonFileSource { 40 - fn parse_lexicon_record(record_data: &Data<'_>) -> Option<LexiconDoc<'static>> { 40 + fn parse_lexicon_record<S: Bos<str> + AsRef<str> + Sync + Serialize>( 41 + record_data: &Data<S>, 42 + ) -> Option<LexiconDoc<'static>> { 41 43 let value = match record_data { 42 44 Data::Object(map) => map.0.get("value")?, 43 45 _ => return None,