A better Rust ATProto crate
103
fork

Configure Feed

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

at main 705 lines 24 kB view raw
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}