···44use alloc::string::{String, ToString};
55use core::convert::Infallible;
66use core::{fmt, hash::Hash, ops::Deref, str::FromStr};
77-use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
88-use smol_str::{SmolStr, ToSmolStr};
77+use serde::{Deserialize, Deserializer, Serialize, Serializer};
98109/// Blob reference for binary data in AT Protocol.
1110///
···7877 }
7978}
80798080+impl<S: Bos<str> + AsRef<str>> Blob<S> {
8181+ /// Convert to a `Blob` with a different backing type.
8282+ pub fn convert<B: Bos<str> + AsRef<str> + From<S>>(self) -> Blob<B> {
8383+ Blob {
8484+ r#ref: self.r#ref.convert(),
8585+ mime_type: self.mime_type.convert(),
8686+ size: self.size,
8787+ }
8888+ }
8989+}
9090+8191/// Tagged blob reference with `$type` field for serde.
8292#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
8393#[serde(tag = "$type", rename_all = "lowercase")]
···126136 }
127137}
128138139139+impl<S: Bos<str> + AsRef<str>> BlobRef<S> {
140140+ /// Convert to a `BlobRef` with a different backing type.
141141+ pub fn convert<B: Bos<str> + AsRef<str> + From<S>>(self) -> BlobRef<B> {
142142+ match self {
143143+ BlobRef::Blob(blob) => BlobRef::Blob(blob.convert()),
144144+ }
145145+ }
146146+}
147147+129148/// MIME type identifier for blob data.
130149///
131150/// Used to specify the content type of blobs. Supports patterns like "image/*" and "*/*".
···142161 }
143162}
144163145145-impl<'m> MimeType<&'m str> {
146146- /// Infallible constructor, borrows from input.
147147- pub fn new(mime_type: &'m str) -> Self {
164164+impl<S: Bos<str>> MimeType<S> {
165165+ /// Infallible constructor, wraps the input directly.
166166+ pub fn new(mime_type: S) -> Self {
148167 Self(mime_type)
149168 }
150169151151- /// Infallible constructor for trusted MIME type strings.
152152- pub fn raw(mime_type: &'m str) -> Self {
153153- Self(mime_type)
170170+ /// Convert to a `MimeType` with a different backing type.
171171+ pub fn convert<B: Bos<str> + From<S>>(self) -> MimeType<B> {
172172+ MimeType(B::from(self.0))
154173 }
155174}
156175157157-impl<S: Bos<str> + From<SmolStr>> MimeType<S> {
176176+impl<S: Bos<str> + FromStr> MimeType<S> {
158177 /// Infallible constructor, takes ownership.
159178 pub fn new_owned(mime_type: impl AsRef<str>) -> Self {
160160- Self(S::from(mime_type.as_ref().to_smolstr()))
179179+ Self(S::from_str(mime_type.as_ref()).unwrap_or_else(|_| unreachable!()))
161180 }
162181163182 /// Infallible constructor for static strings.
164183 pub fn new_static(mime_type: &'static str) -> Self {
165165- Self(S::from(SmolStr::new_static(mime_type)))
166166- }
167167-}
168168-169169-impl<'m> MimeType<CowStr<'m>> {
170170- /// Infallible constructor, borrows if possible.
171171- pub fn new_cow(mime_type: CowStr<'m>) -> Self {
172172- Self(mime_type)
184184+ Self(S::from_str(mime_type).unwrap_or_else(|_| unreachable!()))
173185 }
174186}
175187
+43-15
crates/jacquard-common/src/types/cid.rs
···11use crate::bos::{Bos, DefaultStr};
22-use crate::{CowStr, IntoStatic, cowstr::ToCowStr};
22+use crate::{CowStr, IntoStatic};
33use alloc::string::{String, ToString};
44pub use cid::Cid as IpldCid;
55use core::{convert::Infallible, fmt, ops::Deref, str::FromStr};
···5353 /// Invalid UTF-8 in CID string.
5454 #[error("{:?}", 0)]
5555 Utf8(#[from] core::str::Utf8Error),
5656+ /// Wraps another error with additional context
5757+ #[error("converting from a string slice")]
5858+ #[cfg_attr(feature = "std", diagnostic(code(jacquard::cid::str_conversion)))]
5959+ Conversion,
5660}
57615862// ---------------------------------------------------------------------------
···117121 pub fn str(cid: &'c str) -> Self {
118122 Self::Str(cid)
119123 }
124124+}
120125126126+impl<S: Bos<str>> Cid<S> {
121127 /// Parse a CID from bytes (tries IPLD first, falls back to UTF-8 string).
122122- pub fn new(cid: &'c [u8]) -> Result<Self, Error> {
128128+ pub fn new<'c>(cid: &'c [u8]) -> Result<Cid<S>, Error>
129129+ where
130130+ S: From<&'c str>,
131131+ {
123132 if let Ok(cid) = IpldCid::try_from(cid.as_ref()) {
124124- Ok(Self::ipld(cid))
133133+ Ok(Cid::ipld(cid))
125134 } else {
126135 let cid_str = core::str::from_utf8(cid)?;
127127- Ok(Self::Str(cid_str))
136136+ Ok(Cid::Str(cid_str.into()))
128137 }
129138 }
130139}
131140132132-impl<S: Bos<str> + From<SmolStr>> Cid<S> {
141141+impl<S: Bos<str> + FromStr> Cid<S> {
133142 /// Parse a CID from bytes into an owned value.
134143 pub fn new_owned(cid: &[u8]) -> Result<Self, Error> {
135144 if let Ok(cid) = IpldCid::try_from(cid.as_ref()) {
136145 Ok(Self::ipld(cid))
137146 } else {
138147 let cid_str = core::str::from_utf8(cid)?;
139139- Ok(Cid::Str(S::from(cid_str.to_smolstr())))
148148+ Ok(Cid::Str(
149149+ S::from_str(cid_str).map_err(|_| Error::Conversion)?,
150150+ ))
140151 }
141152 }
142153}
···223234 }
224235}
225236237237+impl<S: Bos<str>> Cid<S> {
238238+ /// Convert to a `Cid` with a different backing type.
239239+ pub fn convert<B: Bos<str> + From<S>>(self) -> Cid<B> {
240240+ match self {
241241+ Cid::Ipld { cid, s } => Cid::Ipld { cid, s },
242242+ Cid::Str(s) => Cid::Str(B::from(s)),
243243+ }
244244+ }
245245+}
246246+226247impl<S: Bos<str> + AsRef<str>> From<Cid<S>> for String {
227248 fn from(value: Cid<S>) -> Self {
228249 value.as_str().to_string()
···296317 pub fn ipld(cid: IpldCid) -> Self {
297318 CidLink(Cid::ipld(cid))
298319 }
299299-}
300320301301-impl<'c> CidLink<&'c str> {
302321 /// Parse a CID link from bytes.
303303- pub fn new(cid: &'c [u8]) -> Result<Self, Error> {
304304- Ok(Self(Cid::new(cid)?))
322322+ pub fn new<'c>(cid: &'c [u8]) -> Result<CidLink<S>, Error>
323323+ where
324324+ S: Bos<str> + From<&'c str>,
325325+ {
326326+ Ok(CidLink(Cid::new(cid)?))
305327 }
328328+}
306329330330+impl<'c> CidLink<&'c str> {
307331 /// Construct a CID link from a string slice.
308332 pub fn str(cid: &'c str) -> Self {
309333 Self(Cid::str(cid))
···315339 }
316340}
317341318318-impl<S: Bos<str> + From<SmolStr>> CidLink<S> {
342342+impl<S: Bos<str> + FromStr> CidLink<S> {
319343 /// Parse a CID link from bytes into an owned value.
320344 pub fn new_owned(cid: &[u8]) -> Result<Self, Error> {
321345 Ok(CidLink(Cid::new_owned(cid)?))
···405429 self.visit_bytes(&v)
406430 }
407431408408- fn visit_newtype_struct<D>(
409409- self,
410410- deserializer: D,
411411- ) -> Result<Self::Value, D::Error>
432432+ fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
412433 where
413434 D: serde::de::Deserializer<'de>,
414435 {
···500521501522 fn into_static(self) -> Self::Output {
502523 CidLink(self.0.into_static())
524524+ }
525525+}
526526+527527+impl<S: Bos<str>> CidLink<S> {
528528+ /// Convert to a `CidLink` with a different backing type.
529529+ pub fn convert<B: Bos<str> + From<S>>(self) -> CidLink<B> {
530530+ CidLink(self.0.convert())
503531 }
504532}
505533
+7-8
crates/jacquard-common/src/types/collection.rs
···11use alloc::string::String;
22use core::fmt;
33-use core::str::FromStr;
33+use smol_str::SmolStr;
4455use serde::{Deserialize, Serialize};
66···88use crate::types::{
99 aturi::RepoPath,
1010 nsid::Nsid,
1111- recordkey::{RecordKey, RecordKeyType, Rkey},
1111+ recordkey::{RecordKey, RecordKeyType},
1212};
1313use crate::xrpc::XrpcResp;
1414-use crate::{BorrowOrShare, Bos, CowStr, IntoStatic};
1414+use crate::{BorrowOrShare, Bos, IntoStatic};
15151616/// Trait for a collection of records that can be stored in a repository.
1717///
···7272)]
7373#[serde(tag = "error", content = "message")]
7474#[non_exhaustive]
7575-pub enum RecordError<'a> {
7575+pub enum RecordError {
7676 /// The requested record was not found
7777 #[error("RecordNotFound")]
7878 #[serde(rename = "RecordNotFound")]
···8080 /// An unknown error occurred
8181 #[error("Unknown")]
8282 #[serde(rename = "Unknown")]
8383- #[serde(borrow)]
8484- Unknown(Data<'a>),
8383+ Unknown(Data<SmolStr>),
8584}
86858787-impl IntoStatic for RecordError<'_> {
8888- type Output = RecordError<'static>;
8686+impl IntoStatic for RecordError {
8787+ type Output = RecordError;
89889089 fn into_static(self) -> Self::Output {
9190 match self {
+11-2
crates/jacquard-common/src/types/did.rs
···1212#[cfg(target_arch = "wasm32")]
1313use regex_lite::Regex;
1414use serde::{Deserialize, Deserializer, Serialize};
1515-use smol_str::{SmolStr, ToSmolStr};
1515+use smol_str::SmolStr;
16161717use super::Lazy;
1818···159159 }
160160}
161161162162+impl<S: Bos<str>> Did<S> {
163163+ /// Convert to a `Did` with a different backing type.
164164+ pub fn convert<B: Bos<str> + From<S>>(self) -> Did<B> {
165165+ Did(B::from(self.0))
166166+ }
167167+}
168168+162169impl FromStr for Did {
163170 type Err = AtStrError;
164171···258265 // new() does not strip — use new_owned() for that.
259266 assert!(Did::<&str>::new("at://did:plc:foo").is_err());
260267 assert_eq!(
261261- Did::<SmolStr>::new_owned("at://did:plc:foo").unwrap().as_str(),
268268+ Did::<SmolStr>::new_owned("at://did:plc:foo")
269269+ .unwrap()
270270+ .as_str(),
262271 "did:plc:foo"
263272 );
264273 assert_eq!(
+14-21
crates/jacquard-common/src/types/did_doc.rs
···11use crate::deps::fluent_uri::Uri;
22use crate::types::crypto::{CryptoError, PublicKey};
33-use crate::types::string::{Did, Handle};
33+use crate::types::string::{AtprotoStr, Did, Handle};
44use crate::types::value::Data;
55-use crate::{Bos, CowStr, DefaultStr, IntoStatic};
55+use crate::{Bos, DefaultStr, IntoStatic};
66use alloc::collections::BTreeMap;
77-use alloc::string::String;
87use alloc::vec::Vec;
98use bon::Builder;
109use serde::{Deserialize, Serialize};
···6968 #[serde(skip_serializing_if = "Option::is_none")]
7069 pub service: Option<Vec<Service<S>>>,
7170 // Forward‑compatible capture of unmodeled fields
7272- // TODO: re-enable extra data fields
7373- // #[serde(flatten)]
7474- // pub extra_data: BTreeMap<SmolStr, Data<'static>>,
7171+ #[serde(flatten)]
7272+ pub extra_data: BTreeMap<SmolStr, Data<S>>,
7573}
76747775/// Default context fields for DID documents
···9896 verification_method: self.verification_method.into_static(),
9997 service: self.service.into_static(),
10098 // TODO: re-enable extra data fields
101101- // extra_data: self.extra_data.into_static(),
9999+ extra_data: self.extra_data.into_static(),
102100 }
103101 }
104102}
···135133 })
136134 }
137135138138- /// Extract the AtprotoPersonalDataServer service endpoint as a `fluent_uri::Uri<String>`.
136136+ /// Extract the AtprotoPersonalDataServer service endpoint as a `fluent_uri::Uri<&str>`.
139137 /// Accepts endpoint as string or object (string preferred).
140138 pub fn pds_endpoint(&self) -> Option<Uri<&str>> {
141139 self.service.as_ref().and_then(|services| {
142140 services.iter().find_map(|s| {
143141 if s.r#type.as_ref() == "AtprotoPersonalDataServer" {
144142 match &s.service_endpoint {
145145- Some(strv) => Uri::parse(strv.as_ref()).ok(),
146146-143143+ Some(Data::String(AtprotoStr::Uri(u))) => Uri::parse(u.as_ref()).ok(),
147144 _ => None,
148145 }
149146 } else {
···185182 #[serde(skip_serializing_if = "Option::is_none")]
186183 pub public_key_multibase: Option<S>,
187184 // Forward‑compatible capture of unmodeled fields
188188- // TODO: re-enable extra data fields
189189- // #[serde(flatten)]
190190- // pub extra_data: BTreeMap<SmolStr, Data<'static>>,
185185+ #[serde(flatten)]
186186+ pub extra_data: BTreeMap<SmolStr, Data<S>>,
191187}
192188193189impl<S> crate::IntoStatic for VerificationMethod<S>
···204200 controller: self.controller.into_static(),
205201 public_key_multibase: self.public_key_multibase.into_static(),
206202 // TODO: re-enable extra data fields
207207- // extra_data: self.extra_data.into_static(),
203203+ extra_data: self.extra_data.into_static(),
208204 }
209205 }
210206}
···224220 #[serde(rename = "type")]
225221 pub r#type: S,
226222 /// currently atproto expects this to be a url
227227- ///
228228- /// TODO: add back in map/set support once Data<'_> is migrated
229223 #[serde(skip_serializing_if = "Option::is_none")]
230230- pub service_endpoint: Option<S>,
224224+ pub service_endpoint: Option<Data<S>>,
231225 // Forward‑compatible capture of unmodeled fields
232232- // TODO: re-enable extra data fields
233233- // #[serde(flatten)]
234234- // pub extra_data: BTreeMap<SmolStr, Data<'static>>,
226226+ #[serde(flatten)]
227227+ pub extra_data: BTreeMap<SmolStr, Data<S>>,
235228}
236229237230impl<S> crate::IntoStatic for Service<S>
···247240 r#type: self.r#type.into_static(),
248241 service_endpoint: self.service_endpoint.into_static(),
249242 // TODO: re-enable extra data fields
250250- // extra_data: self.extra_data.into_static(),
243243+ extra_data: self.extra_data.into_static(),
251244 }
252245 }
253246}
+7
crates/jacquard-common/src/types/handle.rs
···232232 }
233233}
234234235235+impl<S: Bos<str>> Handle<S> {
236236+ /// Convert to a `Handle` with a different backing type.
237237+ pub fn convert<B: Bos<str> + From<S>>(self) -> Handle<B> {
238238+ Handle(B::from(self.0))
239239+ }
240240+}
241241+235242impl FromStr for Handle {
236243 type Err = AtStrError;
237244
+12-2
crates/jacquard-common/src/types/ident.rs
···12121313use serde::{Deserialize, Serialize};
14141515-use smol_str::SmolStr;
1616-1715/// AT Protocol identifier (either a DID or handle).
1816///
1917/// Represents the union of DIDs and handles, which can both be used to identify
···131129 }
132130}
133131132132+impl<S: Bos<str> + AsRef<str>> AtIdentifier<S> {
133133+ /// Convert to an `AtIdentifier` with a different backing type.
134134+ pub fn convert<B: Bos<str> + AsRef<str> + From<S>>(self) -> AtIdentifier<B> {
135135+ match self {
136136+ AtIdentifier::Did(did) => AtIdentifier::Did(did.convert()),
137137+ AtIdentifier::Handle(handle) => AtIdentifier::Handle(handle.convert()),
138138+ }
139139+ }
140140+}
141141+134142impl<S: Bos<str> + AsRef<str>> From<Did<S>> for AtIdentifier<S> {
135143 fn from(did: Did<S>) -> Self {
136144 AtIdentifier::Did(did)
···202210203211#[cfg(test)]
204212mod tests {
213213+ use smol_str::SmolStr;
214214+205215 use super::*;
206216 use crate::cowstr::ToCowStr;
207217
+7
crates/jacquard-common/src/types/nsid.rs
···131131 }
132132}
133133134134+impl<S: Bos<str>> Nsid<S> {
135135+ /// Convert to an `Nsid` with a different backing type.
136136+ pub fn convert<B: Bos<str> + From<S>>(self) -> Nsid<B> {
137137+ Nsid(B::from(self.0))
138138+ }
139139+}
140140+134141impl FromStr for Nsid {
135142 type Err = AtStrError;
136143
+14
crates/jacquard-common/src/types/recordkey.rs
···9898 }
9999}
100100101101+impl<T: RecordKeyType> RecordKey<T> {
102102+ /// Convert the inner key to a different type.
103103+ pub fn convert<U: RecordKeyType + From<T>>(self) -> RecordKey<U> {
104104+ RecordKey(U::from(self.0))
105105+ }
106106+}
107107+101108/// AT Protocol record key (generic "any" type)
102109///
103110/// Record keys uniquely identify records within a collection. This is the catch-all
···229236230237 fn into_static(self) -> Self::Output {
231238 Rkey(self.0.into_static())
239239+ }
240240+}
241241+242242+impl<S: Bos<str>> Rkey<S> {
243243+ /// Convert to an `Rkey` with a different backing type.
244244+ pub fn convert<B: Bos<str> + From<S>>(self) -> Rkey<B> {
245245+ Rkey(B::from(self.0))
232246 }
233247}
234248
+29-10
crates/jacquard-common/src/types/string.rs
···11+use crate::bos::{Bos, DefaultStr};
12use alloc::string::{String, ToString};
23use alloc::sync::Arc;
34use core::str::FromStr;
45#[cfg(feature = "std")]
56use miette::{Diagnostic, SourceSpan};
67use serde::{Deserialize, Deserializer, Serialize, Serializer};
77-use smol_str::{SmolStr, ToSmolStr};
88+use smol_str::SmolStr;
89910/// Source span for error reporting (offset, length)
1011/// With `std` feature, this is `miette::SourceSpan`. Without, a simple tuple struct.
···2627 }
2728}
28292929-use crate::bos::{Bos, DefaultStr};
3030-use crate::cowstr::ToCowStr;
3130pub use crate::{
3231 CowStr,
3332 types::{
···5857/// record keys are intentionally NOT parsed from bare strings as the validation
5958/// is too permissive and would catch too many values.
6059#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6161-pub enum AtprotoStr<S: Bos<str> + AsRef<str> + Clone + Serialize = DefaultStr> {
6060+pub enum AtprotoStr<S: Bos<str> + AsRef<str> = DefaultStr> {
6261 /// ISO 8601 datetime
6362 Datetime(Datetime),
6463 /// BCP 47 language tag
···9089use crate::types::handle::validate_handle;
9190use crate::types::nsid::validate_nsid;
92919393-impl<S: Bos<str> + AsRef<str> + Clone + Serialize> AtprotoStr<S> {
9292+impl<S: Bos<str> + AsRef<str>> AtprotoStr<S> {
9493 /// Classify and wrap a string value into the appropriate variant.
9594 ///
9695 /// This is fairly exhaustive and potentially **slow**, prefer using anything
···191190 }
192191}
193192194194-impl<S: Bos<str> + AsRef<str> + Clone + Serialize> AsRef<str> for AtprotoStr<S> {
193193+impl<S: Bos<str> + AsRef<str>> AsRef<str> for AtprotoStr<S> {
195194 fn as_ref(&self) -> &str {
196195 self.as_str()
197196 }
198197}
199198200200-impl<S: Bos<str> + AsRef<str> + Clone + Serialize> Serialize for AtprotoStr<S> {
199199+impl<S: Bos<str> + AsRef<str> + Serialize> Serialize for AtprotoStr<S> {
201200 fn serialize<Ser>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error>
202201 where
203202 Ser: Serializer,
···208207209208impl<'de, S> Deserialize<'de> for AtprotoStr<S>
210209where
211211- S: Bos<str> + AsRef<str> + Clone + Serialize + Deserialize<'de>,
210210+ S: Bos<str> + AsRef<str> + Deserialize<'de>,
212211{
213212 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
214213 where
···219218 }
220219}
221220222222-impl<S: Bos<str> + AsRef<str> + Clone + Serialize + IntoStatic> IntoStatic for AtprotoStr<S>
221221+impl<S: Bos<str> + AsRef<str>> AtprotoStr<S> {
222222+ /// Convert to an `AtprotoStr` with a different backing type.
223223+ pub fn convert<B: Bos<str> + AsRef<str> + From<S>>(self) -> AtprotoStr<B> {
224224+ match self {
225225+ AtprotoStr::Datetime(dt) => AtprotoStr::Datetime(dt),
226226+ AtprotoStr::Language(lang) => AtprotoStr::Language(lang),
227227+ AtprotoStr::Tid(tid) => AtprotoStr::Tid(tid),
228228+ AtprotoStr::Nsid(nsid) => AtprotoStr::Nsid(nsid.convert()),
229229+ AtprotoStr::Did(did) => AtprotoStr::Did(did.convert()),
230230+ AtprotoStr::Handle(handle) => AtprotoStr::Handle(handle.convert()),
231231+ AtprotoStr::AtIdentifier(ident) => AtprotoStr::AtIdentifier(ident.convert()),
232232+ AtprotoStr::AtUri(at_uri) => AtprotoStr::AtUri(at_uri.convert()),
233233+ AtprotoStr::Uri(uri) => AtprotoStr::Uri(uri.convert()),
234234+ AtprotoStr::Cid(cid) => AtprotoStr::Cid(cid.convert()),
235235+ AtprotoStr::RecordKey(rkey) => AtprotoStr::RecordKey(RecordKey(rkey.0.convert())),
236236+ AtprotoStr::String(s) => AtprotoStr::String(B::from(s)),
237237+ }
238238+ }
239239+}
240240+241241+impl<S: Bos<str> + AsRef<str> + IntoStatic> IntoStatic for AtprotoStr<S>
223242where
224224- S::Output: Bos<str> + AsRef<str> + Clone + Serialize,
243243+ S::Output: Bos<str> + AsRef<str>,
225244{
226245 type Output = AtprotoStr<S::Output>;
227246