diff --git a/crates/jacquard-common/src/xrpc.rs b/crates/jacquard-common/src/xrpc.rs index 37965b76..035b1b4a 100644 --- a/crates/jacquard-common/src/xrpc.rs +++ b/crates/jacquard-common/src/xrpc.rs @@ -290,7 +290,7 @@ impl IntoStatic for CallOptions<'_> { /// ``` pub trait XrpcExt: HttpClient { /// Start building an XRPC call for the given base URI. - fn xrpc<'a>(&'a self, base: Uri) -> XrpcCall<'a, Self> + fn xrpc<'a>(&'a self, base: Uri<&'a str>) -> XrpcCall<'a, Self> where Self: Sized, { @@ -454,7 +454,7 @@ pub trait XrpcStreamingClient: XrpcClient + HttpClientExt { /// ``` pub struct XrpcCall<'a, C: HttpClient> { pub(crate) client: &'a C, - pub(crate) base: Uri, + pub(crate) base: Uri<&'a str>, pub(crate) opts: CallOptions<'a>, } @@ -578,11 +578,7 @@ impl From
for HeaderName { /// 3. Builds new path: `{base_path}/xrpc/{nsid}` /// 4. Optionally sets query from serialized parameters /// 5. Returns the constructed URI -fn xrpc_endpoint_uri( - base: &Uri, - nsid: &str, - query: Option<&str>, -) -> XrpcResult> { +fn xrpc_endpoint_uri(base: &Uri<&str>, nsid: &str, query: Option<&str>) -> XrpcResult> { use crate::error::ClientError; let base_path = base.path().as_str().trim_end_matches('/'); @@ -615,13 +611,12 @@ fn xrpc_endpoint_uri( } Uri::parse(uri_str) - .map(|u| u.to_owned()) .map_err(|_| ClientError::invalid_request("Failed to construct XRPC endpoint URI")) } /// Build an HTTP request for an XRPC call given base URI and options pub fn build_http_request<'s, R>( - base: &Uri, + base: &Uri<&str>, req: &R, opts: &CallOptions<'_>, ) -> XrpcResult>> @@ -1207,9 +1202,7 @@ mod tests { let opts = CallOptions::default(); // AC1.1: Base URI without trailing slash + NSID produces correct `/xrpc/{nsid}` path - let base1 = Uri::parse("https://pds.example.com") - .expect("URI should be valid") - .to_owned(); + let base1 = Uri::parse("https://pds.example.com").expect("URI should be valid"); let req1 = build_http_request(&base1, &Req, &opts).unwrap(); let uri1 = req1.uri().to_string(); assert!( @@ -1223,9 +1216,7 @@ mod tests { ); // AC1.2: Base URI with sub-path preserves it: `/base/xrpc/{nsid}` - let base2 = Uri::parse("https://pds.example.com/base") - .expect("URI should be valid") - .to_owned(); + let base2 = Uri::parse("https://pds.example.com/base").expect("URI should be valid"); let req2 = build_http_request(&base2, &Req, &opts).unwrap(); let uri2 = req2.uri().to_string(); assert!( @@ -1239,9 +1230,7 @@ mod tests { ); // AC1.5: Base URI with trailing slash is normalized (slash stripped) before construction - let base_with_slash = Uri::parse("https://pds.example.com/") - .expect("URI should be valid") - .to_owned(); + let base_with_slash = Uri::parse("https://pds.example.com/").expect("URI should be valid"); let req_slash = build_http_request(&base_with_slash, &Req, &opts).unwrap(); let uri_slash = req_slash.uri().to_string(); assert!( @@ -1285,9 +1274,7 @@ mod tests { } let opts = CallOptions::default(); - let base = Uri::parse("https://pds.example.com") - .expect("URI should be valid") - .to_owned(); + let base = Uri::parse("https://pds.example.com").expect("URI should be valid"); // AC1.3: Query parameters from serde serialisation are set correctly let req_with_params = QueryReq { @@ -1359,9 +1346,7 @@ mod tests { } let opts = CallOptions::default(); - let base = Uri::parse("https://pds.example.com") - .expect("URI should be valid") - .to_owned(); + let base = Uri::parse("https://pds.example.com").expect("URI should be valid"); // AC1.3: Test with spaces (serde_html_form uses + for spaces per application/x-www-form-urlencoded) let req_spaces = QueryReq { @@ -1447,9 +1432,7 @@ mod tests { let opts = CallOptions::default(); // Ensure no double slashes in path - let base1 = Uri::parse("https://pds") - .expect("URI should be valid") - .to_owned(); + let base1 = Uri::parse("https://pds").expect("URI should be valid"); let req1 = build_http_request(&base1, &Req, &opts).unwrap(); let uri1 = req1.uri().to_string(); assert!( @@ -1458,9 +1441,7 @@ mod tests { uri1 ); - let base2 = Uri::parse("https://pds/base") - .expect("URI should be valid") - .to_owned(); + let base2 = Uri::parse("https://pds/base").expect("URI should be valid"); let req2 = build_http_request(&base2, &Req, &opts).unwrap(); let uri2 = req2.uri().to_string(); assert!( diff --git a/crates/jacquard-identity/src/lexicon_resolver.rs b/crates/jacquard-identity/src/lexicon_resolver.rs index b44277dc..68799144 100644 --- a/crates/jacquard-identity/src/lexicon_resolver.rs +++ b/crates/jacquard-identity/src/lexicon_resolver.rs @@ -7,7 +7,7 @@ use crate::resolver::{IdentityError, IdentityResolver}; use jacquard_common::{ - IntoStatic, + bos::Bos, deps::smol_str, types::{cid::Cid, did::Did, string::Nsid}, }; @@ -23,19 +23,19 @@ pub trait LexiconAuthorityResolver { /// (e.g., `app.bsky.feed` → query `_lexicon.feed.bsky.app`). /// /// Note: No hierarchical fallback - per the spec, only exact authority match is checked. - async fn resolve_lexicon_authority( + async fn resolve_lexicon_authority + AsRef + Sync>( &self, - nsid: &Nsid, - ) -> std::result::Result, LexiconResolutionError>; + nsid: &Nsid, + ) -> std::result::Result; } /// Resolve lexicon schemas (NSID → schema document) #[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))] pub trait LexiconSchemaResolver { /// Resolve a complete lexicon schema for an NSID - async fn resolve_lexicon_schema( + async fn resolve_lexicon_schema + AsRef + Sync>( &self, - nsid: &Nsid, + nsid: &Nsid, ) -> std::result::Result, LexiconResolutionError>; } @@ -43,11 +43,11 @@ pub trait LexiconSchemaResolver { #[derive(Debug, Clone)] pub struct ResolvedLexiconSchema<'s> { /// The NSID of the schema - pub nsid: Nsid<'s>, + pub nsid: Nsid, /// DID of the repository this schema was fetched from - pub repo: Did<'s>, + pub repo: Did, /// Content ID of the record (for cache invalidation) - pub cid: Cid<'s>, + pub cid: Cid, /// Parsed lexicon document pub doc: jacquard_lexicon::lexicon::LexiconDoc<'s>, } @@ -321,10 +321,10 @@ impl crate::JacquardResolver { /// /// Queries `_lexicon.{reversed-authority}` for a TXT record containing `did=...` #[cfg(all(feature = "dns", not(target_family = "wasm")))] - async fn resolve_lexicon_authority_dns( + async fn resolve_lexicon_authority_dns + AsRef + Sync>( &self, - nsid: &Nsid<'_>, - ) -> std::result::Result, LexiconResolutionError> { + nsid: &Nsid, + ) -> std::result::Result { let Some(dns) = &self.dns else { return Err(LexiconResolutionError::dns_not_configured()); }; @@ -347,14 +347,10 @@ impl crate::JacquardResolver { for data in txt.txt_data().iter() { let text = std::str::from_utf8(data).unwrap_or(""); if let Some(did_str) = text.strip_prefix("did=") { - use jacquard_common::IntoStatic; - - return Did::new_owned(did_str) - .map(|d| d.into_static()) - .map_err(|_| { - LexiconResolutionError::invalid_did(authority, did_str) - .with_context(format!("resolving NSID {}", nsid)) - }); + return Did::new_owned(did_str).map_err(|_| { + LexiconResolutionError::invalid_did(authority, did_str) + .with_context(format!("resolving NSID {}", nsid)) + }); } } } @@ -363,13 +359,12 @@ impl crate::JacquardResolver { } } - #[cfg(all(feature = "dns", not(target_family = "wasm")))] impl LexiconAuthorityResolver for crate::JacquardResolver { - async fn resolve_lexicon_authority( + async fn resolve_lexicon_authority + AsRef + Sync>( &self, - nsid: &Nsid<'_>, - ) -> std::result::Result, LexiconResolutionError> { + nsid: &Nsid, + ) -> std::result::Result { // Try cache first #[cfg(feature = "cache")] if let Some(caches) = &self.caches { @@ -404,10 +399,10 @@ impl LexiconAuthorityResolver for crate::JacquardResolver { #[cfg(not(all(feature = "dns", not(target_family = "wasm"))))] impl LexiconAuthorityResolver for crate::JacquardResolver { - async fn resolve_lexicon_authority( + async fn resolve_lexicon_authority + AsRef + Sync>( &self, - nsid: &Nsid<'_>, - ) -> std::result::Result, LexiconResolutionError> { + nsid: &Nsid, + ) -> std::result::Result { // Use DNS-over-HTTPS fallback for WASM/non-DNS builds self.resolve_lexicon_authority_doh(nsid).await } @@ -416,10 +411,10 @@ impl LexiconAuthorityResolver for crate::JacquardResolver { impl crate::JacquardResolver { /// Resolve lexicon authority via DNS-over-HTTPS (for WASM compatibility) #[allow(dead_code)] - async fn resolve_lexicon_authority_doh( + async fn resolve_lexicon_authority_doh + AsRef + Sync>( &self, - nsid: &Nsid<'_>, - ) -> std::result::Result, LexiconResolutionError> { + nsid: &Nsid, + ) -> std::result::Result { // Try cache first #[cfg(feature = "cache")] if let Some(caches) = &self.caches { @@ -453,12 +448,10 @@ impl crate::JacquardResolver { let txt_data = data.trim_matches('"'); if let Some(did_str) = txt_data.strip_prefix("did=") { - let result = Did::new_owned(did_str) - .map(|d| d.into_static()) - .map_err(|_| { - LexiconResolutionError::invalid_did(authority, did_str) - .with_context(format!("resolving NSID {}", nsid)) - }); + let result = Did::new_owned(did_str).map_err(|_| { + LexiconResolutionError::invalid_did(authority, did_str) + .with_context(format!("resolving NSID {}", nsid)) + }); // Cache on success #[cfg(feature = "cache")] @@ -484,18 +477,22 @@ impl crate::JacquardResolver { } impl LexiconSchemaResolver for crate::JacquardResolver { - async fn resolve_lexicon_schema( + async fn resolve_lexicon_schema + AsRef + Sync>( &self, - nsid: &Nsid<'_>, + nsid: &Nsid, ) -> std::result::Result, LexiconResolutionError> { use jacquard_common::xrpc::atproto::GetRecord; use jacquard_common::{IntoStatic, xrpc::XrpcExt}; + use jacquard_common::CowStr; + + let nsid_str = nsid.as_str(); + let owned_nsid: Nsid = Nsid::new_owned(nsid_str).expect("already validated NSID"); + // Try cache first #[cfg(feature = "cache")] if let Some(caches) = &self.caches { - let key = nsid.clone().into_static(); - if let Some(schema) = crate::cache_impl::get(&caches.nsid_to_schema, &key) { + if let Some(schema) = crate::cache_impl::get(&caches.nsid_to_schema, &owned_nsid) { return Ok((*schema).clone()); } } @@ -528,9 +525,9 @@ impl LexiconSchemaResolver for crate::JacquardResolver { .map_err(|_| LexiconResolutionError::invalid_collection())?; let request = GetRecord { - repo: authority_did.clone().into(), - collection: collection.into_static(), - rkey: nsid.clone().into(), + repo: authority_did.clone().convert::>().into(), + collection: collection.convert::>(), + rkey: CowStr::from(nsid_str), cid: None, }; @@ -538,30 +535,29 @@ impl LexiconSchemaResolver for crate::JacquardResolver { .xrpc(pds) .send(&request) .await - .map_err(|e| LexiconResolutionError::fetch_failed(nsid.as_str(), e))?; + .map_err(|e| LexiconResolutionError::fetch_failed(nsid_str, e))?; let output = response .into_output() - .map_err(|e| LexiconResolutionError::fetch_failed(nsid.as_str(), e))?; + .map_err(|e| LexiconResolutionError::fetch_failed(nsid_str, e))?; // 4. Parse lexicon document from value let json_str = serde_json::to_string(&output.value) - .map_err(|e| LexiconResolutionError::parse_failed(nsid.as_str(), e))?; + .map_err(|e| LexiconResolutionError::parse_failed(nsid_str, e))?; let doc: jacquard_lexicon::lexicon::LexiconDoc = serde_json::from_str(&json_str) - .map_err(|e| LexiconResolutionError::parse_failed(nsid.as_str(), e))?; + .map_err(|e| LexiconResolutionError::parse_failed(nsid_str, e))?; #[cfg(feature = "tracing")] tracing::trace!("successfully parsed lexicon schema {}", nsid); let cid = output .cid - .ok_or_else(|| LexiconResolutionError::missing_cid(nsid.as_str()))? - .into_static(); + .ok_or_else(|| LexiconResolutionError::missing_cid(nsid_str))?; Ok(ResolvedLexiconSchema { - nsid: nsid.clone().into_static(), - repo: authority_did.into_static(), + nsid: owned_nsid.clone(), + repo: authority_did, cid, doc: doc.into_static(), }) @@ -576,7 +572,7 @@ impl LexiconSchemaResolver for crate::JacquardResolver { if let Some(caches) = &self.caches { crate::cache_impl::insert( &caches.nsid_to_schema, - nsid.clone().into_static(), + owned_nsid, std::sync::Arc::new(schema.clone()), ); } diff --git a/crates/jacquard-identity/src/lib.rs b/crates/jacquard-identity/src/lib.rs index 1757d2af..599e4c13 100644 --- a/crates/jacquard-identity/src/lib.rs +++ b/crates/jacquard-identity/src/lib.rs @@ -75,9 +75,9 @@ use crate::resolver::{ ResolverOptions, }; use bytes::Bytes; -use jacquard_common::xrpc::atproto::{ResolveDid, ResolveHandle}; #[cfg(feature = "streaming")] use jacquard_common::ByteStream; +use jacquard_common::bos::Bos; use jacquard_common::deps::fluent_uri::Uri; use jacquard_common::deps::fluent_uri::pct_enc::{ EString, @@ -88,8 +88,9 @@ use jacquard_common::http_client::HttpClient; use jacquard_common::types::did::Did; use jacquard_common::types::did_doc::DidDocument; use jacquard_common::types::ident::AtIdentifier; +use jacquard_common::types::string::Handle; use jacquard_common::xrpc::XrpcExt; -use jacquard_common::{IntoStatic, types::string::Handle}; +use jacquard_common::xrpc::atproto::{ResolveDid, ResolveHandle}; use reqwest::StatusCode; #[cfg(all(feature = "dns", not(target_family = "wasm")))] @@ -283,13 +284,13 @@ impl CacheConfig { #[derive(Clone)] pub struct ResolverCaches { /// Cache mapping handles to their resolved DIDs. - pub handle_to_did: cache_impl::Cache, Did<'static>>, + pub handle_to_did: cache_impl::Cache, /// Cache mapping DIDs to their full DID documents. - pub did_to_doc: cache_impl::Cache, Arc>, + pub did_to_doc: cache_impl::Cache>, /// Cache mapping authority strings (e.g., PDS hosts) to DIDs. - pub authority_to_did: cache_impl::Cache>, + pub authority_to_did: cache_impl::Cache, /// Cache mapping NSIDs to their resolved lexicon schemas. - pub nsid_to_schema: cache_impl::Cache, Arc>>, + pub nsid_to_schema: cache_impl::Cache>>, } #[cfg(feature = "cache")] @@ -420,7 +421,10 @@ impl JacquardResolver { /// /// - `did:web:example.com` → `https://example.com/.well-known/did.json` /// - `did:web:example.com:user:alice` → `https://example.com/user/alice/did.json` - fn did_web_url(&self, did: &Did<'_>) -> resolver::Result> { + fn did_web_url + AsRef + Sync>( + &self, + did: &Did, + ) -> resolver::Result> { // did:web:example.com[:path:segments] let s = did.as_str(); let rest = s @@ -555,80 +559,78 @@ impl JacquardResolver { Ok(results) } - fn parse_atproto_did_body(body: &str, identifier: &str) -> resolver::Result> { + fn parse_atproto_did_body(body: &str, identifier: &str) -> resolver::Result { let line = body .lines() .find(|l| !l.trim().is_empty()) .ok_or_else(|| IdentityError::invalid_well_known(identifier))?; - let did = Did::new(line.trim()) - .map_err(|e| IdentityError::invalid_well_known_with_source(identifier, e))?; - Ok(did.into_static()) + Did::new_owned(line.trim()) + .map_err(|e| IdentityError::invalid_well_known_with_source(identifier, e)) } } impl JacquardResolver { /// Resolve handle to DID via a PDS XRPC call (stateless, unauth by default) - pub async fn resolve_handle_via_pds( + pub async fn resolve_handle_via_pds + AsRef + Sync>( &self, - handle: &Handle<'_>, - ) -> resolver::Result> { + handle: &Handle, + ) -> resolver::Result { let pds = match &self.opts.pds_fallback { Some(u) => u.clone(), None => return Err(IdentityError::no_pds_fallback()), }; + let owned_handle: Handle = + Handle::new_owned(handle.as_str()).expect("already validated handle"); let req = ResolveHandle { - handle: handle.clone().into_static(), + handle: owned_handle, }; - let resp = self.http.xrpc(pds).send(&req).await.map_err(|e| { + let resp = self.http.xrpc(pds.borrow()).send(&req).await.map_err(|e| { IdentityError::from(e).with_context(format!("resolving handle {}", handle)) })?; // Note: XrpcError has GAT lifetimes that prevent boxing; use debug format - let out = resp.parse().map_err(|e| { + let out = resp.parse::().map_err(|e| { IdentityError::xrpc(jacquard_common::deps::smol_str::format_smolstr!("{:?}", e)) .with_context(format!("parsing response for handle {}", handle)) })?; - Did::new_owned(out.did.as_str()) - .map(|d| d.into_static()) - .map_err(|e| { - IdentityError::invalid_doc(jacquard_common::deps::smol_str::format_smolstr!( - "PDS returned invalid DID '{}': {}", - out.did, - e - )) - }) + Did::new_owned(out.did.as_str()).map_err(|e| { + IdentityError::invalid_doc(jacquard_common::deps::smol_str::format_smolstr!( + "PDS returned invalid DID '{}': {}", + out.did, + e + )) + }) } /// Fetch DID document via PDS resolveDid (returns owned DidDocument) - pub async fn fetch_did_doc_via_pds_owned( + pub async fn fetch_did_doc_via_pds_owned + AsRef + Sync>( &self, - did: &Did<'_>, - ) -> resolver::Result> { + did: &Did, + ) -> resolver::Result { let pds = match &self.opts.pds_fallback { Some(u) => u.clone(), None => return Err(IdentityError::no_pds_fallback()), }; - let req = ResolveDid { - did: did.clone(), - }; - let resp = self.http.xrpc(pds).send(&req).await.map_err(|e| { + let owned_did: Did = Did::new_owned(did.as_str()).expect("already validated DID"); + let req = ResolveDid { did: owned_did }; + let resp = self.http.xrpc(pds.borrow()).send(&req).await.map_err(|e| { IdentityError::from(e).with_context(format!("fetching DID doc for {}", did)) })?; // Note: XrpcError has GAT lifetimes that prevent boxing; use debug format - let out = resp.parse().map_err(|e| { + let out = resp.parse::().map_err(|e| { IdentityError::xrpc(jacquard_common::deps::smol_str::format_smolstr!("{:?}", e)) .with_context(format!("parsing DID doc response for {}", did)) })?; let doc_json = serde_json::to_value(&out.did_doc)?; let s = serde_json::to_string(&doc_json)?; - let doc_borrowed: DidDocument<'_> = serde_json::from_str(&s)?; - Ok(doc_borrowed.into_static()) + let doc: DidDocument = serde_json::from_str(&s)?; + Ok(doc) } /// Fetch a minimal DID document via a Slingshot mini-doc endpoint, if your PlcSource uses Slingshot. /// Returns the raw response wrapper for borrowed parsing and validation. - pub async fn fetch_mini_doc_via_slingshot( + pub async fn fetch_mini_doc_via_slingshot + AsRef + Sync>( &self, - did: &Did<'_>, + did: &Did, ) -> resolver::Result { let base = match &self.opts.plc_source { PlcSource::Slingshot { base } => base.clone(), @@ -640,11 +642,10 @@ impl JacquardResolver { } }; // Build URL using string manipulation, then parse - let qs = serde_html_form::to_string( - &ResolveDid { - did: did.clone().into_static(), - }, - ) + let owned_did: Did = Did::new_owned(did.as_str()).expect("already validated DID"); + let qs = serde_html_form::to_string(&ResolveDid { + did: owned_did.clone(), + }) .unwrap_or_default(); let url_str = if qs.is_empty() { format!( @@ -665,7 +666,7 @@ impl JacquardResolver { Ok(DidDocResponse { buffer: buf, status, - requested: Some(did.clone().into_static()), + requested: Some(owned_did), }) } } @@ -675,18 +676,21 @@ impl IdentityResolver for JacquardResolver { &self.opts } #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(handle = %handle)))] - async fn resolve_handle(&self, handle: &Handle<'_>) -> resolver::Result> { + async fn resolve_handle + AsRef + Sync>( + &self, + handle: &Handle, + ) -> resolver::Result { // Try cache first #[cfg(feature = "cache")] if let Some(caches) = &self.caches { - let key = handle.clone().into_static(); + let key = Handle::new_owned(handle.as_str()).expect("already validated handle"); if let Some(did) = cache_impl::get(&caches.handle_to_did, &key) { return Ok(did); } } let host = handle.as_str(); - let mut resolved_did: Option> = None; + let mut resolved_did: Option = None; 'outer: for step in &self.opts.handle_order { match step { @@ -694,8 +698,8 @@ impl IdentityResolver for JacquardResolver { if let Ok(txts) = self.dns_txt(host).await { for txt in txts { if let Some(did_str) = txt.strip_prefix("did=") { - if let Ok(did) = Did::new(did_str) { - resolved_did = Some(did.into_static()); + if let Ok(did) = Did::new_owned(did_str) { + resolved_did = Some(did); break 'outer; } } @@ -722,11 +726,11 @@ impl IdentityResolver for JacquardResolver { } // Public unauth fallback if self.opts.public_fallback_for_handle { - if let Ok(qs) = serde_html_form::to_string( - &ResolveHandle { - handle: (*handle).clone(), - }, - ) { + let owned_handle: Handle = + Handle::new_owned(handle.as_str()).expect("already validated handle"); + if let Ok(qs) = serde_html_form::to_string(&ResolveHandle { + handle: owned_handle, + }) { let url_str = format!( "https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?{}", qs @@ -744,7 +748,7 @@ impl IdentityResolver for JacquardResolver { val.get("did").and_then(|v| v.as_str()) { if let Ok(did) = Did::new_owned(did_str) { - resolved_did = Some(did.into_static()); + resolved_did = Some(did); break 'outer; } } @@ -758,11 +762,11 @@ impl IdentityResolver for JacquardResolver { } // Non-auth path: if PlcSource is Slingshot, use its resolveHandle endpoint. if let PlcSource::Slingshot { base } = &self.opts.plc_source { - let qs = serde_html_form::to_string( - &ResolveHandle { - handle: (*handle).clone(), - }, - ) + let owned_handle: Handle = + Handle::new_owned(handle.as_str()).expect("already validated handle"); + let qs = serde_html_form::to_string(&ResolveHandle { + handle: owned_handle, + }) .unwrap_or_default(); let url_str = if qs.is_empty() { format!( @@ -790,7 +794,7 @@ impl IdentityResolver for JacquardResolver { val.get("did").and_then(|v| v.as_str()) { if let Ok(did) = Did::new_owned(did_str) { - resolved_did = Some(did.into_static()); + resolved_did = Some(did); break 'outer; } } @@ -810,7 +814,7 @@ impl IdentityResolver for JacquardResolver { if let Some(caches) = &self.caches { cache_impl::insert( &caches.handle_to_did, - handle.clone().into_static(), + Handle::new_owned(handle.as_str()).expect("already validated handle"), did.clone(), ); } @@ -825,12 +829,16 @@ impl IdentityResolver for JacquardResolver { } #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(did = %did)))] - async fn resolve_did_doc(&self, did: &Did<'_>) -> resolver::Result { + async fn resolve_did_doc + AsRef + Sync>( + &self, + did: &Did, + ) -> resolver::Result { + let owned_did: Did = Did::new_owned(did.as_str()).expect("already validated DID"); + // Try cache first #[cfg(feature = "cache")] if let Some(caches) = &self.caches { - let key = did.clone().into_static(); - if let Some(doc_resp) = cache_impl::get(&caches.did_to_doc, &key) { + if let Some(doc_resp) = cache_impl::get(&caches.did_to_doc, &owned_did) { return Ok((*doc_resp).clone()); } } @@ -846,7 +854,7 @@ impl IdentityResolver for JacquardResolver { resolved_doc = Some(DidDocResponse { buffer: buf, status, - requested: Some(did.clone().into_static()), + requested: Some(owned_did.clone()), }); break 'outer; } @@ -869,7 +877,7 @@ impl IdentityResolver for JacquardResolver { resolved_doc = Some(DidDocResponse { buffer: buf, status, - requested: Some(did.clone().into_static()), + requested: Some(owned_did.clone()), }); break 'outer; } @@ -882,7 +890,7 @@ impl IdentityResolver for JacquardResolver { resolved_doc = Some(DidDocResponse { buffer: Bytes::from(buf), status: StatusCode::OK, - requested: Some(did.clone().into_static()), + requested: Some(owned_did.clone()), }); break 'outer; } @@ -893,7 +901,7 @@ impl IdentityResolver for JacquardResolver { resolved_doc = Some(DidDocResponse { buffer: buf, status, - requested: Some(did.clone().into_static()), + requested: Some(owned_did.clone()), }); break 'outer; } @@ -907,11 +915,7 @@ impl IdentityResolver for JacquardResolver { // Cache successful resolution #[cfg(feature = "cache")] if let Some(caches) = &self.caches { - cache_impl::insert( - &caches.did_to_doc, - did.clone().into_static(), - Arc::new(doc_resp.clone()), - ); + cache_impl::insert(&caches.did_to_doc, owned_did, Arc::new(doc_resp.clone())); } Ok(doc_resp) } else { @@ -1029,26 +1033,30 @@ pub enum IdentityWarning { /// The DID doc did not contain the expected handle alias under alsoKnownAs HandleAliasMismatch { #[allow(missing_docs)] - expected: Handle<'static>, + expected: Handle, }, } impl JacquardResolver { /// Resolve a handle to its DID, fetch the DID document, and return doc plus any warnings. /// This applies the default equality check on the document id (error with doc if mismatch). - pub async fn resolve_handle_and_doc( + pub async fn resolve_handle_and_doc + AsRef + Sync>( &self, - handle: &Handle<'_>, - ) -> resolver::Result<(Did<'static>, DidDocResponse, Vec)> { + handle: &Handle, + ) -> resolver::Result<(Did, DidDocResponse, Vec)> { let did = self.resolve_handle(handle).await?; let resp = self.resolve_did_doc(&did).await?; - let resp_for_parse = resp.clone(); - let doc_borrowed = resp_for_parse.parse()?; + let doc_borrowed = resp.parse()?; if self.opts.validate_doc_id && doc_borrowed.id.as_str() != did.as_str() { - return Err(IdentityError::doc_id_mismatch( - did.clone().into_static(), - doc_borrowed.clone().into_static(), - )); + let owned_doc = resp.clone().into_owned().unwrap_or_else(|_| DidDocument { + context: jacquard_common::types::did_doc::default_context(), + id: Did::new_owned(doc_borrowed.id.as_str()).expect("already validated DID"), + also_known_as: None, + verification_method: None, + service: None, + extra_data: std::collections::BTreeMap::new(), + }); + return Err(IdentityError::doc_id_mismatch(did.clone(), owned_doc)); } let mut warnings = Vec::new(); // Check handle alias presence (soft warning) @@ -1057,14 +1065,14 @@ impl JacquardResolver { .as_ref() .map(|v| { v.iter().any(|s| { - let s = s.strip_prefix("at://").unwrap_or(s); + let s = s.as_ref().strip_prefix("at://").unwrap_or(s.as_ref()); s == handle.as_str() }) }) .unwrap_or(false); if !has_alias { warnings.push(IdentityWarning::HandleAliasMismatch { - expected: handle.clone().into_static(), + expected: Handle::new_owned(handle.as_str()).expect("already validated handle"), }); } Ok((did, resp, warnings)) @@ -1091,17 +1099,17 @@ impl JacquardResolver { } #[cfg(feature = "cache")] - async fn invalidate_handle_chain(&self, handle: &Handle<'_>) { + async fn invalidate_handle_chain + AsRef + Sync>(&self, handle: &Handle) { if let Some(caches) = &self.caches { - let key = handle.clone().into_static(); + let key = Handle::new_owned(handle.as_str()).expect("already validated handle"); cache_impl::invalidate(&caches.handle_to_did, &key); } } #[cfg(feature = "cache")] - async fn invalidate_did_chain(&self, did: &Did<'_>) { + async fn invalidate_did_chain + AsRef + Sync>(&self, did: &Did) { if let Some(caches) = &self.caches { - let did_key = did.clone().into_static(); + let did_key = Did::new_owned(did.as_str()).expect("already validated DID"); // Get doc before evicting to extract handles if let Some(doc_resp) = cache_impl::get(&caches.did_to_doc, &did_key) { let doc_resp_clone = (*doc_resp).clone(); @@ -1109,9 +1117,8 @@ impl JacquardResolver { if let Some(aliases) = &doc.also_known_as { for alias in aliases { if let Some(handle_str) = alias.as_ref().strip_prefix("at://") { - if let Ok(handle) = Handle::new(handle_str) { - let handle_key = handle.into_static(); - cache_impl::invalidate(&caches.handle_to_did, &handle_key); + if let Ok(handle) = Handle::new_owned(handle_str) { + cache_impl::invalidate(&caches.handle_to_did, &handle); } } } @@ -1131,9 +1138,12 @@ impl JacquardResolver { } #[cfg(feature = "cache")] - async fn invalidate_lexicon_chain(&self, nsid: &jacquard_common::types::string::Nsid<'_>) { + async fn invalidate_lexicon_chain + AsRef + Sync>( + &self, + nsid: &jacquard_common::types::string::Nsid, + ) { if let Some(caches) = &self.caches { - let nsid_key = nsid.clone().into_static(); + let nsid_key = Nsid::new_owned(nsid.as_str()).expect("already validated NSID"); if let Some(schema) = cache_impl::get(&caches.nsid_to_schema, &nsid_key) { let authority = SmolStr::from(nsid.domain_authority()); cache_impl::invalidate(&caches.authority_to_did, &authority); @@ -1144,9 +1154,9 @@ impl JacquardResolver { } /// Fetch a minimal DID document via Slingshot's mini-doc endpoint using a generic at-identifier - pub async fn fetch_mini_doc_via_slingshot_identifier( + pub async fn fetch_mini_doc_via_slingshot_identifier + AsRef + Sync>( &self, - identifier: &AtIdentifier<'_>, + identifier: &AtIdentifier, ) -> resolver::Result { let base = match &self.opts.plc_source { PlcSource::Slingshot { base } => base.clone(), diff --git a/crates/jacquard-identity/src/resolver.rs b/crates/jacquard-identity/src/resolver.rs index d2293af5..e09a5101 100644 --- a/crates/jacquard-identity/src/resolver.rs +++ b/crates/jacquard-identity/src/resolver.rs @@ -12,6 +12,7 @@ use bon::Builder; use bytes::Bytes; use http::StatusCode; +use jacquard_common::bos::Bos; use jacquard_common::deps::fluent_uri::Uri; use jacquard_common::error::BoxError; use jacquard_common::types::did::Did; @@ -20,7 +21,7 @@ use jacquard_common::types::ident::AtIdentifier; use jacquard_common::types::string::{AtprotoStr, Handle}; use jacquard_common::types::uri::UriValue; use jacquard_common::types::value::{AtDataError, Data}; -use jacquard_common::{CowStr, IntoStatic, deps::smol_str}; +use jacquard_common::{CowStr, deps::smol_str}; use n0_future::time::Duration; use smol_str::SmolStr; use std::collections::BTreeMap; @@ -81,14 +82,14 @@ pub struct DidDocResponse { #[allow(missing_docs)] pub status: StatusCode, /// Optional DID we intended to resolve; used for validation helpers - pub requested: Option>, + pub requested: Option, } impl DidDocResponse { - /// Parse as borrowed DidDocument<'_> - pub fn parse<'b>(&'b self) -> Result> { + /// Parse as borrowed DidDocument + pub fn parse<'b>(&'b self) -> Result>> { if self.status.is_success() { - if let Ok(doc) = serde_json::from_slice::>(&self.buffer) { + if let Ok(doc) = serde_json::from_slice::>>(&self.buffer) { Ok(doc) } else if let Ok(mini_doc) = serde_json::from_slice::>(&self.buffer) { let pds_uri = Uri::parse(mini_doc.pds.as_ref()) @@ -97,7 +98,9 @@ impl DidDocResponse { Ok(DidDocument { context: default_context(), id: mini_doc.did, - also_known_as: Some(vec![CowStr::from(mini_doc.handle)]), + also_known_as: Some(vec![CowStr::Owned(SmolStr::from( + mini_doc.handle.as_str(), + ))]), verification_method: None, service: Some(vec![Service { id: CowStr::new_static("#atproto_pds"), @@ -131,21 +134,31 @@ impl DidDocResponse { /// Parse and validate that the DID in the document matches the requested DID if present. /// /// On mismatch, returns an error that contains the owned document for inspection. - pub fn parse_validated<'b>(&'b self) -> Result> { + pub fn parse_validated<'b>(&'b self) -> Result>> { let doc = self.parse()?; if let Some(expected) = &self.requested { if doc.id.as_str() != expected.as_str() { - return Err(IdentityError::doc_id_mismatch( - expected.clone(), - doc.clone().into_static(), - )); + // Re-parse as owned for the error payload. + let owned_doc = + serde_json::from_slice::(&self.buffer).unwrap_or_else(|_| { + // Fallback: construct minimal doc for error reporting. + DidDocument { + context: default_context(), + id: Did::new_owned(doc.id.as_str()).expect("already validated DID"), + also_known_as: None, + verification_method: None, + service: None, + extra_data: BTreeMap::new(), + } + }); + return Err(IdentityError::doc_id_mismatch(expected.clone(), owned_doc)); } } Ok(doc) } - /// Parse as owned DidDocument<'static> - pub fn into_owned(self) -> Result> { + /// Parse as owned DidDocument + pub fn into_owned(self) -> Result { let did_str = self .requested .as_ref() @@ -153,28 +166,27 @@ impl DidDocResponse { .unwrap_or_else(|| SmolStr::new_static("unknown")); if self.status.is_success() { - if let Ok(doc) = serde_json::from_slice::>(&self.buffer) { - Ok(doc.into_static()) + if let Ok(doc) = serde_json::from_slice::(&self.buffer) { + Ok(doc) } else if let Ok(mini_doc) = serde_json::from_slice::>(&self.buffer) { let pds_uri = Uri::parse(mini_doc.pds.as_ref()) .map_err(|e| IdentityError::url(e))? .to_owned(); Ok(DidDocument { context: default_context(), - id: mini_doc.did, - also_known_as: Some(vec![CowStr::from(mini_doc.handle)]), + id: Did::new_owned(mini_doc.did.as_str()).expect("already validated DID"), + also_known_as: Some(vec![SmolStr::from(mini_doc.handle.as_str())]), verification_method: None, service: Some(vec![Service { - id: CowStr::new_static("#atproto_pds"), - r#type: CowStr::new_static("AtprotoPersonalDataServer"), + id: SmolStr::new_static("#atproto_pds"), + r#type: SmolStr::new_static("AtprotoPersonalDataServer"), service_endpoint: Some(Data::String(AtprotoStr::Uri(UriValue::Https( pds_uri, )))), extra_data: BTreeMap::new(), }]), extra_data: BTreeMap::new(), - } - .into_static()) + }) } else { Err(IdentityError::missing_pds_endpoint(did_str)) } @@ -191,9 +203,9 @@ impl DidDocResponse { #[allow(missing_docs)] pub struct MiniDoc<'a> { #[serde(borrow)] - pub did: Did<'a>, + pub did: Did>, #[serde(borrow)] - pub handle: Handle<'a>, + pub handle: Handle>, #[serde(borrow)] pub pds: CowStr<'a>, #[serde(borrow, rename = "signingKey", alias = "signing_key")] @@ -303,38 +315,50 @@ pub trait IdentityResolver { /// Resolve handle #[cfg(not(target_arch = "wasm32"))] - fn resolve_handle(&self, handle: &Handle<'_>) -> impl Future>> + fn resolve_handle + AsRef + Sync>( + &self, + handle: &Handle, + ) -> impl Future> where Self: Sync; /// Resolve handle #[cfg(target_arch = "wasm32")] - fn resolve_handle(&self, handle: &Handle<'_>) -> impl Future>>; + fn resolve_handle + AsRef + Sync>( + &self, + handle: &Handle, + ) -> impl Future>; /// Resolve DID document #[cfg(not(target_arch = "wasm32"))] - fn resolve_did_doc(&self, did: &Did<'_>) -> impl Future> + fn resolve_did_doc + AsRef + Sync>( + &self, + did: &Did, + ) -> impl Future> where Self: Sync; /// Resolve DID document #[cfg(target_arch = "wasm32")] - fn resolve_did_doc(&self, did: &Did<'_>) -> impl Future>; + fn resolve_did_doc + AsRef + Sync>( + &self, + did: &Did, + ) -> impl Future>; /// Resolve DID doc from an identifier #[cfg(not(target_arch = "wasm32"))] - fn resolve_ident( + fn resolve_ident + AsRef + Sync>( &self, - actor: &AtIdentifier<'_>, + actor: &AtIdentifier, ) -> impl Future> where Self: Sync, { async move { match actor { - AtIdentifier::Did(did) => self.resolve_did_doc(&did).await, + AtIdentifier::Did(did) => self.resolve_did_doc(did).await, AtIdentifier::Handle(handle) => { - let did = self.resolve_handle(&handle).await?; + let did = self.resolve_handle(handle).await?; self.resolve_did_doc(&did).await } } @@ -343,15 +367,15 @@ pub trait IdentityResolver { /// Resolve DID doc from an identifier #[cfg(target_arch = "wasm32")] - fn resolve_ident( + fn resolve_ident + AsRef + Sync>( &self, - actor: &AtIdentifier<'_>, + actor: &AtIdentifier, ) -> impl Future> { async move { match actor { - AtIdentifier::Did(did) => self.resolve_did_doc(&did).await, + AtIdentifier::Did(did) => self.resolve_did_doc(did).await, AtIdentifier::Handle(handle) => { - let did = self.resolve_handle(&handle).await?; + let did = self.resolve_handle(handle).await?; self.resolve_did_doc(&did).await } } @@ -360,18 +384,18 @@ pub trait IdentityResolver { /// Resolve DID doc from an identifier #[cfg(not(target_arch = "wasm32"))] - fn resolve_ident_owned( + fn resolve_ident_owned + AsRef + Sync>( &self, - actor: &AtIdentifier<'_>, - ) -> impl Future>> + actor: &AtIdentifier, + ) -> impl Future> where Self: Sync, { async move { match actor { - AtIdentifier::Did(did) => self.resolve_did_doc_owned(&did).await, + AtIdentifier::Did(did) => self.resolve_did_doc_owned(did).await, AtIdentifier::Handle(handle) => { - let did = self.resolve_handle(&handle).await?; + let did = self.resolve_handle(handle).await?; self.resolve_did_doc_owned(&did).await } } @@ -380,15 +404,15 @@ pub trait IdentityResolver { /// Resolve DID doc from an identifier #[cfg(target_arch = "wasm32")] - fn resolve_ident_owned( + fn resolve_ident_owned + AsRef + Sync>( &self, - actor: &AtIdentifier<'_>, - ) -> impl Future>> { + actor: &AtIdentifier, + ) -> impl Future> { async move { match actor { - AtIdentifier::Did(did) => self.resolve_did_doc_owned(&did).await, + AtIdentifier::Did(did) => self.resolve_did_doc_owned(did).await, AtIdentifier::Handle(handle) => { - let did = self.resolve_handle(&handle).await?; + let did = self.resolve_handle(handle).await?; self.resolve_did_doc_owned(&did).await } } @@ -397,10 +421,10 @@ pub trait IdentityResolver { /// Resolve the DID document and return an owned version #[cfg(not(target_arch = "wasm32"))] - fn resolve_did_doc_owned( + fn resolve_did_doc_owned + AsRef + Sync>( &self, - did: &Did<'_>, - ) -> impl Future>> + did: &Did, + ) -> impl Future> where Self: Sync, { @@ -409,18 +433,18 @@ pub trait IdentityResolver { /// Resolve the DID document and return an owned version #[cfg(target_arch = "wasm32")] - fn resolve_did_doc_owned( + fn resolve_did_doc_owned + AsRef + Sync>( &self, - did: &Did<'_>, - ) -> impl Future>> { + did: &Did, + ) -> impl Future> { async { self.resolve_did_doc(did).await?.into_owned() } } /// Return the PDS url for a DID #[cfg(not(target_arch = "wasm32"))] - fn pds_for_did( + fn pds_for_did + AsRef + Sync>( &self, - did: &Did<'_>, + did: &Did, ) -> impl Future>> where Self: Sync, @@ -431,22 +455,31 @@ pub trait IdentityResolver { // Default-on doc id equality check if self.options().validate_doc_id { if doc.id.as_str() != did.as_str() { + let owned_doc = resp.clone().into_owned().unwrap_or_else(|_| DidDocument { + context: default_context(), + id: Did::new_owned(doc.id.as_str()).expect("already validated DID"), + also_known_as: None, + verification_method: None, + service: None, + extra_data: BTreeMap::new(), + }); return Err(IdentityError::doc_id_mismatch( - did.clone().into_static(), - doc.clone().into_static(), + Did::new_owned(did.as_str()).expect("already validated DID"), + owned_doc, )); } } doc.pds_endpoint() + .map(|u| u.to_owned()) .ok_or_else(|| IdentityError::missing_pds_endpoint(did.as_str())) } } /// Return the PDS url for a DID #[cfg(target_arch = "wasm32")] - fn pds_for_did( + fn pds_for_did + AsRef + Sync>( &self, - did: &Did<'_>, + did: &Did, ) -> impl Future>> { async { let resp = self.resolve_did_doc(did).await?; @@ -454,23 +487,32 @@ pub trait IdentityResolver { // Default-on doc id equality check if self.options().validate_doc_id { if doc.id.as_str() != did.as_str() { + let owned_doc = resp.clone().into_owned().unwrap_or_else(|_| DidDocument { + context: default_context(), + id: Did::new_owned(doc.id.as_str()).expect("already validated DID"), + also_known_as: None, + verification_method: None, + service: None, + extra_data: BTreeMap::new(), + }); return Err(IdentityError::doc_id_mismatch( - did.clone().into_static(), - doc.clone().into_static(), + Did::new_owned(did.as_str()).expect("already validated DID"), + owned_doc, )); } } doc.pds_endpoint() + .map(|u| u.to_owned()) .ok_or_else(|| IdentityError::missing_pds_endpoint(did.as_str())) } } - /// Return the DIS and PDS url for a handle + /// Return the DID and PDS url for a handle #[cfg(not(target_arch = "wasm32"))] - fn pds_for_handle( + fn pds_for_handle + AsRef + Sync>( &self, - handle: &Handle<'_>, - ) -> impl Future, jacquard_common::deps::fluent_uri::Uri)>> + handle: &Handle, + ) -> impl Future)>> where Self: Sync, { @@ -481,13 +523,12 @@ pub trait IdentityResolver { } } - /// Return the DIS and PDS url for a handle + /// Return the DID and PDS url for a handle #[cfg(target_arch = "wasm32")] - fn pds_for_handle( + fn pds_for_handle + AsRef + Sync>( &self, - handle: &Handle<'_>, - ) -> impl Future, jacquard_common::deps::fluent_uri::Uri)>> - { + handle: &Handle, + ) -> impl Future)>> { async { let did = self.resolve_handle(handle).await?; let pds = self.pds_for_did(&did).await?; @@ -503,12 +544,18 @@ impl IdentityResolver for std::sync::Arc { } /// Resolve handle - async fn resolve_handle(&self, handle: &Handle<'_>) -> Result> { + async fn resolve_handle + AsRef + Sync>( + &self, + handle: &Handle, + ) -> Result { self.as_ref().resolve_handle(handle).await } /// Resolve DID document - async fn resolve_did_doc(&self, did: &Did<'_>) -> Result { + async fn resolve_did_doc + AsRef + Sync>( + &self, + did: &Did, + ) -> Result { self.as_ref().resolve_did_doc(did).await } } @@ -520,12 +567,18 @@ impl IdentityResolver for std::sync::Arc { } /// Resolve handle - async fn resolve_handle(&self, handle: &Handle<'_>) -> Result> { + async fn resolve_handle + AsRef + Sync>( + &self, + handle: &Handle, + ) -> Result { self.as_ref().resolve_handle(handle).await } /// Resolve DID document - async fn resolve_did_doc(&self, did: &Did<'_>) -> Result { + async fn resolve_did_doc + AsRef + Sync>( + &self, + did: &Did, + ) -> Result { self.as_ref().resolve_did_doc(did).await } } @@ -662,9 +715,9 @@ pub enum IdentityErrorKind { )] DocIdMismatch { /// The DID that was requested and expected to appear as the document `id`. - expected: Did<'static>, + expected: Did, /// The DID document we *actually* got - doc: DidDocument<'static>, + doc: DidDocument, }, } @@ -791,7 +844,7 @@ impl IdentityError { } /// Create a doc id mismatch error - pub fn doc_id_mismatch(expected: Did<'static>, doc: DidDocument<'static>) -> Self { + pub fn doc_id_mismatch(expected: Did, doc: DidDocument) -> Self { Self::new(IdentityErrorKind::DocIdMismatch { expected, doc }, None) } } diff --git a/crates/jacquard-lexgen/src/fetch/sources/atproto.rs b/crates/jacquard-lexgen/src/fetch/sources/atproto.rs index a207fb1c..8c8aac67 100644 --- a/crates/jacquard-lexgen/src/fetch/sources/atproto.rs +++ b/crates/jacquard-lexgen/src/fetch/sources/atproto.rs @@ -1,14 +1,16 @@ use super::LexiconSource; -use jacquard_common::xrpc::atproto::{ListRecords, ListRecordsRecord}; +use jacquard_common::deps::smol_str::SmolStr; use jacquard_common::types::ident::AtIdentifier; use jacquard_common::types::string::Nsid; use jacquard_common::xrpc::XrpcExt; -use jacquard_common::{CowStr, IntoStatic}; +use jacquard_common::xrpc::atproto::{ListRecords, ListRecordsRecord}; +use jacquard_common::{Bos, IntoStatic}; use jacquard_identity::JacquardResolver; use jacquard_identity::lexicon_resolver::LexiconSchemaResolver; use jacquard_identity::resolver::{IdentityResolver, ResolverOptions}; use jacquard_lexicon::lexicon::LexiconDoc; use miette::{Result, miette}; +use serde::Serialize; use std::collections::HashMap; #[derive(Debug, Clone)] @@ -19,10 +21,10 @@ pub struct AtProtoSource { impl AtProtoSource { /// Fetch a single lexicon schema by NSID using DNS + XRPC resolution - async fn fetch_single_lexicon( + async fn fetch_single_lexicon + AsRef + Sync>( &self, resolver: &JacquardResolver, - nsid: &Nsid<'_>, + nsid: &Nsid, ) -> Result>> { let schema = resolver .resolve_lexicon_schema(nsid) @@ -35,7 +37,9 @@ impl AtProtoSource { Ok(lexicons) } - fn parse_lexicon_record(record_data: &ListRecordsRecord<'_>) -> Option> { + fn parse_lexicon_record + AsRef + Sync + Serialize>( + record_data: &ListRecordsRecord, + ) -> Option> { // // Extract the 'value' field from the record // let value = match record_data { // jacquard_common::types::value::Data::Object(map) => map.0.get("value")?, @@ -67,12 +71,12 @@ impl LexiconSource for AtProtoSource { let resolver = JacquardResolver::new_dns(http.clone(), ResolverOptions::default()); // Try parsing as NSID first (for single lexicon fetch) - if let Ok(nsid) = Nsid::new(&self.endpoint) { + if let Ok(nsid) = Nsid::new(self.endpoint.as_ref()) { return self.fetch_single_lexicon(&resolver, &nsid).await; } // Otherwise parse as at-identifier (handle or DID) for bulk fetch - let identifier = AtIdentifier::new(&self.endpoint) + let identifier = AtIdentifier::new(self.endpoint.as_ref()) .map_err(|e| miette!("Invalid endpoint '{}': {}", self.endpoint, e))?; // Resolve to get PDS endpoint @@ -91,7 +95,7 @@ impl LexiconSource for AtProtoSource { // Determine repo - use slice if provided, otherwise use the resolved DID let repo = if let Some(ref slice) = self.slice { - AtIdentifier::new(slice) + AtIdentifier::new(slice.as_ref()) .map_err(|e| miette!("Invalid slice '{}': {}", slice, e))? .into_static() } else { @@ -129,7 +133,7 @@ impl LexiconSource for AtProtoSource { eprintln!("Warning: Batch decode failed from {}: {}", self.endpoint, e); eprintln!("Retrying with limit=1 to skip invalid records..."); - let mut cursor: Option = None; + let mut cursor: Option = None; loop { let req = ListRecords { repo: repo.clone().into_static().into(), diff --git a/crates/jacquard-lexgen/src/fetch/sources/jsonfile.rs b/crates/jacquard-lexgen/src/fetch/sources/jsonfile.rs index 6898d749..96c27f50 100644 --- a/crates/jacquard-lexgen/src/fetch/sources/jsonfile.rs +++ b/crates/jacquard-lexgen/src/fetch/sources/jsonfile.rs @@ -1,9 +1,9 @@ use super::LexiconSource; -use jacquard_common::IntoStatic; use jacquard_common::types::value::Data; +use jacquard_common::{Bos, IntoStatic}; use jacquard_lexicon::lexicon::LexiconDoc; use miette::{IntoDiagnostic, Result}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; @@ -15,7 +15,7 @@ pub struct JsonFileSource { #[derive(Deserialize)] struct RecordsFile<'a> { #[serde(borrow)] - records: Vec>, + records: Vec>, } impl LexiconSource for JsonFileSource { @@ -37,7 +37,9 @@ impl LexiconSource for JsonFileSource { } impl JsonFileSource { - fn parse_lexicon_record(record_data: &Data<'_>) -> Option> { + fn parse_lexicon_record + AsRef + Sync + Serialize>( + record_data: &Data, + ) -> Option> { let value = match record_data { Data::Object(map) => map.0.get("value")?, _ => return None,