A better Rust ATProto crate
101
fork

Configure Feed

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

marketplace

+619
+25
.claude-plugin/marketplace.json
··· 1 + { 2 + "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", 3 + "name": "jacquard", 4 + "version": "1.0.0", 5 + "owner": { 6 + "name": "Orual", 7 + "url": "https://tangled.org/nonbinary.computer" 8 + }, 9 + "metadata": { 10 + "description": "Plugin marketplace for the Jacquard AT Protocol library" 11 + }, 12 + "plugins": [ 13 + { 14 + "name": "jacquard", 15 + "version": "0.1.0", 16 + "description": "Skill for working with the Jacquard AT Protocol library for Rust — teaches correct BosStr usage, borrow-first patterns, and common pitfalls", 17 + "source": "./plugins/jacquard", 18 + "category": "development", 19 + "author": { 20 + "name": "Orual", 21 + "url": "https://tangled.org/nonbinary.computer" 22 + } 23 + } 24 + ] 25 + }
+13
.claude-plugin/plugin.json
··· 1 + { 2 + "name": "jacquard", 3 + "version": "0.1.0", 4 + "description": "Skill for working with the Jacquard AT Protocol library for Rust — teaches correct BosStr usage, borrow-first patterns, and common pitfalls", 5 + "author": { 6 + "name": "Orual", 7 + "url": "https://tangled.org/nonbinary.computer" 8 + }, 9 + "homepage": "https://tangled.org/nonbinary.computer/jacquard", 10 + "repository": "https://tangled.org/nonbinary.computer/jacquard", 11 + "license": "MIT OR Apache-2.0", 12 + "keywords": ["rust", "atproto", "bluesky", "jacquard"] 13 + }
+3
.gitignore
··· 5 5 !.tangled 6 6 !.github 7 7 !.cargo 8 + !.claude-plugin 9 + !.gitignore 10 + !.envrc 8 11 /.pre-commit-config.yaml 9 12 crates/jacquard-lexicon/target 10 13 /plans
+13
plugins/jacquard/.claude-plugin/plugin.json
··· 1 + { 2 + "name": "jacquard", 3 + "version": "0.1.0", 4 + "description": "Skill for working with the Jacquard AT Protocol library for Rust — teaches correct BosStr usage, borrow-first patterns, and common pitfalls", 5 + "author": { 6 + "name": "Orual", 7 + "url": "https://tangled.org/nonbinary.computer" 8 + }, 9 + "homepage": "https://tangled.org/nonbinary.computer/jacquard", 10 + "repository": "https://tangled.org/nonbinary.computer/jacquard", 11 + "license": "MIT OR Apache-2.0", 12 + "keywords": ["rust", "atproto", "bluesky", "jacquard"] 13 + }
+565
plugins/jacquard/skills/working-with-jacquard/SKILL.md
··· 1 + --- 2 + name: working-with-jacquard 3 + description: Use when working with Jacquard AT Protocol library for Rust - prevents type parameterization mistakes, string allocation antipatterns, and incorrect BosStr usage patterns 4 + --- 5 + 6 + # Working with Jacquard 7 + 8 + ## Overview 9 + 10 + Jacquard is a Rust AT Protocol library that uses a borrow-or-share type system for correctness, performance, and ergonomics. All API types are parameterized on `S: BosStr = DefaultStr` — the caller chooses the backing string type, not the library. 11 + 12 + **Core principle:** Borrow wherever possible, own when needed. The `DefaultStr` (`SmolStr`) default makes ownership painless, but good code still prefers borrowed deserialization, borrowed function parameters, and borrowed intermediates. Validate at construction time. 13 + 14 + **Announce at start:** "I'm using the working-with-jacquard skill to ensure correct BosStr and type usage." 15 + 16 + ## The BosStr Type System 17 + 18 + All API types use a single generic parameter `S: BosStr` instead of lifetimes: 19 + 20 + ```rust 21 + // Generated API type (e.g. Post) 22 + pub struct Post<S: BosStr = DefaultStr> { 23 + pub text: S, 24 + pub created_at: Datetime, 25 + pub embed: Option<PostEmbed<S>>, 26 + // ... 27 + } 28 + ``` 29 + 30 + **BosStr implementors** (caller's choice): 31 + 32 + | Type | Allocates? | `DeserializeOwned`? | Use when | 33 + |------|-----------|---------------------|----------| 34 + | `&str` | No | No | **Preferred.** Function params, ephemeral/internal values | 35 + | `CowStr<'a>` | Only if needed | No | **Preferred for deser.** Zero-copy from buffers | 36 + | `SmolStr` (= `DefaultStr`) | Inline <=23 bytes, Arc longer | Yes | When data must outlive its borrow source | 37 + | `String` | Yes | Yes | Interop with String-based APIs | 38 + 39 + **Key insight:** The `SmolStr` default makes ownership ergonomic — no lifetime ceremony, `DeserializeOwned`, `'static` — but it's a convenience floor, not a ceiling. Prefer borrowed types (`&str`, `CowStr<'_>`) wherever possible and only reach for owned when the data must outlive its source. 40 + 41 + 42 + ## Critical Patterns 43 + 44 + ### Client traits in scope 45 + 46 + Nearly all methods for making API calls are provided via traits, which must be in scope. 47 + There are many extension traits that are auto-implemented for **any** struct meeting the prerequisites. 48 + 49 + The most common pair is `IdentityResolver + AgentSession`, which gives access to `AgentSessionExt` with record CRUD helpers. 50 + 51 + `use jacquard::prelude::*;` brings the critical ones into scope. 52 + 53 + **Rule:** Always import the prelude. If the compiler says a method doesn't exist, you likely need a trait in scope. READ the error output carefully. 54 + 55 + ### String Type Constructors 56 + 57 + All validated types (`Did`, `Handle`, `AtUri`, `Nsid`, `Tid`, `Cid`, etc.) are parameterized on `S: BosStr = DefaultStr`: 58 + 59 + ```rust 60 + // PREFERRED: SmolStr-backed (inline, no heap for short strings) 61 + let did = Did::new("did:plc:abc123")?; 62 + 63 + // BEST: Zero-allocation for string literals 64 + let nsid = Nsid::new_static("com.atproto.repo.getRecord"); 65 + 66 + // When you already have an owned String 67 + let owned = Did::new_owned(some_string)?; 68 + 69 + // AVOID: FromStr always allocates a fresh SmolStr 70 + let did: Did = "did:plc:abc123".parse()?; 71 + ``` 72 + 73 + **Borrowing and conversion:** 74 + 75 + ```rust 76 + // Cheap borrow — returns Type<&str> 77 + let did_ref: Did<&str> = did.borrow(); 78 + 79 + // Cross-type conversion 80 + let did_string: Did<String> = did.convert::<String>(); 81 + ``` 82 + 83 + **Rule:** Use `new()` for default, `new_static()` for string literals, `new_owned()` when you have a `String`. Avoid `FromStr::parse()` unless performance is irrelevant. 84 + 85 + ### Function Parameters: Borrow by Default 86 + 87 + Function parameters should almost always accept borrowed types. There are three levels of borrowing and you should use the cheapest one that works: 88 + 89 + ```rust 90 + // BEST: Reference to default-backed type (most flexible for callers) 91 + fn process_did(did: &Did) { /* ... */ } 92 + 93 + // GOOD: Borrow-parameterized type (zero-copy inner string) 94 + fn process_did(did: Did<&str>) { /* ... */ } 95 + 96 + // MOST BORROWED: Both (reference to borrow-parameterized type) 97 + fn process_did(did: &Did<&str>) { /* ... */ } 98 + 99 + // AVOID: Taking ownership when you don't need it 100 + fn process_did(did: Did) { /* ... */ } // consumes unnecessarily 101 + ``` 102 + 103 + For compound types, same principle — borrow the outer type and/or parameterize with a borrowed backing: 104 + 105 + ```rust 106 + // GOOD: Borrow the struct, strings are still SmolStr inside 107 + fn process_post(post: &Post) { /* ... */ } 108 + 109 + // BETTER: Zero-copy all the way down 110 + fn process_post(post: &Post<CowStr<'_>>) { /* ... */ } 111 + 112 + // Generic: Accept any backing type 113 + fn process_post<S: BosStr>(post: &Post<S>) { /* ... */ } 114 + ``` 115 + 116 + **Rule:** Don't take ownership of types unless you need to store or return them. Use `&Type`, `Type<&str>`, or `&Type<&str>` for function parameters. 117 + 118 + ### Response Parsing: Prefer Borrowed 119 + 120 + XRPC responses wrap a `Bytes` buffer. Caller chooses backing type at parse time: 121 + 122 + ```rust 123 + let response = agent.send(request).await?; 124 + 125 + // PREFERRED: Zero-copy borrow from buffer 126 + let output = response.parse::<CowStr<'_>>()?; 127 + // output borrows from response — both must stay in scope 128 + // use this when processing in the same scope (the common case) 129 + 130 + // When the borrow can't live long enough: 131 + let output = response.into_output()?; 132 + // output is SmolStr-backed, 'static, DeserializeOwned 133 + 134 + // Untyped (when you don't know the schema): 135 + let data = response.parse_data()?; // Data<CowStr<'_>> 136 + let raw = response.parse_raw()?; // RawData<'_> 137 + ``` 138 + 139 + **Rule:** Prefer `.parse::<CowStr<'_>>()` and only fall back to `.into_output()` when the compiler tells you the borrow doesn't live long enough. Borrowed types *can* cross async boundaries depending on scope and lifetime inference — don't assume async means owned. 140 + 141 + **When you genuinely need `DeserializeOwned`:** Some frameworks require it structurally — axum extractors (e.g. `Json<T>`) call `serde_json::from_reader` internally which requires `DeserializeOwned`, and dioxus `use_server_future()` closure arguments have the same constraint. In these cases, use `SmolStr`-backed types (the default). These are the situations the default exists for. 142 + 143 + ### IntoStatic Trait 144 + 145 + Converts any `BosStr`-backed type to its `SmolStr` (owned, `'static`) equivalent: 146 + 147 + ```rust 148 + use jacquard::common::IntoStatic; 149 + 150 + // Convert a CowStr-backed (borrowed) type to owned 151 + let borrowed = response.parse::<CowStr<'_>>()?; 152 + let owned: Post<SmolStr> = borrowed.into_static(); 153 + 154 + // Also works for validated string types 155 + let did_ref: Did<&str> = did.borrow(); 156 + let did_owned: Did<SmolStr> = did_ref.into_static(); 157 + ``` 158 + 159 + **When you need it:** 160 + - Converting borrowed (`CowStr<'_>` or `&str`) types to owned for storage or return 161 + - Custom types that use non-default backing strings 162 + 163 + **When you don't need it:** 164 + - `SmolStr`-backed types are already `'static` — no conversion needed 165 + - Using `.into_output()` already gives you `SmolStr`-backed types 166 + 167 + **Rule:** All custom types with `S: BosStr` parameter MUST derive `IntoStatic`. But for typical usage with default `SmolStr` backing, you rarely call it directly. 168 + 169 + ### Data vs serde_json::Value 170 + 171 + **Never use `serde_json::Value` with Jacquard.** 172 + 173 + ```rust 174 + // NEVER 175 + let value: serde_json::Value = serde_json::from_slice(bytes)?; 176 + 177 + // ALWAYS use Data<S> (owned) or RawData<'a> (zero-copy) 178 + use jacquard::common::{Data, from_data, to_data}; 179 + 180 + let data: Data = serde_json::from_slice(bytes)?; // Data<SmolStr> 181 + let post: Post = from_data(&data)?; 182 + 183 + // Convert typed -> untyped -> typed 184 + let post = Post::builder().text("test").build(); 185 + let data: Data = to_data(&post)?; 186 + let post2: Post = from_data(&data)?; 187 + ``` 188 + 189 + **Two value types:** 190 + - `Data<S: BosStr>`: Typed, owned by default. Use for storage, manipulation, serialization. 191 + - `RawData<'a>`: Lifetime-based, zero-copy. Use for transient parsing from buffers. 192 + 193 + **Path access on Data:** 194 + ```rust 195 + let data: Data = /* ... */; 196 + 197 + // Path syntax: field.nested, [0] for arrays 198 + if let Some(alt) = data.get_at_path("embed.images[0].alt") { 199 + println!("{}", alt.as_str().unwrap()); 200 + } 201 + 202 + // Query syntax: [..] wildcard, ..field scoped recursion, ...field global recursion 203 + let alts = data.query("embed.[..].alt"); 204 + let handle = data.query("post..handle"); // finds post.author.handle 205 + let all_cids = data.query("...cid"); // all CIDs anywhere 206 + ``` 207 + 208 + Path access has `_mut` and `set_at` equivalents for mutation. 209 + 210 + **Rule:** `Data<S>` is Jacquard's replacement for `serde_json::Value`. Always use it for untyped AT Protocol values. 211 + 212 + 213 + ## Common Mistakes 214 + 215 + ### Using Lifetimes Instead of BosStr on Custom Types 216 + 217 + ```rust 218 + // WRONG: Old lifetime-based pattern 219 + struct MyOutput<'a> { 220 + #[serde(borrow)] 221 + field: CowStr<'a>, 222 + } 223 + 224 + // CORRECT: BosStr-parameterized 225 + #[derive(Serialize, Deserialize, IntoStatic)] 226 + #[serde(bound(deserialize = "S: Deserialize<'de> + BosStr"))] 227 + struct MyOutput<S: BosStr = DefaultStr> { 228 + field: S, 229 + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] 230 + extra_data: Option<BTreeMap<SmolStr, Data<S>>>, 231 + } 232 + ``` 233 + 234 + **Why:** Jacquard types no longer use lifetime parameters. Using `<'a>` instead of `<S: BosStr>` means your type can't compose with Jacquard's API types or response parsing. 235 + 236 + ### Forgetting the Serde Bound 237 + 238 + ```rust 239 + // WRONG: Missing serde bound — deserialization will fail 240 + #[derive(Serialize, Deserialize)] 241 + struct MyType<S: BosStr = DefaultStr> { 242 + name: S, 243 + } 244 + 245 + // CORRECT: Explicit serde bound for BosStr 246 + #[derive(Serialize, Deserialize)] 247 + #[serde(bound(deserialize = "S: Deserialize<'de> + BosStr"))] 248 + struct MyType<S: BosStr = DefaultStr> { 249 + name: S, 250 + } 251 + ``` 252 + 253 + **Why:** Serde can't infer the correct bounds for generic types. Without the explicit bound, the derive generates `S: Deserialize<'de>` which is insufficient. 254 + 255 + ### Roundtripping Through String 256 + 257 + ```rust 258 + // BAD: Pointless allocation 259 + let did_str = did.as_str().to_string(); 260 + let did2 = Did::new(&did_str)?; 261 + 262 + // GOOD: Clone, borrow, or convert 263 + let did2 = did.clone(); 264 + let did_ref = did.borrow(); // Did<&str>, zero-cost 265 + let did_string = did.convert::<String>(); // cross-type 266 + ``` 267 + 268 + ### Dropping Response While Holding Borrowed Parse 269 + 270 + ```rust 271 + // WILL NOT COMPILE 272 + let output = { 273 + let response = agent.send(request).await?; 274 + response.parse::<CowStr<'_>>()? // borrows from response 275 + }; // response dropped! 276 + 277 + // CORRECT: Keep response alive 278 + let response = agent.send(request).await?; 279 + let output = response.parse::<CowStr<'_>>()?; 280 + 281 + // OR: Use into_output() for owned types (usually better) 282 + let output = agent.send(request).await?.into_output()?; 283 + ``` 284 + 285 + ### Calling .into_static() When You Don't Need To 286 + 287 + ```rust 288 + // WASTEFUL: into_output() already gives SmolStr-backed types 289 + let response = agent.send(request).await?; 290 + let output = response.into_output()?; 291 + let owned = output.into_static(); // redundant! already SmolStr 292 + 293 + // CORRECT: into_output() is sufficient 294 + let output = agent.send(request).await?.into_output()?; 295 + // output is already SmolStr-backed and 'static 296 + ``` 297 + 298 + ### Not Deriving IntoStatic on Custom Types 299 + 300 + ```rust 301 + // WILL NOT COMPILE with zero-copy parsing 302 + #[derive(Serialize, Deserialize)] 303 + #[serde(bound(deserialize = "S: Deserialize<'de> + BosStr"))] 304 + struct MyOutput<S: BosStr = DefaultStr> { 305 + field: S, 306 + } 307 + 308 + // REQUIRED: Derive IntoStatic 309 + #[derive(Serialize, Deserialize, IntoStatic)] 310 + #[serde(bound(deserialize = "S: Deserialize<'de> + BosStr"))] 311 + struct MyOutput<S: BosStr = DefaultStr> { 312 + field: S, 313 + } 314 + ``` 315 + 316 + ### Forgetting MST Immutability 317 + 318 + MST (Merkle Search Tree) is immutable and persistent: 319 + 320 + ```rust 321 + // WRONG: Loses result 322 + mst.add(key, cid).await?; 323 + 324 + // CORRECT: Reassign 325 + let mst = mst.add(key, cid).await?; 326 + ``` 327 + 328 + ### Skipping Issuer Verification in OAuth 329 + 330 + ```rust 331 + // SECURITY VULNERABILITY 332 + let token_response = exchange_code(...).await?; 333 + // Immediately trusting token_response.sub without verification! 334 + 335 + // ALWAYS VERIFY 336 + let token_response = exchange_code(...).await?; 337 + let pds = resolver.verify_issuer(&server_metadata, &token_response.sub).await?; 338 + ``` 339 + 340 + ### Non-Exhaustive Union Matches 341 + 342 + Open unions have `Unknown(Data<S>)` variant: 343 + 344 + ```rust 345 + // WILL NOT COMPILE (missing Unknown variant) 346 + match embed { 347 + PostEmbed::Images(img) => { /* ... */ } 348 + PostEmbed::Video(vid) => { /* ... */ } 349 + } 350 + 351 + // HANDLE ALL VARIANTS 352 + match embed { 353 + PostEmbed::Images(img) => { /* ... */ } 354 + PostEmbed::Video(vid) => { /* ... */ } 355 + _ => { /* Unknown or other variants */ } 356 + } 357 + ``` 358 + 359 + ### Forgetting extra_data Field 360 + 361 + When constructing types without builders: 362 + 363 + ```rust 364 + // WRONG: Missing extra_data 365 + let record = MyRecord { 366 + known_field: value, 367 + }; 368 + 369 + // CORRECT: Include extra_data 370 + let record = MyRecord { 371 + known_field: value, 372 + extra_data: None, 373 + }; 374 + 375 + // BETTER: Use builder (handles it automatically) 376 + let record = MyRecord::builder() 377 + .known_field(value) 378 + .build(); 379 + ``` 380 + 381 + 382 + ## Quick Reference 383 + 384 + ### String Type Constructors 385 + 386 + | Method | Allocates? | Use When | 387 + |--------|-----------|----------| 388 + | `new(s)` | SmolStr (inline <=23b) | Default construction | 389 + | `new_static(&'static str)` | No | String literals | 390 + | `new_owned(String)` | Reuses buffer | Already have a String | 391 + | `.borrow()` | No | Cheap `Type<&str>` reference | 392 + | `.convert::<B>()` | Depends on B | Cross-type conversion | 393 + | `FromStr::parse()` | **Always** | **Avoid** | 394 + 395 + ### Response Parsing 396 + 397 + | Method | Backing | Allocates? | Use When | 398 + |--------|---------|-----------|----------| 399 + | `.parse::<CowStr<'_>>()` | `CowStr<'_>` | No (zero-copy) | **Preferred.** Processing in same scope | 400 + | `.parse_data()` | `Data<CowStr<'_>>` | No | Untyped, zero-copy | 401 + | `.parse_raw()` | `RawData<'_>` | No | Raw untyped, zero-copy | 402 + | `.into_output()` | `SmolStr` | Yes (inline) | When borrow can't live long enough | 403 + 404 + ### BosStr Backing Types 405 + 406 + | Type | Owned? | `DeserializeOwned`? | Typical Use | 407 + |------|--------|---------------------|-------------| 408 + | `SmolStr` (default) | Yes | Yes | When borrow can't live long enough | 409 + | `&str` | No | No | Function params, ephemeral | 410 + | `CowStr<'a>` | Borrow-or-own | No | Zero-copy from buffers | 411 + | `String` | Yes | Yes | Interop | 412 + 413 + ### Data Types 414 + 415 + | Use | Don't Use | 416 + |-----|-----------| 417 + | `Data<S>` (owned) | `serde_json::Value` | 418 + | `RawData<'a>` (zero-copy) | `serde_json::Value` | 419 + | `from_data()` | `serde_json::from_value()` | 420 + | `to_data()` | `serde_json::to_value()` | 421 + 422 + 423 + ## Red Flags 424 + 425 + **CATASTROPHIC (stop immediately):** 426 + - Using `serde_json::Value` instead of `Data` 427 + - Skipping OAuth issuer verification 428 + - Using lifetime parameters (`<'a>`) on types that should use `<S: BosStr>` 429 + 430 + **WRONG PATTERN (rewrite):** 431 + - Taking ownership (`Did`, `Post`) in function parameters instead of borrowing (`&Did`, `Did<&str>`, `&Post<CowStr<'_>>`) 432 + - Using `.into_output()` when `.parse::<CowStr<'_>>()` would suffice (data processed in same scope) 433 + - Using `#[serde(borrow)]` on BosStr-parameterized fields 434 + - Missing `#[serde(bound(deserialize = "S: Deserialize<'de> + BosStr"))]` on custom types 435 + - Calling `.into_static()` on types that are already `SmolStr`-backed 436 + - Using `FromStr::parse()` on validated types 437 + - Roundtripping through `String` or `to_string()` 438 + 439 + **WILL NOT COMPILE:** 440 + - Custom types with `S: BosStr` missing `IntoStatic` derive 441 + - Dropping response while holding borrowed `CowStr<'_>` parse 442 + - Missing `Unknown` variant in union matches 443 + - Forgetting `extra_data` field in manual construction 444 + 445 + **SECURITY:** 446 + - Not verifying issuer in OAuth flows 447 + - Not validating DID document ID matches request 448 + - Reusing DPoP proofs across requests 449 + - Not implementing JTI tracking in server code 450 + 451 + 452 + ## Documentation 453 + 454 + **Always read the docs before implementing:** 455 + - [jacquard](https://docs.rs/jacquard/latest/jacquard/) 456 + - [jacquard-common](https://docs.rs/jacquard-common/latest/jacquard_common/) 457 + - [jacquard-api](https://docs.rs/jacquard-api/latest/jacquard_api/) 458 + - [jacquard-oauth](https://docs.rs/jacquard-oauth/latest/jacquard_oauth/) 459 + - [jacquard-identity](https://docs.rs/jacquard-identity/latest/jacquard_identity/) 460 + 461 + **LLMs.txt for comprehensive patterns:** 462 + - https://tangled.org/@nonbinary.computer/jacquard/raw/main/llms.txt 463 + 464 + 465 + ## Example Patterns 466 + 467 + ### Making an XRPC Call 468 + 469 + ```rust 470 + use jacquard::prelude::*; 471 + use jacquard::api::app_bsky::feed::get_author_feed::GetAuthorFeed; 472 + 473 + let request = GetAuthorFeed::new() 474 + .actor("alice.bsky.social".into()) 475 + .limit(50) 476 + .build(); 477 + 478 + let response = agent.send(request).await?; 479 + let output = response.into_output()?; // SmolStr-backed, owned 480 + 481 + for item in output.feed { 482 + println!("{}", item.post.author.handle); 483 + } 484 + ``` 485 + 486 + ### Creating a Record 487 + 488 + ```rust 489 + use jacquard::prelude::*; 490 + use jacquard::api::app_bsky::feed::post::Post; 491 + 492 + let post = Post::builder() 493 + .text("Hello ATProto!") 494 + .created_at(Datetime::now()) 495 + .build(); 496 + 497 + agent.create_record(post, None).await?; 498 + ``` 499 + 500 + ### Identity Resolution 501 + 502 + ```rust 503 + use jacquard::identity::PublicResolver; 504 + 505 + let resolver = PublicResolver::default(); 506 + 507 + // Handle -> DID 508 + let did = resolver.resolve_handle(&handle).await?; 509 + 510 + // DID -> PDS endpoint 511 + let pds = resolver.pds_for_did(&did).await?; 512 + ``` 513 + 514 + ### Custom Type with BosStr 515 + 516 + ```rust 517 + use jacquard::common::types::BosStr; 518 + use jacquard::common::types::value::Data; 519 + use jacquard::common::DefaultStr; 520 + use jacquard_derive::IntoStatic; 521 + use serde::{Serialize, Deserialize}; 522 + use smol_str::SmolStr; 523 + use std::collections::BTreeMap; 524 + 525 + #[derive(Serialize, Deserialize, IntoStatic, Debug, Clone)] 526 + #[serde( 527 + rename_all = "camelCase", 528 + bound(deserialize = "S: Deserialize<'de> + BosStr"), 529 + )] 530 + struct MyRecord<S: BosStr = DefaultStr> { 531 + name: S, 532 + count: u32, 533 + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] 534 + extra_data: Option<BTreeMap<SmolStr, Data<S>>>, 535 + } 536 + ``` 537 + 538 + ### Zero-Copy Response Processing 539 + 540 + ```rust 541 + use jacquard::common::CowStr; 542 + 543 + let response = agent.send(request).await?; 544 + 545 + // Zero-copy: borrow strings directly from response buffer 546 + let output = response.parse::<CowStr<'_>>()?; 547 + process_immediately(&output); 548 + // response and output dropped together — no allocations 549 + 550 + // If you need to keep it: convert to owned 551 + let owned = output.into_static(); 552 + ``` 553 + 554 + 555 + ## Philosophy 556 + 557 + **Jacquard is designed for correctness, performance, and ergonomics — in that order:** 558 + 559 + 1. **Borrow first, own when needed** — Use `&str`, `CowStr<'_>`, and references by default. Own (`SmolStr`) only when data must outlive its source. 560 + 2. **The default is a floor, not a ceiling** — `SmolStr` (= `DefaultStr`) makes ownership painless, but good code still prefers borrowed deserialization and borrowed parameters. 561 + 3. **Validation at construction** — Invalid inputs fail fast at `new()`, not deep in application logic. 562 + 4. **Caller chooses backing type** — The `S: BosStr` parameter lets you pick the right trade-off per call site. 563 + 5. **Batteries included, but replaceable** — High-level `Agent` for convenience, low-level primitives for control. 564 + 565 + **When in doubt:** Borrow. Use `.parse::<CowStr<'_>>()`, pass `&Did` or `Did<&str>`, and only reach for owned types when the compiler tells you the data needs to live longer.