···11+use crate::bos::{Bos, DefaultStr};
12use crate::types::Literal;
23use crate::types::string::AtStrError;
34use crate::{CowStr, IntoStatic};
···4041#[repr(transparent)]
4142pub struct RecordKey<T: RecordKeyType>(pub T);
42434343-impl<'a> RecordKey<Rkey<'a>> {
4444- /// Create a new `RecordKey` from a string slice
4444+impl<'a> RecordKey<Rkey<&'a str>> {
4545+ /// Create a new `RecordKey` from a string slice.
4546 pub fn any(str: &'a str) -> Result<Self, AtStrError> {
4647 Ok(RecordKey(Rkey::new(str)?))
4748 }
4949+}
5050+5151+impl<S: Bos<str> + AsRef<str> + Clone + Serialize + From<SmolStr>> RecordKey<Rkey<S>> {
5252+ /// Create a new `RecordKey` from a static string slice.
5353+ pub fn any_static(str: &'static str) -> Result<Self, AtStrError> {
5454+ Ok(RecordKey(Rkey::new_static(str)?))
5555+ }
5656+5757+ /// Create a new `RecordKey` from an owned string.
5858+ pub fn any_owned(str: impl AsRef<str>) -> Result<Self, AtStrError> {
5959+ Ok(RecordKey(Rkey::new_owned(str)?))
6060+ }
6161+}
48624949- /// Create a new `RecordKey` from a CowStr
6363+impl<'a> RecordKey<Rkey<CowStr<'a>>> {
6464+ /// Create a new `RecordKey` from a CowStr.
5065 pub fn any_cow(str: CowStr<'a>) -> Result<Self, AtStrError> {
5166 Ok(RecordKey(Rkey::new_cow(str)?))
5267 }
5353-5454- /// Create a new `RecordKey` from a static string slice
5555- pub fn any_static(str: &'static str) -> Result<Self, AtStrError> {
5656- Ok(RecordKey(Rkey::new_static(str)?))
5757- }
5868}
59696060-impl<T> From<T> for RecordKey<Rkey<'_>>
7070+impl<T> From<T> for RecordKey<Rkey>
6171where
6272 T: RecordKeyType,
6373{
6474 fn from(value: T) -> Self {
6565- RecordKey(Rkey::from_str(value.as_str()).expect("Invalid rkey"))
7575+ RecordKey(Rkey::new_owned(value.as_str()).expect("Invalid rkey"))
6676 }
6777}
68786969-impl FromStr for RecordKey<Rkey<'_>> {
7979+impl FromStr for RecordKey<Rkey> {
7080 type Err = AtStrError;
71817282 fn from_str(s: &str) -> Result<Self, Self::Err> {
···108118/// - Any: flexible strings matching the validation rules
109119///
110120/// See: <https://atproto.com/specs/record-key>
111111-#[derive(Clone, PartialEq, Eq, Serialize, Hash)]
121121+/// AT Protocol record key (generic "any" type).
122122+///
123123+/// See: <https://atproto.com/specs/record-key>
124124+#[derive(Clone, PartialEq, Eq, Hash, Serialize)]
112125#[serde(transparent)]
113126#[repr(transparent)]
114114-pub struct Rkey<'r>(pub(crate) CowStr<'r>);
127127+pub struct Rkey<S: Bos<str> = DefaultStr>(pub(crate) S);
115128116116-unsafe impl<'r> RecordKeyType for Rkey<'r> {
129129+unsafe impl<S: Bos<str> + AsRef<str> + Clone + Serialize> RecordKeyType for Rkey<S> {
117130 fn as_str(&self) -> &str {
118131 self.0.as_ref()
119132 }
120133}
121134122122-/// Regex for record key validation per AT Protocol spec
135135+/// Regex for record key validation per AT Protocol spec.
123136pub static RKEY_REGEX: Lazy<Regex> =
124137 Lazy::new(|| Regex::new(r"^[a-zA-Z0-9.\-_:~]{1,512}$").unwrap());
125138126126-impl<'r> Rkey<'r> {
127127- /// Fallible constructor, validates, borrows from input
139139+pub(crate) fn validate_rkey(rkey: &str) -> Result<(), AtStrError> {
140140+ if [".", ".."].contains(&rkey) {
141141+ Err(AtStrError::disallowed("record-key", rkey, &[".", ".."]))
142142+ } else if !RKEY_REGEX.is_match(rkey) {
143143+ Err(AtStrError::regex(
144144+ "record-key",
145145+ rkey,
146146+ SmolStr::new_static("doesn't match 'any' schema"),
147147+ ))
148148+ } else {
149149+ Ok(())
150150+ }
151151+}
152152+153153+impl<S: Bos<str> + AsRef<str>> Rkey<S> {
154154+ /// Get the record key as a string slice.
155155+ pub fn as_str(&self) -> &str {
156156+ self.0.as_ref()
157157+ }
158158+}
159159+160160+impl<S: Bos<str>> Rkey<S> {
161161+ /// # Safety
162162+ ///
163163+ /// The caller must ensure the rkey is valid.
164164+ pub unsafe fn unchecked(rkey: S) -> Self {
165165+ Rkey(rkey)
166166+ }
167167+}
168168+169169+impl<'r> Rkey<&'r str> {
170170+ /// Fallible constructor, validates, borrows from input.
128171 pub fn new(rkey: &'r str) -> Result<Self, AtStrError> {
129129- if [".", ".."].contains(&rkey) {
130130- Err(AtStrError::disallowed("record-key", rkey, &[".", ".."]))
131131- } else if !RKEY_REGEX.is_match(rkey) {
132132- Err(AtStrError::regex(
133133- "record-key",
134134- rkey,
135135- SmolStr::new_static("doesn't match 'any' schema"),
136136- ))
137137- } else {
138138- Ok(Self(CowStr::Borrowed(rkey)))
139139- }
172172+ validate_rkey(rkey)?;
173173+ Ok(Self(rkey))
174174+ }
175175+176176+ /// Infallible constructor. Panics on invalid rkeys.
177177+ pub fn raw(rkey: &'r str) -> Self {
178178+ Self::new(rkey).expect("invalid rkey")
140179 }
180180+}
141181142142- /// Fallible constructor, validates, takes ownership
182182+impl<S: Bos<str> + From<SmolStr>> Rkey<S> {
183183+ /// Fallible constructor, validates, takes ownership.
143184 pub fn new_owned(rkey: impl AsRef<str>) -> Result<Self, AtStrError> {
144185 let rkey = rkey.as_ref();
145145- if [".", ".."].contains(&rkey) {
146146- Err(AtStrError::disallowed("record-key", rkey, &[".", ".."]))
147147- } else if !RKEY_REGEX.is_match(rkey) {
148148- Err(AtStrError::regex(
149149- "record-key",
150150- rkey,
151151- SmolStr::new_static("doesn't match 'any' schema"),
152152- ))
153153- } else {
154154- Ok(Self(CowStr::Owned(rkey.to_smolstr())))
155155- }
186186+ validate_rkey(rkey)?;
187187+ Ok(Self(S::from(rkey.to_smolstr())))
156188 }
157189158158- /// Fallible constructor, validates, doesn't allocate
190190+ /// Fallible constructor for static strings.
159191 pub fn new_static(rkey: &'static str) -> Result<Self, AtStrError> {
160160- if [".", ".."].contains(&rkey) {
161161- Err(AtStrError::disallowed("record-key", rkey, &[".", ".."]))
162162- } else if !RKEY_REGEX.is_match(rkey) {
163163- Err(AtStrError::regex(
164164- "record-key",
165165- rkey,
166166- SmolStr::new_static("doesn't match 'any' schema"),
167167- ))
168168- } else {
169169- Ok(Self(CowStr::new_static(rkey)))
170170- }
192192+ validate_rkey(rkey)?;
193193+ Ok(Self(S::from(SmolStr::new_static(rkey))))
171194 }
195195+}
172196173173- /// Fallible constructor, validates, borrows from input if possible
197197+impl<'r> Rkey<CowStr<'r>> {
198198+ /// Fallible constructor, borrows if possible.
174199 pub fn new_cow(rkey: CowStr<'r>) -> Result<Self, AtStrError> {
175175- if [".", ".."].contains(&rkey.as_ref()) {
176176- Err(AtStrError::disallowed("record-key", &rkey, &[".", ".."]))
177177- } else if !RKEY_REGEX.is_match(&rkey) {
178178- Err(AtStrError::regex(
179179- "record-key",
180180- &rkey,
181181- SmolStr::new_static("doesn't match 'any' schema"),
182182- ))
183183- } else {
184184- Ok(Self(rkey))
185185- }
200200+ validate_rkey(&rkey)?;
201201+ Ok(Self(rkey))
186202 }
187203188188- /// Infallible constructor for when you *know* the string is a valid rkey.
189189- /// Will panic on invalid rkeys. If you're manually decoding atproto records
190190- /// or API values you know are valid (rather than using serde), this is the one to use.
191191- /// The From impls use the same logic.
192192- pub fn raw(rkey: &'r str) -> Self {
193193- if [".", ".."].contains(&rkey) {
194194- panic!("Disallowed rkey")
195195- } else if !RKEY_REGEX.is_match(rkey) {
196196- panic!("Invalid rkey")
197197- } else {
198198- Self(CowStr::Borrowed(rkey))
199199- }
204204+ /// Infallible unchecked constructor for CowStr.
205205+ pub unsafe fn unchecked_cow(rkey: CowStr<'r>) -> Self {
206206+ Self(rkey)
200207 }
208208+}
201209202202- /// Infallible constructor for when you *know* the string is a valid rkey.
203203- /// Marked unsafe because responsibility for upholding the invariant is on the developer.
204204- pub unsafe fn unchecked(rkey: &'r str) -> Self {
205205- Self(CowStr::Borrowed(rkey))
210210+impl<'de, S> Deserialize<'de> for Rkey<S>
211211+where
212212+ S: Bos<str> + AsRef<str> + Deserialize<'de>,
213213+{
214214+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
215215+ where
216216+ D: Deserializer<'de>,
217217+ {
218218+ let s = S::deserialize(deserializer)?;
219219+ validate_rkey(s.as_ref()).map_err(D::Error::custom)?;
220220+ Ok(Rkey(s))
206221 }
222222+}
207223208208- /// Get the record key as a string slice
209209- pub fn as_str(&self) -> &str {
210210- {
211211- let this = &self.0;
212212- this
213213- }
224224+impl<S: Bos<str> + IntoStatic> IntoStatic for Rkey<S>
225225+where
226226+ S::Output: Bos<str>,
227227+{
228228+ type Output = Rkey<S::Output>;
229229+230230+ fn into_static(self) -> Self::Output {
231231+ Rkey(self.0.into_static())
214232 }
215233}
216234217217-impl<'r> FromStr for Rkey<'r> {
235235+impl FromStr for Rkey {
218236 type Err = AtStrError;
219237220238 fn from_str(s: &str) -> Result<Self, Self::Err> {
221221- if [".", ".."].contains(&s) {
222222- Err(AtStrError::disallowed("record-key", s, &[".", ".."]))
223223- } else if !RKEY_REGEX.is_match(s) {
224224- Err(AtStrError::regex(
225225- "record-key",
226226- s,
227227- SmolStr::new_static("doesn't match 'any' schema"),
228228- ))
229229- } else {
230230- Ok(Self(CowStr::Owned(s.to_smolstr())))
231231- }
239239+ Self::new_owned(s)
232240 }
233241}
234242235235-impl IntoStatic for Rkey<'_> {
236236- type Output = Rkey<'static>;
243243+impl FromStr for Rkey<CowStr<'static>> {
244244+ type Err = AtStrError;
237245238238- fn into_static(self) -> Self::Output {
239239- Rkey(self.0.into_static())
246246+ fn from_str(s: &str) -> Result<Self, Self::Err> {
247247+ Self::new_owned(s)
240248 }
241249}
242250243243-impl<'de, 'a> Deserialize<'de> for Rkey<'a>
244244-where
245245- 'de: 'a,
246246-{
247247- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
248248- where
249249- D: Deserializer<'de>,
250250- {
251251- let value = Deserialize::deserialize(deserializer)?;
252252- Self::new_cow(value).map_err(D::Error::custom)
251251+impl FromStr for Rkey<String> {
252252+ type Err = AtStrError;
253253+254254+ fn from_str(s: &str) -> Result<Self, Self::Err> {
255255+ Self::new_owned(s)
253256 }
254257}
255258256256-impl fmt::Display for Rkey<'_> {
259259+impl<S: Bos<str> + AsRef<str>> fmt::Display for Rkey<S> {
257260 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258258- f.write_str(&self.0)
261261+ f.write_str(self.as_str())
259262 }
260263}
261264262262-impl fmt::Debug for Rkey<'_> {
265265+impl<S: Bos<str> + AsRef<str>> fmt::Debug for Rkey<S> {
263266 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264264- write!(f, "record-key:{}", self.0)
267267+ write!(f, "record-key:{}", self.as_str())
265268 }
266269}
267270268268-impl From<Rkey<'_>> for String {
269269- fn from(value: Rkey<'_>) -> Self {
270270- value.0.to_string()
271271+impl<S: Bos<str> + AsRef<str>> From<Rkey<S>> for String {
272272+ fn from(value: Rkey<S>) -> Self {
273273+ value.as_str().to_string()
271274 }
272275}
273276274274-impl<'r> From<Rkey<'r>> for CowStr<'r> {
275275- fn from(value: Rkey<'r>) -> Self {
276276- value.0
277277+impl<S: Bos<str> + AsRef<str>> From<Rkey<S>> for SmolStr {
278278+ fn from(value: Rkey<S>) -> Self {
279279+ value.as_str().to_smolstr()
277280 }
278281}
279282280280-impl<'r> From<Rkey<'r>> for SmolStr {
281281- fn from(value: Rkey) -> Self {
282282- value.0.to_smolstr()
283283- }
284284-}
285285-286286-impl<'r> From<String> for Rkey<'r> {
283283+impl From<String> for Rkey {
287284 fn from(value: String) -> Self {
288288- if [".", ".."].contains(&value.as_str()) {
289289- panic!("Disallowed rkey")
290290- } else if !RKEY_REGEX.is_match(&value) {
291291- panic!("Invalid rkey")
292292- } else {
293293- Self(CowStr::Owned(value.to_smolstr()))
294294- }
285285+ Self::new_owned(value).unwrap()
295286 }
296287}
297288298298-impl<'r> From<CowStr<'r>> for Rkey<'r> {
289289+impl<'r> From<CowStr<'r>> for Rkey<CowStr<'r>> {
299290 fn from(value: CowStr<'r>) -> Self {
300300- if [".", ".."].contains(&value.as_ref()) {
301301- panic!("Disallowed rkey")
302302- } else if !RKEY_REGEX.is_match(&value) {
303303- panic!("Invalid rkey")
304304- } else {
305305- Self(value)
306306- }
291291+ Self::new_cow(value).unwrap()
307292 }
308293}
309294310310-impl AsRef<str> for Rkey<'_> {
295295+impl<S: Bos<str> + AsRef<str>> AsRef<str> for Rkey<S> {
311296 fn as_ref(&self) -> &str {
312297 self.as_str()
313298 }
314299}
315300316316-impl Deref for Rkey<'_> {
301301+impl<S: Bos<str> + AsRef<str>> Deref for Rkey<S> {
317302 type Target = str;
318303319304 fn deref(&self) -> &Self::Target {
320320- self.0.as_ref()
305305+ self.as_str()
321306 }
322307}
323308
+82-136
crates/jacquard-common/src/types/string.rs
···2626 }
2727}
28282929+use crate::bos::{Bos, DefaultStr};
2930use crate::cowstr::ToCowStr;
3031pub use crate::{
3132 CowStr,
···5758/// record keys are intentionally NOT parsed from bare strings as the validation
5859/// is too permissive and would catch too many values.
5960#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6060-pub enum AtprotoStr<'s> {
6161+pub enum AtprotoStr<S: Bos<str> + AsRef<str> + Clone + Serialize = DefaultStr> {
6162 /// ISO 8601 datetime
6263 Datetime(Datetime),
6364 /// BCP 47 language tag
···6566 /// Timestamp identifier
6667 Tid(Tid),
6768 /// Namespaced identifier
6868- Nsid(Nsid<CowStr<'s>>),
6969+ Nsid(Nsid<S>),
6970 /// Decentralized identifier
7070- Did(Did<CowStr<'s>>),
7171+ Did(Did<S>),
7172 /// Account handle
7272- Handle(Handle<CowStr<'s>>),
7373+ Handle(Handle<S>),
7374 /// Identifier (DID or handle)
7474- AtIdentifier(AtIdentifier<CowStr<'s>>),
7575+ AtIdentifier(AtIdentifier<S>),
7676+ // TODO(bos-migration): parameterise on S once AtUri is migrated.
7577 /// AT URI
7676- AtUri(AtUri<'s>),
7878+ AtUri(AtUri<'static>),
7979+ // TODO(bos-migration): parameterise on S once UriValue is migrated.
7780 /// Generic URI
7878- Uri(UriValue<'s>),
8181+ Uri(UriValue<'static>),
7982 /// Content identifier
8080- Cid(Cid<'s>),
8383+ Cid(Cid<S>),
8184 /// Record key
8282- RecordKey(RecordKey<Rkey<'s>>),
8585+ RecordKey(RecordKey<Rkey<S>>),
8386 /// Plain string (fallback)
8484- String(CowStr<'s>),
8787+ String(S),
8588}
86898787-impl<'s> AtprotoStr<'s> {
8888- /// Borrowing constructor for bare atproto string values
9090+use crate::types::cid::IpldCid;
9191+use crate::types::did::validate_did;
9292+use crate::types::handle::validate_handle;
9393+use crate::types::nsid::validate_nsid;
9494+9595+impl<S: Bos<str> + AsRef<str> + Clone + Serialize> AtprotoStr<S> {
9696+ /// Classify and wrap a string value into the appropriate variant.
9797+ ///
8998 /// This is fairly exhaustive and potentially **slow**, prefer using anything
9099 /// that narrows down the search field quicker.
91100 ///
9292- /// Note: We don't construct record keys from bare strings in this because
9393- /// the type is too permissive and too many things would be classified as rkeys.
9494- ///
9595- /// Value object deserialization checks against the field names for common
9696- /// names (uri, cid, did, handle, createdAt, indexedAt, etc.) to improve
9797- /// performance of the happy path.
9898- pub fn new(string: &'s str) -> Self {
9999- // TODO: do some quick prefix checks like in Uri to drop through faster
100100- if let Ok(datetime) = Datetime::from_str(string) {
101101- Self::Datetime(datetime)
102102- } else if let Ok(lang) = Language::new(string) {
103103- Self::Language(lang)
104104- } else if let Ok(tid) = Tid::from_str(string) {
105105- Self::Tid(tid)
106106- } else if let Ok(did) = Did::new_cow(string.to_cowstr()) {
107107- Self::Did(did)
108108- } else if let Ok(handle) = Handle::new_cow(string.to_cowstr()) {
109109- Self::Handle(handle)
110110- } else if let Ok(atid) = AtIdentifier::new_cow(string.to_cowstr()) {
111111- Self::AtIdentifier(atid)
112112- } else if let Ok(nsid) = Nsid::new_cow(string.to_cowstr()) {
113113- Self::Nsid(nsid)
114114- } else if let Ok(aturi) = AtUri::new(string) {
115115- Self::AtUri(aturi)
116116- } else if let Ok(uri) = UriValue::new(string) {
117117- Self::Uri(uri)
118118- } else if let Ok(cid) = Cid::new(string.as_bytes()) {
119119- Self::Cid(cid)
120120- } else {
121121- // We don't construct record keys from bare strings because the type is too permissive
122122- Self::String(CowStr::Borrowed(string))
101101+ /// Inspects the string content, validates against known AT Protocol types,
102102+ /// and moves `string` into the matching variant via unchecked constructors
103103+ /// (safe because we validate first).
104104+ pub fn new(string: S) -> Self {
105105+ let s: &str = string.as_ref();
106106+ // Non-string-backed types first (they don't consume S).
107107+ if let Ok(datetime) = Datetime::from_str(s) {
108108+ return Self::Datetime(datetime);
109109+ }
110110+ if let Ok(lang) = Language::new(s) {
111111+ return Self::Language(lang);
112112+ }
113113+ if let Ok(tid) = Tid::from_str(s) {
114114+ return Self::Tid(tid);
115115+ }
116116+ // String-backed types: validate then wrap S directly.
117117+ if validate_did(s).is_ok() {
118118+ return Self::Did(unsafe { Did::unchecked(string) });
119119+ }
120120+ if validate_handle(s).is_ok() {
121121+ return Self::Handle(unsafe { Handle::unchecked(string) });
122122+ }
123123+ if validate_nsid(s).is_ok() {
124124+ return Self::Nsid(unsafe { Nsid::unchecked(string) });
125125+ }
126126+ // TODO(bos-migration): AtUri and UriValue still use lifetimes.
127127+ // For now, construct owned versions for those variants.
128128+ if let Ok(aturi) = AtUri::new_owned(s) {
129129+ return Self::AtUri(aturi);
130130+ }
131131+ if let Ok(uri) = UriValue::new_owned(s) {
132132+ return Self::Uri(uri);
133133+ }
134134+ // CID: try to parse as IPLD first, otherwise wrap as string CID.
135135+ if IpldCid::try_from(s).is_ok() || s.starts_with("bafy") {
136136+ return Self::Cid(unsafe { Cid::unchecked_str(string) });
123137 }
138138+ // Fallback: plain string.
139139+ Self::String(string)
124140 }
125141126126- /// Get the string value regardless of variant
142142+ /// Get the string value regardless of variant.
127143 pub fn as_str(&self) -> &str {
128144 match self {
129145 Self::Datetime(datetime) => datetime.as_str(),
···141157 }
142158 }
143159144144- /// detailed string type
160160+ /// Detailed string type classification.
145161 pub fn string_type(&self) -> LexiconStringType {
146162 match self {
147163 Self::Datetime(_) => LexiconStringType::Datetime,
···167183 }
168184}
169185170170-impl AtprotoStr<'static> {
171171- /// Owned constructor for bare atproto string values
172172- /// This is fairly exhaustive and potentially **slow**, prefer using anything
173173- /// that narrows down the search field quicker.
174174- ///
175175- /// Note: We don't construct record keys from bare strings in this because
176176- /// the type is too permissive and too many things would be classified as rkeys.
177177- ///
178178- /// Value object deserialization checks against the field names for common
179179- /// names (uri, cid, did, handle, createdAt, indexedAt, etc.) to improve
180180- /// performance of the happy path.
181181- pub fn new_owned(string: impl AsRef<str>) -> AtprotoStr<'static> {
182182- let string = string.as_ref();
183183- // TODO: do some quick prefix checks like in Uri to drop through faster
184184- if let Ok(datetime) = Datetime::from_str(string) {
185185- Self::Datetime(datetime)
186186- } else if let Ok(lang) = Language::new(string) {
187187- Self::Language(lang)
188188- } else if let Ok(tid) = Tid::from_str(string) {
189189- Self::Tid(tid)
190190- } else if let Ok(did) = Did::new_owned(string) {
191191- Self::Did(did)
192192- } else if let Ok(handle) = Handle::new_owned(string) {
193193- Self::Handle(handle)
194194- } else if let Ok(atid) = AtIdentifier::new_owned(string) {
195195- Self::AtIdentifier(atid)
196196- } else if let Ok(nsid) = Nsid::new_owned(string) {
197197- Self::Nsid(nsid)
198198- } else if let Ok(aturi) = AtUri::new_owned(string) {
199199- Self::AtUri(aturi)
200200- } else if let Ok(uri) = UriValue::new_owned(string) {
201201- Self::Uri(uri)
202202- } else if let Ok(cid) = Cid::new_owned(string.as_bytes()) {
203203- Self::Cid(cid)
204204- } else {
205205- // We don't construct record keys from bare strings because the type is too permissive
206206- Self::String(CowStr::Owned(string.to_smolstr()))
207207- }
208208- }
209209-}
210210-211211-impl<'s> AsRef<str> for AtprotoStr<'s> {
186186+impl<S: Bos<str> + AsRef<str> + Clone + Serialize> AsRef<str> for AtprotoStr<S> {
212187 fn as_ref(&self) -> &str {
213213- match self {
214214- Self::Datetime(datetime) => datetime.as_str(),
215215- Self::Language(lang) => lang.as_ref(),
216216- Self::Tid(tid) => tid.as_ref(),
217217- Self::Did(did) => did.as_ref(),
218218- Self::Handle(handle) => handle.as_ref(),
219219- Self::AtIdentifier(atid) => atid.as_ref(),
220220- Self::Nsid(nsid) => nsid.as_ref(),
221221- Self::AtUri(aturi) => aturi.as_ref(),
222222- Self::Uri(uri) => uri.as_str(),
223223- Self::Cid(cid) => cid.as_ref(),
224224- Self::RecordKey(rkey) => rkey.as_ref(),
225225- Self::String(string) => string.as_ref(),
226226- }
188188+ self.as_str()
227189 }
228190}
229191230230-impl Serialize for AtprotoStr<'_> {
231231- fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
192192+impl<S: Bos<str> + AsRef<str> + Clone + Serialize> Serialize for AtprotoStr<S> {
193193+ fn serialize<Ser>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error>
232194 where
233233- S: Serializer,
195195+ Ser: Serializer,
234196 {
235235- serializer.serialize_str(self.as_ref())
197197+ serializer.serialize_str(self.as_str())
236198 }
237199}
238200239239-impl<'de, 'a> Deserialize<'de> for AtprotoStr<'a>
201201+impl<'de, S> Deserialize<'de> for AtprotoStr<S>
240202where
241241- 'de: 'a,
203203+ S: Bos<str> + AsRef<str> + Clone + Serialize + Deserialize<'de>,
242204{
243205 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
244206 where
245207 D: Deserializer<'de>,
246208 {
247247- let value = Deserialize::deserialize(deserializer)?;
209209+ let value = S::deserialize(deserializer)?;
248210 Ok(Self::new(value))
249211 }
250212}
251213252252-impl IntoStatic for AtprotoStr<'_> {
253253- type Output = AtprotoStr<'static>;
214214+impl<S: Bos<str> + AsRef<str> + Clone + Serialize + IntoStatic> IntoStatic for AtprotoStr<S>
215215+where
216216+ S::Output: Bos<str> + AsRef<str> + Clone + Serialize,
217217+{
218218+ type Output = AtprotoStr<S::Output>;
254219255220 fn into_static(self) -> Self::Output {
256221 match self {
···261226 AtprotoStr::Did(did) => AtprotoStr::Did(did.into_static()),
262227 AtprotoStr::Handle(handle) => AtprotoStr::Handle(handle.into_static()),
263228 AtprotoStr::AtIdentifier(ident) => AtprotoStr::AtIdentifier(ident.into_static()),
264264- AtprotoStr::AtUri(at_uri) => AtprotoStr::AtUri(at_uri.into_static()),
265265- AtprotoStr::Uri(uri) => AtprotoStr::Uri(uri.into_static()),
229229+ // AtUri and UriValue are already 'static in this enum.
230230+ AtprotoStr::AtUri(at_uri) => AtprotoStr::AtUri(at_uri),
231231+ AtprotoStr::Uri(uri) => AtprotoStr::Uri(uri),
266232 AtprotoStr::Cid(cid) => AtprotoStr::Cid(cid.into_static()),
267233 AtprotoStr::RecordKey(record_key) => AtprotoStr::RecordKey(record_key.into_static()),
268268- AtprotoStr::String(cow_str) => AtprotoStr::String(cow_str.into_static()),
234234+ AtprotoStr::String(s) => AtprotoStr::String(s.into_static()),
269235 }
270236 }
271237}
272238273273-impl From<AtprotoStr<'_>> for String {
274274- fn from(value: AtprotoStr<'_>) -> Self {
275275- match value {
276276- AtprotoStr::AtIdentifier(ident) => ident.to_string(),
277277- AtprotoStr::AtUri(at_uri) => at_uri.to_string(),
278278- AtprotoStr::Uri(uri) => match uri {
279279- UriValue::At(at_uri) => at_uri.to_string(),
280280- UriValue::Cid(cid) => cid.to_string(),
281281- UriValue::Did(did) => did.to_string(),
282282- UriValue::Https(url) => url.to_string(),
283283- UriValue::Wss(url) => url.to_string(),
284284- UriValue::Any(cow_str) => cow_str.to_string(),
285285- },
286286- AtprotoStr::Cid(cid) => cid.to_string(),
287287- AtprotoStr::RecordKey(record_key) => record_key.as_ref().to_string(),
288288- AtprotoStr::String(cow_str) => cow_str.to_string(),
289289- AtprotoStr::Datetime(datetime) => datetime.to_string(),
290290- AtprotoStr::Language(language) => language.to_string(),
291291- AtprotoStr::Tid(tid) => tid.to_string(),
292292- AtprotoStr::Nsid(nsid) => nsid.to_string(),
293293- AtprotoStr::Did(did) => did.to_string(),
294294- AtprotoStr::Handle(handle) => handle.to_string(),
295295- }
239239+impl<S: Bos<str> + AsRef<str> + Clone + Serialize> From<AtprotoStr<S>> for String {
240240+ fn from(value: AtprotoStr<S>) -> Self {
241241+ value.as_str().to_string()
296242 }
297243}
298244
+23-10
crates/jacquard-common/src/types/uri.rs
···2828 /// WebSocket Secure URL
2929 Wss(Uri<String>),
3030 /// IPLD CID URI
3131- Cid(Cid<'u>),
3131+ Cid(Cid<CowStr<'u>>),
3232 /// Unrecognized URI scheme (catch-all)
3333 Any(CowStr<'u>),
3434}
···6060 } else if uri.starts_with("wss://") {
6161 Ok(UriValue::Wss(Uri::parse(uri)?.to_owned()))
6262 } else if uri.starts_with("ipld://") {
6363- match Cid::from_str(&uri[7..]) {
6464- Ok(cid) => Ok(UriValue::Cid(cid)),
6565- Err(_) => Ok(UriValue::Any(CowStr::Borrowed(uri))),
6363+ // Borrow the slice after "ipld://" prefix (7 bytes) from the input &'u str.
6464+ let cid_part = &uri[7..];
6565+ if cid_part.is_empty() {
6666+ Ok(UriValue::Any(CowStr::Borrowed(uri)))
6767+ } else {
6868+ Ok(UriValue::Cid(Cid::cow_str(CowStr::Borrowed(cid_part))))
6669 }
6770 } else {
6871 Ok(UriValue::Any(CowStr::Borrowed(uri)))
···8184 } else if uri.starts_with("wss://") {
8285 Ok(UriValue::Wss(Uri::parse(uri)?.to_owned()))
8386 } else if uri.starts_with("ipld://") {
8484- match Cid::from_str(&uri[7..]) {
8585- Ok(cid) => Ok(UriValue::Cid(cid)),
8686- Err(_) => Ok(UriValue::Any(CowStr::Owned(uri.to_smolstr()))),
8787+ // Owned context: use SmolStr via CowStr::Owned.
8888+ let cid_part = &uri[7..];
8989+ if cid_part.is_empty() {
9090+ Ok(UriValue::Any(CowStr::Owned(uri.to_smolstr())))
9191+ } else {
9292+ Ok(UriValue::Cid(Cid::cow_str(CowStr::Owned(cid_part.to_smolstr()))))
8793 }
8894 } else {
8995 Ok(UriValue::Any(CowStr::Owned(uri.to_smolstr())))
···101107 } else if uri.starts_with("wss://") {
102108 Ok(UriValue::Wss(Uri::parse(uri.as_ref())?.to_owned()))
103109 } else if uri.starts_with("ipld://") {
104104- match Cid::from_str(&uri.as_str()[7..]) {
105105- Ok(cid) => Ok(UriValue::Cid(cid)),
106106- Err(_) => Ok(UriValue::Any(uri)),
110110+ // Determine whether the CID part (after "ipld://") is non-empty before consuming uri.
111111+ if uri.as_ref()[7..].is_empty() {
112112+ Ok(UriValue::Any(uri))
113113+ } else {
114114+ // Build a CowStr for the CID part, preserving the ownership variant.
115115+ let cid_cow: CowStr<'u> = match uri {
116116+ CowStr::Borrowed(s) => CowStr::Borrowed(&s[7..]),
117117+ CowStr::Owned(ref s) => CowStr::Owned(s[7..].to_smolstr()),
118118+ };
119119+ Ok(UriValue::Cid(Cid::cow_str(cid_cow)))
107120 }
108121 } else {
109122 Ok(UriValue::Any(uri))
+6-6
crates/jacquard-common/src/types/value.rs
···3939 /// Integer value (no floats in AT Protocol)
4040 Integer(i64),
4141 /// String value (parsed into specific AT Protocol types when possible)
4242- String(AtprotoStr<'s>),
4242+ String(AtprotoStr<CowStr<'s>>),
4343 /// Raw bytes
4444 Bytes(Bytes),
4545 /// CID link reference
4646- CidLink(Cid<'s>),
4646+ CidLink(Cid<CowStr<'s>>),
4747 /// Array of values
4848 Array(Array<'s>),
4949 /// Object/map of values
5050 Object(Object<'s>),
5151 /// Blob reference with metadata
5252- Blob(Blob<'s>),
5252+ Blob(Blob<CowStr<'s>>),
5353}
54545555/// Errors that can occur when working with AT Protocol data
···170170 }
171171172172 /// Get as string if this is a String variant
173173- pub fn as_str_mut(&'s mut self) -> Option<&'s mut AtprotoStr<'s>> {
173173+ pub fn as_str_mut(&'s mut self) -> Option<&'s mut AtprotoStr<CowStr<'s>>> {
174174 if let Data::String(s) = self {
175175 Some(s)
176176 } else {
···609609 /// Raw bytes
610610 Bytes(Bytes),
611611 /// CID link reference
612612- CidLink(Cid<'s>),
612612+ CidLink(Cid<CowStr<'s>>),
613613 /// Array of raw values
614614 Array(Vec<RawData<'s>>),
615615 /// Object/map of raw values
616616 Object(BTreeMap<SmolStr, RawData<'s>>),
617617 /// Valid blob reference
618618- Blob(Blob<'s>),
618618+ Blob(Blob<CowStr<'s>>),
619619 /// Invalid blob structure (captured for debugging)
620620 InvalidBlob(Box<RawData<'s>>),
621621 /// Invalid number format, generally a floating point number (captured as bytes)