A better Rust ATProto crate
1//! Serde spike: empirical validation of serde behaviour with type-parameterised structs.
2//!
3//! This module answers three questions from the borrow-or-share design plan:
4//!
5//! 1. Does `#[serde(borrow)]` on an `S`-typed field prevent `DeserializeOwned` when `S = SmolStr`?
6//! **Answer: YES.** `#[serde(borrow)]` is sugar for `#[serde(bound(deserialize = "'de: 'a"))]`
7//! and requires the field type to contain a lifetime. Type params like `S` have no lifetime,
8//! so the macro rejects it outright. Even if it didn't, the injected bound would prevent
9//! `DeserializeOwned`. Strategy A is dead.
10//!
11//! 2. Does `Deserialize<'de>` work for `S = &'de str` without `#[serde(borrow)]`?
12//! **Tested below** in strategies B and C.
13//!
14//! 3. What serde attribute combinations should codegen emit?
15//! **Tested below** — strategies B (no attrs) and C (explicit bounds) are the candidates.
16
17use alloc::collections::BTreeMap;
18
19use serde::{Deserialize, Serialize};
20use smol_str::SmolStr;
21
22// ---------------------------------------------------------------------------
23// Minimal Bos/BorrowOrShare trait copies (will live in jacquard-common later)
24// ---------------------------------------------------------------------------
25
26mod bos {
27 mod internal {
28 pub trait Ref<T: ?Sized> {
29 fn cast<'a>(self) -> &'a T
30 where
31 Self: 'a;
32 }
33
34 impl<T: ?Sized> Ref<T> for &T {
35 #[inline]
36 fn cast<'a>(self) -> &'a T
37 where
38 Self: 'a,
39 {
40 self
41 }
42 }
43 }
44
45 use alloc::borrow::ToOwned;
46
47 use internal::Ref;
48
49 /// Borrow or share — the base trait with a GAT for the reference type.
50 pub trait Bos<T: ?Sized> {
51 type Ref<'this>: Ref<T>
52 where
53 Self: 'this;
54
55 fn borrow_or_share(this: &Self) -> Self::Ref<'_>;
56 }
57
58 /// Convenience trait with split lifetimes for borrowed vs shared access.
59 pub trait BorrowOrShare<'i, 'o, T: ?Sized>: Bos<T> {
60 fn borrow_or_share(&'i self) -> &'o T;
61 }
62
63 impl<'i, 'o, T: ?Sized, B> BorrowOrShare<'i, 'o, T> for B
64 where
65 B: Bos<T> + ?Sized + 'i,
66 B::Ref<'i>: 'o,
67 {
68 #[inline]
69 fn borrow_or_share(&'i self) -> &'o T {
70 (B::borrow_or_share(self) as B::Ref<'i>).cast()
71 }
72 }
73
74 // --- Implementations ---
75
76 impl<'a, T: ?Sized> Bos<T> for &'a T {
77 type Ref<'this>
78 = &'a T
79 where
80 Self: 'this;
81
82 #[inline]
83 fn borrow_or_share(this: &Self) -> Self::Ref<'_> {
84 this
85 }
86 }
87
88 impl Bos<str> for smol_str::SmolStr {
89 type Ref<'this> = &'this str;
90
91 #[inline]
92 fn borrow_or_share(this: &Self) -> Self::Ref<'_> {
93 this.as_str()
94 }
95 }
96
97 impl Bos<str> for String {
98 type Ref<'this> = &'this str;
99
100 #[inline]
101 fn borrow_or_share(this: &Self) -> Self::Ref<'_> {
102 this.as_str()
103 }
104 }
105
106 impl<'a, B: ?Sized + ToOwned> Bos<B> for alloc::borrow::Cow<'a, B> {
107 type Ref<'this>
108 = &'this B
109 where
110 Self: 'this;
111
112 #[inline]
113 fn borrow_or_share(this: &Self) -> Self::Ref<'_> {
114 this.as_ref()
115 }
116 }
117
118 impl<'a> Bos<str> for jacquard_common::cowstr::CowStr<'a> {
119 type Ref<'this>
120 = &'this str
121 where
122 Self: 'this;
123
124 #[inline]
125 fn borrow_or_share(this: &Self) -> Self::Ref<'_> {
126 this.as_str()
127 }
128 }
129}
130
131use bos::Bos;
132
133// ---------------------------------------------------------------------------
134// Strategy B: no serde attributes at all — let serde derive infer everything
135// ---------------------------------------------------------------------------
136
137/// Flat struct with no serde annotations on fields.
138#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
139pub struct FlatNoBorrow<S: Bos<str> = SmolStr> {
140 pub name: S,
141 pub label: Option<S>,
142 pub tags: Vec<S>,
143}
144
145/// Nested struct containing `FlatNoBorrow<S>`.
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
147pub struct NestedNoBorrow<S: Bos<str> = SmolStr> {
148 pub inner: FlatNoBorrow<S>,
149 pub count: u32,
150}
151
152/// Struct with `BTreeMap<SmolStr, S>` — mixed ownership (keys always SmolStr).
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
154pub struct WithMapNoBorrow<S: Bos<str> = SmolStr> {
155 pub title: S,
156 pub metadata: BTreeMap<SmolStr, S>,
157}
158
159// ---------------------------------------------------------------------------
160// Strategy C: explicit #[serde(bound(...))] — override serde's inferred bounds
161// ---------------------------------------------------------------------------
162
163/// Flat struct with explicit serde bounds.
164#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
165#[serde(bound(serialize = "S: Serialize", deserialize = "S: Deserialize<'de>"))]
166pub struct FlatExplicitBound<S: Bos<str> = SmolStr> {
167 pub name: S,
168 pub label: Option<S>,
169 pub tags: Vec<S>,
170}
171
172/// Nested struct with explicit serde bounds.
173#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
174#[serde(bound(serialize = "S: Serialize", deserialize = "S: Deserialize<'de>"))]
175pub struct NestedExplicitBound<S: Bos<str> = SmolStr> {
176 pub inner: FlatExplicitBound<S>,
177 pub count: u32,
178}
179
180/// Map struct with explicit serde bounds.
181#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
182#[serde(bound(serialize = "S: Serialize", deserialize = "S: Deserialize<'de>"))]
183pub struct WithMapExplicitBound<S: Bos<str> = SmolStr> {
184 pub title: S,
185 pub metadata: BTreeMap<SmolStr, S>,
186}
187
188// ---------------------------------------------------------------------------
189// Tests
190// ---------------------------------------------------------------------------
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use jacquard_common::cowstr::CowStr;
196 use serde::de::DeserializeOwned;
197
198 const TEST_JSON: &str = r#"{
199 "name": "alice",
200 "label": "admin",
201 "tags": ["rust", "atproto"]
202 }"#;
203
204 const TEST_NESTED_JSON: &str = r#"{
205 "inner": {
206 "name": "alice",
207 "label": "admin",
208 "tags": ["rust", "atproto"]
209 },
210 "count": 42
211 }"#;
212
213 const TEST_MAP_JSON: &str = r#"{
214 "title": "hello",
215 "metadata": {
216 "key1": "val1",
217 "key2": "val2"
218 }
219 }"#;
220
221 // -----------------------------------------------------------------------
222 // Compile-time assertions
223 // -----------------------------------------------------------------------
224
225 fn assert_deserialize_owned<T: DeserializeOwned>() {}
226 fn assert_deserialize<'de, T: Deserialize<'de>>() {}
227
228 // ===== Strategy B: no attributes =====
229
230 #[test]
231 fn strategy_b_smolstr_deserialize_owned() {
232 assert_deserialize_owned::<FlatNoBorrow<SmolStr>>();
233 assert_deserialize_owned::<NestedNoBorrow<SmolStr>>();
234 assert_deserialize_owned::<WithMapNoBorrow<SmolStr>>();
235 }
236
237 #[test]
238 fn strategy_b_string_deserialize_owned() {
239 assert_deserialize_owned::<FlatNoBorrow<String>>();
240 assert_deserialize_owned::<NestedNoBorrow<String>>();
241 assert_deserialize_owned::<WithMapNoBorrow<String>>();
242 }
243
244 #[test]
245 fn strategy_b_borrowed_deserialize() {
246 // Does &str satisfy Deserialize<'de> via strategy B (no attrs)?
247 assert_deserialize::<FlatNoBorrow<&str>>();
248 assert_deserialize::<NestedNoBorrow<&str>>();
249 assert_deserialize::<WithMapNoBorrow<&str>>();
250 }
251
252 // CowStr compile-time shape tests.
253 //
254 // We can't use assert_deserialize/assert_deserialize_owned for CowStr because:
255 // - CowStr<'static> does NOT satisfy DeserializeOwned (the Deserialize impl
256 // has 'de: 'a, and Rust can't specialise that away when 'a = 'static)
257 // - CowStr<'_> with an elided lifetime can't relate to the 'de on the helper
258 //
259 // Instead we prove the shape compiles by writing functions with the right
260 // lifetime relationship. The runtime tests below exercise actual behaviour.
261
262 #[allow(dead_code)]
263 fn cowstr_deserialize_shape_b(input: &str) -> FlatNoBorrow<CowStr<'_>> {
264 serde_json::from_str(input).unwrap()
265 }
266
267 #[allow(dead_code)]
268 fn cowstr_nested_deserialize_shape_b(input: &str) -> NestedNoBorrow<CowStr<'_>> {
269 serde_json::from_str(input).unwrap()
270 }
271
272 // ===== Strategy C: explicit bounds =====
273
274 #[test]
275 fn strategy_c_smolstr_deserialize_owned() {
276 assert_deserialize_owned::<FlatExplicitBound<SmolStr>>();
277 assert_deserialize_owned::<NestedExplicitBound<SmolStr>>();
278 assert_deserialize_owned::<WithMapExplicitBound<SmolStr>>();
279 }
280
281 #[test]
282 fn strategy_c_string_deserialize_owned() {
283 assert_deserialize_owned::<FlatExplicitBound<String>>();
284 assert_deserialize_owned::<NestedExplicitBound<String>>();
285 assert_deserialize_owned::<WithMapExplicitBound<String>>();
286 }
287
288 #[test]
289 fn strategy_c_borrowed_deserialize() {
290 assert_deserialize::<FlatExplicitBound<&str>>();
291 assert_deserialize::<NestedExplicitBound<&str>>();
292 assert_deserialize::<WithMapExplicitBound<&str>>();
293 }
294
295 // CowStr shape tests for strategy C (same limitation as B).
296
297 #[allow(dead_code)]
298 fn cowstr_deserialize_shape_c(input: &str) -> FlatExplicitBound<CowStr<'_>> {
299 serde_json::from_str(input).unwrap()
300 }
301
302 #[allow(dead_code)]
303 fn cowstr_nested_deserialize_shape_c(input: &str) -> NestedExplicitBound<CowStr<'_>> {
304 serde_json::from_str(input).unwrap()
305 }
306
307 // -----------------------------------------------------------------------
308 // Runtime: JSON roundtrips — Strategy B
309 // -----------------------------------------------------------------------
310
311 #[test]
312 fn strategy_b_json_roundtrip_flat_smolstr() {
313 let parsed: FlatNoBorrow<SmolStr> = serde_json::from_str(TEST_JSON).unwrap();
314 assert_eq!(parsed.name, SmolStr::new("alice"));
315 assert_eq!(parsed.label, Some(SmolStr::new("admin")));
316 assert_eq!(
317 parsed.tags,
318 vec![SmolStr::new("rust"), SmolStr::new("atproto")]
319 );
320
321 let json = serde_json::to_string(&parsed).unwrap();
322 let reparsed: FlatNoBorrow<SmolStr> = serde_json::from_str(&json).unwrap();
323 assert_eq!(parsed, reparsed);
324 }
325
326 #[test]
327 fn strategy_b_json_roundtrip_nested_smolstr() {
328 let parsed: NestedNoBorrow<SmolStr> = serde_json::from_str(TEST_NESTED_JSON).unwrap();
329 assert_eq!(parsed.inner.name, SmolStr::new("alice"));
330 assert_eq!(parsed.count, 42);
331
332 let json = serde_json::to_string(&parsed).unwrap();
333 let reparsed: NestedNoBorrow<SmolStr> = serde_json::from_str(&json).unwrap();
334 assert_eq!(parsed, reparsed);
335 }
336
337 #[test]
338 fn strategy_b_json_roundtrip_map_smolstr() {
339 let parsed: WithMapNoBorrow<SmolStr> = serde_json::from_str(TEST_MAP_JSON).unwrap();
340 assert_eq!(parsed.title, SmolStr::new("hello"));
341 assert_eq!(
342 parsed.metadata.get(&SmolStr::new("key1")),
343 Some(&SmolStr::new("val1"))
344 );
345
346 let json = serde_json::to_string(&parsed).unwrap();
347 let reparsed: WithMapNoBorrow<SmolStr> = serde_json::from_str(&json).unwrap();
348 assert_eq!(parsed, reparsed);
349 }
350
351 #[test]
352 fn strategy_b_json_roundtrip_flat_string() {
353 let parsed: FlatNoBorrow<String> = serde_json::from_str(TEST_JSON).unwrap();
354 assert_eq!(parsed.name, "alice");
355
356 let json = serde_json::to_string(&parsed).unwrap();
357 let reparsed: FlatNoBorrow<String> = serde_json::from_str(&json).unwrap();
358 assert_eq!(parsed, reparsed);
359 }
360
361 #[test]
362 fn strategy_b_json_borrowed_flat() {
363 let parsed: FlatNoBorrow<&str> = serde_json::from_str(TEST_JSON).unwrap();
364 assert_eq!(parsed.name, "alice");
365 assert_eq!(parsed.label, Some("admin"));
366 assert_eq!(parsed.tags, vec!["rust", "atproto"]);
367 }
368
369 #[test]
370 fn strategy_b_json_borrowed_nested() {
371 let parsed: NestedNoBorrow<&str> = serde_json::from_str(TEST_NESTED_JSON).unwrap();
372 assert_eq!(parsed.inner.name, "alice");
373 assert_eq!(parsed.count, 42);
374 }
375
376 #[test]
377 fn strategy_b_json_borrowed_map() {
378 let parsed: WithMapNoBorrow<&str> = serde_json::from_str(TEST_MAP_JSON).unwrap();
379 assert_eq!(parsed.title, "hello");
380 assert_eq!(parsed.metadata.get(&SmolStr::new("key1")), Some(&"val1"));
381 }
382
383 #[test]
384 fn strategy_b_json_cowstr() {
385 let parsed: FlatNoBorrow<CowStr> = serde_json::from_str(TEST_JSON).unwrap();
386 assert_eq!(parsed.name.as_str(), "alice");
387 assert_eq!(parsed.label.as_ref().map(|c| c.as_str()), Some("admin"));
388
389 let json = serde_json::to_string(&parsed).unwrap();
390 let reparsed: FlatNoBorrow<CowStr> = serde_json::from_str(&json).unwrap();
391 assert_eq!(parsed, reparsed);
392 }
393
394 // -----------------------------------------------------------------------
395 // Runtime: JSON roundtrips — Strategy C
396 // -----------------------------------------------------------------------
397
398 #[test]
399 fn strategy_c_json_roundtrip_flat_smolstr() {
400 let parsed: FlatExplicitBound<SmolStr> = serde_json::from_str(TEST_JSON).unwrap();
401 assert_eq!(parsed.name, SmolStr::new("alice"));
402
403 let json = serde_json::to_string(&parsed).unwrap();
404 let reparsed: FlatExplicitBound<SmolStr> = serde_json::from_str(&json).unwrap();
405 assert_eq!(parsed, reparsed);
406 }
407
408 #[test]
409 fn strategy_c_json_roundtrip_nested_smolstr() {
410 let parsed: NestedExplicitBound<SmolStr> = serde_json::from_str(TEST_NESTED_JSON).unwrap();
411 assert_eq!(parsed.inner.name, SmolStr::new("alice"));
412 assert_eq!(parsed.count, 42);
413
414 let json = serde_json::to_string(&parsed).unwrap();
415 let reparsed: NestedExplicitBound<SmolStr> = serde_json::from_str(&json).unwrap();
416 assert_eq!(parsed, reparsed);
417 }
418
419 #[test]
420 fn strategy_c_json_borrowed_flat() {
421 let parsed: FlatExplicitBound<&str> = serde_json::from_str(TEST_JSON).unwrap();
422 assert_eq!(parsed.name, "alice");
423 assert_eq!(parsed.label, Some("admin"));
424 assert_eq!(parsed.tags, vec!["rust", "atproto"]);
425 }
426
427 #[test]
428 fn strategy_c_json_borrowed_nested() {
429 let parsed: NestedExplicitBound<&str> = serde_json::from_str(TEST_NESTED_JSON).unwrap();
430 assert_eq!(parsed.inner.name, "alice");
431 assert_eq!(parsed.count, 42);
432 }
433
434 #[test]
435 fn strategy_c_json_cowstr() {
436 let parsed: FlatExplicitBound<CowStr> = serde_json::from_str(TEST_JSON).unwrap();
437 assert_eq!(parsed.name.as_str(), "alice");
438
439 let json = serde_json::to_string(&parsed).unwrap();
440 let reparsed: FlatExplicitBound<CowStr> = serde_json::from_str(&json).unwrap();
441 assert_eq!(parsed, reparsed);
442 }
443
444 // -----------------------------------------------------------------------
445 // DAG-CBOR roundtrips — Strategy B (if JSON works, CBOR should too)
446 // -----------------------------------------------------------------------
447
448 #[test]
449 fn strategy_b_dagcbor_roundtrip_flat_smolstr() {
450 let original = FlatNoBorrow {
451 name: SmolStr::new("alice"),
452 label: Some(SmolStr::new("admin")),
453 tags: vec![SmolStr::new("rust"), SmolStr::new("atproto")],
454 };
455
456 let bytes = serde_ipld_dagcbor::to_vec(&original).unwrap();
457 let parsed: FlatNoBorrow<SmolStr> = serde_ipld_dagcbor::from_slice(&bytes).unwrap();
458 assert_eq!(original, parsed);
459 }
460
461 #[test]
462 fn strategy_b_dagcbor_roundtrip_flat_string() {
463 let original = FlatNoBorrow {
464 name: String::from("alice"),
465 label: Some(String::from("admin")),
466 tags: vec![String::from("rust"), String::from("atproto")],
467 };
468
469 let bytes = serde_ipld_dagcbor::to_vec(&original).unwrap();
470 let parsed: FlatNoBorrow<String> = serde_ipld_dagcbor::from_slice(&bytes).unwrap();
471 assert_eq!(original, parsed);
472 }
473
474 #[test]
475 fn strategy_b_dagcbor_roundtrip_nested_smolstr() {
476 let original = NestedNoBorrow {
477 inner: FlatNoBorrow {
478 name: SmolStr::new("bob"),
479 label: None,
480 tags: vec![],
481 },
482 count: 99,
483 };
484
485 let bytes = serde_ipld_dagcbor::to_vec(&original).unwrap();
486 let parsed: NestedNoBorrow<SmolStr> = serde_ipld_dagcbor::from_slice(&bytes).unwrap();
487 assert_eq!(original, parsed);
488 }
489
490 #[test]
491 fn strategy_b_dagcbor_borrowed_flat() {
492 // DAG-CBOR stores strings as CBOR text strings. Whether borrowed
493 // deserialization works depends on whether the deserializer calls
494 // visit_borrowed_str. This test documents the actual behaviour.
495 let original = FlatNoBorrow {
496 name: SmolStr::new("alice"),
497 label: Some(SmolStr::new("admin")),
498 tags: vec![SmolStr::new("rust")],
499 };
500
501 let bytes = serde_ipld_dagcbor::to_vec(&original).unwrap();
502 let result: Result<FlatNoBorrow<&str>, _> = serde_ipld_dagcbor::from_slice(&bytes);
503
504 if let Ok(parsed) = &result {
505 assert_eq!(parsed.name, "alice");
506 }
507
508 // Document the finding regardless of outcome.
509 eprintln!(
510 "dagcbor borrowed &str deserialization: {}",
511 if result.is_ok() {
512 "WORKS"
513 } else {
514 "FAILS (expected — CBOR deserializer may not support borrowing)"
515 }
516 );
517 }
518
519 // -----------------------------------------------------------------------
520 // DAG-CBOR — Strategy C
521 // -----------------------------------------------------------------------
522
523 #[test]
524 fn strategy_c_dagcbor_roundtrip_flat_smolstr() {
525 let original = FlatExplicitBound {
526 name: SmolStr::new("alice"),
527 label: Some(SmolStr::new("admin")),
528 tags: vec![SmolStr::new("rust"), SmolStr::new("atproto")],
529 };
530
531 let bytes = serde_ipld_dagcbor::to_vec(&original).unwrap();
532 let parsed: FlatExplicitBound<SmolStr> = serde_ipld_dagcbor::from_slice(&bytes).unwrap();
533 assert_eq!(original, parsed);
534 }
535
536 #[test]
537 fn strategy_c_dagcbor_roundtrip_nested_smolstr() {
538 let original = NestedExplicitBound {
539 inner: FlatExplicitBound {
540 name: SmolStr::new("bob"),
541 label: None,
542 tags: vec![],
543 },
544 count: 99,
545 };
546
547 let bytes = serde_ipld_dagcbor::to_vec(&original).unwrap();
548 let parsed: NestedExplicitBound<SmolStr> = serde_ipld_dagcbor::from_slice(&bytes).unwrap();
549 assert_eq!(original, parsed);
550 }
551
552 // -----------------------------------------------------------------------
553 // Zero-copy verification: prove borrowed &str points into the input buffer
554 // -----------------------------------------------------------------------
555
556 /// Returns true if `s` points into the memory range of `buf`.
557 fn points_into(s: &str, buf: &str) -> bool {
558 let buf_start = buf.as_ptr() as usize;
559 let buf_end = buf_start + buf.len();
560 let s_start = s.as_ptr() as usize;
561 s_start >= buf_start && s_start + s.len() <= buf_end
562 }
563
564 /// Same as above but for byte slices.
565 fn points_into_bytes(s: &str, buf: &[u8]) -> bool {
566 let buf_start = buf.as_ptr() as usize;
567 let buf_end = buf_start + buf.len();
568 let s_start = s.as_ptr() as usize;
569 s_start >= buf_start && s_start + s.len() <= buf_end
570 }
571
572 #[test]
573 fn json_borrowed_str_is_zero_copy() {
574 let input = r#"{"name":"alice","label":"admin","tags":["rust","atproto"]}"#;
575 let parsed: FlatNoBorrow<&str> = serde_json::from_str(input).unwrap();
576
577 assert!(
578 points_into(parsed.name, input),
579 "name should point into input buffer"
580 );
581 assert!(
582 points_into(parsed.label.unwrap(), input),
583 "label should point into input buffer"
584 );
585 for tag in &parsed.tags {
586 assert!(
587 points_into(tag, input),
588 "tag {:?} should point into input buffer",
589 tag
590 );
591 }
592 }
593
594 #[test]
595 fn dagcbor_borrowed_str_is_zero_copy() {
596 let original = FlatNoBorrow {
597 name: SmolStr::new("alice"),
598 label: Some(SmolStr::new("admin")),
599 tags: vec![SmolStr::new("rust")],
600 };
601
602 let bytes = serde_ipld_dagcbor::to_vec(&original).unwrap();
603 let parsed: FlatNoBorrow<&str> = serde_ipld_dagcbor::from_slice(&bytes).unwrap();
604
605 assert!(
606 points_into_bytes(parsed.name, &bytes),
607 "name should point into CBOR buffer"
608 );
609 assert!(
610 points_into_bytes(parsed.label.unwrap(), &bytes),
611 "label should point into CBOR buffer"
612 );
613 for tag in &parsed.tags {
614 assert!(
615 points_into_bytes(tag, &bytes),
616 "tag {:?} should point into CBOR buffer",
617 tag
618 );
619 }
620 }
621
622 #[test]
623 fn json_cowstr_borrows_from_input() {
624 // CowStr's Deserialize impl calls visit_borrowed_str -> CowStr::Borrowed,
625 // so when deserializing from &str the result should be zero-copy.
626 let input = r#"{"name":"alice","label":"admin","tags":["rust","atproto"]}"#;
627 let parsed: FlatNoBorrow<CowStr> = serde_json::from_str(input).unwrap();
628
629 assert!(
630 matches!(parsed.name, CowStr::Borrowed(_)),
631 "name should be CowStr::Borrowed, got Owned"
632 );
633 assert!(
634 points_into(parsed.name.as_str(), input),
635 "name should point into input buffer"
636 );
637
638 let label = parsed.label.unwrap();
639 assert!(
640 matches!(label, CowStr::Borrowed(_)),
641 "label should be CowStr::Borrowed, got Owned"
642 );
643 assert!(
644 points_into(label.as_str(), input),
645 "label should point into input buffer"
646 );
647
648 for tag in &parsed.tags {
649 assert!(
650 matches!(tag, CowStr::Borrowed(_)),
651 "tag {:?} should be CowStr::Borrowed, got Owned",
652 tag.as_str()
653 );
654 assert!(
655 points_into(tag.as_str(), input),
656 "tag {:?} should point into input buffer",
657 tag.as_str()
658 );
659 }
660 }
661
662 #[test]
663 fn dagcbor_cowstr_borrows_from_buffer() {
664 let original = FlatNoBorrow {
665 name: SmolStr::new("alice"),
666 label: Some(SmolStr::new("admin")),
667 tags: vec![SmolStr::new("rust")],
668 };
669
670 let bytes = serde_ipld_dagcbor::to_vec(&original).unwrap();
671 let parsed: FlatNoBorrow<CowStr> = serde_ipld_dagcbor::from_slice(&bytes).unwrap();
672
673 assert!(
674 matches!(parsed.name, CowStr::Borrowed(_)),
675 "name should be CowStr::Borrowed, got Owned"
676 );
677 assert!(
678 points_into_bytes(parsed.name.as_str(), &bytes),
679 "name should point into CBOR buffer"
680 );
681
682 let label = parsed.label.unwrap();
683 assert!(
684 matches!(label, CowStr::Borrowed(_)),
685 "label should be CowStr::Borrowed, got Owned"
686 );
687 assert!(
688 points_into_bytes(label.as_str(), &bytes),
689 "label should point into CBOR buffer"
690 );
691
692 for tag in &parsed.tags {
693 assert!(
694 matches!(tag, CowStr::Borrowed(_)),
695 "tag {:?} should be CowStr::Borrowed, got Owned",
696 tag.as_str()
697 );
698 assert!(
699 points_into_bytes(tag.as_str(), &bytes),
700 "tag {:?} should point into CBOR buffer",
701 tag.as_str()
702 );
703 }
704 }
705}