A better Rust ATProto crate
102
fork

Configure Feed

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

borrow-or-share test and initial implementation bits

+998
+3
Cargo.lock
··· 2475 2475 "jacquard-lexicon", 2476 2476 "miette", 2477 2477 "serde", 2478 + "serde_ipld_dagcbor", 2479 + "serde_json", 2480 + "smol_str", 2478 2481 "thiserror 2.0.18", 2479 2482 ] 2480 2483
+9
crates/jacquard-codegen-tests/Cargo.toml
··· 11 11 jacquard-derive = { path = "../jacquard-derive" } 12 12 jacquard-lexicon = { path = "../jacquard-lexicon" } 13 13 serde = { workspace = true } 14 + smol_str = { workspace = true } 14 15 thiserror = { workspace = true } 15 16 miette = { workspace = true } 17 + 18 + [dev-dependencies] 19 + serde_json = { workspace = true, features = ["std"] } 20 + serde_ipld_dagcbor = { workspace = true } 16 21 17 22 [features] 18 23 default = ["app_bsky", "com_atproto", "test_collision", "test_ns1", "test_ns2", "test_ns3"] ··· 25 30 26 31 [build-dependencies] 27 32 jacquard-lexicon = { path = "../jacquard-lexicon", features = ["codegen"] } 33 + 34 + 35 + [lints.rust] 36 + dead_code = "allow"
+3
crates/jacquard-codegen-tests/src/lib.rs
··· 26 26 #[path = "generated/macro_mode/lib.rs"] 27 27 pub mod macro_mode; 28 28 29 + // Serde spike: empirical validation for borrow-or-share type param + serde interaction. 30 + mod serde_spike; 31 + 29 32 #[cfg(test)] 30 33 mod tests { 31 34 // -- Pretty mode type accessibility --
+705
crates/jacquard-codegen-tests/src/serde_spike.rs
··· 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 + 17 + use alloc::collections::BTreeMap; 18 + 19 + use serde::{Deserialize, Serialize}; 20 + use smol_str::SmolStr; 21 + 22 + // --------------------------------------------------------------------------- 23 + // Minimal Bos/BorrowOrShare trait copies (will live in jacquard-common later) 24 + // --------------------------------------------------------------------------- 25 + 26 + mod 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 + 131 + use 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)] 139 + pub 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)] 147 + pub 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)] 154 + pub 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>"))] 166 + pub 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>"))] 175 + pub 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>"))] 183 + pub 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)] 193 + mod 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 + }
+264
crates/jacquard-common/src/bos.rs
··· 1 + //! Borrow-or-share traits for abstracting over owned and borrowed string representations. 2 + //! 3 + //! This module is a vendored copy of the [`borrow-or-share`](https://docs.rs/borrow-or-share/0.2.4/) 4 + //! crate by yescallop, with additional implementations for [`SmolStr`](smol_str::SmolStr) 5 + //! and [`CowStr`](crate::CowStr). We vendor rather than depend on the crate to avoid 6 + //! orphan rule issues. We need to implement `Bos<str>` for `SmolStr` and `CowStr`, which 7 + //! are foreign types relative to the upstream crate. 8 + //! 9 + //! # Overview 10 + //! 11 + //! [`Bos<T>`] is the base trait providing a GAT for the reference type. Use it as a bound 12 + //! when you need a method that borrows from `*self` regardless of whether the backing type 13 + //! is owned or borrowed: 14 + //! 15 + //! ```ignore 16 + //! impl<T: Bos<str>> AsRef<str> for MyType<T> { 17 + //! fn as_ref(&self) -> &str { 18 + //! self.as_str() 19 + //! } 20 + //! } 21 + //! ``` 22 + //! 23 + //! [`BorrowOrShare<'i, 'o, T>`] is the convenience trait with split lifetimes. Use it when 24 + //! you want a method on `&'i self` that returns `&'o T`, where `'o` may outlive `'i` when 25 + //! the backing type is a reference: 26 + //! 27 + //! ```ignore 28 + //! impl<'i, 'o, T: BorrowOrShare<'i, 'o, str>> MyType<T> { 29 + //! fn as_str(&'i self) -> &'o str { 30 + //! self.0.borrow_or_share() 31 + //! } 32 + //! } 33 + //! ``` 34 + 35 + use alloc::{ 36 + borrow::{Cow, ToOwned}, 37 + boxed::Box, 38 + string::String, 39 + vec::Vec, 40 + }; 41 + 42 + use smol_str::SmolStr; 43 + 44 + use crate::CowStr; 45 + 46 + mod internal { 47 + pub trait Ref<T: ?Sized> { 48 + fn cast<'a>(self) -> &'a T 49 + where 50 + Self: 'a; 51 + } 52 + 53 + impl<T: ?Sized> Ref<T> for &T { 54 + #[inline] 55 + fn cast<'a>(self) -> &'a T 56 + where 57 + Self: 'a, 58 + { 59 + self 60 + } 61 + } 62 + } 63 + 64 + use internal::Ref; 65 + 66 + /// A trait for either borrowing or sharing data. 67 + /// 68 + /// See the [module-level documentation](self) for more details. 69 + pub trait Bos<T: ?Sized> { 70 + /// The resulting reference type. May only be `&T`. 71 + type Ref<'this>: Ref<T> 72 + where 73 + Self: 'this; 74 + 75 + /// Borrows from `*this` or from behind a reference it holds, 76 + /// returning a reference of type [`Self::Ref`]. 77 + /// 78 + /// In the latter case, the returned reference is said to be *shared* with `*this`. 79 + fn borrow_or_share(this: &Self) -> Self::Ref<'_>; 80 + } 81 + 82 + /// A helper trait for writing "data borrowing or sharing" functions. 83 + /// 84 + /// See the [module-level documentation](self) for more details. 85 + pub trait BorrowOrShare<'i, 'o, T: ?Sized>: Bos<T> { 86 + /// Borrows from `*self` or from behind a reference it holds. 87 + /// 88 + /// In the latter case, the returned reference is said to be *shared* with `*self`. 89 + fn borrow_or_share(&'i self) -> &'o T; 90 + } 91 + 92 + impl<'i, 'o, T: ?Sized, B> BorrowOrShare<'i, 'o, T> for B 93 + where 94 + B: Bos<T> + ?Sized + 'i, 95 + B::Ref<'i>: 'o, 96 + { 97 + #[inline] 98 + fn borrow_or_share(&'i self) -> &'o T { 99 + (B::borrow_or_share(self) as B::Ref<'i>).cast() 100 + } 101 + } 102 + 103 + // --- Reference impl (sharing) --- 104 + 105 + impl<'a, T: ?Sized> Bos<T> for &'a T { 106 + type Ref<'this> 107 + = &'a T 108 + where 109 + Self: 'this; 110 + 111 + #[inline] 112 + fn borrow_or_share(this: &Self) -> Self::Ref<'_> { 113 + this 114 + } 115 + } 116 + 117 + // --- Macro for borrowing impls --- 118 + 119 + /// Implement [`Bos`] for types that always borrow from `*self`. 120 + /// 121 + /// Each entry maps a concrete type to the target slice/str type it derefs to. 122 + /// The generated impl uses `Ref<'this> = &'this $target` — pure borrowing, no sharing. 123 + #[macro_export] 124 + macro_rules! impl_bos { 125 + ($($(#[$attr:meta])? $({$($params:tt)*})? $ty:ty => $target:ty)*) => { 126 + $( 127 + $(#[$attr])? 128 + impl $(<$($params)*>)? $crate::bos::Bos<$target> for $ty { 129 + type Ref<'this> = &'this $target where Self: 'this; 130 + 131 + #[inline] 132 + fn borrow_or_share(this: &Self) -> Self::Ref<'_> { 133 + this 134 + } 135 + } 136 + )* 137 + }; 138 + } 139 + 140 + // --- Standard library impls --- 141 + 142 + impl_bos! { 143 + {T: ?Sized} &mut T => T 144 + 145 + {T, const N: usize} [T; N] => [T] 146 + 147 + {T} Vec<T> => [T] 148 + 149 + String => str 150 + 151 + {T: ?Sized} Box<T> => T 152 + {B: ?Sized + ToOwned} Cow<'_, B> => B 153 + 154 + {T: ?Sized} alloc::sync::Arc<T> => T 155 + {T: ?Sized} alloc::rc::Rc<T> => T 156 + } 157 + 158 + #[cfg(feature = "std")] 159 + impl_bos! { 160 + std::ffi::OsString => std::ffi::OsStr 161 + std::path::PathBuf => std::path::Path 162 + alloc::ffi::CString => core::ffi::CStr 163 + } 164 + 165 + // --- SmolStr impl --- 166 + 167 + impl_bos! { 168 + SmolStr => str 169 + } 170 + 171 + // --- CowStr impl --- 172 + 173 + impl<'a> Bos<str> for CowStr<'a> { 174 + type Ref<'this> 175 + = &'this str 176 + where 177 + Self: 'this; 178 + 179 + #[inline] 180 + fn borrow_or_share(this: &Self) -> Self::Ref<'_> { 181 + this.as_str() 182 + } 183 + } 184 + 185 + /// The default string backing type for jacquard's type-parameterised types. 186 + /// 187 + /// `SmolStr` is used as the default because it satisfies `DeserializeOwned` (no lifetime 188 + /// annotation required) and provides small-string inline storage without heap allocation 189 + /// for strings of 22 bytes or fewer. 190 + pub type DefaultStr = SmolStr; 191 + 192 + #[cfg(test)] 193 + mod tests { 194 + use super::*; 195 + 196 + // Verify BorrowOrShare works for all backing types. 197 + 198 + fn as_str_via_bos<'i, 'o, S: BorrowOrShare<'i, 'o, str>>(s: &'i S) -> &'o str { 199 + s.borrow_or_share() 200 + } 201 + 202 + #[test] 203 + fn bos_smolstr() { 204 + let s = SmolStr::new("hello"); 205 + assert_eq!(as_str_via_bos(&s), "hello"); 206 + } 207 + 208 + #[test] 209 + fn bos_string() { 210 + let s = String::from("hello"); 211 + assert_eq!(as_str_via_bos(&s), "hello"); 212 + } 213 + 214 + #[test] 215 + fn bos_ref_str() { 216 + let s: &str = "hello"; 217 + assert_eq!(as_str_via_bos(&s), "hello"); 218 + } 219 + 220 + #[test] 221 + fn bos_cowstr_borrowed() { 222 + let s = CowStr::Borrowed("hello"); 223 + assert_eq!(as_str_via_bos(&s), "hello"); 224 + } 225 + 226 + #[test] 227 + fn bos_cowstr_owned() { 228 + let s = CowStr::Owned(SmolStr::new("hello")); 229 + assert_eq!(as_str_via_bos(&s), "hello"); 230 + } 231 + 232 + // Verify Bos (non-sharing) works via AsRef-style usage. 233 + 234 + fn as_ref_via_bos<S: Bos<str>>(s: &S) -> &str { 235 + let r = S::borrow_or_share(s); 236 + r.cast() 237 + } 238 + 239 + #[test] 240 + fn bos_as_ref_smolstr() { 241 + let s = SmolStr::new("world"); 242 + assert_eq!(as_ref_via_bos(&s), "world"); 243 + } 244 + 245 + #[test] 246 + fn bos_as_ref_ref_str() { 247 + let s: &str = "world"; 248 + assert_eq!(as_ref_via_bos(&s), "world"); 249 + } 250 + 251 + // Verify sharing semantics: &str reference outlives the wrapper. 252 + 253 + #[test] 254 + fn ref_str_sharing_outlives_wrapper() { 255 + let original: &str = "shared"; 256 + let result: &str; 257 + { 258 + let wrapper: &&str = &original; 259 + result = as_str_via_bos(wrapper); 260 + } 261 + // result outlives wrapper because &str shares, not borrows. 262 + assert_eq!(result, "shared"); 263 + } 264 + }
+1
crates/jacquard-common/src/cowstr.rs
··· 263 263 } 264 264 } 265 265 266 + // TODO(bos-migration): Change Output to SmolStr once types are parameterised by S: Bos<str>. 266 267 impl IntoStatic for CowStr<'_> { 267 268 type Output = CowStr<'static>; 268 269
+9
crates/jacquard-common/src/into_static.rs
··· 101 101 crate::deps::smol_str::SmolStr 102 102 ); 103 103 104 + impl IntoStatic for &str { 105 + type Output = crate::deps::smol_str::SmolStr; 106 + 107 + #[inline] 108 + fn into_static(self) -> Self::Output { 109 + crate::deps::smol_str::SmolStr::new(self) 110 + } 111 + } 112 + 104 113 impl<T: IntoStatic> IntoStatic for Box<T> { 105 114 type Output = Box<T::Output>; 106 115
+4
crates/jacquard-common/src/lib.rs
··· 214 214 215 215 pub use cowstr::CowStr; 216 216 pub use into_static::IntoStatic; 217 + pub use bos::{Bos, BorrowOrShare, DefaultStr}; 217 218 218 219 /// A copy-on-write immutable string type that uses [`smol_str::SmolStr`] for 219 220 /// the "owned" variant. ··· 222 223 #[macro_use] 223 224 /// Trait for taking ownership of most borrowed types in jacquard. 224 225 pub mod into_static; 226 + /// Borrow-or-share traits for abstracting over owned and borrowed string representations. 227 + #[macro_use] 228 + pub mod bos; 225 229 /// Re-exports of external crate dependencies for consistent access across jacquard. 226 230 pub mod deps; 227 231 pub mod error;