···2626#[path = "generated/macro_mode/lib.rs"]
2727pub mod macro_mode;
28282929+// Serde spike: empirical validation for borrow-or-share type param + serde interaction.
3030+mod serde_spike;
3131+2932#[cfg(test)]
3033mod tests {
3134 // -- Pretty mode type accessibility --
+705
crates/jacquard-codegen-tests/src/serde_spike.rs
···11+//! Serde spike: empirical validation of serde behaviour with type-parameterised structs.
22+//!
33+//! This module answers three questions from the borrow-or-share design plan:
44+//!
55+//! 1. Does `#[serde(borrow)]` on an `S`-typed field prevent `DeserializeOwned` when `S = SmolStr`?
66+//! **Answer: YES.** `#[serde(borrow)]` is sugar for `#[serde(bound(deserialize = "'de: 'a"))]`
77+//! and requires the field type to contain a lifetime. Type params like `S` have no lifetime,
88+//! so the macro rejects it outright. Even if it didn't, the injected bound would prevent
99+//! `DeserializeOwned`. Strategy A is dead.
1010+//!
1111+//! 2. Does `Deserialize<'de>` work for `S = &'de str` without `#[serde(borrow)]`?
1212+//! **Tested below** in strategies B and C.
1313+//!
1414+//! 3. What serde attribute combinations should codegen emit?
1515+//! **Tested below** — strategies B (no attrs) and C (explicit bounds) are the candidates.
1616+1717+use alloc::collections::BTreeMap;
1818+1919+use serde::{Deserialize, Serialize};
2020+use smol_str::SmolStr;
2121+2222+// ---------------------------------------------------------------------------
2323+// Minimal Bos/BorrowOrShare trait copies (will live in jacquard-common later)
2424+// ---------------------------------------------------------------------------
2525+2626+mod bos {
2727+ mod internal {
2828+ pub trait Ref<T: ?Sized> {
2929+ fn cast<'a>(self) -> &'a T
3030+ where
3131+ Self: 'a;
3232+ }
3333+3434+ impl<T: ?Sized> Ref<T> for &T {
3535+ #[inline]
3636+ fn cast<'a>(self) -> &'a T
3737+ where
3838+ Self: 'a,
3939+ {
4040+ self
4141+ }
4242+ }
4343+ }
4444+4545+ use alloc::borrow::ToOwned;
4646+4747+ use internal::Ref;
4848+4949+ /// Borrow or share — the base trait with a GAT for the reference type.
5050+ pub trait Bos<T: ?Sized> {
5151+ type Ref<'this>: Ref<T>
5252+ where
5353+ Self: 'this;
5454+5555+ fn borrow_or_share(this: &Self) -> Self::Ref<'_>;
5656+ }
5757+5858+ /// Convenience trait with split lifetimes for borrowed vs shared access.
5959+ pub trait BorrowOrShare<'i, 'o, T: ?Sized>: Bos<T> {
6060+ fn borrow_or_share(&'i self) -> &'o T;
6161+ }
6262+6363+ impl<'i, 'o, T: ?Sized, B> BorrowOrShare<'i, 'o, T> for B
6464+ where
6565+ B: Bos<T> + ?Sized + 'i,
6666+ B::Ref<'i>: 'o,
6767+ {
6868+ #[inline]
6969+ fn borrow_or_share(&'i self) -> &'o T {
7070+ (B::borrow_or_share(self) as B::Ref<'i>).cast()
7171+ }
7272+ }
7373+7474+ // --- Implementations ---
7575+7676+ impl<'a, T: ?Sized> Bos<T> for &'a T {
7777+ type Ref<'this>
7878+ = &'a T
7979+ where
8080+ Self: 'this;
8181+8282+ #[inline]
8383+ fn borrow_or_share(this: &Self) -> Self::Ref<'_> {
8484+ this
8585+ }
8686+ }
8787+8888+ impl Bos<str> for smol_str::SmolStr {
8989+ type Ref<'this> = &'this str;
9090+9191+ #[inline]
9292+ fn borrow_or_share(this: &Self) -> Self::Ref<'_> {
9393+ this.as_str()
9494+ }
9595+ }
9696+9797+ impl Bos<str> for String {
9898+ type Ref<'this> = &'this str;
9999+100100+ #[inline]
101101+ fn borrow_or_share(this: &Self) -> Self::Ref<'_> {
102102+ this.as_str()
103103+ }
104104+ }
105105+106106+ impl<'a, B: ?Sized + ToOwned> Bos<B> for alloc::borrow::Cow<'a, B> {
107107+ type Ref<'this>
108108+ = &'this B
109109+ where
110110+ Self: 'this;
111111+112112+ #[inline]
113113+ fn borrow_or_share(this: &Self) -> Self::Ref<'_> {
114114+ this.as_ref()
115115+ }
116116+ }
117117+118118+ impl<'a> Bos<str> for jacquard_common::cowstr::CowStr<'a> {
119119+ type Ref<'this>
120120+ = &'this str
121121+ where
122122+ Self: 'this;
123123+124124+ #[inline]
125125+ fn borrow_or_share(this: &Self) -> Self::Ref<'_> {
126126+ this.as_str()
127127+ }
128128+ }
129129+}
130130+131131+use bos::Bos;
132132+133133+// ---------------------------------------------------------------------------
134134+// Strategy B: no serde attributes at all — let serde derive infer everything
135135+// ---------------------------------------------------------------------------
136136+137137+/// Flat struct with no serde annotations on fields.
138138+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
139139+pub struct FlatNoBorrow<S: Bos<str> = SmolStr> {
140140+ pub name: S,
141141+ pub label: Option<S>,
142142+ pub tags: Vec<S>,
143143+}
144144+145145+/// Nested struct containing `FlatNoBorrow<S>`.
146146+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
147147+pub struct NestedNoBorrow<S: Bos<str> = SmolStr> {
148148+ pub inner: FlatNoBorrow<S>,
149149+ pub count: u32,
150150+}
151151+152152+/// Struct with `BTreeMap<SmolStr, S>` — mixed ownership (keys always SmolStr).
153153+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
154154+pub struct WithMapNoBorrow<S: Bos<str> = SmolStr> {
155155+ pub title: S,
156156+ pub metadata: BTreeMap<SmolStr, S>,
157157+}
158158+159159+// ---------------------------------------------------------------------------
160160+// Strategy C: explicit #[serde(bound(...))] — override serde's inferred bounds
161161+// ---------------------------------------------------------------------------
162162+163163+/// Flat struct with explicit serde bounds.
164164+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
165165+#[serde(bound(serialize = "S: Serialize", deserialize = "S: Deserialize<'de>"))]
166166+pub struct FlatExplicitBound<S: Bos<str> = SmolStr> {
167167+ pub name: S,
168168+ pub label: Option<S>,
169169+ pub tags: Vec<S>,
170170+}
171171+172172+/// Nested struct with explicit serde bounds.
173173+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
174174+#[serde(bound(serialize = "S: Serialize", deserialize = "S: Deserialize<'de>"))]
175175+pub struct NestedExplicitBound<S: Bos<str> = SmolStr> {
176176+ pub inner: FlatExplicitBound<S>,
177177+ pub count: u32,
178178+}
179179+180180+/// Map struct with explicit serde bounds.
181181+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
182182+#[serde(bound(serialize = "S: Serialize", deserialize = "S: Deserialize<'de>"))]
183183+pub struct WithMapExplicitBound<S: Bos<str> = SmolStr> {
184184+ pub title: S,
185185+ pub metadata: BTreeMap<SmolStr, S>,
186186+}
187187+188188+// ---------------------------------------------------------------------------
189189+// Tests
190190+// ---------------------------------------------------------------------------
191191+192192+#[cfg(test)]
193193+mod tests {
194194+ use super::*;
195195+ use jacquard_common::cowstr::CowStr;
196196+ use serde::de::DeserializeOwned;
197197+198198+ const TEST_JSON: &str = r#"{
199199+ "name": "alice",
200200+ "label": "admin",
201201+ "tags": ["rust", "atproto"]
202202+ }"#;
203203+204204+ const TEST_NESTED_JSON: &str = r#"{
205205+ "inner": {
206206+ "name": "alice",
207207+ "label": "admin",
208208+ "tags": ["rust", "atproto"]
209209+ },
210210+ "count": 42
211211+ }"#;
212212+213213+ const TEST_MAP_JSON: &str = r#"{
214214+ "title": "hello",
215215+ "metadata": {
216216+ "key1": "val1",
217217+ "key2": "val2"
218218+ }
219219+ }"#;
220220+221221+ // -----------------------------------------------------------------------
222222+ // Compile-time assertions
223223+ // -----------------------------------------------------------------------
224224+225225+ fn assert_deserialize_owned<T: DeserializeOwned>() {}
226226+ fn assert_deserialize<'de, T: Deserialize<'de>>() {}
227227+228228+ // ===== Strategy B: no attributes =====
229229+230230+ #[test]
231231+ fn strategy_b_smolstr_deserialize_owned() {
232232+ assert_deserialize_owned::<FlatNoBorrow<SmolStr>>();
233233+ assert_deserialize_owned::<NestedNoBorrow<SmolStr>>();
234234+ assert_deserialize_owned::<WithMapNoBorrow<SmolStr>>();
235235+ }
236236+237237+ #[test]
238238+ fn strategy_b_string_deserialize_owned() {
239239+ assert_deserialize_owned::<FlatNoBorrow<String>>();
240240+ assert_deserialize_owned::<NestedNoBorrow<String>>();
241241+ assert_deserialize_owned::<WithMapNoBorrow<String>>();
242242+ }
243243+244244+ #[test]
245245+ fn strategy_b_borrowed_deserialize() {
246246+ // Does &str satisfy Deserialize<'de> via strategy B (no attrs)?
247247+ assert_deserialize::<FlatNoBorrow<&str>>();
248248+ assert_deserialize::<NestedNoBorrow<&str>>();
249249+ assert_deserialize::<WithMapNoBorrow<&str>>();
250250+ }
251251+252252+ // CowStr compile-time shape tests.
253253+ //
254254+ // We can't use assert_deserialize/assert_deserialize_owned for CowStr because:
255255+ // - CowStr<'static> does NOT satisfy DeserializeOwned (the Deserialize impl
256256+ // has 'de: 'a, and Rust can't specialise that away when 'a = 'static)
257257+ // - CowStr<'_> with an elided lifetime can't relate to the 'de on the helper
258258+ //
259259+ // Instead we prove the shape compiles by writing functions with the right
260260+ // lifetime relationship. The runtime tests below exercise actual behaviour.
261261+262262+ #[allow(dead_code)]
263263+ fn cowstr_deserialize_shape_b(input: &str) -> FlatNoBorrow<CowStr<'_>> {
264264+ serde_json::from_str(input).unwrap()
265265+ }
266266+267267+ #[allow(dead_code)]
268268+ fn cowstr_nested_deserialize_shape_b(input: &str) -> NestedNoBorrow<CowStr<'_>> {
269269+ serde_json::from_str(input).unwrap()
270270+ }
271271+272272+ // ===== Strategy C: explicit bounds =====
273273+274274+ #[test]
275275+ fn strategy_c_smolstr_deserialize_owned() {
276276+ assert_deserialize_owned::<FlatExplicitBound<SmolStr>>();
277277+ assert_deserialize_owned::<NestedExplicitBound<SmolStr>>();
278278+ assert_deserialize_owned::<WithMapExplicitBound<SmolStr>>();
279279+ }
280280+281281+ #[test]
282282+ fn strategy_c_string_deserialize_owned() {
283283+ assert_deserialize_owned::<FlatExplicitBound<String>>();
284284+ assert_deserialize_owned::<NestedExplicitBound<String>>();
285285+ assert_deserialize_owned::<WithMapExplicitBound<String>>();
286286+ }
287287+288288+ #[test]
289289+ fn strategy_c_borrowed_deserialize() {
290290+ assert_deserialize::<FlatExplicitBound<&str>>();
291291+ assert_deserialize::<NestedExplicitBound<&str>>();
292292+ assert_deserialize::<WithMapExplicitBound<&str>>();
293293+ }
294294+295295+ // CowStr shape tests for strategy C (same limitation as B).
296296+297297+ #[allow(dead_code)]
298298+ fn cowstr_deserialize_shape_c(input: &str) -> FlatExplicitBound<CowStr<'_>> {
299299+ serde_json::from_str(input).unwrap()
300300+ }
301301+302302+ #[allow(dead_code)]
303303+ fn cowstr_nested_deserialize_shape_c(input: &str) -> NestedExplicitBound<CowStr<'_>> {
304304+ serde_json::from_str(input).unwrap()
305305+ }
306306+307307+ // -----------------------------------------------------------------------
308308+ // Runtime: JSON roundtrips — Strategy B
309309+ // -----------------------------------------------------------------------
310310+311311+ #[test]
312312+ fn strategy_b_json_roundtrip_flat_smolstr() {
313313+ let parsed: FlatNoBorrow<SmolStr> = serde_json::from_str(TEST_JSON).unwrap();
314314+ assert_eq!(parsed.name, SmolStr::new("alice"));
315315+ assert_eq!(parsed.label, Some(SmolStr::new("admin")));
316316+ assert_eq!(
317317+ parsed.tags,
318318+ vec![SmolStr::new("rust"), SmolStr::new("atproto")]
319319+ );
320320+321321+ let json = serde_json::to_string(&parsed).unwrap();
322322+ let reparsed: FlatNoBorrow<SmolStr> = serde_json::from_str(&json).unwrap();
323323+ assert_eq!(parsed, reparsed);
324324+ }
325325+326326+ #[test]
327327+ fn strategy_b_json_roundtrip_nested_smolstr() {
328328+ let parsed: NestedNoBorrow<SmolStr> = serde_json::from_str(TEST_NESTED_JSON).unwrap();
329329+ assert_eq!(parsed.inner.name, SmolStr::new("alice"));
330330+ assert_eq!(parsed.count, 42);
331331+332332+ let json = serde_json::to_string(&parsed).unwrap();
333333+ let reparsed: NestedNoBorrow<SmolStr> = serde_json::from_str(&json).unwrap();
334334+ assert_eq!(parsed, reparsed);
335335+ }
336336+337337+ #[test]
338338+ fn strategy_b_json_roundtrip_map_smolstr() {
339339+ let parsed: WithMapNoBorrow<SmolStr> = serde_json::from_str(TEST_MAP_JSON).unwrap();
340340+ assert_eq!(parsed.title, SmolStr::new("hello"));
341341+ assert_eq!(
342342+ parsed.metadata.get(&SmolStr::new("key1")),
343343+ Some(&SmolStr::new("val1"))
344344+ );
345345+346346+ let json = serde_json::to_string(&parsed).unwrap();
347347+ let reparsed: WithMapNoBorrow<SmolStr> = serde_json::from_str(&json).unwrap();
348348+ assert_eq!(parsed, reparsed);
349349+ }
350350+351351+ #[test]
352352+ fn strategy_b_json_roundtrip_flat_string() {
353353+ let parsed: FlatNoBorrow<String> = serde_json::from_str(TEST_JSON).unwrap();
354354+ assert_eq!(parsed.name, "alice");
355355+356356+ let json = serde_json::to_string(&parsed).unwrap();
357357+ let reparsed: FlatNoBorrow<String> = serde_json::from_str(&json).unwrap();
358358+ assert_eq!(parsed, reparsed);
359359+ }
360360+361361+ #[test]
362362+ fn strategy_b_json_borrowed_flat() {
363363+ let parsed: FlatNoBorrow<&str> = serde_json::from_str(TEST_JSON).unwrap();
364364+ assert_eq!(parsed.name, "alice");
365365+ assert_eq!(parsed.label, Some("admin"));
366366+ assert_eq!(parsed.tags, vec!["rust", "atproto"]);
367367+ }
368368+369369+ #[test]
370370+ fn strategy_b_json_borrowed_nested() {
371371+ let parsed: NestedNoBorrow<&str> = serde_json::from_str(TEST_NESTED_JSON).unwrap();
372372+ assert_eq!(parsed.inner.name, "alice");
373373+ assert_eq!(parsed.count, 42);
374374+ }
375375+376376+ #[test]
377377+ fn strategy_b_json_borrowed_map() {
378378+ let parsed: WithMapNoBorrow<&str> = serde_json::from_str(TEST_MAP_JSON).unwrap();
379379+ assert_eq!(parsed.title, "hello");
380380+ assert_eq!(parsed.metadata.get(&SmolStr::new("key1")), Some(&"val1"));
381381+ }
382382+383383+ #[test]
384384+ fn strategy_b_json_cowstr() {
385385+ let parsed: FlatNoBorrow<CowStr> = serde_json::from_str(TEST_JSON).unwrap();
386386+ assert_eq!(parsed.name.as_str(), "alice");
387387+ assert_eq!(parsed.label.as_ref().map(|c| c.as_str()), Some("admin"));
388388+389389+ let json = serde_json::to_string(&parsed).unwrap();
390390+ let reparsed: FlatNoBorrow<CowStr> = serde_json::from_str(&json).unwrap();
391391+ assert_eq!(parsed, reparsed);
392392+ }
393393+394394+ // -----------------------------------------------------------------------
395395+ // Runtime: JSON roundtrips — Strategy C
396396+ // -----------------------------------------------------------------------
397397+398398+ #[test]
399399+ fn strategy_c_json_roundtrip_flat_smolstr() {
400400+ let parsed: FlatExplicitBound<SmolStr> = serde_json::from_str(TEST_JSON).unwrap();
401401+ assert_eq!(parsed.name, SmolStr::new("alice"));
402402+403403+ let json = serde_json::to_string(&parsed).unwrap();
404404+ let reparsed: FlatExplicitBound<SmolStr> = serde_json::from_str(&json).unwrap();
405405+ assert_eq!(parsed, reparsed);
406406+ }
407407+408408+ #[test]
409409+ fn strategy_c_json_roundtrip_nested_smolstr() {
410410+ let parsed: NestedExplicitBound<SmolStr> = serde_json::from_str(TEST_NESTED_JSON).unwrap();
411411+ assert_eq!(parsed.inner.name, SmolStr::new("alice"));
412412+ assert_eq!(parsed.count, 42);
413413+414414+ let json = serde_json::to_string(&parsed).unwrap();
415415+ let reparsed: NestedExplicitBound<SmolStr> = serde_json::from_str(&json).unwrap();
416416+ assert_eq!(parsed, reparsed);
417417+ }
418418+419419+ #[test]
420420+ fn strategy_c_json_borrowed_flat() {
421421+ let parsed: FlatExplicitBound<&str> = serde_json::from_str(TEST_JSON).unwrap();
422422+ assert_eq!(parsed.name, "alice");
423423+ assert_eq!(parsed.label, Some("admin"));
424424+ assert_eq!(parsed.tags, vec!["rust", "atproto"]);
425425+ }
426426+427427+ #[test]
428428+ fn strategy_c_json_borrowed_nested() {
429429+ let parsed: NestedExplicitBound<&str> = serde_json::from_str(TEST_NESTED_JSON).unwrap();
430430+ assert_eq!(parsed.inner.name, "alice");
431431+ assert_eq!(parsed.count, 42);
432432+ }
433433+434434+ #[test]
435435+ fn strategy_c_json_cowstr() {
436436+ let parsed: FlatExplicitBound<CowStr> = serde_json::from_str(TEST_JSON).unwrap();
437437+ assert_eq!(parsed.name.as_str(), "alice");
438438+439439+ let json = serde_json::to_string(&parsed).unwrap();
440440+ let reparsed: FlatExplicitBound<CowStr> = serde_json::from_str(&json).unwrap();
441441+ assert_eq!(parsed, reparsed);
442442+ }
443443+444444+ // -----------------------------------------------------------------------
445445+ // DAG-CBOR roundtrips — Strategy B (if JSON works, CBOR should too)
446446+ // -----------------------------------------------------------------------
447447+448448+ #[test]
449449+ fn strategy_b_dagcbor_roundtrip_flat_smolstr() {
450450+ let original = FlatNoBorrow {
451451+ name: SmolStr::new("alice"),
452452+ label: Some(SmolStr::new("admin")),
453453+ tags: vec![SmolStr::new("rust"), SmolStr::new("atproto")],
454454+ };
455455+456456+ let bytes = serde_ipld_dagcbor::to_vec(&original).unwrap();
457457+ let parsed: FlatNoBorrow<SmolStr> = serde_ipld_dagcbor::from_slice(&bytes).unwrap();
458458+ assert_eq!(original, parsed);
459459+ }
460460+461461+ #[test]
462462+ fn strategy_b_dagcbor_roundtrip_flat_string() {
463463+ let original = FlatNoBorrow {
464464+ name: String::from("alice"),
465465+ label: Some(String::from("admin")),
466466+ tags: vec![String::from("rust"), String::from("atproto")],
467467+ };
468468+469469+ let bytes = serde_ipld_dagcbor::to_vec(&original).unwrap();
470470+ let parsed: FlatNoBorrow<String> = serde_ipld_dagcbor::from_slice(&bytes).unwrap();
471471+ assert_eq!(original, parsed);
472472+ }
473473+474474+ #[test]
475475+ fn strategy_b_dagcbor_roundtrip_nested_smolstr() {
476476+ let original = NestedNoBorrow {
477477+ inner: FlatNoBorrow {
478478+ name: SmolStr::new("bob"),
479479+ label: None,
480480+ tags: vec![],
481481+ },
482482+ count: 99,
483483+ };
484484+485485+ let bytes = serde_ipld_dagcbor::to_vec(&original).unwrap();
486486+ let parsed: NestedNoBorrow<SmolStr> = serde_ipld_dagcbor::from_slice(&bytes).unwrap();
487487+ assert_eq!(original, parsed);
488488+ }
489489+490490+ #[test]
491491+ fn strategy_b_dagcbor_borrowed_flat() {
492492+ // DAG-CBOR stores strings as CBOR text strings. Whether borrowed
493493+ // deserialization works depends on whether the deserializer calls
494494+ // visit_borrowed_str. This test documents the actual behaviour.
495495+ let original = FlatNoBorrow {
496496+ name: SmolStr::new("alice"),
497497+ label: Some(SmolStr::new("admin")),
498498+ tags: vec![SmolStr::new("rust")],
499499+ };
500500+501501+ let bytes = serde_ipld_dagcbor::to_vec(&original).unwrap();
502502+ let result: Result<FlatNoBorrow<&str>, _> = serde_ipld_dagcbor::from_slice(&bytes);
503503+504504+ if let Ok(parsed) = &result {
505505+ assert_eq!(parsed.name, "alice");
506506+ }
507507+508508+ // Document the finding regardless of outcome.
509509+ eprintln!(
510510+ "dagcbor borrowed &str deserialization: {}",
511511+ if result.is_ok() {
512512+ "WORKS"
513513+ } else {
514514+ "FAILS (expected — CBOR deserializer may not support borrowing)"
515515+ }
516516+ );
517517+ }
518518+519519+ // -----------------------------------------------------------------------
520520+ // DAG-CBOR — Strategy C
521521+ // -----------------------------------------------------------------------
522522+523523+ #[test]
524524+ fn strategy_c_dagcbor_roundtrip_flat_smolstr() {
525525+ let original = FlatExplicitBound {
526526+ name: SmolStr::new("alice"),
527527+ label: Some(SmolStr::new("admin")),
528528+ tags: vec![SmolStr::new("rust"), SmolStr::new("atproto")],
529529+ };
530530+531531+ let bytes = serde_ipld_dagcbor::to_vec(&original).unwrap();
532532+ let parsed: FlatExplicitBound<SmolStr> = serde_ipld_dagcbor::from_slice(&bytes).unwrap();
533533+ assert_eq!(original, parsed);
534534+ }
535535+536536+ #[test]
537537+ fn strategy_c_dagcbor_roundtrip_nested_smolstr() {
538538+ let original = NestedExplicitBound {
539539+ inner: FlatExplicitBound {
540540+ name: SmolStr::new("bob"),
541541+ label: None,
542542+ tags: vec![],
543543+ },
544544+ count: 99,
545545+ };
546546+547547+ let bytes = serde_ipld_dagcbor::to_vec(&original).unwrap();
548548+ let parsed: NestedExplicitBound<SmolStr> = serde_ipld_dagcbor::from_slice(&bytes).unwrap();
549549+ assert_eq!(original, parsed);
550550+ }
551551+552552+ // -----------------------------------------------------------------------
553553+ // Zero-copy verification: prove borrowed &str points into the input buffer
554554+ // -----------------------------------------------------------------------
555555+556556+ /// Returns true if `s` points into the memory range of `buf`.
557557+ fn points_into(s: &str, buf: &str) -> bool {
558558+ let buf_start = buf.as_ptr() as usize;
559559+ let buf_end = buf_start + buf.len();
560560+ let s_start = s.as_ptr() as usize;
561561+ s_start >= buf_start && s_start + s.len() <= buf_end
562562+ }
563563+564564+ /// Same as above but for byte slices.
565565+ fn points_into_bytes(s: &str, buf: &[u8]) -> bool {
566566+ let buf_start = buf.as_ptr() as usize;
567567+ let buf_end = buf_start + buf.len();
568568+ let s_start = s.as_ptr() as usize;
569569+ s_start >= buf_start && s_start + s.len() <= buf_end
570570+ }
571571+572572+ #[test]
573573+ fn json_borrowed_str_is_zero_copy() {
574574+ let input = r#"{"name":"alice","label":"admin","tags":["rust","atproto"]}"#;
575575+ let parsed: FlatNoBorrow<&str> = serde_json::from_str(input).unwrap();
576576+577577+ assert!(
578578+ points_into(parsed.name, input),
579579+ "name should point into input buffer"
580580+ );
581581+ assert!(
582582+ points_into(parsed.label.unwrap(), input),
583583+ "label should point into input buffer"
584584+ );
585585+ for tag in &parsed.tags {
586586+ assert!(
587587+ points_into(tag, input),
588588+ "tag {:?} should point into input buffer",
589589+ tag
590590+ );
591591+ }
592592+ }
593593+594594+ #[test]
595595+ fn dagcbor_borrowed_str_is_zero_copy() {
596596+ let original = FlatNoBorrow {
597597+ name: SmolStr::new("alice"),
598598+ label: Some(SmolStr::new("admin")),
599599+ tags: vec![SmolStr::new("rust")],
600600+ };
601601+602602+ let bytes = serde_ipld_dagcbor::to_vec(&original).unwrap();
603603+ let parsed: FlatNoBorrow<&str> = serde_ipld_dagcbor::from_slice(&bytes).unwrap();
604604+605605+ assert!(
606606+ points_into_bytes(parsed.name, &bytes),
607607+ "name should point into CBOR buffer"
608608+ );
609609+ assert!(
610610+ points_into_bytes(parsed.label.unwrap(), &bytes),
611611+ "label should point into CBOR buffer"
612612+ );
613613+ for tag in &parsed.tags {
614614+ assert!(
615615+ points_into_bytes(tag, &bytes),
616616+ "tag {:?} should point into CBOR buffer",
617617+ tag
618618+ );
619619+ }
620620+ }
621621+622622+ #[test]
623623+ fn json_cowstr_borrows_from_input() {
624624+ // CowStr's Deserialize impl calls visit_borrowed_str -> CowStr::Borrowed,
625625+ // so when deserializing from &str the result should be zero-copy.
626626+ let input = r#"{"name":"alice","label":"admin","tags":["rust","atproto"]}"#;
627627+ let parsed: FlatNoBorrow<CowStr> = serde_json::from_str(input).unwrap();
628628+629629+ assert!(
630630+ matches!(parsed.name, CowStr::Borrowed(_)),
631631+ "name should be CowStr::Borrowed, got Owned"
632632+ );
633633+ assert!(
634634+ points_into(parsed.name.as_str(), input),
635635+ "name should point into input buffer"
636636+ );
637637+638638+ let label = parsed.label.unwrap();
639639+ assert!(
640640+ matches!(label, CowStr::Borrowed(_)),
641641+ "label should be CowStr::Borrowed, got Owned"
642642+ );
643643+ assert!(
644644+ points_into(label.as_str(), input),
645645+ "label should point into input buffer"
646646+ );
647647+648648+ for tag in &parsed.tags {
649649+ assert!(
650650+ matches!(tag, CowStr::Borrowed(_)),
651651+ "tag {:?} should be CowStr::Borrowed, got Owned",
652652+ tag.as_str()
653653+ );
654654+ assert!(
655655+ points_into(tag.as_str(), input),
656656+ "tag {:?} should point into input buffer",
657657+ tag.as_str()
658658+ );
659659+ }
660660+ }
661661+662662+ #[test]
663663+ fn dagcbor_cowstr_borrows_from_buffer() {
664664+ let original = FlatNoBorrow {
665665+ name: SmolStr::new("alice"),
666666+ label: Some(SmolStr::new("admin")),
667667+ tags: vec![SmolStr::new("rust")],
668668+ };
669669+670670+ let bytes = serde_ipld_dagcbor::to_vec(&original).unwrap();
671671+ let parsed: FlatNoBorrow<CowStr> = serde_ipld_dagcbor::from_slice(&bytes).unwrap();
672672+673673+ assert!(
674674+ matches!(parsed.name, CowStr::Borrowed(_)),
675675+ "name should be CowStr::Borrowed, got Owned"
676676+ );
677677+ assert!(
678678+ points_into_bytes(parsed.name.as_str(), &bytes),
679679+ "name should point into CBOR buffer"
680680+ );
681681+682682+ let label = parsed.label.unwrap();
683683+ assert!(
684684+ matches!(label, CowStr::Borrowed(_)),
685685+ "label should be CowStr::Borrowed, got Owned"
686686+ );
687687+ assert!(
688688+ points_into_bytes(label.as_str(), &bytes),
689689+ "label should point into CBOR buffer"
690690+ );
691691+692692+ for tag in &parsed.tags {
693693+ assert!(
694694+ matches!(tag, CowStr::Borrowed(_)),
695695+ "tag {:?} should be CowStr::Borrowed, got Owned",
696696+ tag.as_str()
697697+ );
698698+ assert!(
699699+ points_into_bytes(tag.as_str(), &bytes),
700700+ "tag {:?} should point into CBOR buffer",
701701+ tag.as_str()
702702+ );
703703+ }
704704+ }
705705+}
+264
crates/jacquard-common/src/bos.rs
···11+//! Borrow-or-share traits for abstracting over owned and borrowed string representations.
22+//!
33+//! This module is a vendored copy of the [`borrow-or-share`](https://docs.rs/borrow-or-share/0.2.4/)
44+//! crate by yescallop, with additional implementations for [`SmolStr`](smol_str::SmolStr)
55+//! and [`CowStr`](crate::CowStr). We vendor rather than depend on the crate to avoid
66+//! orphan rule issues. We need to implement `Bos<str>` for `SmolStr` and `CowStr`, which
77+//! are foreign types relative to the upstream crate.
88+//!
99+//! # Overview
1010+//!
1111+//! [`Bos<T>`] is the base trait providing a GAT for the reference type. Use it as a bound
1212+//! when you need a method that borrows from `*self` regardless of whether the backing type
1313+//! is owned or borrowed:
1414+//!
1515+//! ```ignore
1616+//! impl<T: Bos<str>> AsRef<str> for MyType<T> {
1717+//! fn as_ref(&self) -> &str {
1818+//! self.as_str()
1919+//! }
2020+//! }
2121+//! ```
2222+//!
2323+//! [`BorrowOrShare<'i, 'o, T>`] is the convenience trait with split lifetimes. Use it when
2424+//! you want a method on `&'i self` that returns `&'o T`, where `'o` may outlive `'i` when
2525+//! the backing type is a reference:
2626+//!
2727+//! ```ignore
2828+//! impl<'i, 'o, T: BorrowOrShare<'i, 'o, str>> MyType<T> {
2929+//! fn as_str(&'i self) -> &'o str {
3030+//! self.0.borrow_or_share()
3131+//! }
3232+//! }
3333+//! ```
3434+3535+use alloc::{
3636+ borrow::{Cow, ToOwned},
3737+ boxed::Box,
3838+ string::String,
3939+ vec::Vec,
4040+};
4141+4242+use smol_str::SmolStr;
4343+4444+use crate::CowStr;
4545+4646+mod internal {
4747+ pub trait Ref<T: ?Sized> {
4848+ fn cast<'a>(self) -> &'a T
4949+ where
5050+ Self: 'a;
5151+ }
5252+5353+ impl<T: ?Sized> Ref<T> for &T {
5454+ #[inline]
5555+ fn cast<'a>(self) -> &'a T
5656+ where
5757+ Self: 'a,
5858+ {
5959+ self
6060+ }
6161+ }
6262+}
6363+6464+use internal::Ref;
6565+6666+/// A trait for either borrowing or sharing data.
6767+///
6868+/// See the [module-level documentation](self) for more details.
6969+pub trait Bos<T: ?Sized> {
7070+ /// The resulting reference type. May only be `&T`.
7171+ type Ref<'this>: Ref<T>
7272+ where
7373+ Self: 'this;
7474+7575+ /// Borrows from `*this` or from behind a reference it holds,
7676+ /// returning a reference of type [`Self::Ref`].
7777+ ///
7878+ /// In the latter case, the returned reference is said to be *shared* with `*this`.
7979+ fn borrow_or_share(this: &Self) -> Self::Ref<'_>;
8080+}
8181+8282+/// A helper trait for writing "data borrowing or sharing" functions.
8383+///
8484+/// See the [module-level documentation](self) for more details.
8585+pub trait BorrowOrShare<'i, 'o, T: ?Sized>: Bos<T> {
8686+ /// Borrows from `*self` or from behind a reference it holds.
8787+ ///
8888+ /// In the latter case, the returned reference is said to be *shared* with `*self`.
8989+ fn borrow_or_share(&'i self) -> &'o T;
9090+}
9191+9292+impl<'i, 'o, T: ?Sized, B> BorrowOrShare<'i, 'o, T> for B
9393+where
9494+ B: Bos<T> + ?Sized + 'i,
9595+ B::Ref<'i>: 'o,
9696+{
9797+ #[inline]
9898+ fn borrow_or_share(&'i self) -> &'o T {
9999+ (B::borrow_or_share(self) as B::Ref<'i>).cast()
100100+ }
101101+}
102102+103103+// --- Reference impl (sharing) ---
104104+105105+impl<'a, T: ?Sized> Bos<T> for &'a T {
106106+ type Ref<'this>
107107+ = &'a T
108108+ where
109109+ Self: 'this;
110110+111111+ #[inline]
112112+ fn borrow_or_share(this: &Self) -> Self::Ref<'_> {
113113+ this
114114+ }
115115+}
116116+117117+// --- Macro for borrowing impls ---
118118+119119+/// Implement [`Bos`] for types that always borrow from `*self`.
120120+///
121121+/// Each entry maps a concrete type to the target slice/str type it derefs to.
122122+/// The generated impl uses `Ref<'this> = &'this $target` — pure borrowing, no sharing.
123123+#[macro_export]
124124+macro_rules! impl_bos {
125125+ ($($(#[$attr:meta])? $({$($params:tt)*})? $ty:ty => $target:ty)*) => {
126126+ $(
127127+ $(#[$attr])?
128128+ impl $(<$($params)*>)? $crate::bos::Bos<$target> for $ty {
129129+ type Ref<'this> = &'this $target where Self: 'this;
130130+131131+ #[inline]
132132+ fn borrow_or_share(this: &Self) -> Self::Ref<'_> {
133133+ this
134134+ }
135135+ }
136136+ )*
137137+ };
138138+}
139139+140140+// --- Standard library impls ---
141141+142142+impl_bos! {
143143+ {T: ?Sized} &mut T => T
144144+145145+ {T, const N: usize} [T; N] => [T]
146146+147147+ {T} Vec<T> => [T]
148148+149149+ String => str
150150+151151+ {T: ?Sized} Box<T> => T
152152+ {B: ?Sized + ToOwned} Cow<'_, B> => B
153153+154154+ {T: ?Sized} alloc::sync::Arc<T> => T
155155+ {T: ?Sized} alloc::rc::Rc<T> => T
156156+}
157157+158158+#[cfg(feature = "std")]
159159+impl_bos! {
160160+ std::ffi::OsString => std::ffi::OsStr
161161+ std::path::PathBuf => std::path::Path
162162+ alloc::ffi::CString => core::ffi::CStr
163163+}
164164+165165+// --- SmolStr impl ---
166166+167167+impl_bos! {
168168+ SmolStr => str
169169+}
170170+171171+// --- CowStr impl ---
172172+173173+impl<'a> Bos<str> for CowStr<'a> {
174174+ type Ref<'this>
175175+ = &'this str
176176+ where
177177+ Self: 'this;
178178+179179+ #[inline]
180180+ fn borrow_or_share(this: &Self) -> Self::Ref<'_> {
181181+ this.as_str()
182182+ }
183183+}
184184+185185+/// The default string backing type for jacquard's type-parameterised types.
186186+///
187187+/// `SmolStr` is used as the default because it satisfies `DeserializeOwned` (no lifetime
188188+/// annotation required) and provides small-string inline storage without heap allocation
189189+/// for strings of 22 bytes or fewer.
190190+pub type DefaultStr = SmolStr;
191191+192192+#[cfg(test)]
193193+mod tests {
194194+ use super::*;
195195+196196+ // Verify BorrowOrShare works for all backing types.
197197+198198+ fn as_str_via_bos<'i, 'o, S: BorrowOrShare<'i, 'o, str>>(s: &'i S) -> &'o str {
199199+ s.borrow_or_share()
200200+ }
201201+202202+ #[test]
203203+ fn bos_smolstr() {
204204+ let s = SmolStr::new("hello");
205205+ assert_eq!(as_str_via_bos(&s), "hello");
206206+ }
207207+208208+ #[test]
209209+ fn bos_string() {
210210+ let s = String::from("hello");
211211+ assert_eq!(as_str_via_bos(&s), "hello");
212212+ }
213213+214214+ #[test]
215215+ fn bos_ref_str() {
216216+ let s: &str = "hello";
217217+ assert_eq!(as_str_via_bos(&s), "hello");
218218+ }
219219+220220+ #[test]
221221+ fn bos_cowstr_borrowed() {
222222+ let s = CowStr::Borrowed("hello");
223223+ assert_eq!(as_str_via_bos(&s), "hello");
224224+ }
225225+226226+ #[test]
227227+ fn bos_cowstr_owned() {
228228+ let s = CowStr::Owned(SmolStr::new("hello"));
229229+ assert_eq!(as_str_via_bos(&s), "hello");
230230+ }
231231+232232+ // Verify Bos (non-sharing) works via AsRef-style usage.
233233+234234+ fn as_ref_via_bos<S: Bos<str>>(s: &S) -> &str {
235235+ let r = S::borrow_or_share(s);
236236+ r.cast()
237237+ }
238238+239239+ #[test]
240240+ fn bos_as_ref_smolstr() {
241241+ let s = SmolStr::new("world");
242242+ assert_eq!(as_ref_via_bos(&s), "world");
243243+ }
244244+245245+ #[test]
246246+ fn bos_as_ref_ref_str() {
247247+ let s: &str = "world";
248248+ assert_eq!(as_ref_via_bos(&s), "world");
249249+ }
250250+251251+ // Verify sharing semantics: &str reference outlives the wrapper.
252252+253253+ #[test]
254254+ fn ref_str_sharing_outlives_wrapper() {
255255+ let original: &str = "shared";
256256+ let result: &str;
257257+ {
258258+ let wrapper: &&str = &original;
259259+ result = as_str_via_bos(wrapper);
260260+ }
261261+ // result outlives wrapper because &str shares, not borrows.
262262+ assert_eq!(result, "shared");
263263+ }
264264+}
+1
crates/jacquard-common/src/cowstr.rs
···263263 }
264264}
265265266266+// TODO(bos-migration): Change Output to SmolStr once types are parameterised by S: Bos<str>.
266267impl IntoStatic for CowStr<'_> {
267268 type Output = CowStr<'static>;
268269
+9
crates/jacquard-common/src/into_static.rs
···101101 crate::deps::smol_str::SmolStr
102102);
103103104104+impl IntoStatic for &str {
105105+ type Output = crate::deps::smol_str::SmolStr;
106106+107107+ #[inline]
108108+ fn into_static(self) -> Self::Output {
109109+ crate::deps::smol_str::SmolStr::new(self)
110110+ }
111111+}
112112+104113impl<T: IntoStatic> IntoStatic for Box<T> {
105114 type Output = Box<T::Output>;
106115
+4
crates/jacquard-common/src/lib.rs
···214214215215pub use cowstr::CowStr;
216216pub use into_static::IntoStatic;
217217+pub use bos::{Bos, BorrowOrShare, DefaultStr};
217218218219/// A copy-on-write immutable string type that uses [`smol_str::SmolStr`] for
219220/// the "owned" variant.
···222223#[macro_use]
223224/// Trait for taking ownership of most borrowed types in jacquard.
224225pub mod into_static;
226226+/// Borrow-or-share traits for abstracting over owned and borrowed string representations.
227227+#[macro_use]
228228+pub mod bos;
225229/// Re-exports of external crate dependencies for consistent access across jacquard.
226230pub mod deps;
227231pub mod error;