A better Rust ATProto crate
0
fork

Configure Feed

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

jacquard crate most of the way there, just tests unmigrated

+1407 -937
+1 -1
crates/jacquard-api/Cargo.toml
··· 30 30 unused_imports = "allow" 31 31 32 32 [features] 33 - default = [ "minimal", "std", "streaming"] 33 + default = [ "minimal", "std"] 34 34 std = ["serde/std", "jacquard-common/std"] 35 35 minimal = [ "com_atproto", "com_bad_example"] 36 36 bluesky = [ "com_atproto", "app_bsky", "chat_bsky", "tools_ozone", "minimal"]
+2 -1
crates/jacquard-axum/src/did_web.rs
··· 39 39 response::IntoResponse, 40 40 routing::get, 41 41 }; 42 + use jacquard::deps::smol_str::SmolStr; 42 43 use jacquard_common::types::did_doc::DidDocument; 43 44 44 45 /// Create a router that serves a DID document at `/.well-known/did.json` ··· 58 59 /// .merge(did_web_router(did_doc)); 59 60 /// # } 60 61 /// ``` 61 - pub fn did_web_router(did_doc: DidDocument<'static>) -> Router { 62 + pub fn did_web_router(did_doc: DidDocument<SmolStr>) -> Router { 62 63 Router::new().route( 63 64 "/.well-known/did.json", 64 65 get(move || async move {
+8 -7
crates/jacquard-axum/src/lib.rs
··· 57 57 response::{IntoResponse, Response}, 58 58 }; 59 59 use jacquard::{ 60 - IntoStatic, 60 + BosStr, IntoStatic, 61 61 xrpc::{XrpcEndpoint, XrpcError, XrpcMethod, XrpcRequest}, 62 62 }; 63 63 use serde_json::json; ··· 66 66 /// 67 67 /// Deserializes incoming requests based on the endpoint's method type (Query or Procedure) 68 68 /// and returns the owned (`'static`) request type ready for handler logic. 69 - pub struct ExtractXrpc<E: XrpcEndpoint>(pub E::Request<'static>); 69 + pub struct ExtractXrpc<E: XrpcEndpoint, S: BosStr>(pub E::Request<S>); 70 70 71 - impl<S, R> FromRequest<S> for ExtractXrpc<R> 71 + impl<S, R, B> FromRequest<S> for ExtractXrpc<R, B> 72 72 where 73 - S: Send + Sync, 73 + S: BosStr + Send + Sync, 74 + B: BosStr, 74 75 R: XrpcEndpoint, 75 - for<'a> R::Request<'a>: IntoStatic<Output = R::Request<'static>>, 76 + R::Request<S>: IntoStatic<Output = R::Request<B>>, 76 77 { 77 78 type Rejection = Response; 78 79 ··· 102 103 XrpcMethod::Query => { 103 104 if let Some(path_query) = req.uri().path_and_query() { 104 105 let query = path_query.query().unwrap_or(""); 105 - let value: R::Request<'_> = 106 - serde_html_form::from_str::<R::Request<'_>>(query).map_err(|e| { 106 + let value: R::Request<S> = 107 + serde_html_form::from_str::<R::Request<S>>(query).map_err(|e| { 107 108 ( 108 109 StatusCode::BAD_REQUEST, 109 110 Json(json!({
+21 -21
crates/jacquard-axum/src/service_auth.rs
··· 66 66 type Resolver: IdentityResolver; 67 67 68 68 /// Get the service DID (expected audience) 69 - fn service_did(&self) -> &Did<'_>; 69 + fn service_did(&self) -> Did<&str>; 70 70 71 71 /// Get a reference to the identity resolver 72 72 fn resolver(&self) -> &Self::Resolver; ··· 81 81 /// by the `ExtractServiceAuth` extractor. 82 82 pub struct ServiceAuthConfig<R> { 83 83 /// The DID of your service (the expected audience) 84 - service_did: Did<'static>, 84 + service_did: Did, 85 85 /// Identity resolver for fetching DID documents 86 86 resolver: Arc<R>, 87 87 /// Whether to require the `lxm` (method binding) field ··· 103 103 /// 104 104 /// This enables `lxm` (method binding). If you need backward compatibility, 105 105 /// use `ServiceAuthConfig::new_legacy()` 106 - pub fn new(service_did: Did<'static>, resolver: R) -> Self { 106 + pub fn new(service_did: Did, resolver: R) -> Self { 107 107 Self { 108 108 service_did, 109 109 resolver: Arc::new(resolver), ··· 114 114 /// Create a new service auth config. 115 115 /// 116 116 /// `lxm` (method binding) is disabled for backwards compatibility 117 - pub fn new_legacy(service_did: Did<'static>, resolver: R) -> Self { 117 + pub fn new_legacy(service_did: Did, resolver: R) -> Self { 118 118 Self { 119 119 service_did, 120 120 resolver: Arc::new(resolver), ··· 132 132 } 133 133 134 134 /// Get the service DID. 135 - pub fn service_did(&self) -> &Did<'static> { 136 - &self.service_did 135 + pub fn service_did(&self) -> Did<&str> { 136 + self.service_did.borrow() 137 137 } 138 138 139 139 /// Get a reference to the identity resolver. ··· 145 145 impl<R: IdentityResolver> ServiceAuth for ServiceAuthConfig<R> { 146 146 type Resolver = R; 147 147 148 - fn service_did(&self) -> &Did<'_> { 149 - &self.service_did 148 + fn service_did(&self) -> Did<&str> { 149 + self.service_did.borrow() 150 150 } 151 151 152 152 fn resolver(&self) -> &Self::Resolver { ··· 165 165 #[derive(Debug, Clone, jacquard_derive::IntoStatic)] 166 166 pub struct VerifiedServiceAuth<'a> { 167 167 /// The authenticated user's DID (from `iss` claim) 168 - did: Did<'a>, 168 + did: Did, 169 169 /// The audience (should match your service DID) 170 - aud: Did<'a>, 170 + aud: Did, 171 171 /// The lexicon method NSID, if present 172 - lxm: Option<Nsid<'a>>, 172 + lxm: Option<Nsid>, 173 173 /// JWT ID (nonce), if present 174 174 jti: Option<CowStr<'a>>, 175 175 } 176 176 177 177 impl<'a> VerifiedServiceAuth<'a> { 178 178 /// Get the authenticated user's DID. 179 - pub fn did(&self) -> &Did<'a> { 180 - &self.did 179 + pub fn did(&self) -> Did<&str> { 180 + self.did.borrow() 181 181 } 182 182 183 183 /// Get the audience (your service DID). 184 - pub fn aud(&self) -> &Did<'a> { 185 - &self.aud 184 + pub fn aud(&self) -> Did<&str> { 185 + self.aud.borrow() 186 186 } 187 187 188 188 /// Get the lexicon method NSID, if present. 189 - pub fn lxm(&self) -> Option<&Nsid<'a>> { 190 - self.lxm.as_ref() 189 + pub fn lxm(&self) -> Option<Nsid<&str>> { 190 + self.lxm.as_ref().map(|l| l.borrow()) 191 191 } 192 192 193 193 /// Get the JWT ID (nonce), if present. ··· 310 310 /// DID resolution failed 311 311 #[error("failed to resolve DID {did}: {source}")] 312 312 DidResolutionFailed { 313 - did: Did<'static>, 313 + did: Did, 314 314 #[source] 315 315 source: Box<dyn std::error::Error + Send + Sync>, 316 316 }, 317 317 318 318 /// No valid signing key found in DID document 319 319 #[error("no valid signing key found in DID document for {0}")] 320 - NoSigningKey(Did<'static>), 320 + NoSigningKey(Did), 321 321 322 322 /// Method binding required but missing 323 323 #[error("lxm (method binding) is required but missing from token")] ··· 445 445 service_auth::verify_signature(&parsed, &signing_key)?; 446 446 447 447 // Now validate claims (audience, expiration, etc.) 448 - claims.validate(state.service_did())?; 448 + claims.validate(&state.service_did())?; 449 449 450 450 // Check method binding if required 451 451 if state.require_lxm() && claims.lxm.is_none() { ··· 549 549 /// 550 550 /// This looks for a key with type "atproto" or the first available key 551 551 /// if no atproto-specific key is found. 552 - fn extract_signing_key(methods: &[VerificationMethod]) -> Option<PublicKey> { 552 + fn extract_signing_key(methods: &[VerificationMethod<SmolStr>]) -> Option<PublicKey> { 553 553 // First try to find an atproto-specific key 554 554 let atproto_method = methods 555 555 .iter()
+11 -7
crates/jacquard-common/src/lib.rs
··· 212 212 #[cfg(not(feature = "std"))] 213 213 pub use spin::Lazy; 214 214 215 + pub use bos::{BorrowOrShare, Bos, BosStr, DefaultStr, FromStaticStr}; 215 216 pub use cowstr::CowStr; 216 217 pub use into_static::IntoStatic; 217 - pub use bos::{Bos, BorrowOrShare, BosStr, DefaultStr, FromStaticStr}; 218 218 219 219 /// A copy-on-write immutable string type that uses [`smol_str::SmolStr`] for 220 220 /// the "owned" variant. ··· 266 266 267 267 /// Authorization token types for XRPC requests. 268 268 #[derive(Debug, Clone, PartialEq, Eq)] 269 - pub enum AuthorizationToken<'s> { 269 + pub enum AuthorizationToken<S: BosStr = DefaultStr> { 270 270 /// Bearer token (access JWT, refresh JWT to refresh the session) 271 - Bearer(CowStr<'s>), 271 + Bearer(S), 272 272 /// DPoP token (proof-of-possession) for OAuth 273 - Dpop(CowStr<'s>), 273 + Dpop(S), 274 274 } 275 275 276 - impl<'s> IntoStatic for AuthorizationToken<'s> { 277 - type Output = AuthorizationToken<'static>; 278 - fn into_static(self) -> AuthorizationToken<'static> { 276 + impl<S: BosStr> IntoStatic for AuthorizationToken<S> 277 + where 278 + S: IntoStatic, 279 + S::Output: BosStr, 280 + { 281 + type Output = AuthorizationToken<S::Output>; 282 + fn into_static(self) -> AuthorizationToken<S::Output> { 279 283 match self { 280 284 AuthorizationToken::Bearer(token) => AuthorizationToken::Bearer(token.into_static()), 281 285 AuthorizationToken::Dpop(token) => AuthorizationToken::Dpop(token.into_static()),
+5 -5
crates/jacquard-common/src/service_auth.rs
··· 144 144 pub jti: Option<CowStr<'a>>, 145 145 146 146 /// Lexicon method NSID (method binding) 147 - #[serde(borrow, skip_serializing_if = "Option::is_none")] 148 - pub lxm: Option<Nsid<CowStr<'a>>>, 147 + #[serde(skip_serializing_if = "Option::is_none")] 148 + pub lxm: Option<Nsid>, 149 149 } 150 150 151 151 impl<'a> IntoStatic for ServiceAuthClaims<'a> { ··· 169 169 /// Checks: 170 170 /// - Audience matches expected DID 171 171 /// - Token is not expired 172 - pub fn validate(&self, expected_aud: &Did) -> Result<(), ServiceAuthError> { 172 + pub fn validate(&self, expected_aud: &Did<&str>) -> Result<(), ServiceAuthError> { 173 173 // Check audience 174 174 if self.aud.as_str() != expected_aud.as_str() { 175 175 return Err(ServiceAuthError::AudienceMismatch { ··· 456 456 lxm: None, 457 457 }; 458 458 459 - let expected_aud = Did::new_static("did:web:example.com").unwrap(); 459 + let expected_aud = Did::new("did:web:example.com").unwrap(); 460 460 assert!(claims.validate(&expected_aud).is_ok()); 461 461 462 - let wrong_aud = Did::new_static("did:web:wrong.com").unwrap(); 462 + let wrong_aud = Did::new("did:web:wrong.com").unwrap(); 463 463 assert!(matches!( 464 464 claims.validate(&wrong_aud), 465 465 Err(ServiceAuthError::AudienceMismatch { .. })
+9
crates/jacquard-common/src/types/did.rs
··· 164 164 pub fn convert<B: Bos<str> + From<S>>(self) -> Did<B> { 165 165 Did(B::from(self.0)) 166 166 } 167 + 168 + /// Borrow as a `Did<&str>`, analogous to `Uri::borrow()`. 169 + pub fn borrow(&self) -> Did<&str> 170 + where 171 + S: AsRef<str>, 172 + { 173 + // SAFETY: self is already validated. 174 + unsafe { Did::unchecked(self.0.as_ref()) } 175 + } 167 176 } 168 177 169 178 impl<S: Bos<str> + FromStr> FromStr for Did<S> {
+15 -2
crates/jacquard-common/src/types/handle.rs
··· 95 95 pub unsafe fn unchecked(handle: S) -> Self { 96 96 Handle(handle) 97 97 } 98 + 99 + /// Borrow as a `Handle<&str>`, analogous to `Uri::borrow()`. 100 + pub fn borrow(&self) -> Handle<&str> 101 + where 102 + S: AsRef<str>, 103 + { 104 + // SAFETY: self is already validated. 105 + unsafe { Handle::unchecked(self.0.as_ref()) } 106 + } 98 107 } 99 108 100 109 // --------------------------------------------------------------------------- ··· 359 368 assert!(Handle::<&str>::new("@alice.test").is_err()); 360 369 assert!(Handle::<&str>::new("at://alice.test").is_err()); 361 370 assert_eq!( 362 - Handle::<SmolStr>::new_owned("@alice.test").unwrap().as_str(), 371 + Handle::<SmolStr>::new_owned("@alice.test") 372 + .unwrap() 373 + .as_str(), 363 374 "alice.test" 364 375 ); 365 376 assert_eq!( 366 - Handle::<SmolStr>::new_owned("at://alice.test").unwrap().as_str(), 377 + Handle::<SmolStr>::new_owned("at://alice.test") 378 + .unwrap() 379 + .as_str(), 367 380 "alice.test" 368 381 ); 369 382 assert_eq!(
+9
crates/jacquard-common/src/types/nsid.rs
··· 72 72 pub unsafe fn unchecked(nsid: S) -> Self { 73 73 Nsid(nsid) 74 74 } 75 + 76 + /// Borrow as an `Nsid<&str>`, analogous to `Uri::borrow()`. 77 + pub fn borrow(&self) -> Nsid<&str> 78 + where 79 + S: AsRef<str>, 80 + { 81 + // SAFETY: self is already validated. 82 + unsafe { Nsid::unchecked(self.0.as_ref()) } 83 + } 75 84 } 76 85 77 86 impl<S: Bos<str> + AsRef<str>> Nsid<S> {
+16
crates/jacquard-common/src/types/recordkey.rs
··· 105 105 } 106 106 } 107 107 108 + impl<S: Bos<str> + AsRef<str>> RecordKey<Rkey<S>> { 109 + /// Borrow as a `RecordKey<Rkey<&str>>`, analogous to `Uri::borrow()`. 110 + pub fn borrow(&self) -> RecordKey<Rkey<&str>> { 111 + RecordKey(self.0.borrow()) 112 + } 113 + } 114 + 108 115 /// AT Protocol record key (generic "any" type) 109 116 /// 110 117 /// Record keys uniquely identify records within a collection. This is the catch-all ··· 163 170 /// The caller must ensure the rkey is valid. 164 171 pub unsafe fn unchecked(rkey: S) -> Self { 165 172 Rkey(rkey) 173 + } 174 + 175 + /// Borrow as an `Rkey<&str>`, analogous to `Uri::borrow()`. 176 + pub fn borrow(&self) -> Rkey<&str> 177 + where 178 + S: AsRef<str>, 179 + { 180 + // SAFETY: self is already validated. 181 + unsafe { Rkey::unchecked(self.0.as_ref()) } 166 182 } 167 183 } 168 184
+2 -7
crates/jacquard-common/src/types/value.rs
··· 4 4 }; 5 5 use alloc::boxed::Box; 6 6 use alloc::collections::BTreeMap; 7 - use alloc::string::ToString; 8 7 use alloc::vec::Vec; 9 8 use bytes::Bytes; 10 9 use core::convert::Infallible; ··· 874 873 /// # Ok(()) 875 874 /// # } 876 875 /// ``` 877 - pub fn to_data<'s, T, S>(value: &T) -> Result<Data<S>, convert::ConversionError> 876 + pub fn to_data<'s, T>(value: &T) -> Result<Data<SmolStr>, serde_impl::RawDataSerializerError> 878 877 where 879 878 T: serde::Serialize, 880 - S: Bos<str> + AsRef<str> + serde::Serialize + From<CowStr<'s>>, 881 879 { 882 - let raw = to_raw_data(value).map_err(|e| convert::ConversionError::InvalidRawData { 883 - message: e.to_string(), 884 - })?; 885 - raw.try_into() 880 + value.serialize(serde_impl::DataSerializer) 886 881 } 887 882 888 883 /// Parse and traverse a path through nested Data structures
+360 -24
crates/jacquard-common/src/types/value/serde_impl.rs
··· 1631 1631 } 1632 1632 1633 1633 /// Serializer that produces RawData values 1634 + pub struct DataSerializer; 1635 + 1636 + impl serde::Serializer for DataSerializer { 1637 + type Ok = Data<SmolStr>; 1638 + type Error = RawDataSerializerError; 1639 + 1640 + type SerializeSeq = DataSeqSerializer; 1641 + type SerializeTuple = DataSeqSerializer; 1642 + type SerializeTupleStruct = DataSeqSerializer; 1643 + type SerializeTupleVariant = DataSeqSerializer; 1644 + type SerializeMap = DataMapSerializer; 1645 + type SerializeStruct = DataMapSerializer; 1646 + type SerializeStructVariant = DataMapSerializer; 1647 + 1648 + fn serialize_bool(self, v: bool) -> Result<Self::Ok, Self::Error> { 1649 + Ok(Data::Boolean(v)) 1650 + } 1651 + 1652 + fn serialize_i8(self, v: i8) -> Result<Self::Ok, Self::Error> { 1653 + Ok(Data::Integer(v as i64)) 1654 + } 1655 + 1656 + fn serialize_i16(self, v: i16) -> Result<Self::Ok, Self::Error> { 1657 + Ok(Data::Integer(v as i64)) 1658 + } 1659 + 1660 + fn serialize_i32(self, v: i32) -> Result<Self::Ok, Self::Error> { 1661 + Ok(Data::Integer(v as i64)) 1662 + } 1663 + 1664 + fn serialize_i64(self, v: i64) -> Result<Self::Ok, Self::Error> { 1665 + Ok(Data::Integer(v as i64)) 1666 + } 1667 + 1668 + fn serialize_u8(self, v: u8) -> Result<Self::Ok, Self::Error> { 1669 + Ok(Data::Integer(v as i64)) 1670 + } 1671 + 1672 + fn serialize_u16(self, v: u16) -> Result<Self::Ok, Self::Error> { 1673 + Ok(Data::Integer(v as i64)) 1674 + } 1675 + 1676 + fn serialize_u32(self, v: u32) -> Result<Self::Ok, Self::Error> { 1677 + Ok(Data::Integer(v as i64)) 1678 + } 1679 + 1680 + fn serialize_u64(self, v: u64) -> Result<Self::Ok, Self::Error> { 1681 + Ok(Data::Integer((v as i128 % (i64::MAX as i128)) as i64)) 1682 + } 1683 + 1684 + fn serialize_f32(self, v: f32) -> Result<Self::Ok, Self::Error> { 1685 + Ok(Data::InvalidNumber(SmolStr::from(v.to_string()))) 1686 + } 1687 + 1688 + fn serialize_f64(self, v: f64) -> Result<Self::Ok, Self::Error> { 1689 + Ok(Data::InvalidNumber(SmolStr::from(v.to_string()))) 1690 + } 1691 + 1692 + fn serialize_char(self, v: char) -> Result<Self::Ok, Self::Error> { 1693 + Ok(Data::String(AtprotoStr::String(v.to_smolstr()))) 1694 + } 1695 + 1696 + fn serialize_str(self, v: &str) -> Result<Self::Ok, Self::Error> { 1697 + Ok(Data::String(parse_string(v).convert())) 1698 + } 1699 + 1700 + fn serialize_bytes(self, v: &[u8]) -> Result<Self::Ok, Self::Error> { 1701 + Ok(Data::Bytes(Bytes::copy_from_slice(v))) 1702 + } 1703 + 1704 + fn serialize_none(self) -> Result<Self::Ok, Self::Error> { 1705 + Ok(Data::Null) 1706 + } 1707 + 1708 + fn serialize_some<T: ?Sized>(self, value: &T) -> Result<Self::Ok, Self::Error> 1709 + where 1710 + T: Serialize, 1711 + { 1712 + value.serialize(self) 1713 + } 1714 + 1715 + fn serialize_unit(self) -> Result<Self::Ok, Self::Error> { 1716 + Ok(Data::Null) 1717 + } 1718 + 1719 + fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> { 1720 + Ok(Data::Null) 1721 + } 1722 + 1723 + fn serialize_unit_variant( 1724 + self, 1725 + _name: &'static str, 1726 + _variant_index: u32, 1727 + variant: &'static str, 1728 + ) -> Result<Self::Ok, Self::Error> { 1729 + Ok(Data::String(AtprotoStr::String(SmolStr::new_static( 1730 + variant, 1731 + )))) 1732 + } 1733 + 1734 + fn serialize_newtype_struct<T: ?Sized>( 1735 + self, 1736 + _name: &'static str, 1737 + value: &T, 1738 + ) -> Result<Self::Ok, Self::Error> 1739 + where 1740 + T: Serialize, 1741 + { 1742 + value.serialize(self) 1743 + } 1744 + 1745 + fn serialize_newtype_variant<T: ?Sized>( 1746 + self, 1747 + _name: &'static str, 1748 + _variant_index: u32, 1749 + variant: &'static str, 1750 + value: &T, 1751 + ) -> Result<Self::Ok, Self::Error> 1752 + where 1753 + T: Serialize, 1754 + { 1755 + let mut map = BTreeMap::new(); 1756 + map.insert(variant.to_smolstr(), value.serialize(DataSerializer)?); 1757 + Ok(Data::Object(map.into())) 1758 + } 1759 + 1760 + fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> { 1761 + Ok(DataSeqSerializer { 1762 + items: Vec::with_capacity(len.unwrap_or(0)), 1763 + }) 1764 + } 1765 + 1766 + fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, Self::Error> { 1767 + self.serialize_seq(Some(len)) 1768 + } 1769 + 1770 + fn serialize_tuple_struct( 1771 + self, 1772 + _name: &'static str, 1773 + len: usize, 1774 + ) -> Result<Self::SerializeTupleStruct, Self::Error> { 1775 + self.serialize_seq(Some(len)) 1776 + } 1777 + 1778 + fn serialize_tuple_variant( 1779 + self, 1780 + _name: &'static str, 1781 + _variant_index: u32, 1782 + _variant: &'static str, 1783 + len: usize, 1784 + ) -> Result<Self::SerializeTupleVariant, Self::Error> { 1785 + self.serialize_seq(Some(len)) 1786 + } 1787 + 1788 + fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> { 1789 + Ok(DataMapSerializer { 1790 + map: BTreeMap::new(), 1791 + next_key: None, 1792 + }) 1793 + } 1794 + 1795 + fn serialize_struct( 1796 + self, 1797 + _name: &'static str, 1798 + len: usize, 1799 + ) -> Result<Self::SerializeStruct, Self::Error> { 1800 + self.serialize_map(Some(len)) 1801 + } 1802 + 1803 + fn serialize_struct_variant( 1804 + self, 1805 + _name: &'static str, 1806 + _variant_index: u32, 1807 + _variant: &'static str, 1808 + len: usize, 1809 + ) -> Result<Self::SerializeStructVariant, Self::Error> { 1810 + self.serialize_map(Some(len)) 1811 + } 1812 + } 1813 + 1814 + /// Sequence serializer accumulator 1815 + pub struct RawDataSeqSerializer { 1816 + items: Vec<RawData<'static>>, 1817 + } 1818 + 1819 + impl serde::ser::SerializeSeq for RawDataSeqSerializer { 1820 + type Ok = RawData<'static>; 1821 + type Error = RawDataSerializerError; 1822 + 1823 + fn serialize_element<T: ?Sized>(&mut self, value: &T) -> Result<(), Self::Error> 1824 + where 1825 + T: Serialize, 1826 + { 1827 + self.items.push(value.serialize(RawDataSerializer)?); 1828 + Ok(()) 1829 + } 1830 + 1831 + fn end(self) -> Result<Self::Ok, Self::Error> { 1832 + Ok(RawData::Array(self.items)) 1833 + } 1834 + } 1835 + 1836 + impl serde::ser::SerializeTuple for RawDataSeqSerializer { 1837 + type Ok = RawData<'static>; 1838 + type Error = RawDataSerializerError; 1839 + 1840 + fn serialize_element<T: ?Sized>(&mut self, value: &T) -> Result<(), Self::Error> 1841 + where 1842 + T: Serialize, 1843 + { 1844 + serde::ser::SerializeSeq::serialize_element(self, value) 1845 + } 1846 + 1847 + fn end(self) -> Result<Self::Ok, Self::Error> { 1848 + serde::ser::SerializeSeq::end(self) 1849 + } 1850 + } 1851 + 1852 + impl serde::ser::SerializeTupleStruct for RawDataSeqSerializer { 1853 + type Ok = RawData<'static>; 1854 + type Error = RawDataSerializerError; 1855 + 1856 + fn serialize_field<T: ?Sized>(&mut self, value: &T) -> Result<(), Self::Error> 1857 + where 1858 + T: Serialize, 1859 + { 1860 + serde::ser::SerializeSeq::serialize_element(self, value) 1861 + } 1862 + 1863 + fn end(self) -> Result<Self::Ok, Self::Error> { 1864 + serde::ser::SerializeSeq::end(self) 1865 + } 1866 + } 1867 + 1868 + impl serde::ser::SerializeTupleVariant for RawDataSeqSerializer { 1869 + type Ok = RawData<'static>; 1870 + type Error = RawDataSerializerError; 1871 + 1872 + fn serialize_field<T: ?Sized>(&mut self, value: &T) -> Result<(), Self::Error> 1873 + where 1874 + T: Serialize, 1875 + { 1876 + serde::ser::SerializeSeq::serialize_element(self, value) 1877 + } 1878 + 1879 + fn end(self) -> Result<Self::Ok, Self::Error> { 1880 + serde::ser::SerializeSeq::end(self) 1881 + } 1882 + } 1883 + 1884 + /// Map serializer accumulator 1885 + pub struct RawDataMapSerializer { 1886 + map: BTreeMap<SmolStr, RawData<'static>>, 1887 + next_key: Option<SmolStr>, 1888 + } 1889 + 1890 + impl serde::ser::SerializeMap for RawDataMapSerializer { 1891 + type Ok = RawData<'static>; 1892 + type Error = RawDataSerializerError; 1893 + 1894 + fn serialize_key<T: ?Sized>(&mut self, key: &T) -> Result<(), Self::Error> 1895 + where 1896 + T: Serialize, 1897 + { 1898 + let key_data = key.serialize(RawDataSerializer)?; 1899 + match key_data { 1900 + RawData::String(s) => { 1901 + self.next_key = Some(s.to_smolstr()); 1902 + Ok(()) 1903 + } 1904 + _ => Err(RawDataSerializerError::Message( 1905 + "map keys must be strings".to_string(), 1906 + )), 1907 + } 1908 + } 1909 + 1910 + fn serialize_value<T: ?Sized>(&mut self, value: &T) -> Result<(), Self::Error> 1911 + where 1912 + T: Serialize, 1913 + { 1914 + let key = self 1915 + .next_key 1916 + .take() 1917 + .ok_or_else(|| RawDataSerializerError::Message("missing key".to_string()))?; 1918 + self.map.insert(key, value.serialize(RawDataSerializer)?); 1919 + Ok(()) 1920 + } 1921 + 1922 + fn end(self) -> Result<Self::Ok, Self::Error> { 1923 + Ok(RawData::Object(self.map)) 1924 + } 1925 + } 1926 + 1927 + impl serde::ser::SerializeStruct for RawDataMapSerializer { 1928 + type Ok = RawData<'static>; 1929 + type Error = RawDataSerializerError; 1930 + 1931 + fn serialize_field<T: ?Sized>( 1932 + &mut self, 1933 + key: &'static str, 1934 + value: &T, 1935 + ) -> Result<(), Self::Error> 1936 + where 1937 + T: Serialize, 1938 + { 1939 + self.map 1940 + .insert(key.to_smolstr(), value.serialize(RawDataSerializer)?); 1941 + Ok(()) 1942 + } 1943 + 1944 + fn end(self) -> Result<Self::Ok, Self::Error> { 1945 + Ok(RawData::Object(self.map)) 1946 + } 1947 + } 1948 + 1949 + impl serde::ser::SerializeStructVariant for RawDataMapSerializer { 1950 + type Ok = RawData<'static>; 1951 + type Error = RawDataSerializerError; 1952 + 1953 + fn serialize_field<T: ?Sized>( 1954 + &mut self, 1955 + key: &'static str, 1956 + value: &T, 1957 + ) -> Result<(), Self::Error> 1958 + where 1959 + T: Serialize, 1960 + { 1961 + serde::ser::SerializeStruct::serialize_field(self, key, value) 1962 + } 1963 + 1964 + fn end(self) -> Result<Self::Ok, Self::Error> { 1965 + serde::ser::SerializeStruct::end(self) 1966 + } 1967 + } 1968 + 1969 + /// Serializer that produces RawData values 1634 1970 pub struct RawDataSerializer; 1635 1971 1636 1972 impl serde::Serializer for RawDataSerializer { ··· 1814 2150 } 1815 2151 1816 2152 /// Sequence serializer accumulator 1817 - pub struct RawDataSeqSerializer { 1818 - items: Vec<RawData<'static>>, 2153 + pub struct DataSeqSerializer { 2154 + items: Vec<Data<SmolStr>>, 1819 2155 } 1820 2156 1821 - impl serde::ser::SerializeSeq for RawDataSeqSerializer { 1822 - type Ok = RawData<'static>; 2157 + impl serde::ser::SerializeSeq for DataSeqSerializer { 2158 + type Ok = Data<SmolStr>; 1823 2159 type Error = RawDataSerializerError; 1824 2160 1825 2161 fn serialize_element<T: ?Sized>(&mut self, value: &T) -> Result<(), Self::Error> 1826 2162 where 1827 2163 T: Serialize, 1828 2164 { 1829 - self.items.push(value.serialize(RawDataSerializer)?); 2165 + self.items.push(value.serialize(DataSerializer)?); 1830 2166 Ok(()) 1831 2167 } 1832 2168 1833 2169 fn end(self) -> Result<Self::Ok, Self::Error> { 1834 - Ok(RawData::Array(self.items)) 2170 + Ok(Data::Array(self.items.into())) 1835 2171 } 1836 2172 } 1837 2173 1838 - impl serde::ser::SerializeTuple for RawDataSeqSerializer { 1839 - type Ok = RawData<'static>; 2174 + impl serde::ser::SerializeTuple for DataSeqSerializer { 2175 + type Ok = Data<SmolStr>; 1840 2176 type Error = RawDataSerializerError; 1841 2177 1842 2178 fn serialize_element<T: ?Sized>(&mut self, value: &T) -> Result<(), Self::Error> ··· 1851 2187 } 1852 2188 } 1853 2189 1854 - impl serde::ser::SerializeTupleStruct for RawDataSeqSerializer { 1855 - type Ok = RawData<'static>; 2190 + impl serde::ser::SerializeTupleStruct for DataSeqSerializer { 2191 + type Ok = Data<SmolStr>; 1856 2192 type Error = RawDataSerializerError; 1857 2193 1858 2194 fn serialize_field<T: ?Sized>(&mut self, value: &T) -> Result<(), Self::Error> ··· 1867 2203 } 1868 2204 } 1869 2205 1870 - impl serde::ser::SerializeTupleVariant for RawDataSeqSerializer { 1871 - type Ok = RawData<'static>; 2206 + impl serde::ser::SerializeTupleVariant for DataSeqSerializer { 2207 + type Ok = Data<SmolStr>; 1872 2208 type Error = RawDataSerializerError; 1873 2209 1874 2210 fn serialize_field<T: ?Sized>(&mut self, value: &T) -> Result<(), Self::Error> ··· 1884 2220 } 1885 2221 1886 2222 /// Map serializer accumulator 1887 - pub struct RawDataMapSerializer { 1888 - map: BTreeMap<SmolStr, RawData<'static>>, 2223 + pub struct DataMapSerializer { 2224 + map: BTreeMap<SmolStr, Data<SmolStr>>, 1889 2225 next_key: Option<SmolStr>, 1890 2226 } 1891 2227 1892 - impl serde::ser::SerializeMap for RawDataMapSerializer { 1893 - type Ok = RawData<'static>; 2228 + impl serde::ser::SerializeMap for DataMapSerializer { 2229 + type Ok = Data<SmolStr>; 1894 2230 type Error = RawDataSerializerError; 1895 2231 1896 2232 fn serialize_key<T: ?Sized>(&mut self, key: &T) -> Result<(), Self::Error> ··· 1917 2253 .next_key 1918 2254 .take() 1919 2255 .ok_or_else(|| RawDataSerializerError::Message("missing key".to_string()))?; 1920 - self.map.insert(key, value.serialize(RawDataSerializer)?); 2256 + self.map.insert(key, value.serialize(DataSerializer)?); 1921 2257 Ok(()) 1922 2258 } 1923 2259 1924 2260 fn end(self) -> Result<Self::Ok, Self::Error> { 1925 - Ok(RawData::Object(self.map)) 2261 + Ok(Data::Object(self.map.into())) 1926 2262 } 1927 2263 } 1928 2264 1929 - impl serde::ser::SerializeStruct for RawDataMapSerializer { 1930 - type Ok = RawData<'static>; 2265 + impl serde::ser::SerializeStruct for DataMapSerializer { 2266 + type Ok = Data<SmolStr>; 1931 2267 type Error = RawDataSerializerError; 1932 2268 1933 2269 fn serialize_field<T: ?Sized>( ··· 1939 2275 T: Serialize, 1940 2276 { 1941 2277 self.map 1942 - .insert(key.to_smolstr(), value.serialize(RawDataSerializer)?); 2278 + .insert(key.to_smolstr(), value.serialize(DataSerializer)?); 1943 2279 Ok(()) 1944 2280 } 1945 2281 1946 2282 fn end(self) -> Result<Self::Ok, Self::Error> { 1947 - Ok(RawData::Object(self.map)) 2283 + Ok(Data::Object(self.map.into())) 1948 2284 } 1949 2285 } 1950 2286 1951 - impl serde::ser::SerializeStructVariant for RawDataMapSerializer { 1952 - type Ok = RawData<'static>; 2287 + impl serde::ser::SerializeStructVariant for DataMapSerializer { 2288 + type Ok = Data<SmolStr>; 1953 2289 type Error = RawDataSerializerError; 1954 2290 1955 2291 fn serialize_field<T: ?Sized>(
+1 -1
crates/jacquard-common/src/types/value/tests.rs
··· 2 2 3 3 use super::*; 4 4 use core::str::FromStr; 5 - use std::string::String; 5 + use std::string::{String, ToString}; 6 6 7 7 /// Canonicalize JSON by sorting object keys recursively 8 8 fn canonicalize_json(value: &serde_json::Value) -> serde_json::Value {
+18 -18
crates/jacquard-common/src/xrpc.rs
··· 248 248 #[derive(Debug, Default, Clone)] 249 249 pub struct CallOptions<'a> { 250 250 /// Optional Authorization to apply (`Bearer` or `DPoP`). 251 - pub auth: Option<AuthorizationToken<'a>>, 251 + pub auth: Option<AuthorizationToken<SmolStr>>, 252 252 /// `atproto-proxy` header value. 253 253 pub atproto_proxy: Option<CowStr<'a>>, 254 254 /// `atproto-accept-labelers` header values. ··· 397 397 398 398 /// Stream an XRPC procedure call and its response 399 399 #[cfg(not(target_arch = "wasm32"))] 400 - fn stream<S>( 400 + fn stream<S, B>( 401 401 &self, 402 - stream: XrpcProcedureSend<S::Frame<'static>>, 402 + stream: XrpcProcedureSend<S::Frame<B>>, 403 403 ) -> impl Future< 404 404 Output = Result< 405 - XrpcResponseStream< 406 - <<S as XrpcProcedureStream>::Response as XrpcStreamResp>::Frame<'static>, 407 - >, 405 + XrpcResponseStream<<<S as XrpcProcedureStream>::Response as XrpcStreamResp>::Frame<B>>, 408 406 StreamError, 409 407 >, 410 408 > 411 409 where 410 + B: BosStr + 'static, 412 411 S: XrpcProcedureStream + 'static, 413 - <<S as XrpcProcedureStream>::Response as XrpcStreamResp>::Frame<'static>: XrpcStreamResp, 412 + <<S as XrpcProcedureStream>::Response as XrpcStreamResp>::Frame<B>: XrpcStreamResp, 414 413 Self: Sync; 415 414 416 415 /// Stream an XRPC procedure call and its response ··· 460 459 461 460 impl<'a, C: HttpClient> XrpcCall<'a, C> { 462 461 /// Apply Authorization to this call. 463 - pub fn auth(mut self, token: AuthorizationToken<'a>) -> Self { 462 + pub fn auth(mut self, token: AuthorizationToken<SmolStr>) -> Self { 464 463 self.opts.auth = Some(token); 465 464 self 466 465 } ··· 662 661 if let Some(token) = &opts.auth { 663 662 let hv = match token { 664 663 AuthorizationToken::Bearer(t) => { 665 - HeaderValue::from_str(&format!("Bearer {}", t.as_ref())) 664 + HeaderValue::from_str(&format!("Bearer {}", t.as_str())) 666 665 } 667 - AuthorizationToken::Dpop(t) => HeaderValue::from_str(&format!("DPoP {}", t.as_ref())), 666 + AuthorizationToken::Dpop(t) => HeaderValue::from_str(&format!("DPoP {}", t.as_str())), 668 667 } 669 668 .map_err(|e| ClientError::invalid_request(format!("Invalid authorization token: {}", e)))?; 670 669 builder = builder.header(Header::Authorization, hv); ··· 1044 1043 /// 1045 1044 /// Useful for streaming upload of large payloads, or for "pipe-through" operations 1046 1045 /// where you are processing a large payload. 1047 - pub async fn stream<S>( 1046 + pub async fn stream<S, B>( 1048 1047 self, 1049 - stream: XrpcProcedureSend<S::Frame<'static>>, 1050 - ) -> Result<XrpcResponseStream<<S::Response as XrpcStreamResp>::Frame<'static>>, StreamError> 1048 + stream: XrpcProcedureSend<S::Frame<B>>, 1049 + ) -> Result<XrpcResponseStream<<S::Response as XrpcStreamResp>::Frame<B>>, StreamError> 1051 1050 where 1052 1051 S: XrpcProcedureStream + 'static, 1053 - <<S as XrpcProcedureStream>::Response as XrpcStreamResp>::Frame<'static>: XrpcStreamResp, 1052 + B: BosStr + 'static, 1053 + <<S as XrpcProcedureStream>::Response as XrpcStreamResp>::Frame<B>: XrpcStreamResp, 1054 1054 { 1055 1055 use alloc::boxed::Box; 1056 1056 use futures::TryStreamExt; ··· 1064 1064 if let Some(token) = &self.opts.auth { 1065 1065 let hv = match token { 1066 1066 AuthorizationToken::Bearer(t) => { 1067 - HeaderValue::from_str(&format!("Bearer {}", t.as_ref())) 1067 + HeaderValue::from_str(&format!("Bearer {}", t.as_str())) 1068 1068 } 1069 1069 AuthorizationToken::Dpop(t) => { 1070 - HeaderValue::from_str(&format!("DPoP {}", t.as_ref())) 1070 + HeaderValue::from_str(&format!("DPoP {}", t.as_str())) 1071 1071 } 1072 1072 } 1073 1073 .map_err(|e| StreamError::protocol(format!("Invalid authorization token: {}", e)))?; ··· 1108 1108 let (parts, body) = resp.into_parts(); 1109 1109 1110 1110 Ok(XrpcResponseStream::< 1111 - <<S as XrpcProcedureStream>::Response as XrpcStreamResp>::Frame<'static>, 1112 - >::from_typed_parts(parts, body)) 1111 + <<S as XrpcProcedureStream>::Response as XrpcStreamResp>::Frame<B>, 1112 + >::from_typed_parts::<B>(parts, body)) 1113 1113 } 1114 1114 } 1115 1115
+20 -20
crates/jacquard-common/src/xrpc/streaming.rs
··· 1 1 //! Streaming support for XRPC requests and responses 2 2 3 - use crate::{IntoStatic, StreamError, stream::ByteStream, xrpc::XrpcRequest}; 3 + use crate::{BosStr, StreamError, stream::ByteStream, xrpc::XrpcRequest}; 4 4 use alloc::boxed::Box; 5 5 use bytes::Bytes; 6 6 use core::{marker::PhantomData, pin::Pin}; ··· 28 28 const ENCODING: &'static str; 29 29 30 30 /// Frame type for this streaming procedure 31 - type Frame<'de>; 31 + type Frame<S: BosStr>; 32 32 33 33 /// Associated request type 34 34 type Request: XrpcRequest; ··· 39 39 /// Encode a frame into bytes for transmission. 40 40 /// 41 41 /// Default implementation uses DAG-CBOR encoding. 42 - fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError> 42 + fn encode_frame<S: BosStr>(data: Self::Frame<S>) -> Result<Bytes, StreamError> 43 43 where 44 - Self::Frame<'de>: Serialize, 44 + Self::Frame<S>: Serialize, 45 45 { 46 46 Ok(Bytes::from_owner( 47 47 serde_ipld_dagcbor::to_vec(&data).map_err(StreamError::encode)?, ··· 51 51 /// Decode the request body for procedures. 52 52 /// 53 53 /// Default implementation deserializes from CBOR. Override for non-CBOR encodings. 54 - fn decode_frame<'de>(frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError> 54 + fn decode_frame<'de, S: BosStr>(frame: &'de [u8]) -> Result<Self::Frame<S>, StreamError> 55 55 where 56 - Self::Frame<'de>: Deserialize<'de>, 56 + Self::Frame<S>: Deserialize<'de>, 57 57 { 58 58 Ok(serde_ipld_dagcbor::from_slice(frame).map_err(StreamError::decode)?) 59 59 } ··· 70 70 const ENCODING: &'static str; 71 71 72 72 /// Response output type 73 - type Frame<'de>: IntoStatic; 73 + type Frame<S: BosStr>; 74 74 75 75 /// Encode a frame into bytes for transmission. 76 76 /// 77 77 /// Default implementation uses DAG-CBOR encoding. 78 - fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError> 78 + fn encode_frame<S: BosStr>(data: Self::Frame<S>) -> Result<Bytes, StreamError> 79 79 where 80 - Self::Frame<'de>: Serialize, 80 + Self::Frame<S>: Serialize, 81 81 { 82 82 Ok(Bytes::from_owner( 83 83 serde_ipld_dagcbor::to_vec(&data).map_err(StreamError::encode)?, ··· 89 89 /// Default implementation deserializes from CBOR. Override for non-CBOR encodings. 90 90 /// 91 91 /// TODO: make this handle when frames are fragmented? 92 - fn decode_frame<'de>(frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError> 92 + fn decode_frame<'de, S: BosStr>(frame: &'de [u8]) -> Result<Self::Frame<S>, StreamError> 93 93 where 94 - Self::Frame<'de>: Deserialize<'de>, 94 + Self::Frame<S>: Deserialize<'de>, 95 95 { 96 96 Ok(serde_ipld_dagcbor::from_slice(frame).map_err(StreamError::decode)?) 97 97 } ··· 147 147 } 148 148 149 149 /// Encode a stream of items into the corresponding XRPC procedure stream. 150 - pub fn encode_stream<P: XrpcProcedureStream + 'static>( 151 - s: Boxed<P::Frame<'static>>, 152 - ) -> XrpcProcedureSend<P::Frame<'static>> 150 + pub fn encode_stream<P: XrpcProcedureStream + 'static, S: BosStr>( 151 + s: Boxed<P::Frame<S>>, 152 + ) -> XrpcProcedureSend<P::Frame<S>> 153 153 where 154 - <P as XrpcProcedureStream>::Frame<'static>: Serialize, 154 + <P as XrpcProcedureStream>::Frame<S>: Serialize + 'static, 155 155 { 156 156 let stream = 157 - s.map(|f| P::encode_frame(f).map(|b| XrpcStreamFrame::new_typed::<P::Frame<'_>>(b))); 157 + s.map(|f| P::encode_frame(f).map(|b| XrpcStreamFrame::new_typed::<P::Frame<S>>(b))); 158 158 159 159 XrpcProcedureSend(Box::pin(stream)) 160 160 } ··· 208 208 209 209 impl<F: XrpcStreamResp> XrpcResponseStream<F> { 210 210 /// Create a typed response stream from a `StreamingResponse` 211 - pub fn from_stream(StreamingResponse { parts, body }: StreamingResponse) -> Self { 211 + pub fn from_stream<S: BosStr>(StreamingResponse { parts, body }: StreamingResponse) -> Self { 212 212 Self { 213 213 parts, 214 214 body: Box::pin( 215 215 body.into_inner() 216 - .map_ok(|b| XrpcStreamFrame::new_typed::<F::Frame<'_>>(b)), 216 + .map_ok(|b| XrpcStreamFrame::new_typed::<F::Frame<S>>(b)), 217 217 ), 218 218 } 219 219 } 220 220 221 221 /// Create a typed response stream from parts and body 222 - pub fn from_typed_parts(parts: http::response::Parts, body: ByteStream) -> Self { 222 + pub fn from_typed_parts<S: BosStr>(parts: http::response::Parts, body: ByteStream) -> Self { 223 223 Self { 224 224 parts, 225 225 body: Box::pin( 226 226 body.into_inner() 227 - .map_ok(|b| XrpcStreamFrame::new_typed::<F::Frame<'_>>(b)), 227 + .map_ok(|b| XrpcStreamFrame::new_typed::<F::Frame<S>>(b)), 228 228 ), 229 229 } 230 230 }
+16 -18
crates/jacquard-oauth/src/client.rs
··· 564 564 /// 565 565 /// The token may be stale if it has expired; use [`OAuthSession::refresh`] or 566 566 /// rely on the automatic refresh performed by `send_with_opts` to obtain a fresh one. 567 - pub async fn access_token(&self) -> AuthorizationToken<'static> { 568 - AuthorizationToken::Dpop(CowStr::Owned( 569 - self.data.read().await.token_set.access_token.clone(), 570 - )) 567 + pub async fn access_token(&self) -> AuthorizationToken<SmolStr> { 568 + AuthorizationToken::Dpop(self.data.read().await.token_set.access_token.clone()) 571 569 } 572 570 573 571 /// Return the current refresh token for this session, if one is present. 574 572 /// 575 573 /// Not all authorization servers issue refresh tokens. When `None` is returned, 576 574 /// the session cannot be silently renewed and the user must re-authenticate. 577 - pub async fn refresh_token(&self) -> Option<AuthorizationToken<'static>> { 575 + pub async fn refresh_token(&self) -> Option<AuthorizationToken<SmolStr>> { 578 576 self.data 579 577 .read() 580 578 .await 581 579 .token_set 582 580 .refresh_token 583 581 .clone() 584 - .map(|t| AuthorizationToken::Dpop(CowStr::Owned(t))) 582 + .map(|t| AuthorizationToken::Dpop(t)) 585 583 } 586 584 587 585 /// Derive an unauthenticated [`OAuthClient`] that shares the same registry and resolver. ··· 653 651 /// The actual token exchange is serialized per `(DID, session_id)` pair via a `Mutex` inside 654 652 /// the registry, so concurrent refresh attempts will not result in duplicate token exchanges. 655 653 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip_all))] 656 - pub async fn refresh(&self) -> Result<AuthorizationToken<'static>> { 654 + pub async fn refresh(&self) -> Result<AuthorizationToken<SmolStr>> { 657 655 // Read identifiers without holding the lock across await 658 656 let (did, sid) = { 659 657 let data = self.data.read().await; 660 658 (data.account_did.clone(), data.session_id.clone()) 661 659 }; 662 660 let refreshed = self.registry.as_ref().get(&did, &sid, true).await?; 663 - let token = 664 - AuthorizationToken::Dpop(CowStr::Owned(refreshed.token_set.access_token.clone())); 661 + let token = AuthorizationToken::Dpop(refreshed.token_set.access_token.clone()); 665 662 // Write back updated session 666 663 *self.data.write().await = refreshed.clone().into_static(); 667 664 // Store in the registry ··· 899 896 } 900 897 } 901 898 902 - async fn stream<Str>( 899 + async fn stream<Str, B>( 903 900 &self, 904 - stream: jacquard_common::xrpc::streaming::XrpcProcedureSend<Str::Frame<'static>>, 901 + stream: jacquard_common::xrpc::streaming::XrpcProcedureSend<Str::Frame<B>>, 905 902 ) -> core::result::Result< 906 903 jacquard_common::xrpc::streaming::XrpcResponseStream< 907 - <<Str as jacquard_common::xrpc::streaming::XrpcProcedureStream>::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame<'static>, 904 + <<Str as jacquard_common::xrpc::streaming::XrpcProcedureStream>::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame<B>, 908 905 >, 909 906 jacquard_common::StreamError, 910 907 > 911 908 where 912 909 Str: jacquard_common::xrpc::streaming::XrpcProcedureStream + 'static, 913 - <<Str as jacquard_common::xrpc::streaming::XrpcProcedureStream>::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame<'static>: jacquard_common::xrpc::streaming::XrpcStreamResp, 910 + <<Str as jacquard_common::xrpc::streaming::XrpcProcedureStream>::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame<B>: jacquard_common::xrpc::streaming::XrpcStreamResp, 911 + B: BosStr + 'static, 914 912 { 915 913 use jacquard_common::StreamError; 916 914 use n0_future::TryStreamExt; ··· 929 927 use jacquard_common::AuthorizationToken; 930 928 let hv = match token { 931 929 AuthorizationToken::Bearer(t) => { 932 - http::HeaderValue::from_str(&format!("Bearer {}", t.as_ref())) 930 + http::HeaderValue::from_str(&format!("Bearer {}", t.as_str())) 933 931 } 934 932 AuthorizationToken::Dpop(t) => { 935 - http::HeaderValue::from_str(&format!("DPoP {}", t.as_ref())) 933 + http::HeaderValue::from_str(&format!("DPoP {}", t.as_str())) 936 934 } 937 935 } 938 936 .map_err(|e| StreamError::protocol(format!("Invalid authorization token: {}", e)))?; ··· 977 975 Ok(response) => { 978 976 let (resp_parts, resp_body) = response.into_parts(); 979 977 Ok( 980 - jacquard_common::xrpc::streaming::XrpcResponseStream::from_typed_parts( 978 + jacquard_common::xrpc::streaming::XrpcResponseStream::from_typed_parts::<B>( 981 979 resp_parts, resp_body, 982 980 ), 983 981 ) ··· 1071 1069 let mut opts = jacquard_common::xrpc::SubscriptionOptions::default(); 1072 1070 let token = self.access_token().await; 1073 1071 let auth_value = match token { 1074 - AuthorizationToken::Bearer(t) => format!("Bearer {}", t.as_ref()), 1075 - AuthorizationToken::Dpop(t) => format!("DPoP {}", t.as_ref()), 1072 + AuthorizationToken::Bearer(t) => format!("Bearer {}", t.as_str()), 1073 + AuthorizationToken::Dpop(t) => format!("DPoP {}", t.as_str()), 1076 1074 }; 1077 1075 opts.headers 1078 1076 .push((CowStr::from("Authorization"), CowStr::from(auth_value)));
+1 -1
crates/jacquard/Cargo.toml
··· 12 12 license.workspace = true 13 13 14 14 [features] 15 - default = ["api_full", "dns", "loopback", "derive", "cache"] 15 + default = ["api_full", "dns", "loopback", "derive", "cache", "websocket"] 16 16 derive = ["dep:jacquard-derive"] 17 17 # Minimal API bindings 18 18 api = ["jacquard-api/minimal"]
+180 -185
crates/jacquard/src/client.rs
··· 53 53 pub use jacquard_common::session::{MemorySessionStore, SessionStore, SessionStoreError}; 54 54 use jacquard_common::types::blob::{Blob, MimeType}; 55 55 use jacquard_common::types::collection::Collection; 56 + #[cfg(feature = "api")] 57 + use jacquard_common::types::ident::AtIdentifier; 56 58 use jacquard_common::types::recordkey::{RecordKey, Rkey}; 57 59 use jacquard_common::types::string::AtUri; 58 60 #[cfg(feature = "api")] ··· 63 65 }; 64 66 use jacquard_common::{AuthorizationToken, xrpc}; 65 67 use jacquard_common::{ 66 - CowStr, IntoStatic, 68 + BosStr, CowStr, IntoStatic, 67 69 types::string::{Did, Handle}, 68 70 }; 69 71 use jacquard_identity::resolver::{ ··· 76 78 use jacquard_oauth::resolver::OAuthResolver; 77 79 use serde::Serialize; 78 80 #[cfg(feature = "api")] 81 + use serde::de::DeserializeOwned; 82 + use smol_str::SmolStr; 83 + #[cfg(feature = "api")] 79 84 use std::marker::Send; 80 85 use std::option::Option; 81 86 use std::sync::Arc; ··· 99 104 /// Identify the kind of session. 100 105 fn session_kind(&self) -> AgentKind; 101 106 /// Return current DID and an optional session id (always Some for OAuth). 102 - fn session_info(&self) 103 - -> impl Future<Output = Option<(Did<'static>, Option<CowStr<'static>>)>>; 107 + fn session_info(&self) -> impl Future<Output = Option<(Did, Option<SmolStr>)>>; 104 108 /// Current base endpoint. 105 109 fn endpoint(&self) -> impl Future<Output = Uri<String>>; 106 110 /// Override per-session call options. 107 111 fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future<Output = ()>; 108 112 /// Refresh the session and return a fresh AuthorizationToken. 109 - fn refresh(&self) -> impl Future<Output = ClientResult<AuthorizationToken<'static>>>; 113 + fn refresh(&self) -> impl Future<Output = ClientResult<AuthorizationToken<SmolStr>>>; 110 114 } 111 115 112 116 /// Alias for an agent over a credential (app‑password) session. ··· 139 143 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> { 140 144 /// let client = BasicClient::unauthenticated(); 141 145 /// let uri = AtUri::new_static("at://did:plc:xyz/app.bsky.feed.post/3l5abc").unwrap(); 142 - /// let response = client.get_record::<Post<'_>>(&uri).await?; 146 + /// let response = client.get_record::<Post, _>(&uri).await?; 143 147 /// # Ok(()) 144 148 /// # } 145 149 /// ``` ··· 238 242 #[cfg(not(target_arch = "wasm32"))] 239 243 fn send<R>(&self, request: R) -> impl Future<Output = XrpcResult<XrpcResponse<R>>> + Send 240 244 where 241 - R: XrpcRequest + Send + Sync, 245 + R: XrpcRequest + Send + Sync + Serialize, 242 246 <R as XrpcRequest>::Response: Send + Sync, 243 247 Self: Sync, 244 248 { ··· 256 260 opts: CallOptions<'_>, 257 261 ) -> impl Future<Output = XrpcResult<XrpcResponse<R>>> + Send 258 262 where 259 - R: XrpcRequest + Send + Sync, 263 + R: XrpcRequest + Send + Sync + Serialize, 260 264 <R as XrpcRequest>::Response: Send + Sync, 261 265 Self: Sync, 262 266 { 263 267 async move { 264 268 let base_uri = self.base_uri().await; 265 269 self.resolver 266 - .xrpc(base_uri) 270 + .xrpc(base_uri.borrow()) 267 271 .with_options(opts.clone()) 268 272 .send(&request) 269 273 .await ··· 274 278 #[cfg(target_arch = "wasm32")] 275 279 fn send<R>(&self, request: R) -> impl Future<Output = XrpcResult<XrpcResponse<R>>> 276 280 where 277 - R: XrpcRequest + Send + Sync, 281 + R: XrpcRequest + Send + Sync + Serialize, 278 282 <R as XrpcRequest>::Response: Send + Sync, 279 283 { 280 284 async move { ··· 291 295 opts: CallOptions<'_>, 292 296 ) -> impl Future<Output = XrpcResult<XrpcResponse<R>>> 293 297 where 294 - R: XrpcRequest + Send + Sync, 298 + R: XrpcRequest + Send + Sync + Serialize, 295 299 <R as XrpcRequest>::Response: Send + Sync, 296 300 { 297 301 async move { 298 302 let base_uri = self.base_uri().await; 299 303 self.resolver 300 - .xrpc(base_uri) 304 + .xrpc(base_uri.borrow()) 301 305 .with_options(opts.clone()) 302 306 .send(&request) 303 307 .await ··· 334 338 AgentKind::AppPassword 335 339 } 336 340 337 - fn session_info( 338 - &self, 339 - ) -> impl Future<Output = Option<(Did<'static>, Option<CowStr<'static>>)>> { 341 + fn session_info(&self) -> impl Future<Output = Option<(Did, Option<SmolStr>)>> { 340 342 async { None } // no session 341 343 } 342 344 ··· 352 354 } 353 355 354 356 #[doc = " Refresh the session and return a fresh AuthorizationToken."] 355 - fn refresh(&self) -> impl Future<Output = ClientResult<AuthorizationToken<'static>>> + Send { 357 + fn refresh(&self) -> impl Future<Output = ClientResult<AuthorizationToken<SmolStr>>> + Send { 356 358 async { 357 359 Err(ClientError::auth( 358 360 jacquard_common::error::AuthError::NotAuthenticated, ··· 369 371 370 372 #[doc = " Resolve handle"] 371 373 #[cfg(not(target_arch = "wasm32"))] 372 - fn resolve_handle( 374 + fn resolve_handle<S: BosStr + Sync>( 373 375 &self, 374 - handle: &Handle<'_>, 375 - ) -> impl Future<Output = std::result::Result<Did<'static>, IdentityError>> + Send 376 + handle: &Handle<S>, 377 + ) -> impl Future<Output = std::result::Result<Did, IdentityError>> + Send 376 378 where 377 379 Self: Sync, 378 380 { ··· 381 383 382 384 #[doc = " Resolve DID document"] 383 385 #[cfg(not(target_arch = "wasm32"))] 384 - fn resolve_did_doc( 386 + fn resolve_did_doc<S: BosStr + Sync>( 385 387 &self, 386 - did: &Did<'_>, 388 + did: &Did<S>, 387 389 ) -> impl Future<Output = std::result::Result<DidDocResponse, IdentityError>> + Send 388 390 where 389 391 Self: Sync, 390 392 { 391 393 self.resolver.resolve_did_doc(did) 392 394 } 395 + 393 396 #[doc = " Resolve handle"] 394 397 #[cfg(target_arch = "wasm32")] 395 - fn resolve_handle( 398 + fn resolve_handle<S: BosStr + Sync>( 396 399 &self, 397 - handle: &Handle<'_>, 398 - ) -> impl Future<Output = std::result::Result<Did<'static>, IdentityError>> { 400 + handle: &Handle<S>, 401 + ) -> impl Future<Output = std::result::Result<Did, IdentityError>> { 399 402 self.resolver.resolve_handle(handle) 400 403 } 401 404 402 405 #[doc = " Resolve DID document"] 403 406 #[cfg(target_arch = "wasm32")] 404 - fn resolve_did_doc( 407 + fn resolve_did_doc<S: BosStr + Sync>( 405 408 &self, 406 - did: &Did<'_>, 409 + did: &Did<S>, 407 410 ) -> impl Future<Output = std::result::Result<DidDocResponse, IdentityError>> { 408 411 self.resolver.resolve_did_doc(did) 409 412 } ··· 479 482 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] 480 483 pub struct AtpSession { 481 484 /// Access token (JWT) used for authenticated requests 482 - #[serde(borrow)] 483 - pub access_jwt: CowStr<'static>, 485 + pub access_jwt: SmolStr, 484 486 /// Refresh token (JWT) used to obtain new access tokens 485 - pub refresh_jwt: CowStr<'static>, 487 + pub refresh_jwt: SmolStr, 486 488 /// User's DID (Decentralized Identifier) 487 - pub did: Did<'static>, 489 + pub did: Did, 488 490 /// User's handle (e.g., "alice.bsky.social") 489 - pub handle: Handle<'static>, 491 + pub handle: Handle, 490 492 } 491 493 492 494 impl IntoStatic for AtpSession { 493 495 type Output = Self; 494 496 495 497 fn into_static(self) -> Self { 496 - Self { 497 - access_jwt: self.access_jwt.into_static(), 498 - refresh_jwt: self.refresh_jwt.into_static(), 499 - did: self.did.into_static(), 500 - handle: self.handle.into_static(), 501 - } 498 + self 502 499 } 503 500 } 504 501 505 502 #[cfg(feature = "api")] 506 - impl From<CreateSessionOutput<'_>> for AtpSession { 507 - fn from(output: CreateSessionOutput<'_>) -> Self { 503 + impl From<CreateSessionOutput> for AtpSession { 504 + fn from(output: CreateSessionOutput) -> Self { 508 505 Self { 509 - access_jwt: output.access_jwt.into_static(), 510 - refresh_jwt: output.refresh_jwt.into_static(), 511 - did: output.did.into_static(), 512 - handle: output.handle.into_static(), 506 + access_jwt: output.access_jwt, 507 + refresh_jwt: output.refresh_jwt, 508 + did: output.did, 509 + handle: output.handle, 513 510 } 514 511 } 515 512 } 516 513 517 514 #[cfg(feature = "api")] 518 - impl From<RefreshSessionOutput<'_>> for AtpSession { 519 - fn from(output: RefreshSessionOutput<'_>) -> Self { 515 + impl From<RefreshSessionOutput> for AtpSession { 516 + fn from(output: RefreshSessionOutput) -> Self { 520 517 Self { 521 - access_jwt: output.access_jwt.into_static(), 522 - refresh_jwt: output.refresh_jwt.into_static(), 523 - did: output.did.into_static(), 524 - handle: output.handle.into_static(), 518 + access_jwt: output.access_jwt, 519 + refresh_jwt: output.refresh_jwt, 520 + did: output.did, 521 + handle: output.handle, 525 522 } 526 523 } 527 524 } ··· 548 545 } 549 546 550 547 /// Return session info if available. 551 - pub async fn info(&self) -> Option<(Did<'static>, Option<CowStr<'static>>)> { 548 + pub async fn info(&self) -> Option<(Did, Option<SmolStr>)> { 552 549 self.inner.session_info().await 553 550 } 554 551 ··· 563 560 } 564 561 565 562 /// Refresh the session and return a fresh token. 566 - pub async fn refresh(&self) -> ClientResult<AuthorizationToken<'static>> { 563 + pub async fn refresh(&self) -> ClientResult<AuthorizationToken<SmolStr>> { 567 564 self.inner.refresh().await 568 565 } 569 566 } 570 567 571 - /// Output type for a collection record retrieval operation 572 - pub type CollectionOutput<'a, R> = <<R as Collection>::Record as XrpcResp>::Output<'a>; 568 + /// Output type for a collection record retrieval operation (SmolStr-backed, as returned by `into_output()`) 569 + pub type CollectionOutput<R> = <<R as Collection>::Record as XrpcResp>::Output<SmolStr>; 573 570 /// Error type for a collection record retrieval operation 574 - pub type CollectionErr<'a, R> = <<R as Collection>::Record as XrpcResp>::Err<'a>; 571 + pub type CollectionErr<R> = <<R as Collection>::Record as XrpcResp>::Err; 575 572 /// Response type for the get request of a vec update operation 576 573 pub type VecGetResponse<U> = <<U as VecUpdate>::GetRequest as XrpcRequest>::Response; 577 574 /// Response type for the put request of a vec update operation 578 575 pub type VecPutResponse<U> = <<U as VecUpdate>::PutRequest as XrpcRequest>::Response; 579 576 580 - type CollectionError<'a, R> = <<R as Collection>::Record as XrpcResp>::Err<'a>; 577 + type CollectionError<R> = <<R as Collection>::Record as XrpcResp>::Err; 581 578 582 - type VecUpdateGetError<'a, U> = 583 - <<<U as VecUpdate>::GetRequest as XrpcRequest>::Response as XrpcResp>::Err<'a>; 579 + type VecUpdateGetError<U> = 580 + <<<U as VecUpdate>::GetRequest as XrpcRequest>::Response as XrpcResp>::Err; 584 581 585 - type VecUpdatePutError<'a, U> = 586 - <<<U as VecUpdate>::PutRequest as XrpcRequest>::Response as XrpcResp>::Err<'a>; 582 + type VecUpdatePutError<U> = 583 + <<<U as VecUpdate>::PutRequest as XrpcRequest>::Response as XrpcResp>::Err; 587 584 588 585 /// Extension trait providing convenience methods for common repository operations. 589 586 /// ··· 621 618 /// let output = agent.create_record(post, None).await?; 622 619 /// 623 620 /// // Read it back 624 - /// let response = agent.get_record::<Post>(&output.uri).await?; 621 + /// let response = agent.get_record::<Post, _>(&output.uri).await?; 625 622 /// let record = response.parse()?; 626 623 /// println!("Post: {}", record.value.text); 627 624 /// # Ok(()) ··· 665 662 fn create_record<R>( 666 663 &self, 667 664 record: R, 668 - rkey: Option<RecordKey<Rkey<'_>>>, 669 - ) -> impl Future<Output = Result<CreateRecordOutput<'static>>> 665 + rkey: Option<RecordKey<Rkey>>, 666 + ) -> impl Future<Output = Result<CreateRecordOutput>> 670 667 where 671 668 R: Collection + serde::Serialize, 672 669 { ··· 688 685 689 686 let request = CreateRecord::new() 690 687 .repo(AtIdentifier::Did(did)) 691 - .collection(R::nsid()) 688 + .collection(R::nsid().into_static()) 692 689 .record(data) 693 - .maybe_rkey(rkey) 690 + .rkey(rkey.map(|k| k.clone())) 694 691 .build(); 695 692 696 693 #[cfg(feature = "tracing")] ··· 725 722 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> { 726 723 /// # let agent: BasicClient = todo!(); 727 724 /// let uri = AtUri::new_static("at://did:plc:xyz/app.bsky.feed.post/3l5bqm7lepk2c").unwrap(); 728 - /// let response = agent.get_record::<Post>(&uri).await?; 725 + /// let response = agent.get_record::<Post, _>(&uri).await?; 729 726 /// let output = response.parse()?; // PostGetRecordOutput<'_> borrowing from buffer 730 727 /// println!("Post text: {}", output.value.text); 731 728 /// ··· 734 731 /// # Ok(()) 735 732 /// # } 736 733 /// ``` 737 - fn get_record<R>( 734 + fn get_record<R, S>( 738 735 &self, 739 - uri: &AtUri<'_>, 736 + uri: &AtUri<S>, 740 737 ) -> impl Future<Output = ClientResult<Response<R::Record>>> 741 738 where 742 739 R: Collection, 740 + S: BosStr + Sync, 743 741 { 744 742 async move { 745 743 #[cfg(feature = "tracing")] ··· 766 764 #[cfg(feature = "tracing")] 767 765 _span.exit(); 768 766 769 - // Resolve authority (DID or handle) to get DID and PDS 770 - use jacquard_common::types::ident::AtIdentifier; 767 + // Resolve authority (DID or handle) to get DID and PDS. 771 768 let (repo_did, pds_url) = match uri.authority() { 772 769 AtIdentifier::Did(did) => { 773 - let pds = self.pds_for_did(did).await.map_err(|e| { 770 + let pds = self.pds_for_did(&did).await.map_err(|e| { 774 771 ClientError::from(e) 775 772 .with_context("DID document resolution failed during record retrieval") 776 773 })?; 777 - (did.clone(), pds) 774 + (did.into_static(), pds) 775 + } 776 + AtIdentifier::Handle(handle) => { 777 + self.pds_for_handle(&handle).await.map_err(|e| { 778 + ClientError::from(e) 779 + .with_context("handle resolution failed during record retrieval") 780 + })? 778 781 } 779 - AtIdentifier::Handle(handle) => self.pds_for_handle(handle).await.map_err(|e| { 780 - ClientError::from(e) 781 - .with_context("handle resolution failed during record retrieval") 782 - })?, 783 782 }; 784 783 785 - // Make stateless XRPC call to that PDS (no auth required for public records) 784 + // Make stateless XRPC call to that PDS (no auth required for public records). 785 + // All fields use SmolStr backing to satisfy the builder's single S type parameter. 786 786 use jacquard_api::com_atproto::repo::get_record::GetRecord; 787 787 let request = GetRecord::new() 788 - .repo(AtIdentifier::Did(repo_did)) 789 - .collection(R::nsid()) 790 - .rkey(rkey.clone()) 788 + .repo(AtIdentifier::Did(repo_did.clone())) 789 + .collection(R::nsid().into_static()) 790 + .rkey(rkey.into_static()) 791 791 .build(); 792 792 793 793 let response: Response<GetRecordResponse> = { 794 794 let http_request = 795 - xrpc::build_http_request(&pds_url, &request, &self.opts().await)?; 795 + xrpc::build_http_request(&pds_url.borrow(), &request, &self.opts().await)?; 796 796 797 797 let http_response = self 798 798 .send_http(http_request) ··· 808 808 809 809 /// Untyped, freeform record fetcher. 810 810 /// Hits <https://slingshot.microcosm.blue> 811 - fn fetch_record_slingshot( 811 + fn fetch_record_slingshot<S>( 812 812 &self, 813 - uri: &AtUri<'_>, 814 - ) -> impl Future<Output = Result<GetRecordOutput<'static>>> { 813 + uri: &AtUri<S>, 814 + ) -> impl Future<Output = Result<GetRecordOutput>> 815 + where 816 + S: BosStr + Sync, 817 + { 815 818 async move { 816 819 #[cfg(feature = "tracing")] 817 820 let _span = tracing::debug_span!("fetch_record_slingshot", uri = %uri).entered(); ··· 829 832 let request = GetRecord::new() 830 833 .repo(uri.authority().clone()) 831 834 .collection(collection.clone()) 832 - .rkey(rkey.clone()) 835 + .rkey(RecordKey(rkey.clone())) 833 836 .build(); 834 837 835 838 #[cfg(feature = "tracing")] ··· 838 841 let response: Response<GetRecordResponse> = { 839 842 let http_request = xrpc::build_http_request( 840 843 &Uri::parse("https://slingshot.microcosm.blue") 841 - .expect("slingshot url is valid") 842 - .to_owned(), 844 + .expect("slingshot url is valid"), 843 845 &request, 844 846 &self.opts().await, 845 847 )?; ··· 864 866 /// 865 867 /// Takes an at:// URI annotated with the collection type, which be constructed with `R::uri(uri)` 866 868 /// where `R` is the type of record you want (e.g. `app_bsky::feed::post::Post::uri(uri)` for Bluesky posts). 867 - fn fetch_record<R>( 869 + fn fetch_record<R, S>( 868 870 &self, 869 - uri: &RecordUri<'_, R>, 870 - ) -> impl Future<Output = Result<CollectionOutput<'static, R>>> 871 + uri: &RecordUri<S, R>, 872 + ) -> impl Future<Output = Result<CollectionOutput<R>>> 871 873 where 872 874 R: Collection, 873 - for<'a> CollectionOutput<'a, R>: IntoStatic<Output = CollectionOutput<'static, R>>, 874 - for<'a> CollectionErr<'a, R>: IntoStatic<Output = CollectionErr<'static, R>> + Send + Sync, 875 + S: BosStr + Sync, 876 + CollectionOutput<R>: serde::de::DeserializeOwned, 877 + CollectionError<R>: Send + Sync + 'static, 875 878 { 876 879 let uri = uri.as_uri(); 877 880 async move { 878 881 use smol_str::format_smolstr; 879 882 880 - let response = self.get_record::<R>(uri).await?; 883 + let response = self.get_record::<R, S>(uri).await?; 881 884 let response: Response<R::Record> = response.transmute(); 882 885 let output = response.into_output().map_err(|e| match e { 883 886 XrpcError::Auth(auth) => AgentError::from(auth), ··· 888 891 None, 889 892 ) 890 893 .with_details(format_smolstr!("{:?}", typed)), 891 - // Note for future orual: the above was done this way due to GAT lifetime inference constraints.. 894 + // Note: typed error formatted as Debug since CollectionErr<R> is not Display. 892 895 e => AgentError::xrpc(e), 893 896 })?; 894 897 Ok(output) ··· 914 917 /// # let agent: BasicClient = todo!(); 915 918 /// let uri = AtUri::new_static("at://did:plc:xyz/app.bsky.actor.profile/self").unwrap(); 916 919 /// // Update profile record in-place 917 - /// agent.update_record::<Profile>(&uri, |profile| { 920 + /// agent.update_record::<Profile, _>(&uri, |profile| { 918 921 /// profile.display_name = Some(CowStr::from("New Name")); 919 922 /// profile.description = Some(CowStr::from("Updated bio")); 920 923 /// }).await?; 921 924 /// # Ok(()) 922 925 /// # } 923 926 /// ``` 924 - fn update_record<R>( 927 + fn update_record<R, S>( 925 928 &self, 926 - uri: &AtUri<'_>, 929 + uri: &AtUri<S>, 927 930 f: impl FnOnce(&mut R), 928 - ) -> impl Future<Output = Result<PutRecordOutput<'static>>> 931 + ) -> impl Future<Output = Result<PutRecordOutput>> 929 932 where 930 933 R: Collection + Serialize, 931 - R: for<'a> From<CollectionOutput<'a, R>>, 932 - for<'a> <CollectionError<'a, R> as IntoStatic>::Output: 933 - IntoStatic + std::error::Error + Send + Sync, 934 - for<'a> CollectionError<'a, R>: Send + Sync + std::error::Error + IntoStatic, 934 + R: From<CollectionOutput<R>>, 935 + CollectionOutput<R>: serde::de::DeserializeOwned, 936 + CollectionError<R>: Send + Sync + std::error::Error + 'static, 937 + S: BosStr + Sync, 935 938 { 936 939 async move { 937 - // Fetch the record - Response<R::Record> where R::Record::Output<'de> = R<'de> 938 - let response = self.get_record::<R>(uri).await?; 940 + // Fetch the record - Response<R::Record> where R::Record::Output<SmolStr> = R 941 + let response = self.get_record::<R, S>(uri).await?; 939 942 940 943 #[cfg(feature = "tracing")] 941 944 let _span = tracing::debug_span!("update_record", collection = %R::nsid(), uri = %uri) 942 945 .entered(); 943 946 944 - // Parse to get R<'_> borrowing from response buffer 947 + // Parse to get the record, borrowing from the response buffer. 948 + // Err is now a plain owned type; no into_static() needed. 945 949 let record = response.parse().map_err(|e| match e { 946 950 XrpcError::Auth(auth) => AgentError::from(auth), 947 - XrpcError::Xrpc(typed) => { 948 - AgentError::sub_operation("parse record", typed.into_static()) 949 - } 951 + XrpcError::Xrpc(typed) => AgentError::sub_operation("parse record", typed), 950 952 e => AgentError::xrpc(e), 951 953 })?; 952 954 ··· 957 959 f(&mut owned); 958 960 959 961 // Put it back 960 - let rkey = uri 961 - .rkey() 962 - .ok_or_else(|| { 963 - use jacquard_common::types::string::AtStrError; 964 - AgentError::sub_operation( 965 - "extract rkey", 966 - AtStrError::missing("at-uri-scheme", &uri, "rkey"), 967 - ) 968 - })? 969 - .clone() 970 - .into_static(); 962 + // Convert the borrowed Rkey<&str> to an owned Rkey<SmolStr>, then wrap in RecordKey. 963 + // The Rkey<SmolStr> is already validated (extracted from a valid AtUri), so direct 964 + // construction is safe. 965 + let rkey = RecordKey( 966 + uri.rkey() 967 + .ok_or_else(|| { 968 + use jacquard_common::types::string::AtStrError; 969 + AgentError::sub_operation( 970 + "extract rkey", 971 + AtStrError::missing("at-uri-scheme", &uri, "rkey"), 972 + ) 973 + })? 974 + .convert::<SmolStr>(), 975 + ); 971 976 972 977 #[cfg(feature = "tracing")] 973 978 _span.exit(); ··· 981 986 /// The repo is automatically filled from the session info. 982 987 fn delete_record<R>( 983 988 &self, 984 - rkey: RecordKey<Rkey<'_>>, 985 - ) -> impl Future<Output = Result<DeleteRecordOutput<'static>>> 989 + rkey: RecordKey<Rkey>, 990 + ) -> impl Future<Output = Result<DeleteRecordOutput>> 986 991 where 987 - R: Collection, 992 + R: Collection + Serialize, 988 993 { 989 - async { 994 + async move { 990 995 let (did, _) = self 991 996 .session_info() 992 997 .await ··· 998 1003 use jacquard_common::types::ident::AtIdentifier; 999 1004 1000 1005 let request = DeleteRecord::new() 1001 - .repo(AtIdentifier::Did(did)) 1002 - .collection(R::nsid()) 1003 - .rkey(rkey) 1006 + .repo(AtIdentifier::Did(did.clone())) 1007 + .collection(R::nsid().into_static()) 1008 + .rkey(rkey.into_static()) 1004 1009 .build(); 1005 1010 1006 1011 #[cfg(feature = "tracing")] ··· 1024 1029 /// The repo is automatically filled from the session info. 1025 1030 fn put_record<R>( 1026 1031 &self, 1027 - rkey: RecordKey<Rkey<'static>>, 1032 + rkey: RecordKey<Rkey>, 1028 1033 record: R, 1029 - ) -> impl Future<Output = Result<PutRecordOutput<'static>>> 1034 + ) -> impl Future<Output = Result<PutRecordOutput>> 1030 1035 where 1031 1036 R: Collection + serde::Serialize, 1032 1037 { ··· 1047 1052 to_data(&record).map_err(|e| AgentError::sub_operation("serialize record", e))?; 1048 1053 1049 1054 let request = PutRecord::new() 1050 - .repo(AtIdentifier::Did(did)) 1051 - .collection(R::nsid()) 1052 - .rkey(rkey) 1055 + .repo(AtIdentifier::Did(did.clone())) 1056 + .collection(R::nsid().into_static()) 1057 + .rkey(rkey.into_static()) 1053 1058 .record(data) 1054 1059 .build(); 1055 1060 ··· 1091 1096 fn upload_blob( 1092 1097 &self, 1093 1098 data: impl Into<bytes::Bytes>, 1094 - mime_type: MimeType<'_>, 1095 - ) -> impl Future<Output = Result<Blob<'static>>> { 1099 + mime_type: MimeType<&str>, 1100 + ) -> impl Future<Output = Result<Blob>> { 1096 1101 async move { 1097 1102 #[cfg(feature = "tracing")] 1098 1103 let _span = tracing::debug_span!("upload_blob", mime_type = %mime_type).entered(); ··· 1121 1126 XrpcError::Xrpc(typed) => AgentError::sub_operation("upload blob", typed), 1122 1127 e => AgentError::xrpc(e), 1123 1128 })?; 1124 - Ok(output.blob.blob().clone().into_static()) 1129 + // Blob is now SmolStr-backed (owned), so no into_static() needed. 1130 + Ok(output.blob.blob().clone()) 1125 1131 } 1126 1132 } 1127 1133 ··· 1138 1144 /// prefs.retain(|p| !matches!(p, Preference::Hidden(_))); 1139 1145 /// }).await?; 1140 1146 /// ``` 1141 - fn update_vec<U>( 1147 + fn update_vec<'a, U>( 1142 1148 &self, 1143 1149 modify: impl FnOnce(&mut Vec<<U as VecUpdate>::Item>), 1144 1150 ) -> impl Future<Output = Result<xrpc::Response<VecPutResponse<U>>>> 1145 1151 where 1146 1152 U: VecUpdate, 1147 - <U as VecUpdate>::PutRequest: Send + Sync, 1148 - <U as VecUpdate>::GetRequest: Send + Sync, 1153 + <U as VecUpdate>::PutRequest: Send + Sync + Serialize, 1154 + <U as VecUpdate>::GetRequest: Send + Sync + Serialize, 1149 1155 VecGetResponse<U>: Send + Sync, 1150 1156 VecPutResponse<U>: Send + Sync, 1151 - for<'a> VecUpdateGetError<'a, U>: Send + Sync + std::error::Error + IntoStatic, 1152 - for<'a> VecUpdatePutError<'a, U>: Send + Sync + std::error::Error + IntoStatic, 1153 - for<'a> <VecUpdateGetError<'a, U> as IntoStatic>::Output: 1154 - Send + Sync + std::error::Error + IntoStatic + 'static, 1155 - for<'a> <VecUpdatePutError<'a, U> as IntoStatic>::Output: 1156 - Send + Sync + std::error::Error + IntoStatic + 'static, 1157 + <VecGetResponse<U> as XrpcResp>::Output<SmolStr>: DeserializeOwned, 1158 + <VecPutResponse<U> as XrpcResp>::Output<SmolStr>: DeserializeOwned, 1159 + VecUpdateGetError<U>: Send + Sync + std::error::Error + 'static, 1160 + VecUpdatePutError<U>: Send + Sync + std::error::Error + 'static, 1157 1161 { 1158 1162 async { 1159 1163 // Fetch current data 1160 1164 let get_request = U::build_get(); 1161 1165 let response = self.send(get_request).await?; 1162 - let output = response.parse().map_err(|e| match e { 1166 + let output = response.into_output().map_err(|e| match e { 1163 1167 XrpcError::Auth(auth) => AgentError::from(auth), 1164 - XrpcError::Xrpc(typed) => { 1165 - AgentError::sub_operation("update vec", typed.into_static()) 1166 - } 1168 + XrpcError::Xrpc(typed) => AgentError::sub_operation("update vec", typed), 1167 1169 e => AgentError::xrpc(e), 1168 1170 })?; 1169 1171 1170 - // Extract vec (converts to owned via IntoStatic) 1172 + // Extract vec 1171 1173 let mut items = U::extract_vec(output); 1172 1174 1173 1175 // Apply modification ··· 1198 1200 ) -> impl Future<Output = Result<xrpc::Response<VecPutResponse<U>>>> 1199 1201 where 1200 1202 U: VecUpdate, 1201 - <U as VecUpdate>::PutRequest: Send + Sync, 1202 - <U as VecUpdate>::GetRequest: Send + Sync, 1203 + <U as VecUpdate>::PutRequest: Send + Sync + Serialize, 1204 + <U as VecUpdate>::GetRequest: Send + Sync + Serialize, 1203 1205 VecGetResponse<U>: Send + Sync, 1204 1206 VecPutResponse<U>: Send + Sync, 1205 - for<'a> VecUpdateGetError<'a, U>: Send + Sync + std::error::Error + IntoStatic, 1206 - for<'a> VecUpdatePutError<'a, U>: Send + Sync + std::error::Error + IntoStatic, 1207 - for<'a> <VecUpdateGetError<'a, U> as IntoStatic>::Output: 1208 - Send + Sync + std::error::Error + IntoStatic + 'static, 1209 - for<'a> <VecUpdatePutError<'a, U> as IntoStatic>::Output: 1210 - Send + Sync + std::error::Error + IntoStatic + 'static, 1207 + <VecGetResponse<U> as XrpcResp>::Output<SmolStr>: DeserializeOwned, 1208 + <VecPutResponse<U> as XrpcResp>::Output<SmolStr>: DeserializeOwned, 1209 + VecUpdateGetError<U>: Send + Sync + std::error::Error + 'static, 1210 + VecUpdatePutError<U>: Send + Sync + std::error::Error + 'static, 1211 1211 { 1212 1212 async { 1213 1213 self.update_vec::<U>(|vec| { ··· 1234 1234 fn session_kind(&self) -> AgentKind { 1235 1235 AgentKind::AppPassword 1236 1236 } 1237 - fn session_info( 1238 - &self, 1239 - ) -> impl Future<Output = Option<(Did<'static>, Option<CowStr<'static>>)>> { 1237 + fn session_info(&self) -> impl Future<Output = Option<(Did, Option<SmolStr>)>> { 1240 1238 async move { 1241 1239 CredentialSession::<S, T, W>::session_info(self) 1242 1240 .await 1241 + // Convert the SmolStr session id to CowStr<'static>. 1243 1242 .map(|key| (key.0, Some(key.1))) 1244 1243 } 1245 1244 } ··· 1249 1248 fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future<Output = ()> { 1250 1249 async move { CredentialSession::<S, T, W>::set_options(self, opts).await } 1251 1250 } 1252 - fn refresh(&self) -> impl Future<Output = ClientResult<AuthorizationToken<'static>>> { 1251 + fn refresh(&self) -> impl Future<Output = ClientResult<AuthorizationToken<SmolStr>>> { 1253 1252 async move { 1254 1253 Ok(CredentialSession::<S, T, W>::refresh(self) 1255 1254 .await? ··· 1267 1266 fn session_kind(&self) -> AgentKind { 1268 1267 AgentKind::OAuth 1269 1268 } 1270 - fn session_info( 1271 - &self, 1272 - ) -> impl Future<Output = Option<(Did<'static>, Option<CowStr<'static>>)>> { 1269 + fn session_info(&self) -> impl Future<Output = Option<(Did, Option<SmolStr>)>> { 1273 1270 async { 1274 1271 let (did, sid) = OAuthSession::<T, S, W>::session_info(self).await; 1275 - Some((did.into_static(), Some(sid.into_static()))) 1272 + // did is already Did<SmolStr>; convert SmolStr sid to CowStr<'static>. 1273 + Some((did, Some(sid))) 1276 1274 } 1277 1275 } 1278 1276 fn endpoint(&self) -> impl Future<Output = Uri<String>> { ··· 1281 1279 fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future<Output = ()> { 1282 1280 async { self.set_options(opts).await } 1283 1281 } 1284 - fn refresh(&self) -> impl Future<Output = ClientResult<AuthorizationToken<'static>>> { 1282 + fn refresh(&self) -> impl Future<Output = ClientResult<AuthorizationToken<SmolStr>>> { 1285 1283 async { 1286 1284 self.refresh() 1287 1285 .await ··· 1299 1297 fn session_kind(&self) -> AgentKind { 1300 1298 AgentKind::OAuth 1301 1299 } 1302 - fn session_info( 1303 - &self, 1304 - ) -> impl Future<Output = Option<(Did<'static>, Option<CowStr<'static>>)>> { 1300 + fn session_info(&self) -> impl Future<Output = Option<(Did, Option<SmolStr>)>> { 1305 1301 async { None } 1306 1302 } 1307 1303 fn endpoint(&self) -> impl Future<Output = Uri<String>> { ··· 1310 1306 fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future<Output = ()> { 1311 1307 async { self.set_opts(opts).await } 1312 1308 } 1313 - fn refresh(&self) -> impl Future<Output = ClientResult<AuthorizationToken<'static>>> { 1309 + fn refresh(&self) -> impl Future<Output = ClientResult<AuthorizationToken<SmolStr>>> { 1314 1310 async { 1315 1311 Err(ClientError::auth( 1316 1312 jacquard_common::error::AuthError::NotAuthenticated, ··· 1431 1427 request: R, 1432 1428 ) -> impl Future<Output = XrpcResult<Response<<R as XrpcRequest>::Response>>> 1433 1429 where 1434 - R: XrpcRequest + Send + Sync, 1430 + R: XrpcRequest + Send + Sync + Serialize, 1435 1431 <R as XrpcRequest>::Response: Send + Sync, 1436 1432 { 1437 1433 async move { self.inner.send(request).await } ··· 1443 1439 opts: CallOptions<'_>, 1444 1440 ) -> XrpcResult<Response<<R as XrpcRequest>::Response>> 1445 1441 where 1446 - R: XrpcRequest + Send + Sync, 1442 + R: XrpcRequest + Send + Sync + Serialize, 1447 1443 <R as XrpcRequest>::Response: Send + Sync, 1448 1444 { 1449 1445 self.inner.send_with_opts(request, opts).await ··· 1466 1462 >, 1467 1463 > + Send 1468 1464 where 1469 - R: XrpcRequest + Send + Sync, 1465 + R: XrpcRequest + Send + Sync + Serialize, 1470 1466 <R as XrpcRequest>::Response: Send + Sync, 1471 1467 Self: Sync, 1472 1468 { ··· 1484 1480 >, 1485 1481 > 1486 1482 where 1487 - R: XrpcRequest + Send + Sync, 1483 + R: XrpcRequest + Send + Sync + Serialize, 1488 1484 <R as XrpcRequest>::Response: Send + Sync, 1489 1485 { 1490 1486 self.inner.download(request) 1491 1487 } 1492 1488 1493 1489 #[cfg(not(target_arch = "wasm32"))] 1494 - fn stream<S>( 1490 + fn stream<S, B>( 1495 1491 &self, 1496 - stream: jacquard_common::xrpc::XrpcProcedureSend<S::Frame<'static>>, 1492 + stream: jacquard_common::xrpc::XrpcProcedureSend<S::Frame<B>>, 1497 1493 ) -> impl Future< 1498 1494 Output = core::result::Result< 1499 - jacquard_common::xrpc::XrpcResponseStream<<<S as jacquard_common::xrpc::XrpcProcedureStream>::Response as jacquard_common::xrpc::XrpcStreamResp>::Frame<'static>>, 1495 + jacquard_common::xrpc::XrpcResponseStream<<<S as jacquard_common::xrpc::XrpcProcedureStream>::Response as jacquard_common::xrpc::XrpcStreamResp>::Frame<B>>, 1500 1496 jacquard_common::StreamError, 1501 1497 >, 1502 1498 > 1503 1499 where 1500 + B: BosStr + 'static, 1504 1501 S: jacquard_common::xrpc::XrpcProcedureStream + 'static, 1505 - <<S as jacquard_common::xrpc::XrpcProcedureStream>::Response as jacquard_common::xrpc::XrpcStreamResp>::Frame<'static>: jacquard_common::xrpc::XrpcStreamResp, 1502 + <<S as jacquard_common::xrpc::XrpcProcedureStream>::Response as jacquard_common::xrpc::XrpcStreamResp>::Frame<B>: jacquard_common::xrpc::XrpcStreamResp, 1506 1503 Self: Sync, 1507 1504 { 1508 - self.inner.stream::<S>(stream) 1505 + self.inner.stream::<S, B>(stream) 1509 1506 } 1510 1507 1511 1508 #[cfg(target_arch = "wasm32")] ··· 1531 1528 self.inner.options() 1532 1529 } 1533 1530 1534 - fn resolve_handle( 1531 + fn resolve_handle<S: BosStr + Sync>( 1535 1532 &self, 1536 - handle: &Handle<'_>, 1537 - ) -> impl Future<Output = core::result::Result<Did<'static>, IdentityError>> { 1533 + handle: &Handle<S>, 1534 + ) -> impl Future<Output = core::result::Result<Did, IdentityError>> { 1538 1535 async { self.inner.resolve_handle(handle).await } 1539 1536 } 1540 1537 1541 - fn resolve_did_doc( 1538 + fn resolve_did_doc<S: BosStr + Sync>( 1542 1539 &self, 1543 - did: &Did<'_>, 1540 + did: &Did<S>, 1544 1541 ) -> impl Future<Output = core::result::Result<DidDocResponse, IdentityError>> { 1545 1542 async { self.inner.resolve_did_doc(did).await } 1546 1543 } ··· 1551 1548 self.kind() 1552 1549 } 1553 1550 1554 - fn session_info( 1555 - &self, 1556 - ) -> impl Future<Output = Option<(Did<'static>, Option<CowStr<'static>>)>> { 1551 + fn session_info(&self) -> impl Future<Output = Option<(Did, Option<SmolStr>)>> { 1557 1552 async { self.info().await } 1558 1553 } 1559 1554 ··· 1565 1560 async { self.set_options(opts).await } 1566 1561 } 1567 1562 1568 - fn refresh(&self) -> impl Future<Output = ClientResult<AuthorizationToken<'static>>> { 1563 + fn refresh(&self) -> impl Future<Output = ClientResult<AuthorizationToken<SmolStr>>> { 1569 1564 async { self.refresh().await } 1570 1565 } 1571 1566 }
+103 -61
crates/jacquard/src/client/credential_session.rs
··· 5 5 }; 6 6 use jacquard_common::{ 7 7 AuthorizationToken, CowStr, IntoStatic, 8 + bos::BosStr, 8 9 deps::fluent_uri::Uri, 9 10 error::{AuthError, ClientError, XrpcResult}, 10 11 http_client::HttpClient, 11 12 session::SessionStore, 12 13 types::{did::Did, string::Handle}, 13 - xrpc::{ 14 - CallOptions, Response, XrpcClient, XrpcError, XrpcExt, XrpcRequest, XrpcResp, XrpcResponse, 15 - }, 14 + xrpc::{CallOptions, Response, XrpcClient, XrpcExt, XrpcRequest, XrpcResp, XrpcResponse}, 16 15 }; 16 + #[cfg(feature = "streaming")] 17 + use serde::Serialize; 18 + use smol_str::SmolStr; 17 19 use tokio::sync::RwLock; 18 20 19 21 use crate::client::AtpSession; ··· 29 31 30 32 /// Storage key for app‑password sessions: `(account DID, session id)`. 31 33 #[derive(Debug, Clone, PartialEq, Eq, Hash)] 32 - pub struct SessionKey(pub Did<'static>, pub CowStr<'static>); 34 + pub struct SessionKey(pub Did, pub SmolStr); 33 35 34 36 /// Stateful client for app‑password based sessions. 35 37 /// ··· 128 130 } 129 131 130 132 /// Current access token (Bearer), if logged in. 131 - pub async fn access_token(&self) -> Option<AuthorizationToken<'_>> { 133 + pub async fn access_token(&self) -> Option<AuthorizationToken> { 132 134 let key = self.key.read().await.clone()?; 133 135 let session = self.store.get(&key).await; 134 136 session.map(|session| AuthorizationToken::Bearer(session.access_jwt)) 135 137 } 136 138 137 139 /// Current refresh token (Bearer), if logged in. 138 - pub async fn refresh_token(&self) -> Option<AuthorizationToken<'_>> { 140 + pub async fn refresh_token(&self) -> Option<AuthorizationToken> { 139 141 let key = self.key.read().await.clone()?; 140 142 let session = self.store.get(&key).await; 141 143 session.map(|session| AuthorizationToken::Bearer(session.refresh_jwt)) ··· 148 150 T: HttpClient, 149 151 { 150 152 /// Refresh the active session by calling `com.atproto.server.refreshSession`. 151 - pub async fn refresh(&self) -> std::result::Result<AuthorizationToken<'_>, ClientError> { 153 + pub async fn refresh(&self) -> std::result::Result<AuthorizationToken, ClientError> { 152 154 let key = self 153 155 .key 154 156 .read() ··· 161 163 opts.auth = session.map(|s| AuthorizationToken::Bearer(s.refresh_jwt)); 162 164 let response = self 163 165 .client 164 - .xrpc(endpoint) 166 + .xrpc(endpoint.borrow()) 165 167 .with_options(opts) 166 168 .send(&RefreshSession) 167 169 .await?; ··· 227 229 let resp = self.client.resolve_did_doc(&did).await.map_err(|e| { 228 230 ClientError::from(e).with_context("DID document resolution failed during login") 229 231 })?; 230 - resp.into_owned()?.pds_endpoint().ok_or_else(|| { 231 - ClientError::invalid_request("missing PDS endpoint") 232 - .with_help("DID document must include a PDS service endpoint") 233 - })? 232 + resp.into_owned()? 233 + .pds_endpoint() 234 + .map(|u| u.to_owned()) 235 + .ok_or_else(|| { 236 + ClientError::invalid_request("missing PDS endpoint") 237 + .with_help("DID document must include a PDS service endpoint") 238 + })? 234 239 } else if identifier.as_ref().contains("@") && !identifier.as_ref().starts_with("@") { 235 240 // we're going to assume its an email 236 241 pds.ok_or_else(|| { ··· 250 255 let resp = self.client.resolve_did_doc(&did).await.map_err(|e| { 251 256 ClientError::from(e).with_context("DID document resolution failed during login") 252 257 })?; 253 - resp.into_owned()?.pds_endpoint().ok_or_else(|| { 254 - ClientError::invalid_request("missing PDS endpoint") 255 - .with_help("DID document must include a PDS service endpoint") 256 - })? 258 + resp.into_owned()? 259 + .pds_endpoint() 260 + .map(|u| u.to_owned()) 261 + .ok_or_else(|| { 262 + ClientError::invalid_request("missing PDS endpoint") 263 + .with_help("DID document must include a PDS service endpoint") 264 + })? 257 265 }; 258 266 259 267 // Build and send createSession ··· 267 275 268 276 let resp = self 269 277 .client 270 - .xrpc(pds.clone()) 278 + .xrpc(pds.borrow()) 271 279 .with_options(self.options.read().await.clone()) 272 280 .send(&req) 273 281 .await?; ··· 279 287 let session = AtpSession::from(out); 280 288 281 289 let sid = session_id.unwrap_or_else(|| CowStr::new_static("session")); 282 - let key = SessionKey(session.did.clone(), sid.into_static()); 290 + let key = SessionKey(session.did.clone().convert::<SmolStr>(), SmolStr::from(sid)); 283 291 self.store 284 292 .set(key.clone(), session.clone()) 285 293 .await ··· 301 309 /// Restore a previously persisted app-password session and set base endpoint. 302 310 pub async fn restore( 303 311 &self, 304 - did: Did<'_>, 312 + did: Did, 305 313 session_id: CowStr<'_>, 306 314 ) -> std::result::Result<(), ClientError> 307 315 where ··· 312 320 tracing::info_span!("credential_session_restore", did = %did, session_id = %session_id) 313 321 .entered(); 314 322 315 - let key = SessionKey(did.clone().into_static(), session_id.clone().into_static()); 323 + let key = SessionKey(did.clone(), SmolStr::from(session_id.clone())); 316 324 let Some(sess) = self.store.get(&key).await else { 317 325 return Err(ClientError::auth(AuthError::NotAuthenticated)); 318 326 }; ··· 326 334 } 327 335 .unwrap_or({ 328 336 let resp = self.client.resolve_did_doc(&did).await?; 329 - resp.into_owned()?.pds_endpoint().ok_or_else(|| { 330 - ClientError::invalid_request("missing PDS endpoint") 331 - .with_help("DID document must include a PDS service endpoint") 332 - })? 337 + resp.into_owned()? 338 + .pds_endpoint() 339 + .map(|u| u.to_owned()) 340 + .ok_or_else(|| { 341 + ClientError::invalid_request("missing PDS endpoint") 342 + .with_help("DID document must include a PDS service endpoint") 343 + })? 333 344 }); 334 345 335 346 // Activate ··· 338 349 *self.endpoint.write().await = Some(pds_uri.clone()); 339 350 // ensure store has the session (no-op if it existed) 340 351 self.store 341 - .set(SessionKey(sess.did.clone(), session_id.into_static()), sess) 352 + .set( 353 + SessionKey( 354 + sess.did.clone().convert::<SmolStr>(), 355 + SmolStr::from(session_id), 356 + ), 357 + sess, 358 + ) 342 359 .await?; 343 360 if let Some(file_store) = 344 361 (&*self.store as &dyn Any).downcast_ref::<crate::client::token::FileAuthStore>() ··· 351 368 /// Switch to a different stored session (and refresh endpoint/PDS). 352 369 pub async fn switch_session( 353 370 &self, 354 - did: Did<'_>, 371 + did: Did, 355 372 session_id: CowStr<'_>, 356 373 ) -> std::result::Result<(), ClientError> 357 374 where 358 375 S: Any + 'static, 359 376 { 360 - let key = SessionKey(did.clone().into_static(), session_id.into_static()); 377 + let key = SessionKey(did.clone(), SmolStr::from(session_id)); 361 378 if self.store.get(&key).await.is_none() { 362 379 return Err(ClientError::auth(AuthError::NotAuthenticated)); 363 380 } ··· 371 388 } 372 389 .unwrap_or({ 373 390 let resp = self.client.resolve_did_doc(&did).await?; 374 - resp.into_owned()?.pds_endpoint().ok_or_else(|| { 375 - ClientError::invalid_request("missing PDS endpoint") 376 - .with_help("DID document must include a PDS service endpoint") 377 - })? 391 + resp.into_owned()? 392 + .pds_endpoint() 393 + .map(|u| u.to_owned()) 394 + .ok_or_else(|| { 395 + ClientError::invalid_request("missing PDS endpoint") 396 + .with_help("DID document must include a PDS service endpoint") 397 + })? 378 398 }); 379 399 *self.key.write().await = Some(key.clone()); 380 400 let pds_uri = jacquard_common::xrpc::normalize_base_uri(pds); ··· 445 465 446 466 async fn send<R>(&self, request: R) -> XrpcResult<XrpcResponse<R>> 447 467 where 448 - R: XrpcRequest + Send + Sync, 468 + R: XrpcRequest + Send + Sync + serde::Serialize, 449 469 <R as XrpcRequest>::Response: Send + Sync, 450 470 { 451 471 let opts = self.options.read().await.clone(); ··· 458 478 mut opts: CallOptions<'_>, 459 479 ) -> XrpcResult<XrpcResponse<R>> 460 480 where 461 - R: XrpcRequest + Send + Sync, 481 + R: XrpcRequest + Send + Sync + serde::Serialize, 462 482 <R as XrpcRequest>::Response: Send + Sync, 463 483 { 464 484 let base_uri = self.base_uri().await; ··· 466 486 opts.auth = auth; 467 487 let resp = self 468 488 .client 469 - .xrpc(base_uri.clone()) 489 + .xrpc(base_uri.borrow()) 470 490 .with_options(opts.clone()) 471 491 .send(&request) 472 492 .await; ··· 475 495 let auth = self.refresh().await?; 476 496 opts.auth = Some(auth); 477 497 self.client 478 - .xrpc(base_uri) 498 + .xrpc(base_uri.borrow()) 479 499 .with_options(opts) 480 500 .send(&request) 481 501 .await ··· 496 516 { 497 517 true 498 518 } 499 - Ok(resp) => match resp.parse() { 500 - Err(XrpcError::Auth(AuthError::TokenExpired)) => true, 501 - _ => false, 502 - }, 519 + Ok(_) => false, 503 520 _ => false, 504 521 } 505 522 } ··· 561 578 request: R, 562 579 ) -> core::result::Result<jacquard_common::xrpc::StreamingResponse, jacquard_common::StreamError> 563 580 where 564 - R: XrpcRequest + Send + Sync, 581 + R: XrpcRequest + Send + Sync + Serialize, 565 582 <R as XrpcRequest>::Response: Send + Sync, 566 583 { 567 584 use jacquard_common::{StreamError, xrpc::build_http_request}; ··· 570 587 let mut opts = self.options.read().await.clone(); 571 588 opts.auth = self.access_token().await; 572 589 573 - let http_request = build_http_request(&base_uri, &request, &opts) 590 + let http_request = build_http_request(&base_uri.borrow(), &request, &opts) 574 591 .map_err(|e| StreamError::protocol(e.to_string()))?; 575 592 576 593 let response = self ··· 588 605 let auth = self.refresh().await.map_err(StreamError::transport)?; 589 606 opts.auth = Some(auth); 590 607 591 - let http_request = build_http_request(&base_uri, &request, &opts) 608 + let http_request = build_http_request(&base_uri.borrow(), &request, &opts) 592 609 .map_err(|e| StreamError::protocol(e.to_string()))?; 593 610 594 611 let response = self ··· 603 620 } 604 621 } 605 622 606 - async fn stream<Str>( 623 + async fn stream<Str, B>( 607 624 &self, 608 - stream: jacquard_common::xrpc::streaming::XrpcProcedureSend<Str::Frame<'static>>, 625 + stream: jacquard_common::xrpc::streaming::XrpcProcedureSend<Str::Frame<B>>, 609 626 ) -> core::result::Result< 610 627 jacquard_common::xrpc::streaming::XrpcResponseStream< 611 - <<Str as jacquard_common::xrpc::streaming::XrpcProcedureStream>::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame<'static>, 628 + <<Str as jacquard_common::xrpc::streaming::XrpcProcedureStream>::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame<B>, 612 629 >, 613 630 jacquard_common::StreamError, 614 631 > 615 632 where 633 + B: BosStr + 'static, 616 634 Str: jacquard_common::xrpc::streaming::XrpcProcedureStream + 'static, 617 - <<Str as jacquard_common::xrpc::streaming::XrpcProcedureStream>::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame<'static>: jacquard_common::xrpc::streaming::XrpcStreamResp, 635 + <<Str as jacquard_common::xrpc::streaming::XrpcProcedureStream>::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame<B>: jacquard_common::xrpc::streaming::XrpcStreamResp, 618 636 { 619 637 use jacquard_common::StreamError; 620 638 use n0_future::TryStreamExt; ··· 633 651 use jacquard_common::AuthorizationToken; 634 652 let hv = match token { 635 653 AuthorizationToken::Bearer(t) => { 636 - http::HeaderValue::from_str(&format!("Bearer {}", t.as_ref())) 654 + http::HeaderValue::from_str(&format!("Bearer {}", t.as_str())) 637 655 } 638 656 AuthorizationToken::Dpop(t) => { 639 - http::HeaderValue::from_str(&format!("DPoP {}", t.as_ref())) 657 + http::HeaderValue::from_str(&format!("DPoP {}", t.as_str())) 640 658 } 641 659 } 642 660 .map_err(|e| StreamError::protocol(format!("Invalid authorization token: {}", e)))?; ··· 692 710 use jacquard_common::AuthorizationToken; 693 711 let hv = match token { 694 712 AuthorizationToken::Bearer(t) => { 695 - http::HeaderValue::from_str(&format!("Bearer {}", t.as_ref())) 713 + http::HeaderValue::from_str(&format!("Bearer {}", t.as_str())) 696 714 } 697 715 AuthorizationToken::Dpop(t) => { 698 - http::HeaderValue::from_str(&format!("DPoP {}", t.as_ref())) 716 + http::HeaderValue::from_str(&format!("DPoP {}", t.as_str())) 699 717 } 700 718 } 701 719 .map_err(|e| { ··· 733 751 .map_err(StreamError::transport)?; 734 752 let (resp_parts, resp_body) = response.into_parts(); 735 753 Ok( 736 - jacquard_common::xrpc::streaming::XrpcResponseStream::from_typed_parts( 754 + jacquard_common::xrpc::streaming::XrpcResponseStream::from_typed_parts::<B>( 737 755 resp_parts, resp_body, 738 756 ), 739 757 ) 740 758 } else { 741 759 Ok( 742 - jacquard_common::xrpc::streaming::XrpcResponseStream::from_typed_parts( 760 + jacquard_common::xrpc::streaming::XrpcResponseStream::from_typed_parts::<B>( 743 761 resp_parts, resp_body, 744 762 ), 745 763 ) ··· 757 775 self.client.options() 758 776 } 759 777 760 - fn resolve_handle( 778 + #[cfg(not(target_arch = "wasm32"))] 779 + fn resolve_handle<Str: BosStr + Sync>( 761 780 &self, 762 - handle: &Handle<'_>, 763 - ) -> impl Future<Output = Result<Did<'static>, IdentityError>> { 781 + handle: &Handle<Str>, 782 + ) -> impl Future<Output = Result<Did, IdentityError>> 783 + where 784 + Self: Sync, 785 + { 786 + async { self.client.resolve_handle(handle).await } 787 + } 788 + 789 + #[cfg(target_arch = "wasm32")] 790 + fn resolve_handle<Str: BosStr + Sync>( 791 + &self, 792 + handle: &Handle<Str>, 793 + ) -> impl Future<Output = Result<Did, IdentityError>> { 764 794 async { self.client.resolve_handle(handle).await } 765 795 } 766 796 767 - fn resolve_did_doc( 797 + #[cfg(not(target_arch = "wasm32"))] 798 + fn resolve_did_doc<Str: BosStr + Sync>( 799 + &self, 800 + did: &Did<Str>, 801 + ) -> impl Future<Output = Result<DidDocResponse, IdentityError>> 802 + where 803 + Self: Sync, 804 + { 805 + async { self.client.resolve_did_doc(did).await } 806 + } 807 + 808 + #[cfg(target_arch = "wasm32")] 809 + fn resolve_did_doc<Str: BosStr + Sync>( 768 810 &self, 769 - did: &Did<'_>, 811 + did: &Did<Str>, 770 812 ) -> impl Future<Output = Result<DidDocResponse, IdentityError>> { 771 813 async { self.client.resolve_did_doc(did).await } 772 814 } ··· 813 855 let mut opts = jacquard_common::xrpc::SubscriptionOptions::default(); 814 856 if let Some(token) = self.access_token().await { 815 857 let auth_value = match token { 816 - AuthorizationToken::Bearer(t) => format!("Bearer {}", t.as_ref()), 817 - AuthorizationToken::Dpop(t) => format!("DPoP {}", t.as_ref()), 858 + AuthorizationToken::Bearer(t) => format!("Bearer {}", t.as_str()), 859 + AuthorizationToken::Dpop(t) => format!("DPoP {}", t.as_str()), 818 860 }; 819 861 opts.headers 820 862 .push((CowStr::from("Authorization"), CowStr::from(auth_value))); ··· 827 869 params: &Sub, 828 870 ) -> Result<jacquard_common::xrpc::SubscriptionStream<Sub::Stream>, Self::Error> 829 871 where 830 - Sub: XrpcSubscription + Send + Sync, 872 + Sub: XrpcSubscription + Send + Sync + serde::Serialize, 831 873 { 832 874 let opts = self.subscription_opts().await; 833 875 self.subscribe_with_opts(params, opts).await ··· 839 881 opts: jacquard_common::xrpc::SubscriptionOptions<'_>, 840 882 ) -> Result<jacquard_common::xrpc::SubscriptionStream<Sub::Stream>, Self::Error> 841 883 where 842 - Sub: XrpcSubscription + Send + Sync, 884 + Sub: XrpcSubscription + Send + Sync + serde::Serialize, 843 885 { 844 886 use jacquard_common::xrpc::SubscriptionExt; 845 887 let base = self.base_uri().await;
+18 -28
crates/jacquard/src/client/error.rs
··· 22 22 url: Option<SmolStr>, 23 23 details: Option<SmolStr>, 24 24 location: Option<SmolStr>, 25 - xrpc: Option<Data<'static>>, 25 + xrpc: Option<Data>, 26 26 } 27 27 28 28 impl std::fmt::Display for AgentError { ··· 78 78 #[diagnostic(code(jacquard::agent::record_operation))] 79 79 RecordOperation { 80 80 /// The repository DID 81 - repo: Did<'static>, 81 + repo: Did, 82 82 /// The collection NSID 83 - collection: Nsid<'static>, 83 + collection: Nsid, 84 84 /// The record key 85 - rkey: RecordKey<Rkey<'static>>, 85 + rkey: RecordKey<Rkey>, 86 86 }, 87 87 88 88 /// Multi-step operation failed at sub-step (e.g., get failed in update_record) ··· 206 206 /// Add XRPC error data to this error for observability 207 207 pub fn with_xrpc<E>(mut self, xrpc: XrpcError<E>) -> Self 208 208 where 209 - E: std::error::Error + jacquard_common::IntoStatic + serde::Serialize, 209 + E: std::error::Error + serde::Serialize, 210 210 { 211 211 use jacquard_common::types::value::to_data; 212 - // Attempt to serialize XrpcError to Data for observability 213 - if let Ok(data) = to_data(&xrpc) { 214 - self.xrpc = Some(data.into_static()); 212 + // Attempt to serialize XrpcError to Data for observability. 213 + if let Ok(data) = to_data::<_>(&xrpc) { 214 + self.xrpc = Some(data); 215 215 } 216 216 self 217 217 } 218 218 219 - /// Create an XRPC error with attached error data for observability 219 + /// Create an XRPC error with attached error data for observability. 220 220 pub fn xrpc<E>(error: XrpcError<E>) -> Self 221 221 where 222 - E: std::error::Error + jacquard_common::IntoStatic + serde::Serialize + Send + Sync, 223 - <E as IntoStatic>::Output: IntoStatic + std::error::Error + Send + Sync, 222 + E: std::error::Error + serde::Serialize + Send + Sync + 'static, 224 223 { 225 224 use jacquard_common::types::value::to_data; 226 - // Attempt to serialize XrpcError to Data for observability 227 - if let Ok(data) = to_data(&error) { 228 - let mut error = Self::new( 229 - AgentErrorKind::XrpcError, 230 - Some(Box::new(error.into_static())), 231 - ); 232 - error.xrpc = Some(data.into_static()); 233 - error 234 - } else { 235 - Self::new( 236 - AgentErrorKind::XrpcError, 237 - Some(Box::new(error.into_static())), 238 - ) 239 - } 225 + // Attempt to serialize XrpcError to Data for observability. 226 + let xrpc = to_data::<_>(&error).ok(); 227 + let mut err = Self::new(AgentErrorKind::XrpcError, Some(Box::new(error))); 228 + err.xrpc = xrpc; 229 + err 240 230 } 241 231 242 232 // Constructors ··· 259 249 260 250 /// Create a record operation error 261 251 pub fn record_operation( 262 - repo: Did<'static>, 263 - collection: Nsid<'static>, 264 - rkey: RecordKey<Rkey<'static>>, 252 + repo: Did, 253 + collection: Nsid, 254 + rkey: RecordKey<Rkey>, 265 255 source: impl std::error::Error + Send + Sync + 'static, 266 256 ) -> Self { 267 257 Self::new(
+86 -72
crates/jacquard/src/client/token.rs
··· 1 - use jacquard_common::IntoStatic; 2 - use jacquard_common::cowstr::ToCowStr; 3 1 use jacquard_common::deps::fluent_uri::Uri; 4 2 use jacquard_common::session::{FileTokenStore, SessionStore, SessionStoreError}; 5 3 use jacquard_common::types::string::{Datetime, Did}; ··· 9 7 use jose_jwk::Key; 10 8 use serde::{Deserialize, Serialize}; 11 9 use serde_json::Value; 10 + use smol_str::SmolStr; 12 11 13 12 /// On-disk session records for app-password and OAuth flows, sharing a single JSON map. 14 13 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] ··· 90 89 pub expires_at: Option<Datetime>, 91 90 } 92 91 93 - impl From<ClientSessionData<'_>> for OAuthSession { 94 - fn from(data: ClientSessionData<'_>) -> Self { 92 + impl<S: jacquard_common::bos::BosStr + Ord> From<ClientSessionData<S>> for OAuthSession { 93 + fn from(data: ClientSessionData<S>) -> Self { 95 94 OAuthSession { 96 - account_did: data.account_did.to_string(), 97 - session_id: data.session_id.to_string(), 95 + account_did: AsRef::<str>::as_ref(&data.account_did).to_owned(), 96 + session_id: AsRef::<str>::as_ref(&data.session_id).to_owned(), 98 97 host_url: data.host_url.clone(), 99 - authserver_url: data.authserver_url.to_string(), 100 - authserver_token_endpoint: data.authserver_token_endpoint.to_string(), 98 + authserver_url: AsRef::<str>::as_ref(&data.authserver_url).to_owned(), 99 + authserver_token_endpoint: AsRef::<str>::as_ref(&data.authserver_token_endpoint) 100 + .to_owned(), 101 101 authserver_revocation_endpoint: data 102 102 .authserver_revocation_endpoint 103 - .map(|s| s.to_string()), 104 - scopes: data.scopes.into_iter().map(|s| s.to_string()).collect(), 103 + .map(|s| AsRef::<str>::as_ref(&s).to_owned()), 104 + scopes: data 105 + .scopes 106 + .into_iter() 107 + .map(|s| String::from(s.to_string_normalized())) 108 + .collect(), 105 109 dpop_key: data.dpop_data.dpop_key, 106 - dpop_authserver_nonce: data.dpop_data.dpop_authserver_nonce.to_string(), 107 - dpop_host_nonce: data.dpop_data.dpop_host_nonce.to_string(), 108 - iss: data.token_set.iss.to_string(), 109 - sub: data.token_set.sub.to_string(), 110 - aud: data.token_set.aud.to_string(), 111 - scope: data.token_set.scope.map(|s| s.to_string()), 112 - refresh_token: data.token_set.refresh_token.map(|s| s.to_string()), 113 - access_token: data.token_set.access_token.to_string(), 110 + dpop_authserver_nonce: AsRef::<str>::as_ref(&data.dpop_data.dpop_authserver_nonce) 111 + .to_owned(), 112 + dpop_host_nonce: AsRef::<str>::as_ref(&data.dpop_data.dpop_host_nonce).to_owned(), 113 + iss: AsRef::<str>::as_ref(&data.token_set.iss).to_owned(), 114 + sub: AsRef::<str>::as_ref(&data.token_set.sub).to_owned(), 115 + aud: AsRef::<str>::as_ref(&data.token_set.aud).to_owned(), 116 + scope: data 117 + .token_set 118 + .scope 119 + .map(|s| AsRef::<str>::as_ref(&s).to_owned()), 120 + refresh_token: data 121 + .token_set 122 + .refresh_token 123 + .map(|s| AsRef::<str>::as_ref(&s).to_owned()), 124 + access_token: AsRef::<str>::as_ref(&data.token_set.access_token).to_owned(), 114 125 token_type: data.token_set.token_type, 115 126 expires_at: data.token_set.expires_at, 116 127 } 117 128 } 118 129 } 119 130 120 - impl From<OAuthSession> for ClientSessionData<'_> { 131 + impl From<OAuthSession> for ClientSessionData { 121 132 fn from(session: OAuthSession) -> Self { 122 133 ClientSessionData { 123 - account_did: session.account_did.into(), 124 - session_id: session.session_id.to_cowstr(), 134 + account_did: Did::new_owned(session.account_did).expect("stored DID should be valid"), 135 + session_id: SmolStr::from(session.session_id), 125 136 host_url: session.host_url, 126 - authserver_url: session.authserver_url.to_cowstr(), 127 - authserver_token_endpoint: session.authserver_token_endpoint.to_cowstr(), 137 + authserver_url: SmolStr::from(session.authserver_url), 138 + authserver_token_endpoint: SmolStr::from(session.authserver_token_endpoint), 128 139 authserver_revocation_endpoint: session 129 140 .authserver_revocation_endpoint 130 - .map(|s| s.to_cowstr().into_static()), 141 + .map(SmolStr::from), 131 142 scopes: session 132 143 .scopes 133 144 .into_iter() 134 - .map(|s| Scope::parse(&s).unwrap().into_static()) 145 + .map(|s| Scope::parse(&s).unwrap()) 135 146 .collect(), 136 147 dpop_data: DpopClientData { 137 148 dpop_key: session.dpop_key, 138 - dpop_authserver_nonce: session.dpop_authserver_nonce.to_cowstr(), 139 - dpop_host_nonce: session.dpop_host_nonce.to_cowstr(), 149 + dpop_authserver_nonce: SmolStr::from(session.dpop_authserver_nonce), 150 + dpop_host_nonce: SmolStr::from(session.dpop_host_nonce), 140 151 }, 141 152 token_set: jacquard_oauth::types::TokenSet { 142 - iss: session.iss.into(), 143 - sub: session.sub.into(), 144 - aud: session.aud.into(), 145 - scope: session.scope.map(|s| s.into()), 146 - refresh_token: session.refresh_token.map(|s| s.into()), 147 - access_token: session.access_token.into(), 153 + iss: SmolStr::from(session.iss), 154 + sub: Did::new_owned(session.sub).expect("stored DID should be valid"), 155 + aud: SmolStr::from(session.aud), 156 + scope: session.scope.map(SmolStr::from), 157 + refresh_token: session.refresh_token.map(SmolStr::from), 158 + access_token: SmolStr::from(session.access_token), 148 159 token_type: session.token_type, 149 160 expires_at: session.expires_at, 150 161 }, 151 162 } 152 - .into_static() 153 163 } 154 164 } 155 165 ··· 189 199 pub dpop_authserver_nonce: Option<String>, 190 200 } 191 201 192 - impl TryFrom<AuthRequestData<'_>> for OAuthState { 202 + impl<S: jacquard_common::bos::BosStr + Ord> TryFrom<AuthRequestData<S>> for OAuthState { 193 203 type Error = jacquard_common::deps::fluent_uri::ParseError; 194 204 195 - fn try_from(value: AuthRequestData) -> Result<Self, Self::Error> { 205 + fn try_from(value: AuthRequestData<S>) -> Result<Self, Self::Error> { 196 206 Ok(OAuthState { 197 - authserver_url: Uri::parse(value.authserver_url.as_str())?.to_owned(), 198 - account_did: value.account_did.map(|s| s.to_string()), 199 - scopes: value.scopes.into_iter().map(|s| s.to_string()).collect(), 200 - request_uri: value.request_uri.to_string(), 201 - authserver_token_endpoint: value.authserver_token_endpoint.to_string(), 207 + authserver_url: Uri::parse(value.authserver_url.as_ref())?.to_owned(), 208 + account_did: value 209 + .account_did 210 + .map(|s| AsRef::<str>::as_ref(&s).to_owned()), 211 + scopes: value 212 + .scopes 213 + .into_iter() 214 + .map(|s| String::from(s.to_string_normalized())) 215 + .collect(), 216 + request_uri: AsRef::<str>::as_ref(&value.request_uri).to_owned(), 217 + authserver_token_endpoint: AsRef::<str>::as_ref(&value.authserver_token_endpoint) 218 + .to_owned(), 202 219 authserver_revocation_endpoint: value 203 220 .authserver_revocation_endpoint 204 - .map(|s| s.to_string()), 205 - pkce_verifier: value.pkce_verifier.to_string(), 221 + .map(|s| AsRef::<str>::as_ref(&s).to_owned()), 222 + pkce_verifier: AsRef::<str>::as_ref(&value.pkce_verifier).to_owned(), 206 223 dpop_key: value.dpop_data.dpop_key, 207 - dpop_authserver_nonce: value.dpop_data.dpop_authserver_nonce.map(|s| s.to_string()), 208 - state: value.state.to_string(), 224 + dpop_authserver_nonce: value 225 + .dpop_data 226 + .dpop_authserver_nonce 227 + .map(|s| AsRef::<str>::as_ref(&s).to_owned()), 228 + state: AsRef::<str>::as_ref(&value.state).to_owned(), 209 229 }) 210 230 } 211 231 } 212 232 213 - impl From<OAuthState> for AuthRequestData<'_> { 233 + impl From<OAuthState> for AuthRequestData { 214 234 fn from(value: OAuthState) -> Self { 215 235 AuthRequestData { 216 - authserver_url: value.authserver_url.as_str().into(), 217 - state: value.state.to_cowstr(), 218 - account_did: value.account_did.map(|s| Did::from(s).into_static()), 219 - authserver_revocation_endpoint: value 220 - .authserver_revocation_endpoint 221 - .map(|s| s.to_cowstr().into_static()), 236 + authserver_url: SmolStr::from(value.authserver_url.as_str()), 237 + state: SmolStr::from(value.state), 238 + account_did: value 239 + .account_did 240 + .map(|s| Did::new_owned(s).expect("stored DID should be valid")), 241 + authserver_revocation_endpoint: value.authserver_revocation_endpoint.map(SmolStr::from), 222 242 scopes: value 223 243 .scopes 224 244 .into_iter() 225 - .map(|s| Scope::parse(&s).unwrap().into_static()) 245 + .map(|s| Scope::parse(&s).unwrap()) 226 246 .collect(), 227 - request_uri: value.request_uri.to_cowstr(), 228 - authserver_token_endpoint: value.authserver_token_endpoint.to_cowstr(), 229 - pkce_verifier: value.pkce_verifier.to_cowstr(), 247 + request_uri: SmolStr::from(value.request_uri), 248 + authserver_token_endpoint: SmolStr::from(value.authserver_token_endpoint), 249 + pkce_verifier: SmolStr::from(value.pkce_verifier), 230 250 dpop_data: DpopReqData { 231 251 dpop_key: value.dpop_key, 232 - dpop_authserver_nonce: value 233 - .dpop_authserver_nonce 234 - .map(|s| s.to_cowstr().into_static()), 252 + dpop_authserver_nonce: value.dpop_authserver_nonce.map(SmolStr::from), 235 253 }, 236 254 } 237 - .into_static() 238 255 } 239 256 } 240 257 ··· 263 280 } 264 281 265 282 impl jacquard_oauth::authstore::ClientAuthStore for FileAuthStore { 266 - async fn get_session( 283 + async fn get_session<D: jacquard_common::bos::BosStr + Send + Sync>( 267 284 &self, 268 - did: &Did<'_>, 285 + did: &Did<D>, 269 286 session_id: &str, 270 - ) -> Result<Option<ClientSessionData<'_>>, SessionStoreError> { 287 + ) -> Result<Option<ClientSessionData>, SessionStoreError> { 271 288 let key = format!("{}_{}", did, session_id); 272 289 if let StoredSession::OAuth(session) = self 273 290 .0 ··· 281 298 } 282 299 } 283 300 284 - async fn upsert_session( 285 - &self, 286 - session: ClientSessionData<'_>, 287 - ) -> Result<(), SessionStoreError> { 301 + async fn upsert_session(&self, session: ClientSessionData) -> Result<(), SessionStoreError> { 288 302 let key = format!("{}_{}", session.account_did, session.session_id); 289 303 self.0 290 304 .set(key, StoredSession::OAuth(session.into())) ··· 292 306 Ok(()) 293 307 } 294 308 295 - async fn delete_session( 309 + async fn delete_session<D: jacquard_common::bos::BosStr + Send + Sync>( 296 310 &self, 297 - did: &Did<'_>, 311 + did: &Did<D>, 298 312 session_id: &str, 299 313 ) -> Result<(), SessionStoreError> { 300 314 let key = format!("{}_{}", did, session_id); ··· 314 328 async fn get_auth_req_info( 315 329 &self, 316 330 state: &str, 317 - ) -> Result<Option<AuthRequestData<'_>>, SessionStoreError> { 331 + ) -> Result<Option<AuthRequestData>, SessionStoreError> { 318 332 let key = format!("authreq_{}", state); 319 333 if let StoredSession::OAuthState(auth_req) = self 320 334 .0 ··· 330 344 331 345 async fn save_auth_req_info( 332 346 &self, 333 - auth_req_info: &AuthRequestData<'_>, 347 + auth_req_info: &AuthRequestData, 334 348 ) -> Result<(), SessionStoreError> { 335 349 let key = format!("authreq_{}", auth_req_info.state); 336 350 let state = auth_req_info.clone().try_into().map_err( ··· 501 515 let restored = jacquard_common::session::SessionStore::get(&store, &key) 502 516 .await 503 517 .unwrap(); 504 - assert_eq!(restored.access_jwt.as_ref(), "a"); 518 + assert_eq!(restored.access_jwt.as_str(), "a"); 505 519 // clean up 506 520 let _ = fs::remove_file(&path); 507 521 }
+3 -3
crates/jacquard/src/client/vec_update.rs
··· 52 52 /// Build the get request 53 53 fn build_get() -> Self::GetRequest; 54 54 55 - /// Extract the vec from the get response output 56 - fn extract_vec<'s>( 57 - output: <<Self::GetRequest as XrpcRequest>::Response as XrpcResp>::Output<'s>, 55 + /// Extract the vec from the get response output (always owned/DefaultStr-backed). 56 + fn extract_vec( 57 + output: <<Self::GetRequest as XrpcRequest>::Response as XrpcResp>::Output<jacquard_common::DefaultStr>, 58 58 ) -> Vec<Self::Item>; 59 59 60 60 /// Build the put request from the modified vec
+6 -13
crates/jacquard/src/client/vec_update/preferences.rs
··· 1 1 use jacquard_api::app_bsky::actor::PreferencesItem; 2 2 use jacquard_api::app_bsky::actor::get_preferences::{GetPreferences, GetPreferencesOutput}; 3 3 use jacquard_api::app_bsky::actor::put_preferences::PutPreferences; 4 - use jacquard_common::IntoStatic; 5 4 6 5 /// VecUpdate implementation for Bluesky actor preferences. 7 6 /// ··· 35 34 36 35 impl super::VecUpdate for PreferencesUpdate { 37 36 type GetRequest = GetPreferences; 38 - type PutRequest = PutPreferences<'static>; 39 - type Item = PreferencesItem<'static>; 37 + type PutRequest = PutPreferences; 38 + type Item = PreferencesItem; 40 39 41 40 fn build_get() -> Self::GetRequest { 42 41 GetPreferences 43 42 } 44 43 45 - fn extract_vec<'s>( 46 - output: GetPreferencesOutput<'s>, 47 - ) -> Vec<<Self::Item as IntoStatic>::Output> { 48 - output 49 - .preferences 50 - .into_iter() 51 - .map(|p| p.into_static()) 52 - .collect() 44 + fn extract_vec(output: GetPreferencesOutput) -> Vec<Self::Item> { 45 + output.preferences 53 46 } 54 47 55 - fn build_put(items: Vec<<Self::Item as IntoStatic>::Output>) -> Self::PutRequest { 48 + fn build_put(items: Vec<Self::Item>) -> Self::PutRequest { 56 49 PutPreferences::new().preferences(items).build() 57 50 } 58 51 59 - fn matches<'s>(a: &'s Self::Item, b: &'s Self::Item) -> bool { 52 + fn matches(a: &Self::Item, b: &Self::Item) -> bool { 60 53 // Match preferences by enum variant discriminant 61 54 std::mem::discriminant(a) == std::mem::discriminant(b) 62 55 }
+1 -1
crates/jacquard/src/moderation.rs
··· 25 25 //! ```ignore 26 26 //! # use jacquard::moderation::*; 27 27 //! # use jacquard_api::app_bsky::feed::PostView; 28 - //! # fn example(post: &PostView<'_>, prefs: &ModerationPrefs<'_>, defs: &LabelerDefs<'_>) { 28 + //! # fn example(post: &PostView, prefs: &ModerationPrefs, defs: &LabelerDefs) { 29 29 //! let decision = moderate(post, prefs, defs, &[]); 30 30 //! if decision.filter { 31 31 //! // hide the post
+83 -62
crates/jacquard/src/moderation/decision.rs
··· 3 3 ModerationPrefs, 4 4 }; 5 5 use jacquard_api::com_atproto::label::{Label, LabelValue}; 6 - use jacquard_common::IntoStatic; 6 + use jacquard_common::bos::BosStr; 7 7 use jacquard_common::types::string::{Datetime, Did}; 8 + use smol_str::SmolStr; 8 9 9 10 /// Apply moderation logic to a single piece of content 10 11 /// ··· 16 17 /// ```ignore 17 18 /// # use jacquard::moderation::*; 18 19 /// # use jacquard_api::app_bsky::feed::PostView; 19 - /// # fn example(post: &PostView<'_>, prefs: &ModerationPrefs<'_>, defs: &LabelerDefs<'_>) { 20 + /// # fn example(post: &PostView, prefs: &ModerationPrefs, defs: &LabelerDefs) { 20 21 /// let decision = moderate(post, prefs, defs, &[]); 21 22 /// if decision.filter { 22 23 /// println!("This post should be hidden"); 23 24 /// } 24 25 /// # } 25 26 /// ``` 26 - pub fn moderate<'a, T: Labeled<'a>>( 27 - item: &'a T, 28 - prefs: &ModerationPrefs<'_>, 29 - defs: &LabelerDefs<'_>, 30 - accepted_labelers: &[Did<'_>], 27 + pub fn moderate<S: BosStr, T: Labeled<S>>( 28 + item: &T, 29 + prefs: &ModerationPrefs, 30 + defs: &LabelerDefs, 31 + accepted_labelers: &[Did], 31 32 ) -> ModerationDecision { 32 33 let mut decision = ModerationDecision::none(); 33 34 let now = Datetime::now(); ··· 41 42 } 42 43 } 43 44 44 - // Skip labels from untrusted labelers (if acceptance list is provided) 45 - if !accepted_labelers.is_empty() && !accepted_labelers.contains(&label.src) { 45 + // Skip labels from untrusted labelers (if acceptance list is provided). 46 + // Compare by string since label.src may use a different backing type. 47 + if !accepted_labelers.is_empty() 48 + && !accepted_labelers 49 + .iter() 50 + .any(|d| d.as_ref() == label.src.as_ref()) 51 + { 46 52 continue; 47 53 } 48 54 49 55 // Handle negation labels (remove previous causes) 50 56 if label.neg.unwrap_or(false) { 51 57 decision.causes.retain(|cause| { 52 - !(cause.label.as_str() == label.val.as_ref() && cause.source == label.src) 58 + !(cause.label.as_str() == label.val.as_ref() 59 + && cause.source.as_ref() == label.src.as_ref()) 53 60 }); 54 61 continue; 55 62 } ··· 60 67 // Process self-labels 61 68 if let Some(self_labels) = item.self_labels() { 62 69 for self_label in self_labels.values { 63 - // Self-labels don't have a source DID, so we'll use a placeholder approach 64 - // In practice, self-labels are usually just used for adult content marking 70 + // Self-labels don't have a source DID, so we'll use a placeholder approach. 71 + // In practice, self-labels are usually just used for adult content marking. 65 72 66 73 // Check user preference for this label 67 74 let pref = prefs 68 75 .labels 69 76 .iter() 70 - .find(|(k, _)| k.as_ref() == self_label.val.as_ref()) 77 + .find(|(k, _)| k.as_str() == self_label.val.as_ref()) 71 78 .map(|(_, v)| v); 72 79 73 80 // For self-labels, we generally respect them as warnings/info 74 - // unless user has explicitly set a preference 81 + // unless user has explicitly set a preference. 75 82 match pref { 76 83 Some(LabelPref::Hide) => { 77 84 decision.filter = true; ··· 94 101 } 95 102 96 103 /// Apply a single label to a moderation decision 97 - fn apply_label( 98 - label: &Label<'_>, 99 - prefs: &ModerationPrefs<'_>, 100 - defs: &LabelerDefs<'_>, 104 + fn apply_label<S: BosStr>( 105 + label: &Label<S>, 106 + prefs: &ModerationPrefs, 107 + defs: &LabelerDefs, 101 108 decision: &mut ModerationDecision, 102 109 ) { 103 110 let label_val = label.val.as_ref(); 104 111 105 - // Get user preference (per-labeler override first, then global) 112 + // Get user preference (per-labeler override first, then global). 113 + // Use string comparison since label.src may use a different backing type than 114 + // the Did<SmolStr> keys in prefs.labelers. 106 115 let pref = prefs 107 116 .labelers 108 - .get(&label.src) 109 - .and_then(|labeler_prefs| { 117 + .iter() 118 + .find(|(k, _)| k.as_str() == label.src.as_ref()) 119 + .and_then(|(_, labeler_prefs)| { 110 120 labeler_prefs 111 121 .iter() 112 - .find(|(k, _)| k.as_ref() == label_val) 122 + .find(|(k, _)| k.as_str() == label_val) 113 123 .map(|(_, v)| v) 114 124 }) 115 125 .or_else(|| { 116 126 prefs 117 127 .labels 118 128 .iter() 119 - .find(|(k, _)| k.as_ref() == label_val) 129 + .find(|(k, _)| k.as_str() == label_val) 120 130 .map(|(_, v)| v) 121 131 }); 122 132 ··· 129 139 decision.filter = true; 130 140 decision.no_override = true; 131 141 decision.causes.push(LabelCause { 132 - label: LabelValue::from(label_val).into_static(), 133 - source: label.src.clone().into_static(), 142 + label: LabelValue::from_value(SmolStr::new(label_val)), 143 + source: Did::new_owned(label.src.as_ref()) 144 + .expect("label.src must be a valid DID"), 134 145 target: determine_target(label), 135 146 }); 136 147 return; ··· 142 153 Some(LabelPref::Hide) => { 143 154 decision.filter = true; 144 155 decision.causes.push(LabelCause { 145 - label: LabelValue::from(label_val).into_static(), 146 - source: label.src.clone().into_static(), 156 + label: LabelValue::from_value(SmolStr::new(label_val)), 157 + source: Did::new_owned(label.src.as_ref()) 158 + .expect("label.src must be a valid DID"), 147 159 target: determine_target(label), 148 160 }); 149 161 } ··· 161 173 } 162 174 163 175 /// Apply warning-level moderation based on label definition 164 - fn apply_warning( 165 - label: &Label<'_>, 166 - def: Option<&jacquard_api::com_atproto::label::LabelValueDefinition<'_>>, 176 + fn apply_warning<S: BosStr>( 177 + label: &Label<S>, 178 + def: Option<&jacquard_api::com_atproto::label::LabelValueDefinition>, 167 179 decision: &mut ModerationDecision, 168 180 ) { 169 181 let label_val = label.val.as_ref(); ··· 203 215 } 204 216 205 217 decision.causes.push(LabelCause { 206 - label: LabelValue::from(label_val).into_static(), 207 - source: label.src.clone().into_static(), 218 + label: LabelValue::from_value(SmolStr::new(label_val)), 219 + source: Did::new_owned(label.src.as_ref()).expect("label.src must be a valid DID"), 208 220 target: determine_target(label), 209 221 }); 210 222 } 211 223 212 224 /// Apply default moderation when user has no preference 213 - fn apply_default( 214 - label: &Label<'_>, 215 - def: Option<&jacquard_api::com_atproto::label::LabelValueDefinition<'_>>, 225 + fn apply_default<S: BosStr>( 226 + label: &Label<S>, 227 + def: Option<&jacquard_api::com_atproto::label::LabelValueDefinition>, 216 228 decision: &mut ModerationDecision, 217 229 ) { 218 230 let label_val = label.val.as_ref(); ··· 224 236 "hide" => { 225 237 decision.filter = true; 226 238 decision.causes.push(LabelCause { 227 - label: LabelValue::from(label_val).into_static(), 228 - source: label.src.clone().into_static(), 239 + label: LabelValue::from_value(SmolStr::new(label_val)), 240 + source: Did::new_owned(label.src.as_ref()) 241 + .expect("label.src must be a valid DID"), 229 242 target: determine_target(label), 230 243 }); 231 244 return; ··· 247 260 decision.filter = true; 248 261 decision.no_override = true; 249 262 decision.causes.push(LabelCause { 250 - label: LabelValue::from(label_val).into_static(), 251 - source: label.src.clone().into_static(), 263 + label: LabelValue::from_value(SmolStr::new(label_val)), 264 + source: Did::new_owned(label.src.as_ref()) 265 + .expect("label.src must be a valid DID"), 252 266 target: determine_target(label), 253 267 }); 254 268 } ··· 267 281 "porn" | "nsfl" => { 268 282 decision.filter = true; 269 283 decision.causes.push(LabelCause { 270 - label: LabelValue::from(label_val).into_static(), 271 - source: label.src.clone().into_static(), 284 + label: LabelValue::from_value(SmolStr::new(label_val)), 285 + source: Did::new_owned(label.src.as_ref()) 286 + .expect("label.src must be a valid DID"), 272 287 target: determine_target(label), 273 288 }); 274 289 } ··· 279 294 // Unknown label - default to informational 280 295 decision.inform = true; 281 296 decision.causes.push(LabelCause { 282 - label: LabelValue::from(label_val).into_static(), 283 - source: label.src.clone().into_static(), 297 + label: LabelValue::from_value(SmolStr::new(label_val)), 298 + source: Did::new_owned(label.src.as_ref()) 299 + .expect("label.src must be a valid DID"), 284 300 target: determine_target(label), 285 301 }); 286 302 } ··· 289 305 } 290 306 291 307 /// Determine whether a label targets an account or content 292 - fn determine_target(label: &Label<'_>) -> LabelTarget { 308 + fn determine_target<S: BosStr>(label: &Label<S>) -> LabelTarget { 293 309 // Try to parse as a DID - this handles both: 294 310 // - Bare DIDs: did:plc:xyz 295 311 // - at:// URIs with only DID authority: at://did:plc:xyz 296 312 // If it parses successfully, it's account-level. 297 313 // If it fails, it must be a full URI with collection/rkey, so content-level. 298 - use jacquard_common::types::string::Did; 299 - 300 - if Did::new(label.uri.as_ref()).is_ok() { 314 + if Did::<SmolStr>::new_owned(label.uri.as_ref()).is_ok() { 301 315 LabelTarget::Account 302 316 } else { 303 317 LabelTarget::Content ··· 313 327 /// ```ignore 314 328 /// # use jacquard::moderation::*; 315 329 /// # use jacquard_api::app_bsky::feed::PostView; 316 - /// # fn example(posts: &[PostView<'_>], prefs: &ModerationPrefs<'_>, defs: &LabelerDefs<'_>) { 330 + /// # fn example(posts: &[PostView], prefs: &ModerationPrefs, defs: &LabelerDefs) { 317 331 /// let results = moderate_all(posts, prefs, defs, &[]); 318 332 /// for (post, decision) in results { 319 333 /// if decision.filter { ··· 322 336 /// } 323 337 /// # } 324 338 /// ``` 325 - pub fn moderate_all<'a, T: Labeled<'a>>( 339 + pub fn moderate_all<'a, S: BosStr, T: Labeled<S>>( 326 340 items: &'a [T], 327 - prefs: &ModerationPrefs<'_>, 328 - defs: &LabelerDefs<'_>, 329 - accepted_labelers: &[Did<'_>], 341 + prefs: &ModerationPrefs, 342 + defs: &LabelerDefs, 343 + accepted_labelers: &[Did], 330 344 ) -> Vec<(&'a T, ModerationDecision)> { 331 345 items 332 346 .iter() ··· 338 352 /// 339 353 /// Provides convenience methods for filtering and mapping moderation decisions 340 354 /// over collections. 341 - pub trait ModerationIterExt<'a, T: Labeled<'a> + 'a>: Iterator<Item = &'a T> + Sized { 355 + pub trait ModerationIterExt<'a, S: BosStr, T: Labeled<S> + 'a>: 356 + Iterator<Item = &'a T> + Sized 357 + { 342 358 /// Map each item to a tuple of (item, decision) 343 359 fn with_moderation( 344 360 self, 345 - prefs: &'a ModerationPrefs<'_>, 346 - defs: &'a LabelerDefs<'_>, 347 - accepted_labelers: &'a [Did<'_>], 361 + prefs: &'a ModerationPrefs, 362 + defs: &'a LabelerDefs, 363 + accepted_labelers: &'a [Did], 348 364 ) -> impl Iterator<Item = (&'a T, ModerationDecision)> { 349 - self.map(move |item| (item, moderate(item, prefs, defs, accepted_labelers))) 365 + self.map(move |item| (item, moderate::<S, T>(item, prefs, defs, accepted_labelers))) 350 366 } 351 367 352 368 /// Filter out items that should be hidden 353 369 fn filter_moderated( 354 370 self, 355 - prefs: &'a ModerationPrefs<'_>, 356 - defs: &'a LabelerDefs<'_>, 357 - accepted_labelers: &'a [Did<'_>], 371 + prefs: &'a ModerationPrefs, 372 + defs: &'a LabelerDefs, 373 + accepted_labelers: &'a [Did], 358 374 ) -> impl Iterator<Item = &'a T> { 359 - self.filter(move |item| !moderate(*item, prefs, defs, accepted_labelers).filter) 375 + self.filter(move |item| { 376 + !moderate::<S, T>(*item, prefs, defs, accepted_labelers).filter 377 + }) 360 378 } 361 379 } 362 380 363 - impl<'a, T: Labeled<'a> + 'a, I: Iterator<Item = &'a T>> ModerationIterExt<'a, T> for I {} 381 + impl<'a, S: BosStr, T: Labeled<S> + 'a, I: Iterator<Item = &'a T>> 382 + ModerationIterExt<'a, S, T> for I 383 + { 384 + }
+29 -29
crates/jacquard/src/moderation/fetch.rs
··· 1 + use std::convert::From; 2 + 1 3 use super::LabelerDefs; 2 4 use crate::client::{AgentError, AgentSessionExt, CollectionErr, CollectionOutput}; 3 5 use crate::moderation::labeled::LabeledRecord; ··· 8 10 service::Service, 9 11 }; 10 12 use jacquard_api::com_atproto::label::{Label, query_labels::QueryLabels}; 11 - use jacquard_common::cowstr::ToCowStr; 13 + use jacquard_common::BosStr; 14 + use jacquard_common::bos::DefaultStr; 12 15 use jacquard_common::error::ClientError; 13 16 use jacquard_common::types::collection::Collection; 14 17 use jacquard_common::types::string::Did; 15 18 use jacquard_common::types::uri::RecordUri; 16 - use jacquard_common::xrpc::{XrpcClient, XrpcError}; 17 - use jacquard_common::{CowStr, IntoStatic}; 18 - use std::convert::From; 19 + use jacquard_common::xrpc::{XrpcClient, XrpcError, XrpcResp}; 20 + use smol_str::SmolStr; 19 21 20 22 /// Fetch labeler definitions from Bluesky's AppView (or a compatible one) 21 23 #[cfg(feature = "api_bluesky")] 22 24 pub async fn fetch_labeler_defs( 23 25 client: &(impl XrpcClient + Sync), 24 - dids: Vec<Did<'_>>, 25 - ) -> Result<LabelerDefs<'static>, ClientError> { 26 + dids: Vec<Did>, 27 + ) -> Result<LabelerDefs, ClientError> { 26 28 #[cfg(feature = "tracing")] 27 29 let _span = tracing::debug_span!("fetch_labeler_defs", count = dids.len()).entered(); 28 30 29 31 let request = GetServices::new().dids(dids).detailed(true).build(); 30 32 31 33 let response = client.send(request).await?; 32 - let output: GetServicesOutput<'static> = response.into_output().map_err(|e| match e { 34 + let output: GetServicesOutput = response.into_output().map_err(|e| match e { 33 35 XrpcError::Auth(auth) => ClientError::auth(auth), 34 36 XrpcError::Generic(g) => ClientError::decode(g.to_string()), 35 37 XrpcError::Decode(e) => ClientError::decode(format!("{:?}", e)), ··· 46 48 GetServicesOutputViewsItem::LabelerViewDetailed(detailed) => { 47 49 if let Some(label_value_definitions) = &detailed.policies.label_value_definitions { 48 50 defs.insert( 49 - detailed.creator.did.clone().into_static(), 50 - label_value_definitions 51 - .iter() 52 - .map(|d| d.clone().into_static()) 53 - .collect(), 51 + detailed.creator.did.clone(), 52 + label_value_definitions.clone(), 54 53 ); 55 54 } 56 55 } ··· 77 76 #[cfg(feature = "api_bluesky")] 78 77 pub async fn fetch_labeler_defs_direct( 79 78 client: &(impl AgentSessionExt + Sync), 80 - dids: Vec<Did<'_>>, 81 - ) -> Result<LabelerDefs<'static>, AgentError> { 79 + dids: Vec<Did>, 80 + ) -> Result<LabelerDefs, AgentError> { 82 81 #[cfg(feature = "tracing")] 83 82 let _span = tracing::debug_span!("fetch_labeler_defs_direct", count = dids.len()).entered(); 84 83 ··· 91 90 })?; 92 91 93 92 let output = client.fetch_record(&record_uri).await?; 94 - let service: Service<'static> = output.value; 93 + let service: Service = output.value; 95 94 96 95 if let Some(label_value_definitions) = service.policies.label_value_definitions { 97 - defs.insert(did.into_static(), label_value_definitions); 96 + defs.insert(did, label_value_definitions); 98 97 } 99 98 } 100 99 ··· 114 113 /// on labelers to tail their output, and index them alongside the data your app cares about. 115 114 pub async fn fetch_labels( 116 115 client: &impl AgentSessionExt, 117 - uri_patterns: Vec<CowStr<'_>>, 118 - sources: Vec<Did<'_>>, 119 - cursor: Option<CowStr<'_>>, 120 - ) -> Result<(Vec<Label<'static>>, Option<CowStr<'static>>), AgentError> { 116 + uri_patterns: Vec<SmolStr>, 117 + sources: Vec<Did>, 118 + cursor: Option<SmolStr>, 119 + ) -> Result<(Vec<Label>, Option<SmolStr>), AgentError> { 121 120 #[cfg(feature = "tracing")] 122 121 let _span = tracing::debug_span!("fetch_labels", count = sources.len()).entered(); 123 122 ··· 147 146 /// 148 147 /// In practice if you are running an app server, you should call [`subscribeLabels`](https://tangled.org/@nonbinary.computer/jacquard/blob/main/crates/jacquard-api/src/com_atproto/label/subscribe_labels.rs) 149 148 /// on labelers to tail their output, and index them alongside the data your app cares about. 150 - pub async fn fetch_labeled_record<R>( 149 + pub async fn fetch_labeled_record<R, S>( 151 150 client: &impl AgentSessionExt, 152 - record_uri: &RecordUri<'_, R>, 153 - sources: Vec<Did<'_>>, 154 - ) -> Result<LabeledRecord<'static, R>, AgentError> 151 + record_uri: &RecordUri<S, R>, 152 + sources: Vec<Did>, 153 + ) -> Result<LabeledRecord<DefaultStr, R>, AgentError> 155 154 where 156 - R: Collection + From<CollectionOutput<'static, R>>, 157 - for<'a> CollectionOutput<'a, R>: IntoStatic<Output = CollectionOutput<'static, R>>, 158 - for<'a> CollectionErr<'a, R>: IntoStatic<Output = CollectionErr<'static, R>> + Send + Sync, 155 + R: Collection + From<<<R as Collection>::Record as XrpcResp>::Output<smol_str::SmolStr>>, 156 + S: BosStr + Sync, 157 + CollectionOutput<R>: serde::de::DeserializeOwned, 158 + CollectionErr<R>: Send + Sync + 'static, 159 159 { 160 160 let record: R = client.fetch_record(record_uri).await?.into(); 161 - let (labels, _) = 162 - fetch_labels(client, vec![record_uri.as_uri().to_cowstr()], sources, None).await?; 161 + let uri_pattern = SmolStr::new(record_uri.as_uri().as_str()); 162 + let (labels, _) = fetch_labels(client, vec![uri_pattern], sources, None).await?; 163 163 164 164 Ok(LabeledRecord { record, labels }) 165 165 }
+56 -49
crates/jacquard/src/moderation/labeled.rs
··· 1 1 use jacquard_api::com_atproto::label::{Label, SelfLabels}; 2 + use jacquard_common::bos::{BosStr, DefaultStr}; 2 3 3 4 /// Trait for content that has labels attached 4 5 /// 5 6 /// Implemented by types that can be moderated based on their labels. 6 7 /// This includes both labels from labeler services and self-labels applied by authors. 7 - pub trait Labeled<'a> { 8 + /// 9 + /// The type parameter `S` is the backing string type used by the label types. 10 + /// In most concrete cases this will be `DefaultStr` (`SmolStr`). 11 + pub trait Labeled<S: BosStr = DefaultStr> { 8 12 /// Get the labels applied to this content by labeler services 9 - fn labels(&self) -> &[Label<'a>]; 13 + fn labels(&self) -> &[Label<S>]; 10 14 11 15 /// Get self-labels applied by the content author 12 - fn self_labels(&'a self) -> Option<SelfLabels<'a>> { 16 + fn self_labels(&self) -> Option<SelfLabels<S>> { 13 17 None 14 18 } 15 19 } ··· 17 21 /// Record with applied labels 18 22 /// 19 23 /// Exists as a bare minimum RecordView type primarily for testing/demonstration. 20 - pub struct LabeledRecord<'a, C> { 24 + pub struct LabeledRecord<S: BosStr = DefaultStr, C = ()> { 21 25 /// The record we grabbed labels for 22 26 pub record: C, 23 27 /// The labels applied to the record 24 - pub labels: Vec<Label<'a>>, 28 + pub labels: Vec<Label<S>>, 25 29 } 26 30 27 - impl<'a, C> Labeled<'a> for LabeledRecord<'a, C> { 28 - fn labels(&self) -> &[Label<'a>] { 31 + impl<S: BosStr, C> Labeled<S> for LabeledRecord<S, C> { 32 + fn labels(&self) -> &[Label<S>] { 29 33 &self.labels 30 34 } 31 35 } ··· 43 47 }; 44 48 use jacquard_common::from_data; 45 49 46 - impl<'a> Labeled<'a> for PostView<'a> { 47 - fn labels(&self) -> &[Label<'a>] { 50 + impl<S: BosStr> Labeled<S> for PostView<S> 51 + where 52 + S: for<'de> serde::Deserialize<'de> + for<'de> core::convert::From<&'de str>, 53 + { 54 + fn labels(&self) -> &[Label<S>] { 48 55 self.labels.as_deref().unwrap_or(&[]) 49 56 } 50 57 51 - fn self_labels(&'a self) -> Option<SelfLabels<'a>> { 52 - let post = from_data::<Post<'a>>(&self.record).ok()?; 58 + fn self_labels(&self) -> Option<SelfLabels<S>> { 59 + let post = from_data::<Post<S>, S>(&self.record).ok()?; 53 60 post.labels 54 61 } 55 62 } 56 63 57 - impl<'a> Labeled<'a> for ProfileView<'a> { 58 - fn labels(&self) -> &[Label<'a>] { 64 + impl<S: BosStr> Labeled<S> for ProfileView<S> { 65 + fn labels(&self) -> &[Label<S>] { 59 66 self.labels.as_deref().unwrap_or(&[]) 60 67 } 61 68 } 62 69 63 - impl<'a> Labeled<'a> for ProfileViewBasic<'a> { 64 - fn labels(&self) -> &[Label<'a>] { 70 + impl<S: BosStr> Labeled<S> for ProfileViewBasic<S> { 71 + fn labels(&self) -> &[Label<S>] { 65 72 self.labels.as_deref().unwrap_or(&[]) 66 73 } 67 74 } 68 75 69 - impl<'a> Labeled<'a> for ProfileViewDetailed<'a> { 70 - fn labels(&self) -> &[Label<'a>] { 76 + impl<S: BosStr> Labeled<S> for ProfileViewDetailed<S> { 77 + fn labels(&self) -> &[Label<S>] { 71 78 self.labels.as_deref().unwrap_or(&[]) 72 79 } 73 80 } 74 81 75 - impl<'a> Labeled<'a> for Post<'a> { 76 - fn labels(&self) -> &[Label<'a>] { 82 + impl<S: BosStr + Clone> Labeled<S> for Post<S> { 83 + fn labels(&self) -> &[Label<S>] { 77 84 &[] 78 85 } 79 86 80 - fn self_labels(&self) -> Option<SelfLabels<'a>> { 87 + fn self_labels(&self) -> Option<SelfLabels<S>> { 81 88 self.labels.clone() 82 89 } 83 90 } 84 91 85 - impl<'a> Labeled<'a> for Profile<'a> { 86 - fn labels(&self) -> &[Label<'a>] { 92 + impl<S: BosStr + Clone> Labeled<S> for Profile<S> { 93 + fn labels(&self) -> &[Label<S>] { 87 94 &[] 88 95 } 89 96 90 - fn self_labels(&self) -> Option<SelfLabels<'a>> { 97 + fn self_labels(&self) -> Option<SelfLabels<S>> { 91 98 self.labels.clone() 92 99 } 93 100 } 94 101 95 - impl<'a> Labeled<'a> for Generator<'a> { 96 - fn labels(&self) -> &[Label<'a>] { 102 + impl<S: BosStr + Clone> Labeled<S> for Generator<S> { 103 + fn labels(&self) -> &[Label<S>] { 97 104 &[] 98 105 } 99 106 100 - fn self_labels(&'a self) -> Option<SelfLabels<'a>> { 107 + fn self_labels(&self) -> Option<SelfLabels<S>> { 101 108 self.labels.clone() 102 109 } 103 110 } 104 111 105 - impl<'a> Labeled<'a> for List<'a> { 106 - fn labels(&self) -> &[Label<'a>] { 112 + impl<S: BosStr + Clone> Labeled<S> for List<S> { 113 + fn labels(&self) -> &[Label<S>] { 107 114 &[] 108 115 } 109 116 110 - fn self_labels(&'a self) -> Option<SelfLabels<'a>> { 117 + fn self_labels(&self) -> Option<SelfLabels<S>> { 111 118 self.labels.clone() 112 119 } 113 120 } 114 121 115 - impl<'a> Labeled<'a> for Service<'a> { 116 - fn labels(&self) -> &[Label<'a>] { 122 + impl<S: BosStr + Clone> Labeled<S> for Service<S> { 123 + fn labels(&self) -> &[Label<S>] { 117 124 &[] 118 125 } 119 126 120 - fn self_labels(&'a self) -> Option<SelfLabels<'a>> { 127 + fn self_labels(&self) -> Option<SelfLabels<S>> { 121 128 self.labels.clone() 122 129 } 123 130 } 124 131 125 - impl<'a> Labeled<'a> for ListView<'a> { 126 - fn labels(&self) -> &[Label<'a>] { 132 + impl<S: BosStr> Labeled<S> for ListView<S> { 133 + fn labels(&self) -> &[Label<S>] { 127 134 self.labels.as_deref().unwrap_or(&[]) 128 135 } 129 136 } 130 137 131 - impl<'a> Labeled<'a> for Notification<'a> { 132 - fn labels(&self) -> &[Label<'a>] { 138 + impl<S: BosStr> Labeled<S> for Notification<S> { 139 + fn labels(&self) -> &[Label<S>] { 133 140 self.labels.as_deref().unwrap_or(&[]) 134 141 } 135 142 } ··· 146 153 147 154 use jacquard_api::net_anisota::feed::{draft::Draft, post::Post}; 148 155 149 - impl<'a> Labeled<'a> for Post<'a> { 150 - fn labels(&self) -> &[Label<'a>] { 156 + impl<S: BosStr + Clone> Labeled<S> for Post<S> { 157 + fn labels(&self) -> &[Label<S>] { 151 158 &[] 152 159 } 153 160 154 - fn self_labels(&self) -> Option<SelfLabels<'a>> { 161 + fn self_labels(&self) -> Option<SelfLabels<S>> { 155 162 self.labels.clone() 156 163 } 157 164 } 158 165 159 - impl<'a> Labeled<'a> for Draft<'a> { 160 - fn labels(&self) -> &[Label<'a>] { 166 + impl<S: BosStr + Clone> Labeled<S> for Draft<S> { 167 + fn labels(&self) -> &[Label<S>] { 161 168 &[] 162 169 } 163 170 164 - fn self_labels(&self) -> Option<SelfLabels<'a>> { 171 + fn self_labels(&self) -> Option<SelfLabels<S>> { 165 172 self.labels.clone() 166 173 } 167 174 } ··· 175 182 gallery::{Gallery, GalleryView}, 176 183 }; 177 184 178 - impl<'a> Labeled<'a> for ProfileView<'a> { 179 - fn labels(&self) -> &[Label<'a>] { 185 + impl<S: BosStr> Labeled<S> for ProfileView<S> { 186 + fn labels(&self) -> &[Label<S>] { 180 187 self.labels.as_deref().unwrap_or(&[]) 181 188 } 182 189 } 183 190 184 - impl<'a> Labeled<'a> for GalleryView<'a> { 185 - fn labels(&self) -> &[Label<'a>] { 191 + impl<S: BosStr> Labeled<S> for GalleryView<S> { 192 + fn labels(&self) -> &[Label<S>] { 186 193 self.labels.as_deref().unwrap_or(&[]) 187 194 } 188 195 } 189 196 190 - impl<'a> Labeled<'a> for Gallery<'a> { 191 - fn labels(&self) -> &[Label<'a>] { 197 + impl<S: BosStr + Clone> Labeled<S> for Gallery<S> { 198 + fn labels(&self) -> &[Label<S>] { 192 199 &[] 193 200 } 194 201 195 - fn self_labels(&self) -> Option<SelfLabels<'a>> { 202 + fn self_labels(&self) -> Option<SelfLabels<S>> { 196 203 self.labels.clone() 197 204 } 198 205 }
+26 -19
crates/jacquard/src/moderation/moderatable.rs
··· 1 1 use super::{LabelerDefs, ModerationDecision, ModerationPrefs, moderate}; 2 + use jacquard_common::bos::BosStr; 2 3 use jacquard_common::types::string::Did; 3 4 4 5 /// Trait for composite types that contain multiple labeled items ··· 13 14 /// ```ignore 14 15 /// # use jacquard::moderation::*; 15 16 /// # use jacquard_api::app_bsky::feed::FeedViewPost; 16 - /// # fn example(feed_post: &FeedViewPost<'_>, prefs: &ModerationPrefs<'_>, defs: &LabelerDefs<'_>) { 17 + /// # fn example(feed_post: &FeedViewPost, prefs: &ModerationPrefs, defs: &LabelerDefs) { 17 18 /// for (tag, decision) in feed_post.moderate_all(prefs, defs, &[]) { 18 19 /// match tag { 19 20 /// "post" if decision.filter => println!("Hide post content"), ··· 23 24 /// } 24 25 /// # } 25 26 /// ``` 26 - pub trait Moderateable<'a> { 27 + pub trait Moderateable<S: BosStr> { 27 28 /// Apply moderation to all labeled parts of this item 28 29 /// 29 30 /// Returns a vector of (tag, decision) tuples where the tag identifies 30 31 /// which part of the composite item the decision applies to. 31 32 fn moderate_all( 32 - &'a self, 33 - prefs: &ModerationPrefs<'_>, 34 - defs: &LabelerDefs<'_>, 35 - accepted_labelers: &[Did<'_>], 33 + &self, 34 + prefs: &ModerationPrefs, 35 + defs: &LabelerDefs, 36 + accepted_labelers: &[Did], 36 37 ) -> Vec<(&'static str, ModerationDecision)>; 37 38 } 38 39 ··· 40 41 /// 41 42 /// Provides convenience methods for filtering and mapping moderation decisions 42 43 /// over collections. 43 - pub trait ModeratableIterExt<'a, T: Moderateable<'a> + 'a>: Iterator<Item = &'a T> + Sized { 44 + pub trait ModeratableIterExt<'a, S: BosStr, T: Moderateable<S> + 'a>: 45 + Iterator<Item = &'a T> + Sized 46 + { 44 47 /// Map each item to a tuple of (item, decision) 45 48 fn with_moderation( 46 49 self, 47 - prefs: &'a ModerationPrefs<'_>, 48 - defs: &'a LabelerDefs<'_>, 49 - accepted_labelers: &'a [Did<'_>], 50 + prefs: &'a ModerationPrefs, 51 + defs: &'a LabelerDefs, 52 + accepted_labelers: &'a [Did], 50 53 ) -> impl Iterator<Item = (&'a T, Vec<(&'static str, ModerationDecision)>)> { 51 54 self.map(move |item| { 52 55 let scoped_decisions = item.moderate_all(prefs, defs, accepted_labelers); ··· 57 60 /// Filter out items that should be hidden 58 61 fn filter_moderated( 59 62 self, 60 - prefs: &'a ModerationPrefs<'_>, 61 - defs: &'a LabelerDefs<'_>, 62 - accepted_labelers: &'a [Did<'_>], 63 + prefs: &'a ModerationPrefs, 64 + defs: &'a LabelerDefs, 65 + accepted_labelers: &'a [Did], 63 66 ) -> impl Iterator<Item = &'a T> { 64 67 self.filter(move |item| { 65 68 let scoped_decisions = item.moderate_all(prefs, defs, accepted_labelers); ··· 68 71 } 69 72 } 70 73 71 - impl<'a, T: Moderateable<'a> + 'a, I: Iterator<Item = &'a T>> ModeratableIterExt<'a, T> for I {} 74 + impl<'a, S: BosStr, T: Moderateable<S> + 'a, I: Iterator<Item = &'a T>> 75 + ModeratableIterExt<'a, S, T> for I 76 + { 77 + } 72 78 73 79 // Implementations for common Bluesky types 74 80 #[cfg(feature = "api_bluesky")] 75 81 mod bluesky_impls { 76 82 use super::*; 77 83 use jacquard_api::app_bsky::feed::{FeedViewPost, ReplyRefParent, ReplyRefRoot}; 84 + use jacquard_common::bos::DefaultStr; 78 85 79 - impl<'a> Moderateable<'a> for FeedViewPost<'a> { 86 + impl Moderateable<DefaultStr> for FeedViewPost { 80 87 fn moderate_all( 81 - &'a self, 82 - prefs: &ModerationPrefs<'_>, 83 - defs: &LabelerDefs<'_>, 84 - accepted_labelers: &[Did<'_>], 88 + &self, 89 + prefs: &ModerationPrefs, 90 + defs: &LabelerDefs, 91 + accepted_labelers: &[Did], 85 92 ) -> Vec<(&'static str, ModerationDecision)> { 86 93 let mut decisions = vec![ 87 94 ("post", moderate(&self.post, prefs, defs, accepted_labelers)),
+58 -53
crates/jacquard/src/moderation/tests.rs
··· 8 8 Label, LabelValueDefinition, LabelValueDefinitionBlurs, LabelValueDefinitionDefaultSetting, 9 9 LabelValueDefinitionSeverity, 10 10 }; 11 - use jacquard_common::CowStr; 12 11 use jacquard_common::types::string::{Datetime, Did, UriValue}; 13 12 use serde::Deserialize; 13 + use smol_str::SmolStr; 14 14 15 15 const LABELER_SERVICES_JSON: &str = include_str!("labeler_services.json"); 16 16 const POSTS_JSON: &str = include_str!("posts.json"); ··· 25 25 26 26 #[test] 27 27 fn test_build_labeler_defs_from_services() { 28 - let services: GetServicesOutput<'static> = 28 + let services: GetServicesOutput = 29 29 serde_json::from_str(LABELER_SERVICES_JSON).expect("failed to parse"); 30 30 31 31 let mut defs = LabelerDefs::new(); ··· 52 52 53 53 // Create a label definition with defaultSetting: "hide" 54 54 let spam_def = LabelValueDefinition { 55 - identifier: CowStr::from("spam"), 55 + identifier: SmolStr::new("spam"), 56 56 blurs: LabelValueDefinitionBlurs::Content, 57 57 severity: LabelValueDefinitionSeverity::Inform, 58 58 default_setting: Some(LabelValueDefinitionDefaultSetting::Hide), ··· 65 65 66 66 // Create a mock labeled item 67 67 struct MockLabeled { 68 - labels: Vec<Label<'static>>, 68 + labels: Vec<Label>, 69 69 } 70 70 71 - impl<'a> Labeled<'a> for MockLabeled { 72 - fn labels(&self) -> &[Label<'a>] { 71 + impl Labeled<SmolStr> for MockLabeled { 72 + fn labels(&self) -> &[Label<SmolStr>] { 73 73 &self.labels 74 74 } 75 75 } ··· 79 79 src: labeler_did.clone(), 80 80 uri: UriValue::new_owned("at://did:plc:test/app.bsky.feed.post/abc123").unwrap(), 81 81 cid: None, 82 - val: CowStr::from("spam"), 82 + val: SmolStr::new("spam"), 83 83 neg: None, 84 84 cts: Datetime::now(), 85 85 exp: None, ··· 104 104 let labeler_did = Did::new_static("did:plc:test").unwrap(); 105 105 106 106 let def = LabelValueDefinition { 107 - identifier: CowStr::from("test-label"), 107 + identifier: SmolStr::new("test-label"), 108 108 blurs: LabelValueDefinitionBlurs::Content, 109 109 severity: LabelValueDefinitionSeverity::Alert, 110 110 default_setting: Some(LabelValueDefinitionDefaultSetting::Hide), ··· 116 116 defs.insert(labeler_did.clone(), vec![def]); 117 117 118 118 struct MockLabeled { 119 - labels: Vec<Label<'static>>, 119 + labels: Vec<Label>, 120 120 } 121 121 122 - impl<'a> Labeled<'a> for MockLabeled { 123 - fn labels(&self) -> &[Label<'a>] { 122 + impl Labeled<SmolStr> for MockLabeled { 123 + fn labels(&self) -> &[Label<SmolStr>] { 124 124 &self.labels 125 125 } 126 126 } ··· 129 129 labels: vec![Label { 130 130 src: labeler_did.clone(), 131 131 uri: UriValue::new_owned("at://did:plc:test/app.bsky.feed.post/abc").unwrap(), 132 - val: CowStr::from("test-label"), 132 + val: SmolStr::new("test-label"), 133 133 neg: None, 134 134 cts: Datetime::now(), 135 135 exp: None, ··· 144 144 let mut prefs = ModerationPrefs::default(); 145 145 prefs 146 146 .labels 147 - .insert(CowStr::from("test-label"), LabelPref::Ignore); 147 + .insert(SmolStr::new("test-label"), LabelPref::Ignore); 148 148 149 149 let decision = moderate(&item, &prefs, &defs, &[labeler_did]); 150 150 ··· 160 160 let labeler_did = Did::new_static("did:plc:test").unwrap(); 161 161 162 162 struct MockLabeled { 163 - labels: Vec<Label<'static>>, 163 + labels: Vec<Label>, 164 164 } 165 165 166 - impl<'a> Labeled<'a> for MockLabeled { 167 - fn labels(&self) -> &[Label<'a>] { 166 + impl Labeled<SmolStr> for MockLabeled { 167 + fn labels(&self) -> &[Label<SmolStr>] { 168 168 &self.labels 169 169 } 170 170 } ··· 174 174 labels: vec![Label { 175 175 src: labeler_did.clone(), 176 176 uri: UriValue::new_owned("did:plc:someuser").unwrap(), 177 - val: CowStr::from("test"), 177 + val: SmolStr::new("test"), 178 178 neg: None, 179 179 cts: Datetime::now(), 180 180 exp: None, ··· 198 198 labels: vec![Label { 199 199 src: labeler_did.clone(), 200 200 uri: UriValue::new_owned("at://did:plc:someuser/app.bsky.feed.post/abc123").unwrap(), 201 - val: CowStr::from("test"), 201 + val: SmolStr::new("test"), 202 202 neg: None, 203 203 cts: Datetime::now(), 204 204 exp: None, ··· 223 223 224 224 // Media blur 225 225 let media_def = LabelValueDefinition { 226 - identifier: CowStr::from("media-label"), 226 + identifier: SmolStr::new("media-label"), 227 227 blurs: LabelValueDefinitionBlurs::Media, 228 228 severity: LabelValueDefinitionSeverity::Alert, 229 229 default_setting: Some(LabelValueDefinitionDefaultSetting::Warn), ··· 234 234 235 235 // Content blur 236 236 let content_def = LabelValueDefinition { 237 - identifier: CowStr::from("content-label"), 237 + identifier: SmolStr::new("content-label"), 238 238 blurs: LabelValueDefinitionBlurs::Content, 239 239 severity: LabelValueDefinitionSeverity::Alert, 240 240 default_setting: Some(LabelValueDefinitionDefaultSetting::Warn), ··· 246 246 defs.insert(labeler_did.clone(), vec![media_def, content_def]); 247 247 248 248 struct MockLabeled { 249 - labels: Vec<Label<'static>>, 249 + labels: Vec<Label>, 250 250 } 251 251 252 - impl<'a> Labeled<'a> for MockLabeled { 253 - fn labels(&self) -> &[Label<'a>] { 252 + impl Labeled<SmolStr> for MockLabeled { 253 + fn labels(&self) -> &[Label<SmolStr>] { 254 254 &self.labels 255 255 } 256 256 } ··· 260 260 labels: vec![Label { 261 261 src: labeler_did.clone(), 262 262 uri: UriValue::new_owned("at://did:plc:test/app.bsky.feed.post/abc").unwrap(), 263 - val: CowStr::from("media-label"), 263 + val: SmolStr::new("media-label"), 264 264 neg: None, 265 265 cts: Datetime::now(), 266 266 exp: None, ··· 281 281 labels: vec![Label { 282 282 src: labeler_did.clone(), 283 283 uri: UriValue::new_owned("at://did:plc:test/app.bsky.feed.post/xyz").unwrap(), 284 - val: CowStr::from("content-label"), 284 + val: SmolStr::new("content-label"), 285 285 neg: None, 286 286 cts: Datetime::now(), 287 287 exp: None, ··· 303 303 let labeler_did = Did::new_static("did:plc:test").unwrap(); 304 304 305 305 let adult_def = LabelValueDefinition { 306 - identifier: CowStr::from("adult-label"), 306 + identifier: SmolStr::new("adult-label"), 307 307 blurs: LabelValueDefinitionBlurs::Content, 308 308 severity: LabelValueDefinitionSeverity::Alert, 309 309 default_setting: Some(LabelValueDefinitionDefaultSetting::Warn), ··· 315 315 defs.insert(labeler_did.clone(), vec![adult_def]); 316 316 317 317 struct MockLabeled { 318 - labels: Vec<Label<'static>>, 318 + labels: Vec<Label>, 319 319 } 320 320 321 - impl<'a> Labeled<'a> for MockLabeled { 322 - fn labels(&self) -> &[Label<'a>] { 321 + impl Labeled<SmolStr> for MockLabeled { 322 + fn labels(&self) -> &[Label<SmolStr>] { 323 323 &self.labels 324 324 } 325 325 } ··· 328 328 labels: vec![Label { 329 329 src: labeler_did.clone(), 330 330 uri: UriValue::new_owned("at://did:plc:test/app.bsky.feed.post/abc").unwrap(), 331 - val: CowStr::from("adult-label"), 331 + val: SmolStr::new("adult-label"), 332 332 neg: None, 333 333 cts: Datetime::now(), 334 334 exp: None, ··· 364 364 let labeler_did = Did::new_static("did:plc:test").unwrap(); 365 365 366 366 struct MockLabeled { 367 - labels: Vec<Label<'static>>, 367 + labels: Vec<Label>, 368 368 } 369 369 370 - impl<'a> Labeled<'a> for MockLabeled { 371 - fn labels(&self) -> &[Label<'a>] { 370 + impl Labeled<SmolStr> for MockLabeled { 371 + fn labels(&self) -> &[Label<SmolStr>] { 372 372 &self.labels 373 373 } 374 374 } ··· 379 379 Label { 380 380 src: labeler_did.clone(), 381 381 uri: UriValue::new_owned("at://did:plc:test/app.bsky.feed.post/abc").unwrap(), 382 - val: CowStr::from("test-label"), 382 + val: SmolStr::new("test-label"), 383 383 neg: None, 384 384 cts: Datetime::now(), 385 385 exp: None, ··· 391 391 Label { 392 392 src: labeler_did.clone(), 393 393 uri: UriValue::new_owned("at://did:plc:test/app.bsky.feed.post/abc").unwrap(), 394 - val: CowStr::from("test-label"), 394 + val: SmolStr::new("test-label"), 395 395 neg: Some(true), // negation 396 396 cts: Datetime::now(), 397 397 exp: None, ··· 422 422 let labeler_did = Did::new_static("did:plc:test").unwrap(); 423 423 424 424 struct MockLabeled { 425 - labels: Vec<Label<'static>>, 425 + labels: Vec<Label>, 426 426 } 427 427 428 - impl<'a> Labeled<'a> for MockLabeled { 429 - fn labels(&self) -> &[Label<'a>] { 428 + impl Labeled<SmolStr> for MockLabeled { 429 + fn labels(&self) -> &[Label<SmolStr>] { 430 430 &self.labels 431 431 } 432 432 } ··· 437 437 labels: vec![Label { 438 438 src: labeler_did.clone(), 439 439 uri: UriValue::new_owned("at://did:plc:test/app.bsky.feed.post/abc").unwrap(), 440 - val: CowStr::from("porn"), 440 + val: SmolStr::new("porn"), 441 441 neg: None, 442 442 cts: Datetime::now(), 443 443 exp: None, ··· 466 466 #[test] 467 467 fn test_end_to_end_feed_moderation() { 468 468 // Parse labeler services and build definitions 469 - let services: GetServicesOutput<'static> = 469 + let services: GetServicesOutput = 470 470 serde_json::from_str(LABELER_SERVICES_JSON).expect("failed to parse labeler services"); 471 471 472 472 let mut defs = LabelerDefs::new(); ··· 487 487 488 488 // Parse posts 489 489 #[derive(Deserialize)] 490 - struct FeedResponse<'a> { 491 - #[serde(borrow)] 492 - feed: Vec<FeedViewPost<'a>>, 490 + struct FeedResponse { 491 + feed: Vec<FeedViewPost>, 493 492 } 494 493 495 - let feed_responses: Vec<FeedResponse<'static>> = 494 + let feed_responses: Vec<FeedResponse> = 496 495 serde_json::from_str(POSTS_JSON).expect("failed to parse posts"); 497 496 498 497 // Combine all feeds to test ··· 557 556 "Post {} has {} labels: {:?}", 558 557 i, 559 558 labels.len(), 560 - labels.iter().map(|l| l.val.as_ref()).collect::<Vec<_>>() 559 + labels.iter().map(|l| l.val.as_str()).collect::<Vec<&str>>() 561 560 ); 562 561 } 563 562 } ··· 587 586 println!("Labeler definitions: {}", defs.defs.len()); 588 587 589 588 // Print all unique labels found and their default settings 590 - let mut all_labels_found = std::collections::HashSet::new(); 589 + let mut all_labels_found: std::collections::HashSet<(&str, &str)> = 590 + std::collections::HashSet::new(); 591 591 for feed_post in &all_posts { 592 592 for label in feed_post.post.labels() { 593 593 all_labels_found.insert((label.val.as_ref(), label.src.as_ref())); ··· 660 660 #[test] 661 661 fn test_moderatable_trait() { 662 662 // Test the Moderatable trait on FeedViewPost 663 - let services: GetServicesOutput<'static> = 663 + let services: GetServicesOutput = 664 664 serde_json::from_str(LABELER_SERVICES_JSON).expect("failed to parse labeler services"); 665 665 666 666 let mut defs = LabelerDefs::new(); ··· 678 678 } 679 679 680 680 #[derive(Deserialize)] 681 - struct FeedResponse<'a> { 682 - #[serde(borrow)] 683 - feed: Vec<FeedViewPost<'a>>, 681 + struct FeedResponse { 682 + feed: Vec<FeedViewPost>, 684 683 } 685 684 686 - let feed_responses: Vec<FeedResponse<'static>> = 685 + let feed_responses: Vec<FeedResponse> = 687 686 serde_json::from_str(POSTS_JSON).expect("failed to parse posts"); 688 687 689 688 let prefs = ModerationPrefs::default(); ··· 694 693 .flat_map(|r| &r.feed) 695 694 .find(|p| { 696 695 p.post.labels().iter().any(|l| { 697 - l.val.as_ref() == "porn" || l.val.as_ref() == "sexual" || l.val.as_ref() == "nudity" 696 + AsRef::<str>::as_ref(&l.val) == "porn" 697 + || AsRef::<str>::as_ref(&l.val) == "sexual" 698 + || AsRef::<str>::as_ref(&l.val) == "nudity" 698 699 }) 699 700 }) 700 701 .expect("should find at least one porn/sexual/nudity labeled post"); ··· 702 703 let post_labels = labeled_post.post.labels(); 703 704 println!("Testing post with {} labels:", post_labels.len()); 704 705 for label in post_labels { 705 - println!(" {} from {}", label.val.as_ref(), label.src.as_ref()); 706 + println!( 707 + " {} from {}", 708 + AsRef::<str>::as_ref(&label.val), 709 + AsRef::<str>::as_ref(&label.src) 710 + ); 706 711 } 707 712 708 713 // Use the Moderateable trait with empty accepted_labelers to trust all labels
+37 -17
crates/jacquard/src/moderation/types.rs
··· 1 1 use jacquard_api::com_atproto::label::{LabelValue, LabelValueDefinition}; 2 - use jacquard_common::CowStr; 2 + use jacquard_common::bos::{BosStr, DefaultStr}; 3 3 use jacquard_common::types::string::Did; 4 + use smol_str::SmolStr; 4 5 use std::collections::HashMap; 6 + use std::hash::Hash; 5 7 6 8 /// User's moderation preferences 7 9 /// 8 10 /// Specifies how the user wants to respond to different label values, 9 11 /// both globally and per-labeler. 12 + /// 13 + /// HashMap keys are owned `SmolStr` values so that lookups remain efficient 14 + /// without needing a generic string type parameter on this struct. 10 15 #[derive(Debug, Clone)] 11 - pub struct ModerationPrefs<'a> { 16 + pub struct ModerationPrefs { 12 17 /// Whether adult content is enabled for this user 13 18 pub adult_content_enabled: bool, 14 19 /// Global label preferences (label value -> preference) 15 - pub labels: HashMap<CowStr<'a>, LabelPref>, 20 + pub labels: HashMap<SmolStr, LabelPref>, 16 21 /// Per-labeler overrides (labeler DID -> label value -> preference) 17 - pub labelers: HashMap<Did<'a>, HashMap<CowStr<'a>, LabelPref>>, 22 + pub labelers: HashMap<Did, HashMap<SmolStr, LabelPref>>, 18 23 } 19 24 20 - impl Default for ModerationPrefs<'_> { 25 + impl Default for ModerationPrefs { 21 26 fn default() -> Self { 22 27 Self { 23 28 adult_content_enabled: false, ··· 42 47 /// 43 48 /// Maps labeler DIDs to their published label value definitions. 44 49 /// These definitions describe what labels mean, their severity, and default settings. 45 - #[derive(Debug, Clone, Default)] 46 - pub struct LabelerDefs<'a> { 50 + /// 51 + /// The type parameter `S` must satisfy `Hash + Eq` in addition to `BosStr` because 52 + /// `Did<S>` is used as a `HashMap` key. 53 + #[derive(Debug, Clone)] 54 + pub struct LabelerDefs<S: BosStr + Hash + Eq = DefaultStr> { 47 55 /// Labeler DID -> label value definitions 48 - pub defs: HashMap<Did<'a>, Vec<LabelValueDefinition<'a>>>, 56 + pub defs: HashMap<Did<S>, Vec<LabelValueDefinition<S>>>, 49 57 } 50 58 51 - impl<'a> LabelerDefs<'a> { 59 + /// Manual `Default` impl to avoid the derive macro adding an unnecessary `S: Default` bound. 60 + impl<S: BosStr + Hash + Eq> Default for LabelerDefs<S> { 61 + fn default() -> Self { 62 + Self { 63 + defs: HashMap::new(), 64 + } 65 + } 66 + } 67 + 68 + impl<S: BosStr + Hash + Eq> LabelerDefs<S> { 52 69 /// Create an empty set of labeler definitions 53 70 pub fn new() -> Self { 54 71 Self::default() 55 72 } 56 73 57 74 /// Add definitions for a labeler 58 - pub fn insert(&mut self, did: Did<'a>, definitions: Vec<LabelValueDefinition<'a>>) { 75 + pub fn insert(&mut self, did: Did<S>, definitions: Vec<LabelValueDefinition<S>>) { 59 76 self.defs.insert(did, definitions); 60 77 } 61 78 62 79 /// Get definitions for a specific labeler 63 - pub fn get(&self, did: &Did<'_>) -> Option<&[LabelValueDefinition<'a>]> { 80 + pub fn get(&self, did: &Did<impl BosStr>) -> Option<&[LabelValueDefinition<S>]> { 64 81 self.defs 65 82 .iter() 66 83 .find(|(k, _)| k.as_ref() == did.as_ref()) ··· 70 87 /// Find a label definition by labeler and identifier 71 88 pub fn find_def( 72 89 &self, 73 - labeler: &Did<'_>, 90 + labeler: &Did<impl BosStr>, 74 91 identifier: &str, 75 - ) -> Option<&LabelValueDefinition<'a>> { 92 + ) -> Option<&LabelValueDefinition<S>> { 76 93 self.defs 77 94 .iter() 78 95 .find(|(k, _)| k.as_ref() == labeler.as_ref()) ··· 97 114 /// Whether user override is allowed (false for legal takedowns) 98 115 pub no_override: bool, 99 116 /// Which labels caused this decision 100 - pub causes: Vec<LabelCause<'static>>, 117 + pub causes: Vec<LabelCause>, 101 118 } 102 119 103 120 impl ModerationDecision { ··· 125 142 } 126 143 127 144 /// Information about a label that contributed to a moderation decision 145 + /// 146 + /// Uses `DefaultStr` (`SmolStr`) as the backing type, since causes are always 147 + /// constructed at the point of moderation with owned values. 128 148 #[derive(Debug, Clone)] 129 - pub struct LabelCause<'a> { 149 + pub struct LabelCause<S: BosStr = DefaultStr> { 130 150 /// The label value that triggered this 131 - pub label: LabelValue<'a>, 151 + pub label: LabelValue<S>, 132 152 /// Which labeler applied this label 133 - pub source: Did<'a>, 153 + pub source: Did<S>, 134 154 /// What the label is targeting 135 155 pub target: LabelTarget, 136 156 }
+84 -89
crates/jacquard/src/richtext.rs
··· 7 7 use crate::api::app_bsky::richtext::facet::Facet; 8 8 #[cfg(feature = "api_bluesky")] 9 9 use crate::api::com_atproto::repo::strong_ref::StrongRef; 10 - use crate::common::CowStr; 11 10 #[cfg(feature = "api_bluesky")] 12 11 use crate::types::aturi::AtUri; 12 + #[cfg(feature = "api_bluesky")] 13 + use jacquard_common::BosStr; 13 14 use jacquard_common::IntoStatic; 14 15 #[cfg(feature = "api_bluesky")] 15 16 use jacquard_common::http_client::HttpClient; ··· 24 25 use regex::{Captures, Regex}; 25 26 #[cfg(target_family = "wasm")] 26 27 use regex_lite::{Captures, Regex}; 28 + #[cfg(feature = "api_bluesky")] 29 + use smol_str::{SmolStr, ToSmolStr, format_smolstr}; 30 + use std::borrow::Cow; 27 31 use std::marker::PhantomData; 28 32 use std::ops::Range; 29 33 use std::sync::LazyLock; ··· 86 90 /// Rich text with facets (mentions, links, tags) 87 91 #[derive(Debug, Clone)] 88 92 #[cfg(feature = "api_bluesky")] 89 - pub struct RichText<'a> { 93 + pub struct RichText { 90 94 /// The text content 91 - pub text: CowStr<'a>, 95 + pub text: SmolStr, 92 96 /// Facets (mentions, links, tags) 93 - pub facets: Option<Vec<Facet<'a>>>, 97 + pub facets: Option<Vec<Facet<SmolStr>>>, 94 98 } 95 99 96 100 #[cfg(feature = "api_bluesky")] 97 - impl RichText<'static> { 101 + impl RichText { 98 102 /// Entry point for parsing text with automatic facet detection 99 103 /// 100 104 /// Uses default embed domains (bsky.app, deer.social) for at-URI extraction. ··· 111 115 /// Detected embed candidate from URL or at-URI 112 116 #[derive(Debug, Clone)] 113 117 #[cfg(feature = "api_bluesky")] 114 - pub enum EmbedCandidate<'a> { 118 + pub enum EmbedCandidate<S: BosStr> { 115 119 /// Bluesky record (post, list, starterpack, feed) 116 120 Record { 117 121 /// The at:// URI identifying the record 118 - at_uri: AtUri<'a>, 122 + at_uri: AtUri<S>, 119 123 /// Strong reference (repo + CID) if resolved 120 - strong_ref: Option<StrongRef<'a>>, 124 + strong_ref: Option<StrongRef<S>>, 121 125 }, 122 126 /// External link embed 123 127 External { 124 128 /// The URL 125 - url: CowStr<'a>, 129 + url: S, 126 130 /// OpenGraph metadata if fetched 127 - metadata: Option<ExternalMetadata<'a>>, 131 + metadata: Option<ExternalMetadata<S>>, 128 132 }, 129 133 } 130 134 131 135 /// External embed metadata (OpenGraph) 132 136 #[derive(Debug, Clone)] 133 137 #[cfg(feature = "api_bluesky")] 134 - pub struct ExternalMetadata<'a> { 138 + pub struct ExternalMetadata<S> { 135 139 /// Page title 136 - pub title: CowStr<'a>, 140 + pub title: S, 137 141 /// Page description 138 - pub description: CowStr<'a>, 142 + pub description: S, 139 143 /// Thumbnail URL 140 - pub thumbnail: Option<CowStr<'a>>, 144 + pub thumbnail: Option<S>, 141 145 } 142 146 143 147 /// Rich text builder supporting both parsing and manual construction 144 148 #[derive(Debug)] 145 149 pub struct RichTextBuilder<State> { 146 - text: String, 150 + text: SmolStr, 147 151 facet_candidates: Vec<FacetCandidate>, 148 152 #[cfg(feature = "api_bluesky")] 149 - embed_candidates: Option<Vec<EmbedCandidate<'static>>>, 153 + embed_candidates: Option<Vec<EmbedCandidate<SmolStr>>>, 150 154 _state: PhantomData<State>, 151 155 } 152 156 ··· 171 175 /// Range in text including @ symbol 172 176 range: Range<usize>, 173 177 /// DID when provided, otherwise resolved later 174 - did: Option<Did<'static>>, 178 + did: Option<Did>, 175 179 }, 176 180 /// Plain URL link 177 181 /// Range points to URL in text, normalize at build time ··· 198 202 /// 199 203 /// And normalizes all newline variants (\r\n, \r, \n) to \n, while collapsing 200 204 /// runs of newlines and invisible chars to at most two newlines. 201 - fn sanitize_text(text: &str) -> String { 202 - SANITIZE_NEWLINES_REGEX 203 - .replace_all(text, |caps: &Captures| { 204 - let matched = caps.get(0).unwrap().as_str(); 205 + fn sanitize_text(text: &str) -> Cow<'_, str> { 206 + SANITIZE_NEWLINES_REGEX.replace_all(text, |caps: &Captures| { 207 + let matched = caps.get(0).unwrap().as_str(); 205 208 206 - // Count newline sequences, treating \r\n as one unit 207 - let mut newline_sequences = 0; 208 - let mut chars = matched.chars().peekable(); 209 + // Count newline sequences, treating \r\n as one unit 210 + let mut newline_sequences = 0; 211 + let mut chars = matched.chars().peekable(); 209 212 210 - while let Some(c) = chars.next() { 211 - if c == '\r' { 212 - // Check if followed by \n 213 - if chars.peek() == Some(&'\n') { 214 - chars.next(); // consume the \n 215 - } 216 - newline_sequences += 1; 217 - } else if c == '\n' { 218 - newline_sequences += 1; 213 + while let Some(c) = chars.next() { 214 + if c == '\r' { 215 + // Check if followed by \n 216 + if chars.peek() == Some(&'\n') { 217 + chars.next(); // consume the \n 219 218 } 220 - // Skip invisible chars (they don't increment count) 219 + newline_sequences += 1; 220 + } else if c == '\n' { 221 + newline_sequences += 1; 221 222 } 223 + // Skip invisible chars (they don't increment count) 224 + } 222 225 223 - if newline_sequences == 0 { 224 - // Only invisible chars, remove them 225 - "" 226 - } else if newline_sequences == 1 { 227 - "\n" 228 - } else { 229 - // Multiple newlines, collapse to \n\n (paragraph break) 230 - "\n\n" 231 - } 232 - }) 233 - .to_string() 226 + if newline_sequences == 0 { 227 + // Only invisible chars, remove them 228 + "" 229 + } else if newline_sequences == 1 { 230 + "\n" 231 + } else { 232 + // Multiple newlines, collapse to \n\n (paragraph break) 233 + "\n\n" 234 + } 235 + }) 234 236 } 235 237 236 238 /// Entry point for parsing text with automatic facet detection ··· 301 303 facet_candidates.extend(tag_facets); 302 304 303 305 RichTextBuilder { 304 - text: text_processed, 306 + text: text_processed.to_smolstr(), 305 307 facet_candidates, 306 308 embed_candidates: if embed_candidates.is_empty() { 307 309 None ··· 347 349 /// Entry point for manual richtext construction 348 350 pub fn builder() -> Self { 349 351 RichTextBuilder { 350 - text: String::new(), 352 + text: SmolStr::new_static(""), 351 353 facet_candidates: Vec::new(), 352 354 #[cfg(feature = "api_bluesky")] 353 355 embed_candidates: None, ··· 381 383 } 382 384 } 383 385 384 - impl<S> RichTextBuilder<S> { 386 + impl<St> RichTextBuilder<St> { 385 387 /// Set the text content 386 388 pub fn text(mut self, text: impl AsRef<str>) -> Self { 387 - self.text = sanitize_text(text.as_ref()); 389 + self.text = sanitize_text(text.as_ref()).to_smolstr(); 388 390 self 389 391 } 390 392 391 393 /// Add a mention facet with a resolved DID (requires explicit range) 392 - pub fn mention(mut self, did: &Did<'_>, range: Range<usize>) -> Self { 394 + pub fn mention(mut self, did: &Did, range: Range<usize>) -> Self { 393 395 self.facet_candidates.push(FacetCandidate::Mention { 394 396 range, 395 - did: Some(did.clone().into_static()), 397 + did: Some(did.clone()), 396 398 }); 397 399 self 398 400 } ··· 433 435 434 436 #[cfg(feature = "api_bluesky")] 435 437 /// Add a record embed candidate 436 - pub fn embed_record( 437 - mut self, 438 - at_uri: AtUri<'static>, 439 - strong_ref: Option<StrongRef<'static>>, 440 - ) -> Self { 438 + pub fn embed_record(mut self, at_uri: AtUri, strong_ref: Option<StrongRef>) -> Self { 441 439 self.embed_candidates 442 440 .get_or_insert_with(Vec::new) 443 441 .push(EmbedCandidate::Record { at_uri, strong_ref }); ··· 448 446 /// Add an external embed candidate 449 447 pub fn embed_external( 450 448 mut self, 451 - url: impl Into<CowStr<'static>>, 452 - metadata: Option<ExternalMetadata<'static>>, 449 + url: impl Into<SmolStr>, 450 + metadata: Option<ExternalMetadata<SmolStr>>, 453 451 ) -> Self { 454 452 self.embed_candidates 455 453 .get_or_insert_with(Vec::new) ··· 617 615 618 616 /// Classifies a URL or at-URI as an embed candidate 619 617 #[cfg(feature = "api_bluesky")] 620 - fn classify_embed(url: &str, embed_domains: &[&str]) -> Option<EmbedCandidate<'static>> { 618 + fn classify_embed<'s>(url: &'s str, embed_domains: &[&str]) -> Option<EmbedCandidate<SmolStr>> { 621 619 // Check if it's an at:// URI 622 620 if url.starts_with("at://") { 623 - if let Ok(at_uri) = AtUri::new(url) { 621 + if let Ok(at_uri) = AtUri::new(url.to_smolstr()) { 624 622 return Some(EmbedCandidate::Record { 625 - at_uri: at_uri.into_static(), 623 + at_uri: at_uri, 626 624 strong_ref: None, 627 625 }); 628 626 } ··· 640 638 641 639 // Otherwise, it's an external embed 642 640 return Some(EmbedCandidate::External { 643 - url: CowStr::from(url.to_string()), 641 + url: url.to_smolstr(), 644 642 metadata: None, 645 643 }); 646 644 } ··· 659 657 /// 660 658 /// Only works for domains in the provided `embed_domains` list. 661 659 #[cfg(feature = "api_bluesky")] 662 - pub fn extract_at_uri_from_url(url: &str, embed_domains: &[&str]) -> Option<AtUri<'static>> { 660 + pub fn extract_at_uri_from_url<'s>(url: &'s str, embed_domains: &[&str]) -> Option<AtUri<SmolStr>> { 663 661 // Parse URL 664 662 use jacquard_common::deps::fluent_uri::Uri; 665 663 ··· 677 675 let at_uri_str = match segments.as_slice() { 678 676 // Known shortcuts 679 677 ["profile", actor, "post", rkey] => { 680 - format!("at://{}/app.bsky.feed.post/{}", actor, rkey) 678 + format_smolstr!("at://{}/app.bsky.feed.post/{}", actor, rkey) 681 679 } 682 680 ["profile", actor, "lists", rkey] => { 683 - format!("at://{}/app.bsky.graph.list/{}", actor, rkey) 681 + format_smolstr!("at://{}/app.bsky.graph.list/{}", actor, rkey) 684 682 } 685 683 ["profile", actor, "feed", rkey] => { 686 - format!("at://{}/app.bsky.feed.generator/{}", actor, rkey) 684 + format_smolstr!("at://{}/app.bsky.feed.generator/{}", actor, rkey) 687 685 } 688 686 ["starter-pack", actor, rkey] => { 689 - format!("at://{}/app.bsky.graph.starterpack/{}", actor, rkey) 687 + format_smolstr!("at://{}/app.bsky.graph.starterpack/{}", actor, rkey) 690 688 } 691 689 // Generic pattern: /profile/{actor}/{collection}/{rkey} 692 690 // Accept if collection looks like it could be an NSID (contains dots) 693 691 ["profile", actor, collection, rkey] if collection.contains('.') => { 694 - format!("at://{}/{}/{}", actor, collection, rkey) 692 + format_smolstr!("at://{}/{}/{}", actor, collection, rkey) 695 693 } 696 694 _ => return None, 697 695 }; 698 696 699 - AtUri::new(&at_uri_str).ok().map(|u| u.into_static()) 697 + AtUri::new(at_uri_str).ok() 700 698 } 701 699 702 700 /// Errors that can occur during richtext building ··· 738 736 #[cfg(feature = "api_bluesky")] 739 737 impl RichTextBuilder<Resolved> { 740 738 /// Build the richtext (sync - all facets must be resolved) 741 - pub fn build(self) -> Result<RichText<'static>, RichTextError> { 739 + pub fn build(self) -> Result<RichText, RichTextError> { 742 740 if self.facet_candidates.is_empty() { 743 741 return Ok(RichText { 744 - text: CowStr::from(self.text), 742 + text: self.text, 745 743 facets: None, 746 744 }); 747 745 } ··· 836 834 .trim_start_matches('#'); 837 835 838 836 let feature = FacetFeaturesItem::Tag(Box::new(Tag { 839 - tag: CowStr::from(tag.to_smolstr()), 837 + tag: tag.to_smolstr(), 840 838 extra_data: None, 841 839 })); 842 840 (range, feature) ··· 871 869 } 872 870 873 871 Ok(RichText { 874 - text: CowStr::from(self.text), 872 + text: self.text, 875 873 facets: Some(facets.into_static()), 876 874 }) 877 875 } ··· 880 878 #[cfg(feature = "api_bluesky")] 881 879 impl RichTextBuilder<Unresolved> { 882 880 /// Build richtext, resolving handles to DIDs using the provided resolver 883 - pub async fn build_async<R>(self, resolver: &R) -> Result<RichText<'static>, RichTextError> 881 + pub async fn build_async<R>(self, resolver: &R) -> Result<RichText, RichTextError> 884 882 where 885 883 R: IdentityResolver + Sync, 886 884 { ··· 890 888 891 889 if self.facet_candidates.is_empty() { 892 890 return Ok(RichText { 893 - text: CowStr::from(self.text), 891 + text: self.text, 894 892 facets: None, 895 893 }); 896 894 } ··· 989 987 .trim_start_matches('#'); 990 988 991 989 let feature = FacetFeaturesItem::Tag(Box::new(Tag { 992 - tag: CowStr::from(tag.to_smolstr()), 990 + tag: tag.to_smolstr(), 993 991 extra_data: None, 994 992 })); 995 993 (range, feature) ··· 1024 1022 } 1025 1023 1026 1024 Ok(RichText { 1027 - text: CowStr::from(self.text), 1025 + text: self.text, 1028 1026 facets: Some(facets.into_static()), 1029 1027 }) 1030 1028 } ··· 1035 1033 pub async fn build_with_embeds_async<C>( 1036 1034 mut self, 1037 1035 client: &C, 1038 - ) -> Result<(RichText<'static>, Option<Vec<EmbedCandidate<'static>>>), RichTextError> 1036 + ) -> Result<(RichText, Option<Vec<EmbedCandidate<SmolStr>>>), RichTextError> 1039 1037 where 1040 1038 C: HttpClient + IdentityResolver + Sync, 1041 1039 { ··· 1046 1044 let richtext = self.build_async(client).await?; 1047 1045 1048 1046 // Now resolve embed candidates 1049 - let mut resolved_embeds = Vec::new(); 1047 + let mut resolved_embeds: Vec<EmbedCandidate<SmolStr>> = Vec::new(); 1050 1048 1051 1049 for candidate in embed_candidates { 1052 1050 match candidate { ··· 1089 1087 pub async fn fetch_opengraph_metadata<C>( 1090 1088 client: &C, 1091 1089 url: &str, 1092 - ) -> Result<Option<ExternalMetadata<'static>>, Box<dyn std::error::Error + Send + Sync>> 1090 + ) -> Result<Option<ExternalMetadata<SmolStr>>, Box<dyn std::error::Error + Send + Sync>> 1093 1091 where 1094 1092 C: HttpClient, 1095 1093 { ··· 1114 1112 if let Some(og) = info { 1115 1113 // Extract title, description, and thumbnail 1116 1114 1117 - use jacquard_common::cowstr::ToCowStr; 1118 - let title = og.properties.get("title").map(|s| s.to_cowstr()); 1115 + let title = og.properties.get("title").map(|s| s.to_smolstr()); 1119 1116 1120 - let description = og.properties.get("description").map(|s| s.to_cowstr()); 1117 + let description = og.properties.get("description").map(|s| s.to_smolstr()); 1121 1118 1122 - let thumbnail = og.images.first().map(|img| CowStr::from(img.url.clone())); 1119 + let thumbnail = og.images.first().map(|img| SmolStr::from(img.url.clone())); 1123 1120 1124 1121 // Only return metadata if we have at least a title 1125 1122 if let Some(title) = title { 1126 1123 return Ok(Some(ExternalMetadata { 1127 - title: title.into_static(), 1128 - description: description 1129 - .unwrap_or_else(|| CowStr::new_static("")) 1130 - .into_static(), 1131 - thumbnail: thumbnail.into_static(), 1124 + title: title, 1125 + description: description.unwrap_or_else(|| SmolStr::new_static("")), 1126 + thumbnail: thumbnail, 1132 1127 })); 1133 1128 } 1134 1129 }
+11 -11
crates/jacquard/src/streaming/blob.rs
··· 3 3 use bytes::Bytes; 4 4 use jacquard_api::com_atproto::repo::upload_blob::{UploadBlob, UploadBlobOutput}; 5 5 use jacquard_common::{ 6 - StreamError, 6 + BosStr, StreamError, 7 7 xrpc::streaming::{XrpcProcedureStream, XrpcStreamResp}, 8 8 }; 9 9 use serde::{Deserialize, Serialize}; ··· 15 15 const NSID: &'static str = "com.atproto.repo.uploadBlob"; 16 16 const ENCODING: &'static str = "*/*"; 17 17 18 - type Frame<'de> = Bytes; 18 + type Frame<S: BosStr> = Bytes; 19 19 type Request = UploadBlob; 20 20 type Response = UploadBlobStreamResponse; 21 21 22 - fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError> 22 + fn encode_frame<S: BosStr>(data: Self::Frame<S>) -> Result<Bytes, StreamError> 23 23 where 24 - Self::Frame<'de>: Serialize, 24 + Self::Frame<S>: Serialize, 25 25 { 26 26 Ok(data) 27 27 } 28 28 29 - fn decode_frame<'de>(frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError> 29 + fn decode_frame<'de, S: BosStr>(frame: &'de [u8]) -> Result<Self::Frame<S>, StreamError> 30 30 where 31 - Self::Frame<'de>: Deserialize<'de>, 31 + Self::Frame<S>: Deserialize<'de>, 32 32 { 33 33 Ok(Bytes::copy_from_slice(frame)) 34 34 } ··· 41 41 const NSID: &'static str = "com.atproto.repo.uploadBlob"; 42 42 const ENCODING: &'static str = "application/json"; 43 43 44 - type Frame<'de> = UploadBlobOutput<'de>; 44 + type Frame<S: BosStr> = UploadBlobOutput<S>; 45 45 46 - fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError> 46 + fn encode_frame<S: BosStr>(data: Self::Frame<S>) -> Result<Bytes, StreamError> 47 47 where 48 - Self::Frame<'de>: Serialize, 48 + Self::Frame<S>: Serialize, 49 49 { 50 50 Ok(Bytes::from_owner( 51 51 serde_json::to_vec(&data).map_err(StreamError::encode)?, 52 52 )) 53 53 } 54 54 55 - fn decode_frame<'de>(frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError> 55 + fn decode_frame<'de, S: BosStr>(frame: &'de [u8]) -> Result<Self::Frame<S>, StreamError> 56 56 where 57 - Self::Frame<'de>: Deserialize<'de>, 57 + Self::Frame<S>: Deserialize<'de>, 58 58 { 59 59 Ok(serde_json::from_slice(frame).map_err(StreamError::decode)?) 60 60 }
+16 -16
crates/jacquard/src/streaming/repo.rs
··· 3 3 use bytes::Bytes; 4 4 use jacquard_api::com_atproto::repo::import_repo::ImportRepo; 5 5 use jacquard_common::{ 6 - StreamError, 6 + BosStr, StreamError, 7 7 xrpc::streaming::{XrpcProcedureStream, XrpcStreamResp}, 8 8 }; 9 9 use serde::{Deserialize, Serialize}; ··· 15 15 const NSID: &'static str = "com.atproto.repo.importRepo"; 16 16 const ENCODING: &'static str = "application/vnd.ipld.car"; 17 17 18 - type Frame<'de> = Bytes; 18 + type Frame<S: BosStr> = Bytes; 19 19 type Request = ImportRepo; 20 20 type Response = ImportRepoStreamResponse; 21 21 22 - fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError> 22 + fn encode_frame<S: BosStr>(data: Self::Frame<S>) -> Result<Bytes, StreamError> 23 23 where 24 - Self::Frame<'de>: Serialize, 24 + Self::Frame<S>: Serialize, 25 25 { 26 26 Ok(data) 27 27 } 28 28 29 - fn decode_frame<'de>(frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError> 29 + fn decode_frame<'de, S: BosStr>(frame: &'de [u8]) -> Result<Self::Frame<S>, StreamError> 30 30 where 31 - Self::Frame<'de>: Deserialize<'de>, 31 + Self::Frame<S>: Deserialize<'de>, 32 32 { 33 33 Ok(Bytes::copy_from_slice(frame)) 34 34 } ··· 41 41 const NSID: &'static str = "com.atproto.repo.importRepo"; 42 42 const ENCODING: &'static str = "application/json"; 43 43 44 - type Frame<'de> = (); 44 + type Frame<S: BosStr> = (); 45 45 46 - fn encode_frame<'de>(_data: Self::Frame<'de>) -> Result<Bytes, StreamError> 46 + fn encode_frame<S: BosStr>(_data: Self::Frame<S>) -> Result<Bytes, StreamError> 47 47 where 48 - Self::Frame<'de>: Serialize, 48 + Self::Frame<S>: Serialize, 49 49 { 50 50 Ok(Bytes::new()) 51 51 } 52 52 53 - fn decode_frame<'de>(_frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError> 53 + fn decode_frame<'de, S: BosStr>(_frame: &'de [u8]) -> Result<Self::Frame<S>, StreamError> 54 54 where 55 - Self::Frame<'de>: Deserialize<'de>, 55 + Self::Frame<S>: Deserialize<'de>, 56 56 { 57 57 Ok(()) 58 58 } ··· 65 65 const NSID: &'static str = "com.atproto.sync.getRepo"; 66 66 const ENCODING: &'static str = "application/vnd.ipld.car"; 67 67 68 - type Frame<'de> = Bytes; 68 + type Frame<S: BosStr> = Bytes; 69 69 70 - fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError> 70 + fn encode_frame<S: BosStr>(data: Self::Frame<S>) -> Result<Bytes, StreamError> 71 71 where 72 - Self::Frame<'de>: Serialize, 72 + Self::Frame<S>: Serialize, 73 73 { 74 74 Ok(data) 75 75 } 76 76 77 - fn decode_frame<'de>(frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError> 77 + fn decode_frame<'de, S: BosStr>(frame: &'de [u8]) -> Result<Self::Frame<S>, StreamError> 78 78 where 79 - Self::Frame<'de>: Deserialize<'de>, 79 + Self::Frame<S>: Deserialize<'de>, 80 80 { 81 81 Ok(Bytes::copy_from_slice(frame)) 82 82 }
+11 -11
crates/jacquard/src/streaming/video.rs
··· 3 3 use bytes::Bytes; 4 4 use jacquard_api::app_bsky::video::upload_video::{UploadVideo, UploadVideoOutput}; 5 5 use jacquard_common::{ 6 - StreamError, 6 + BosStr, StreamError, 7 7 xrpc::streaming::{XrpcProcedureStream, XrpcStreamResp}, 8 8 }; 9 9 use serde::{Deserialize, Serialize}; ··· 15 15 const NSID: &'static str = "app.bsky.video.uploadVideo"; 16 16 const ENCODING: &'static str = "video/mp4"; 17 17 18 - type Frame<'de> = Bytes; 18 + type Frame<S: BosStr> = Bytes; 19 19 type Request = UploadVideo; 20 20 type Response = UploadVideoStreamResponse; 21 21 22 - fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError> 22 + fn encode_frame<S: BosStr>(data: Self::Frame<S>) -> Result<Bytes, StreamError> 23 23 where 24 - Self::Frame<'de>: Serialize, 24 + Self::Frame<S>: Serialize, 25 25 { 26 26 Ok(data) 27 27 } 28 28 29 - fn decode_frame<'de>(frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError> 29 + fn decode_frame<'de, S: BosStr>(frame: &'de [u8]) -> Result<Self::Frame<S>, StreamError> 30 30 where 31 - Self::Frame<'de>: Deserialize<'de>, 31 + Self::Frame<S>: Deserialize<'de>, 32 32 { 33 33 Ok(Bytes::copy_from_slice(frame)) 34 34 } ··· 41 41 const NSID: &'static str = "app.bsky.video.uploadVideo"; 42 42 const ENCODING: &'static str = "application/json"; 43 43 44 - type Frame<'de> = UploadVideoOutput<'de>; 44 + type Frame<S: BosStr> = UploadVideoOutput<S>; 45 45 46 - fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError> 46 + fn encode_frame<S: BosStr>(data: Self::Frame<S>) -> Result<Bytes, StreamError> 47 47 where 48 - Self::Frame<'de>: Serialize, 48 + Self::Frame<S>: Serialize, 49 49 { 50 50 Ok(Bytes::from_owner( 51 51 serde_json::to_vec(&data).map_err(StreamError::encode)?, 52 52 )) 53 53 } 54 54 55 - fn decode_frame<'de>(frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError> 55 + fn decode_frame<'de, S: BosStr>(frame: &'de [u8]) -> Result<Self::Frame<S>, StreamError> 56 56 where 57 - Self::Frame<'de>: Deserialize<'de>, 57 + Self::Frame<S>: Deserialize<'de>, 58 58 { 59 59 Ok(serde_json::from_slice(frame).map_err(StreamError::decode)?) 60 60 }
+56 -62
crates/jacquard/tests/oauth_auto_refresh.rs
··· 7 7 use jacquard::deps::fluent_uri::Uri; 8 8 use jacquard::types::did::Did; 9 9 use jacquard::xrpc::XrpcClient; 10 - use jacquard::{CowStr, IntoStatic}; 10 + use jacquard::{BosStr, IntoStatic}; 11 11 use jacquard_common::http_client::HttpClient; 12 12 use jacquard_oauth::atproto::AtprotoClientMetadata; 13 13 use jacquard_oauth::client::OAuthSession; ··· 16 16 use jacquard_oauth::session::SessionRegistry; 17 17 use jacquard_oauth::session::{ClientData, ClientSessionData, DpopClientData}; 18 18 use jacquard_oauth::types::{OAuthAuthorizationServerMetadata, OAuthTokenType, TokenSet}; 19 + use smol_str::SmolStr; 19 20 use tokio::sync::Mutex; 20 21 21 22 #[derive(Clone, Default)] ··· 53 54 LazyLock::new(jacquard::identity::resolver::ResolverOptions::default); 54 55 &OPTS 55 56 } 56 - async fn resolve_handle( 57 + async fn resolve_handle<S: BosStr>( 57 58 &self, 58 - _handle: &jacquard::types::string::Handle<'_>, 59 - ) -> std::result::Result<Did<'static>, jacquard::identity::resolver::IdentityError> { 59 + _handle: &jacquard::types::string::Handle<S>, 60 + ) -> std::result::Result<Did, jacquard::identity::resolver::IdentityError> { 60 61 Ok(Did::new_static("did:plc:alice").unwrap()) 61 62 } 62 - async fn resolve_did_doc( 63 + async fn resolve_did_doc<S: BosStr>( 63 64 &self, 64 - _did: &Did<'_>, 65 + _did: &Did<S>, 65 66 ) -> std::result::Result< 66 67 jacquard::identity::resolver::DidDocResponse, 67 68 jacquard::identity::resolver::IdentityError, ··· 85 86 impl OAuthResolver for MockClient { 86 87 async fn get_authorization_server_metadata( 87 88 &self, 88 - issuer: &CowStr<'_>, 89 - ) -> Result<OAuthAuthorizationServerMetadata<'static>, jacquard_oauth::resolver::ResolverError> 90 - { 89 + issuer: &str, 90 + ) -> Result<OAuthAuthorizationServerMetadata, jacquard_oauth::resolver::ResolverError> { 91 91 // Return minimal metadata with supported auth method "none" and DPoP support 92 92 let mut md = OAuthAuthorizationServerMetadata::default(); 93 - md.issuer = jacquard::CowStr::from(issuer.as_str()); 94 - md.token_endpoint = jacquard::CowStr::from(format!("{}/token", issuer)); 95 - md.authorization_endpoint = jacquard::CowStr::from(format!("{}/authorize", issuer)); 93 + md.issuer = SmolStr::from(issuer); 94 + md.token_endpoint = SmolStr::from(format!("{}/token", issuer)); 95 + md.authorization_endpoint = SmolStr::from(format!("{}/authorize", issuer)); 96 96 md.require_pushed_authorization_requests = Some(true); 97 - md.pushed_authorization_request_endpoint = 98 - Some(jacquard::CowStr::from(format!("{}/par", issuer))); 99 - md.token_endpoint_auth_methods_supported = Some(vec![jacquard::CowStr::from("none")]); 100 - md.dpop_signing_alg_values_supported = Some(vec![jacquard::CowStr::from("ES256")]); 97 + md.pushed_authorization_request_endpoint = Some(SmolStr::from(format!("{}/par", issuer))); 98 + md.token_endpoint_auth_methods_supported = Some(vec![SmolStr::from("none")]); 99 + md.dpop_signing_alg_values_supported = Some(vec![SmolStr::from("ES256")]); 101 100 use jacquard::IntoStatic; 102 101 Ok(md.into_static()) 103 102 } 104 103 105 104 async fn get_resource_server_metadata( 106 105 &self, 107 - _pds: &CowStr<'_>, 108 - ) -> Result<OAuthAuthorizationServerMetadata<'static>, jacquard_oauth::resolver::ResolverError> 109 - { 106 + _pds: &str, 107 + ) -> Result<OAuthAuthorizationServerMetadata, jacquard_oauth::resolver::ResolverError> { 110 108 // Return metadata pointing to the same issuer as above 111 109 let mut md = OAuthAuthorizationServerMetadata::default(); 112 - md.issuer = jacquard::CowStr::from("https://issuer"); 113 - md.token_endpoint = jacquard::CowStr::from("https://issuer/token"); 114 - md.authorization_endpoint = jacquard::CowStr::from("https://issuer/authorize"); 110 + md.issuer = SmolStr::from("https://issuer"); 111 + md.token_endpoint = SmolStr::from("https://issuer/token"); 112 + md.authorization_endpoint = SmolStr::from("https://issuer/authorize"); 115 113 md.require_pushed_authorization_requests = Some(true); 116 - md.pushed_authorization_request_endpoint = 117 - Some(jacquard::CowStr::from("https://issuer/par")); 118 - md.token_endpoint_auth_methods_supported = Some(vec![jacquard::CowStr::from("none")]); 119 - md.dpop_signing_alg_values_supported = Some(vec![jacquard::CowStr::from("ES256")]); 114 + md.pushed_authorization_request_endpoint = Some(SmolStr::from("https://issuer/par")); 115 + md.token_endpoint_auth_methods_supported = Some(vec![SmolStr::from("none")]); 116 + md.dpop_signing_alg_values_supported = Some(vec![SmolStr::from("ES256")]); 120 117 Ok(md.into_static()) 121 118 } 122 119 123 - async fn verify_issuer( 120 + async fn verify_issuer<S: BosStr>( 124 121 &self, 125 - _server_metadata: &OAuthAuthorizationServerMetadata<'_>, 126 - _sub: &Did<'_>, 122 + _server_metadata: &OAuthAuthorizationServerMetadata, 123 + _sub: &Did<S>, 127 124 ) -> Result<jacquard::deps::fluent_uri::Uri<String>, jacquard_oauth::resolver::ResolverError> 128 125 { 129 126 Ok(jacquard::deps::fluent_uri::Uri::parse("https://pds") ··· 220 217 use jacquard::IntoStatic; 221 218 let session_data = ClientSessionData { 222 219 account_did: Did::new_static("did:plc:alice").unwrap(), 223 - session_id: jacquard::CowStr::from("state"), 220 + session_id: SmolStr::from("state"), 224 221 host_url: Uri::parse("https://pds").expect("valid uri").to_owned(), 225 - authserver_url: CowStr::new_static("https://issuer"), 226 - authserver_token_endpoint: jacquard::CowStr::from("https://issuer/token"), 222 + authserver_url: SmolStr::new_static("https://issuer"), 223 + authserver_token_endpoint: SmolStr::from("https://issuer/token"), 227 224 authserver_revocation_endpoint: None, 228 225 scopes: vec![Scope::Atproto], 229 226 dpop_data: DpopClientData { 230 - dpop_key: jacquard_oauth::utils::generate_key(&[jacquard::CowStr::from("ES256")]) 231 - .unwrap(), 232 - dpop_authserver_nonce: jacquard::CowStr::from(""), 233 - dpop_host_nonce: jacquard::CowStr::from(""), 227 + dpop_key: jacquard_oauth::utils::generate_key(&[SmolStr::from("ES256")]).unwrap(), 228 + dpop_authserver_nonce: SmolStr::from(""), 229 + dpop_host_nonce: SmolStr::from(""), 234 230 }, 235 231 token_set: TokenSet { 236 - iss: jacquard::CowStr::from("https://issuer"), 232 + iss: SmolStr::from("https://issuer"), 237 233 sub: Did::new_static("did:plc:alice").unwrap(), 238 - aud: jacquard::CowStr::from("https://pds"), 234 + aud: SmolStr::from("https://pds"), 239 235 scope: None, 240 - refresh_token: Some(jacquard::CowStr::from("rt1")), 241 - access_token: jacquard::CowStr::from("atk1"), 236 + refresh_token: Some(SmolStr::from("rt1")), 237 + access_token: SmolStr::from("atk1"), 242 238 token_type: OAuthTokenType::DPoP, 243 239 expires_at: None, 244 240 }, ··· 249 245 // Seed the store so refresh can load the session 250 246 let data_store = ClientSessionData { 251 247 account_did: Did::new_static("did:plc:alice").unwrap(), 252 - session_id: jacquard::CowStr::from("state"), 248 + session_id: SmolStr::from("state"), 253 249 host_url: Uri::parse("https://pds").expect("valid uri").to_owned(), 254 - authserver_url: CowStr::new_static("https://issuer"), 255 - authserver_token_endpoint: jacquard::CowStr::from("https://issuer/token"), 250 + authserver_url: SmolStr::new_static("https://issuer"), 251 + authserver_token_endpoint: SmolStr::from("https://issuer/token"), 256 252 authserver_revocation_endpoint: None, 257 253 scopes: vec![Scope::Atproto], 258 254 dpop_data: DpopClientData { 259 - dpop_key: jacquard_oauth::utils::generate_key(&[jacquard::CowStr::from("ES256")]) 260 - .unwrap(), 261 - dpop_authserver_nonce: jacquard::CowStr::from(""), 262 - dpop_host_nonce: jacquard::CowStr::from(""), 255 + dpop_key: jacquard_oauth::utils::generate_key(&[SmolStr::from("ES256")]).unwrap(), 256 + dpop_authserver_nonce: SmolStr::from(""), 257 + dpop_host_nonce: SmolStr::from(""), 263 258 }, 264 259 token_set: TokenSet { 265 - iss: jacquard::CowStr::from("https://issuer"), 260 + iss: SmolStr::from("https://issuer"), 266 261 sub: Did::new_static("did:plc:alice").unwrap(), 267 - aud: jacquard::CowStr::from("https://pds"), 262 + aud: SmolStr::from("https://pds"), 268 263 scope: None, 269 - refresh_token: Some(jacquard::CowStr::from("rt1")), 270 - access_token: jacquard::CowStr::from("atk1"), 264 + refresh_token: Some(SmolStr::from("rt1")), 265 + access_token: SmolStr::from("atk1"), 271 266 token_type: OAuthTokenType::DPoP, 272 267 expires_at: None, 273 268 }, ··· 351 346 use jacquard::IntoStatic; 352 347 let session_data = ClientSessionData { 353 348 account_did: Did::new_static("did:plc:alice").unwrap(), 354 - session_id: jacquard::CowStr::from("state"), 349 + session_id: SmolStr::new_static("state"), 355 350 host_url: Uri::parse("https://pds").expect("valid uri").to_owned(), 356 - authserver_url: CowStr::new_static("https://issuer"), 357 - authserver_token_endpoint: jacquard::CowStr::from("https://issuer/token"), 351 + authserver_url: SmolStr::new_static("https://issuer"), 352 + authserver_token_endpoint: SmolStr::from("https://issuer/token"), 358 353 authserver_revocation_endpoint: None, 359 354 scopes: vec![Scope::Atproto], 360 355 dpop_data: DpopClientData { 361 - dpop_key: jacquard_oauth::utils::generate_key(&[jacquard::CowStr::from("ES256")]) 362 - .unwrap(), 363 - dpop_authserver_nonce: jacquard::CowStr::from(""), 364 - dpop_host_nonce: jacquard::CowStr::from(""), 356 + dpop_key: jacquard_oauth::utils::generate_key(&[SmolStr::from("ES256")]).unwrap(), 357 + dpop_authserver_nonce: SmolStr::from(""), 358 + dpop_host_nonce: SmolStr::from(""), 365 359 }, 366 360 token_set: TokenSet { 367 - iss: jacquard::CowStr::from("https://issuer"), 361 + iss: SmolStr::from("https://issuer"), 368 362 sub: Did::new_static("did:plc:alice").unwrap(), 369 - aud: jacquard::CowStr::from("https://pds"), 363 + aud: SmolStr::from("https://pds"), 370 364 scope: None, 371 - refresh_token: Some(jacquard::CowStr::from("rt1")), 372 - access_token: jacquard::CowStr::from("atk1"), 365 + refresh_token: Some(SmolStr::from("rt1")), 366 + access_token: SmolStr::from("atk1"), 373 367 token_type: OAuthTokenType::DPoP, 374 368 expires_at: None, 375 369 },
+16 -13
crates/jacquard/tests/oauth_flow.rs
··· 5 5 use http::{Response as HttpResponse, StatusCode}; 6 6 use jacquard::client::Agent; 7 7 use jacquard::xrpc::XrpcClient; 8 - use jacquard::{CowStr, IntoStatic}; 8 + use jacquard::BosStr; 9 + use jacquard::IntoStatic; 10 + use smol_str::SmolStr; 9 11 use jacquard_common::http_client::HttpClient; 10 12 use jacquard_oauth::atproto::AtprotoClientMetadata; 11 13 use jacquard_oauth::authstore::ClientAuthStore; ··· 45 47 LazyLock::new(jacquard::identity::resolver::ResolverOptions::default); 46 48 &OPTS 47 49 } 48 - async fn resolve_handle( 50 + async fn resolve_handle<S: BosStr + Sync>( 49 51 &self, 50 - _handle: &jacquard::types::string::Handle<'_>, 52 + _handle: &jacquard::types::string::Handle<S>, 51 53 ) -> std::result::Result< 52 - jacquard::types::did::Did<'static>, 54 + jacquard::types::did::Did, 53 55 jacquard::identity::resolver::IdentityError, 54 56 > { 55 57 Ok(jacquard::types::did::Did::new_static("did:plc:alice").unwrap()) 56 58 } 57 - async fn resolve_did_doc( 59 + async fn resolve_did_doc<S: BosStr + Sync>( 58 60 &self, 59 - _did: &jacquard::types::did::Did<'_>, 61 + _did: &jacquard::types::did::Did<S>, 60 62 ) -> std::result::Result< 61 63 jacquard::identity::resolver::DidDocResponse, 62 64 jacquard::identity::resolver::IdentityError, ··· 217 219 std::fs::write(&path, "{}").unwrap(); 218 220 let store = jacquard::client::FileAuthStore::new(&path); 219 221 220 - let client_data: ClientData<'static> = ClientData { 222 + let client_data: ClientData<_> = ClientData { 221 223 keyset: None, 222 224 config: AtprotoClientMetadata::new_localhost(None, Some(vec![Scope::Atproto])), 223 225 }; ··· 237 239 keyset: None, 238 240 }; 239 241 let login_hint = identity.map(|_| jacquard::CowStr::from("alice.bsky.social")); 240 - let auth_req = jacquard_oauth::request::par(client.as_ref(), login_hint, None, &metadata, None) 241 - .await 242 - .unwrap(); 242 + let auth_req = 243 + jacquard_oauth::request::par(client.as_ref(), login_hint, None, &mut metadata, None) 244 + .await 245 + .unwrap(); 243 246 // Construct authorization URL as OAuthClient::start_auth would do 244 247 #[derive(serde::Serialize)] 245 248 struct Parameters<'s> { ··· 251 254 metadata.server_metadata.authorization_endpoint, 252 255 serde_html_form::to_string(Parameters { 253 256 client_id: metadata.client_metadata.client_id.clone(), 254 - request_uri: auth_req.request_uri.clone(), 257 + request_uri: jacquard::CowStr::Owned(auth_req.request_uri.clone()), 255 258 }) 256 259 .unwrap() 257 260 ); ··· 270 273 use jacquard_oauth::types::CallbackParams; 271 274 let session = oauth 272 275 .callback(CallbackParams { 273 - code: jacquard::CowStr::from("code123"), 276 + code: SmolStr::from("code123"), 274 277 state: Some(state.clone()), 275 278 // Callback compares exact string with metadata.issuer. Must match exactly. 276 - iss: Some(jacquard::CowStr::from("https://issuer")), 279 + iss: Some(SmolStr::from("https://issuer")), 277 280 }) 278 281 .await 279 282 .unwrap();
+1 -1
examples/app_password_create_post.rs
··· 29 29 let agent: Agent<_> = Agent::from(session); 30 30 31 31 // Create a simple text post using the Agent convenience method 32 - let post = Post::new() 32 + let post = Post::<SmolStr>::new() 33 33 .text(args.text) 34 34 .created_at(Datetime::now()) 35 35 .build();
+4 -4
examples/create_whitewind_post.rs
··· 45 45 // Create a WhiteWind blog entry 46 46 // The content field accepts markdown 47 47 let entry = Entry { 48 - title: Some(CowStr::from(args.title)), 49 - subtitle: args.subtitle.map(CowStr::from), 50 - content: CowStr::from(args.content), 48 + title: Some(args.title), 49 + subtitle: args.subtitle, 50 + content: args.content, 51 51 created_at: Some(Datetime::now()), 52 - visibility: Some(CowStr::from("url")), // "url" = public with link, "author" = public on profile 52 + visibility: Some(String::from("url")), // "url" = public with link, "author" = public on profile 53 53 theme: None, 54 54 ogp: None, 55 55 blobs: None,
+3 -3
examples/moderated_timeline.rs
··· 122 122 clean += 1; 123 123 } 124 124 125 - let text = from_data::<Post>(&post.record) 125 + let text = from_data::<Post, _>(&post.record) 126 126 .inspect_err(|e| println!("error: {e}")) 127 127 .ok() 128 128 .map(|p| p.text.to_string()) ··· 132 132 if let ReplyRefParent::PostView(parent) = &reply.parent { 133 133 if let ReplyRefRoot::PostView(root) = &reply.root { 134 134 if root.uri != parent.uri { 135 - let root_text = from_data::<Post>(&root.record) 135 + let root_text = from_data::<Post, _>(&root.record) 136 136 .ok() 137 137 .map(|p| p.text.to_string()) 138 138 .unwrap_or_else(|| "<no text>".to_string()); 139 139 println!("@{}:\n{}", root.author.handle, root_text); 140 140 } 141 141 } 142 - let parent_text = from_data::<Post>(&parent.record) 142 + let parent_text = from_data::<Post, _>(&parent.record) 143 143 .ok() 144 144 .map(|p| p.text.to_string()) 145 145 .unwrap_or_else(|| "<no text>".to_string());
+3 -1
examples/oauth_timeline.rs
··· 71 71 72 72 // Wrap in Agent and fetch the timeline 73 73 let agent: Agent<_> = Agent::from(session); 74 - let output = agent.send(GetTimeline::new().limit(5).build()).await?; 74 + let output = agent 75 + .send(GetTimeline::<SmolStr>::new().limit(5).build()) 76 + .await?; 75 77 let timeline = output.into_output()?; 76 78 for (i, post) in timeline.feed.iter().enumerate() { 77 79 println!("\n{}. by {}", i + 1, post.post.author.handle);
+1 -1
examples/stream_get_blob.rs
··· 37 37 // Use the streaming `.download()` method with the generated API parameter struct 38 38 let output: StreamingResponse = agent 39 39 .download(GetBlob { 40 - did: Did::new_owned(args.did)?, 40 + did: Did::new(args.did)?, 41 41 cid: Cid::str(&args.cid), 42 42 }) 43 43 .await?;