# Jacquard: AT Protocol Library for Rust > Simple and powerful AT Protocol (Bluesky) client library emphasizing spec compliance, flexible string backing types, and minimal boilerplate. ## Core philosophy **Flexible string backing, zero-copy when you want it.** All API types are parameterised on `S: BosStr = DefaultStr` (where `DefaultStr = SmolStr`). By default, types use `SmolStr` for inline storage of short strings and `Arc` for longer ones. When zero-copy is needed, use `CowStr<'_>` or `&str` as the backing type. `DeserializeOwned` works out of the box for `SmolStr`-backed types. **Validated types everywhere.** DIDs, handles, AT-URIs, NSIDs, TIDs, CIDs -- all have strongly-typed, validated wrappers. Invalid inputs fail at construction time, not deep in your application logic. **Batteries included, but replaceable.** High-level `Agent` for convenience, or use stateless `XrpcCall` builder for full control. Mix and match as needed. --- ## Critical patterns to internalize ### String backing types All validated types (Did, Handle, AtUri, Nsid, etc.) and all generated API types are generic over `S: BosStr`: ```rust pub struct Did { /* ... */ } pub struct Post { /* ... */ } ``` The `BosStr` trait combines `Bos + AsRef + FromStaticStr`. Types that implement it: | Type | Allocates? | Use case | |------|-----------|----------| | `SmolStr` (default) | Inline <=23 bytes, Arc for longer | General-purpose owned strings | | `&str` | Never | Zero-copy borrowed access | | `CowStr<'a>` | Only when owned variant needed | Borrow-or-own flexibility | | `String` | Always on heap | Standard owned strings | **Rule**: Use default `SmolStr` for most code. Use `CowStr<'_>` or `&str` when zero-copy parsing matters. Use `String` when interfacing with APIs that require it. ### String type constructors ALL validated string types (Did, Handle, AtUri, Nsid, etc.) follow this pattern: ```rust // Wraps S directly (validates). S defaults to SmolStr. let did = Did::new(SmolStr::new("did:plc:abc123"))?; // Zero-allocation for static strings. let nsid = Nsid::new_static("com.atproto.repo.getRecord")?; // Validates and produces owned (SmolStr-backed by default). let owned = Did::new_owned("did:plc:abc123")?; // Cheap borrow: Did -> Did<&str>. let borrowed: Did<&str> = owned.borrow(); // Cross-type conversion: Did -> Did. let string_did: Did = owned.convert(); // FromStr always allocates into SmolStr. let did: Did = "did:plc:abc123".parse()?; // AVOID: Roundtripping through String. let s = did.as_str().to_string(); let did2 = Did::new_owned(&s)?; // Pointless allocation. ``` **Rule**: Use `new_static()` for string literals, `new_owned()` when validating from `&str`, `borrow()` for cheap temporary access, `convert()` for cross-type conversion. ### Response parsing: choose your backing type XRPC responses wrap a `Bytes` buffer. You choose how to deserialize: ```rust let response = agent.send(request).await?; // Option 1: Owned SmolStr-backed (DeserializeOwned -- just works!) let output: GetPostOutput = response.into_output()?; // Can return from functions, store anywhere, no lifetime concerns. // Option 2: Zero-copy with CowStr (borrows from response buffer) let output: GetPostOutput> = response.parse()?; // output borrows from response, both must stay in scope. // Option 3: Zero-copy with &str (borrows from response buffer) let output: GetPostOutput<&str> = response.parse()?; // Cheapest, but output borrows from response. ``` **Rule**: Use `.into_output()` for most code -- it returns `SmolStr`-backed owned types with no lifetime concerns. Use `.parse::>()` or `.parse::<&str>()` when zero-copy performance is critical and you can keep the response alive. ### Type system: GATs parameterised on S Jacquard uses **Generic Associated Types** on `XrpcResp` parameterised on backing string type: ```rust trait XrpcResp { type Output; // GAT parameterised on S, not lifetime. type Err: Error + DeserializeOwned; // Always owned, always SmolStr-backed. } ``` **Why this matters**: `SmolStr`-backed types implement `DeserializeOwned`, so `.into_output()` just works. For zero-copy, callers pass `CowStr<'_>` or `&str` as `S` via `.parse()`. Error types are always owned since they are uncommon and don't need zero-copy. **When implementing custom types**: Parameterise on `S: BosStr`, not on lifetimes. Use `#[serde(bound(deserialize = "S: Deserialize<'de> + BosStr"))]`. ### Backing type conversion ```rust // borrow() -- cheap Did -> Did<&str> let borrowed: Did<&str> = my_did.borrow(); // convert() -- type-level conversion (e.g., SmolStr -> String) let string_did: Did = my_did.convert(); // IntoStatic -- converts CowStr<'_> -> SmolStr (or other owned) use jacquard::common::IntoStatic; let owned: Post = borrowed_post.into_static(); // as_str() -- get &str from any concrete S (e.g., SmolStr) let s: &str = my_did.as_str(); // as_ref() -- get &str from generic S (via AsRef) fn generic(did: &Did) -> &str { did.as_ref() } ``` **Rule**: Use `.borrow()` for cheap `Type<&str>` access. Use `.convert()` for cross-type conversion. Use `.into_static()` when converting from borrowed (`CowStr<'_>`) to owned. Use `.as_str()` for concrete types, `.as_ref()` for generic `S`. --- ## Crate-by-crate guide ### jacquard (main crate) **Primary entry point**: `Agent` ```rust use jacquard::client::{Agent, CredentialSession, MemorySessionStore}; use jacquard::identity::PublicResolver; // App password auth. let (session, _info) = CredentialSession::authenticated( "alice.bsky.social".into(), "app-password".into(), None, // session_id ).await?; let agent = Agent::from(session); // Make typed XRPC calls. use jacquard::api::app_bsky::feed::get_timeline::GetTimeline; let response = agent.send(&GetTimeline::<&str>::new().limit(50).build()).await?; let timeline = response.into_output()?; ``` **Auto-refresh**: Both `CredentialSession` and `OAuthSession` automatically refresh tokens on 401/expired errors. One retry per request. **Typed record operations** (via `AgentSessionExt` trait): ```rust use jacquard::api::app_bsky::feed::post::Post; // Create. let post = Post::builder() .text("Hello ATProto!") .created_at(Datetime::now()) .build(); agent.create_record::(post, None).await?; // Get (type-safe!). let uri = Post::uri("at://did:plc:abc/app.bsky.feed.post/123")?; let response = agent.get_record::(&uri).await?; let post_output = response.into_output()?; // Update with fetch-modify-put pattern. agent.update_record::(&uri, |profile| { profile.display_name = Some(SmolStr::new("New Name")); }).await?; ``` **Key traits**: - `AgentSession`: Common interface for both auth types - `XrpcClient`: Stateful XRPC (has base URI, auth tokens) - `HttpClient`: Low-level HTTP abstraction **Common mistake**: Forgetting that `SmolStr`-backed types are already owned. You do not need to call `.into_static()` on types returned from `.into_output()`. The `.into_static()` method is only needed when converting from `CowStr<'_>`-backed or `&str`-backed types to owned. ### jacquard-common (foundation) **Core types**: `Did`, `Handle`, `AtUri`, `Nsid`, `Tid`, `Cid`, `CowStr<'a>`, `SmolStr`, `Data`, `RawData<'a>` **String type traits** (ALL validated types implement these): - `new(S)` - Validates, wraps S directly - `new_static(&'static str)` - Validates, zero alloc - `new_owned(impl AsRef)` - Validates, produces owned S - `borrow(&self) -> Type<&str>` - Cheap borrowed view - `convert>(self) -> Type` - Cross-type conversion - `raw(&str)` - Panics on invalid (use when you KNOW it's valid) - `unchecked(&str)` - Unsafe, no validation - `as_str(&self) -> &str` - Get string reference - `Display`, `FromStr`, `Serialize`, `Deserialize`, `IntoStatic` **SmolStr**: The default backing type. Inline storage for strings <=23 bytes (covers most handles, DIDs, rkeys). Longer strings use `Arc` for O(1) clone. Implements `DeserializeOwned`. **CowStr<'a>**: Borrow-or-own string. Uses `SmolStr` for owned variant. This type still uses lifetimes -- it is the borrow-or-own primitive. **XRPC layer**: ```rust // Stateless XRPC (with any HttpClient). use jacquard::common::xrpc::XrpcExt; let http = reqwest::Client::new(); let response = http .xrpc(Uri::parse("https://bsky.social").map_err(|(e, _)| e)?) .auth(AuthorizationToken::Bearer(token)) .proxy(did) .send(&request) .await?; // Stateful XRPC (implement XrpcClient trait). let response = agent.send(request).await?; ``` **Data vs RawData<'a>**: - `Data`: Validated, type-inferred atproto values (strings parsed to Did/Handle/etc.), parameterised on `S: BosStr` - `RawData<'a>`: Minimal validation, lifetime-based (intentionally stays lifetime-based), suitable for pass-through/relay use cases ```rust // Convert typed -> untyped -> typed. let post = Post::builder().text("test").build(); let data: Data = to_data(&post)?; let post2: Post = from_data(&data)?; // NEVER use serde_json::Value. // let value: serde_json::Value = ...; // let data: Data = ...; ``` **Streaming** (feature: `streaming`): - `ByteStream` / `ByteSink`: Platform-agnostic (works on WASM via `n0-future`) - `HttpClientExt::send_http_streaming()`: Stream response - `HttpClientExt::send_http_bidirectional()`: Stream both request and response **WebSocket** (feature: `websocket`): - `WebSocketClient` trait, `WebSocketConnection` - Native + WASM: tokio-tungstenite-wasm **Collection trait** (for record types): ```rust pub trait Collection { const NSID: &'static str; type Record: XrpcResp; // Marker type for get_record(). } // Enables typed record retrieval: let response: Response = agent.get_record(did, rkey).await?; ``` **CallOptions<'a>**: Still uses lifetimes (not parameterised on S). This is intentional for the options builder pattern. ### jacquard-api (generated bindings) **764 lexicon schemas** across 52+ namespaces. **Feature organization**: - `minimal`: Core atproto only - `bluesky`: Bluesky app + chat + ozone - `other`: Curated third-party lexicons - `lexicon_community`: Community extensions - `ufos`: Experimental/niche **Generated patterns**: - All types: `Foo` with `#[serde(bound(deserialize = "S: Deserialize<'de> + BosStr"))]` - All implement `IntoStatic`, `Serialize`, `Deserialize`, `Clone`, `PartialEq`, `Eq` - Builders on types with 2+ fields (some required) - Open unions: `Unknown(Data)` variant via `#[open_union]` macro - Objects: `extra_data: BTreeMap>` via `#[lexicon]` macro - Known-values enums: `Other(S)` catch-all **Union collision detection**: When multiple namespaces define similar types, foreign refs get prefixed: ```rust // If both app.bsky.embed.images and sh.custom.embed.images exist: pub enum SomeUnion { BskyImages(Box>), CustomImages(Box>), } ``` **For each collection (record type)**, generated code includes: 1. Main record struct (e.g., `Post`) 2. `GetRecordOutput` wrapper (`PostGetRecordOutput` with uri, cid, value) 3. Marker struct (`PostRecord`) implementing `XrpcResp` 4. `Collection` trait impl 5. Helper: `Post::uri()` for constructing typed URIs **For each XRPC endpoint**: 1. Request struct with builder 2. Output struct 3. Error enum (open union, includes `Unknown` variant) 4. Response marker implementing `XrpcResp` 5. Request marker implementing `XrpcRequest` 6. Endpoint marker implementing `XrpcEndpoint` (server-side) **Common mistakes**: - Not handling `Unknown` variant in union matches (non-exhaustive!) - Forgetting that when not using the builder pattern, or Default construction, you must supply `extra_data: BTreeMap::new()` in the constructor, in addition to explicitly named fields - Calling `.into_static()` in tight loops (it is a no-op for SmolStr, but is unnecessary) ### jacquard-derive (macros) **`#[lexicon]`**: Adds `extra_data` field to capture unknown fields during deserialization. ```rust #[lexicon] #[derive(Serialize, Deserialize)] struct MyType { known_field: S, // Macro adds: pub extra_data: BTreeMap> } ``` **`#[open_union]`**: Adds `Unknown(Data)` variant to enums. ```rust #[open_union] #[serde(tag = "$type")] enum MyUnion { KnownVariant(Foo), // Macro adds: #[serde(untagged)] Unknown(Data) } ``` **`#[derive(IntoStatic)]`**: Generates conversion from any `S` to `SmolStr`-backed types. ```rust #[derive(IntoStatic)] struct Post { text: S, likes: u32, } // Generates conversion so Post> -> Post, etc. ``` **`#[derive(XrpcRequest)]`**: Generates XRPC boilerplate for custom endpoints. ```rust #[derive(Serialize, Deserialize, XrpcRequest)] #[xrpc( nsid = "com.example.getThing", method = Query, output = GetThingOutput, error = GetThingError, // Optional, defaults to GenericError. server // Optional, generates XrpcEndpoint marker. )] struct GetThing { pub id: SmolStr, } ``` **Critical**: Custom types used as XRPC outputs must derive `IntoStatic` if they will be used with `.parse()` and then converted. For `.into_output()`, they just need `DeserializeOwned` (which `SmolStr`-backed types get for free). ### jacquard-oauth (OAuth/DPoP) **Session types parameterised on S** for flexible backing storage. **OAuth flow**: ```rust use jacquard::oauth::client::OAuthClient; use jacquard::client::FileAuthStore; let oauth = OAuthClient::with_default_config( FileAuthStore::new("./auth.json") ); // Loopback flow (feature: loopback). let session = oauth.login_with_local_server( "alice.bsky.social", Default::default(), LoopbackConfig::default(), ).await?; let agent = Agent::from(session); ``` **OAuthMetadata**: Parameterised so callers can borrow from stored metadata. **Nonce handling**: Automatic retry on `use_dpop_nonce` errors (400 for auth server, 401 for PDS). Max one retry per request. **Token refresh**: Automatic on `invalid_token` errors. Uses `SessionRegistry` with per-DID+session_id locks to prevent concurrent refresh races. **private_key_jwt**: For non-loopback clients. Automatically used if server supports it. ### jacquard-identity (identity resolution) **Resolution chains** (configurable fallback order): **Handle -> DID**: 1. DNS TXT `_atproto.` (feature: `dns`, skipped on WASM) 2. HTTPS `https:///.well-known/atproto-did` 3. PDS XRPC `com.atproto.identity.resolveHandle` 4. Public API fallback `https://public.api.bsky.app` (if enabled) 5. Slingshot mini-doc (if configured) **DID -> Document**: 1. `did:web`: HTTPS `.well-known/did.json` 2. `did:plc`: PLC directory or Slingshot 3. PDS XRPC `com.atproto.identity.resolveDid` **Methods are generic over backing type**: ```rust // resolve_handle accepts any Handle backing type. let did: Did = resolver.resolve_handle(&handle).await?; // Returns Did (= Did) for owned results. // resolve_did_doc same pattern. let response = resolver.resolve_did_doc(&did).await?; let doc = response.parse_validated()?; // Validates doc.id matches requested DID. // Combined: get PDS endpoint. let pds_url = resolver.pds_for_did(&did).await?; ``` **DidDocResponse pattern** (same as XRPC responses): ```rust let response = resolver.resolve_did_doc(&did).await?; // Borrow from buffer. let doc: DidDocument> = response.parse()?; // Validate doc ID. let doc = response.parse_validated()?; // Error if doc.id != requested DID. // Convert to owned. let doc: DidDocument = response.into_owned()?; ``` **Mini-doc fallback**: If full DID document parsing fails, automatically tries parsing as `MiniDoc` (Slingshot's minimal format) and synthesizes a minimal `DidDocument`. This is transparent to caller. **OAuthResolver trait**: Auto-implemented for `JacquardResolver`. Adds OAuth metadata resolution: ```rust // High-level: accepts handle, DID, or HTTPS URL. let (server_metadata, doc_opt) = resolver.resolve_oauth("alice.bsky.social").await?; // From identity (handle or DID). let (server_metadata, doc) = resolver.resolve_from_identity("alice.bsky.social").await?; // From service URL (PDS or entryway). let server_metadata = resolver.resolve_from_service(&pds_url).await?; // Verify issuer authority over DID. let pds = resolver.verify_issuer(&server_metadata, &sub_did).await?; ``` ### jacquard-axum (server-side) **Note**: jacquard-axum is temporarily out of the workspace while the extractor is redesigned for the BosStr type system. **ExtractXrpc**: Type-safe XRPC request extraction, parameterised on `S: BosStr`. ```rust use jacquard_axum::{ExtractXrpc, IntoRouter}; use jacquard::api::com_atproto::identity::resolve_handle::ResolveHandleRequest; use jacquard::common::DefaultStr; async fn handle_resolve( ExtractXrpc(req): ExtractXrpc ) -> Json { let did = resolve_handle_logic(&req.handle).await; Json(ResolveHandleOutput { did, extra_data: Default::default() }) } // Automatic routing. let app = Router::new() .merge(ResolveHandleRequest::into_router(handle_resolve)); ``` **Query vs Procedure**: - Query (GET): Extracts from query string via `serde_html_form` - Procedure (POST): Calls `Request::decode_body()` (default: JSON, override for CBOR) **Custom encodings**: ```rust impl XrpcRequest for MyRequest { fn decode_body(body: &[u8]) -> Result> { let req = serde_ipld_dagcbor::from_slice(body)?; Ok(Box::new(req)) } } ``` **Service auth** (feature: `service-auth`): ```rust use jacquard_axum::service_auth::{ServiceAuthConfig, ExtractServiceAuth}; let config = ServiceAuthConfig::new( Did::new_static("did:web:feedgen.example.com")?, resolver, ); async fn handler( ExtractServiceAuth(auth): ExtractServiceAuth, ) -> String { format!("Authenticated as {}", auth.did()) } let app = Router::new() .route("/xrpc/app.bsky.feed.getFeedSkeleton", get(handler)) .with_state(config); ``` **Method binding (`lxm` claim)**: Default enabled. Binds JWTs to specific XRPC methods to prevent token reuse across endpoints. **JTI replay protection**: NOT built-in. You must implement: ```rust if let Some(jti) = auth.jti() { if state.seen_jtis.contains(jti) { return Err(StatusCode::UNAUTHORIZED); } state.seen_jtis.insert(jti.to_string(), auth.exp); } ``` **Common mistakes**: - Forgetting the second type parameter on `ExtractXrpc` (defaults to `DefaultStr`) - Using wrong trait (use `XrpcEndpoint` marker, not `XrpcRequest`) - Not implementing JTI tracking (allows replay attacks) - Disabling method binding without understanding security implications ### jacquard-repo (repository primitives) **MST (Merkle Search Tree)**: Immutable, persistent data structure. ```rust use jacquard_repo::mst::Mst; use jacquard_repo::storage::MemoryBlockStore; let storage = MemoryBlockStore::new(); let mst = Mst::new(storage.clone()); // IMMUTABLE: Always reassign. let mst = mst.add("app.bsky.feed.post/abc", record_cid).await?; let mst = mst.add("app.bsky.feed.post/xyz", record_cid2).await?; // Persist and get root CID. let root_cid = mst.persist().await?; ``` **Key validation**: `[a-zA-Z0-9._:~-]+` with exactly one `/` separator. Max 256 bytes. Format: `collection/rkey`. **Diff operations**: ```rust let diff = old_mst.diff(&new_mst).await?; diff.validate_limits()?; // Enforce 200 op limit (protocol). // Convert to different formats. let verified_ops = diff.to_verified_ops(); // For batch(). let repo_ops = diff.to_repo_ops(); // For firehose. ``` **Commits**: ```rust use jacquard_repo::commit::Commit; // Create and sign. let commit = Commit::new_unsigned(did, data_cid, rev, prev_cid) .sign(&signing_key)?; // Verify. commit.verify(&public_key)?; ``` **Supported signature algorithms**: Ed25519, ECDSA P-256, ECDSA secp256k1. **Firehose validation**: ```rust // Sync v1.0 (requires prev MST state). let new_root = commit.validate_v1_0(prev_mst_root, prev_storage, pubkey).await?; // Sync v1.1 (inductive, requires prev_data field + op prev CIDs). let new_root = commit.validate_v1_1(pubkey).await?; ``` **v1.1 inductive validation**: Inverts operations on claimed result, verifies inverted result matches `prev_data`. Requires: - `prev_data` field in commit - All operations have `prev` CIDs for updates/deletes - All required MST blocks in CAR **CAR I/O**: ```rust // Read. let parsed = parse_car_bytes(&bytes)?; let root_cid = parsed.root; let blocks = parsed.blocks; // BTreeMap // Write. let bytes = write_car_bytes(root_cid, blocks)?; ``` **BlockStore trait**: Pluggable storage backend. ```rust #[trait_variant::make(Send)] // Conditionally Send on non-WASM. pub trait BlockStore: Clone { async fn get(&self, cid: &CID) -> Result>; async fn put(&self, data: &[u8]) -> Result; async fn apply_commit(&self, commit: CommitData) -> Result<()>; // Atomic. } ``` **Implementations**: - `MemoryBlockStore`: In-memory (testing) - `FileBlockStore`: File-based (persistent) - `LayeredBlockStore`: Read-through cache (e.g., temp over persistent for firehose) **Repository API** (high-level): ```rust use jacquard_repo::repo::Repository; let repo = Repository::create(storage, did, signing_key, None).await?; // Single operations (don't auto-commit). repo.create_record(collection, rkey, cid).await?; let old_cid = repo.update_record(collection, rkey, new_cid).await?; let deleted_cid = repo.delete_record(collection, rkey).await?; // Batch commit. let ops = vec![ RecordWriteOp::Create { collection, rkey, record }, RecordWriteOp::Update { collection, rkey, record, prev }, ]; let (repo_ops, commit_data) = repo.create_commit(&ops, &did, prev, &key).await?; repo.apply_commit(commit_data).await?; ``` **Common mistakes**: - Forgetting immutability (not reassigning MST operations) - Not calling `validate_limits()` before creating commits (protocol violation) - Using v1.1 validation without `prev_data` field (will fail) - Missing `prev` CIDs on update/delete operations for v1.1 - Not implementing `Clone` cheaply on custom `BlockStore` (use `Arc` internally) - Ignoring CID mismatch errors (indicates data corruption) ### jacquard-lexicon (code generation) **ONLY use `just` commands** for code generation: ```bash just lex-gen # Fetch + generate just lex-fetch # Fetch only just generate-api # Generate from existing lexicons ``` **Union collision detection**: When multiple namespaces have similar type names in a union, foreign refs get prefixed with second NSID segment: ``` app.bsky.embed.images -> BskyImages sh.custom.embed.images -> CustomImages ``` **Builder heuristics**: - Has builder: 1+ required fields, not all bare `S` - Has `Default`: 0 required fields OR all required are bare `S` **Empty objects**: Generate as empty structs with `#[lexicon]` attribute (adds `extra_data`), not as `Data`. **Local refs**: `#fragment` normalized to `{current_nsid}#fragment` during generation. **Feature generation**: Tracks cross-namespace dependencies: ```toml net_anisota = ["app_bsky"] # Uses Bluesky embeds ``` **Token types**: Unit structs with `Display` impl: ```rust pub struct ClickthroughAuthor; impl Display for ClickthroughAuthor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "clickthroughAuthor") } } ``` **Common mistakes**: Running codegen commands manually without `just` (wrong flags, paths). --- ## Anti-patterns to avoid ### Roundtripping through String ```rust // BAD. let did_str = did.as_str().to_string(); let did2 = Did::new_owned(&did_str)?; // GOOD. let did2 = did.clone(); // Or for type conversion: let did2: Did = did.convert(); ``` ### Using serde_json::Value ```rust // NEVER. let value: serde_json::Value = serde_json::from_slice(bytes)?; let post: Post = serde_json::from_value(value)?; // ALWAYS. let data: Data = serde_json::from_slice(bytes)?; let post: Post = from_data(&data)?; ``` ### Using FromStr when new_static() or new_owned() suffices ```rust // SLOWER (FromStr always allocates into SmolStr). let did: Did = "did:plc:abc".parse()?; // BETTER (validates and wraps into SmolStr). let did = Did::new_owned("did:plc:abc")?; // BEST for static strings (zero allocation). let did = Did::new_static("did:plc:abc")?; ``` ### Not handling Unknown variant in unions ```rust // WILL NOT COMPILE (missing Unknown variant). match embed { PostEmbed::Images(img) => { /* ... */ } PostEmbed::Video(vid) => { /* ... */ } } // HANDLE ALL VARIANTS. match embed { PostEmbed::Images(img) => { /* ... */ } PostEmbed::Video(vid) => { /* ... */ } _ => { /* Unknown or other variants */ } } ``` ### Forgetting MST immutability ```rust // WRONG (loses result). mst.add(key, cid).await?; // CORRECT (reassign). let mst = mst.add(key, cid).await?; ``` ### Using DeserializeOwned with CowStr or &str backing types `DeserializeOwned` (i.e. `for<'de> Deserialize<'de>`) works perfectly with `SmolStr`-backed types. This is fine: ```rust // FINE -- SmolStr-backed types implement DeserializeOwned. let post: Post = serde_json::from_slice(&bytes)?; let post: Post = serde_json::from_reader(reader)?; response.into_output()?; // Uses DeserializeOwned internally. ``` However, it does NOT work with `CowStr<'_>` or `&str` backing types, because those need to borrow from the input buffer: ```rust // WRONG -- CowStr needs to borrow from input. fn bad(data: &[u8]) -> Post> { serde_json::from_slice(data).unwrap() // Can't borrow from data! } // CORRECT -- use method-level lifetime for zero-copy. fn good(data: &[u8]) -> Post> { serde_json::from_slice(data).unwrap() // Borrows from data. } // ALSO CORRECT -- use SmolStr when you don't need zero-copy. fn also_good(data: &[u8]) -> Post { serde_json::from_slice(data).unwrap() // DeserializeOwned, no borrowing needed. } ``` **Rule**: `DeserializeOwned` is fine for `SmolStr`-backed types (the default). Only avoid it when you specifically want zero-copy with `CowStr<'_>` or `&str`. Use `.into_output()` for owned results and `.parse::>()` for zero-copy. ### Calling .into_static() unnecessarily ```rust // WASTEFUL -- SmolStr types are already owned. let output = response.into_output()?; let owned = output.into_static(); // No-op! Already SmolStr-backed. // USEFUL -- converting from CowStr to SmolStr. let borrowed: Post> = response.parse()?; let owned: Post = borrowed.into_static(); // Converts CowStr -> SmolStr. ``` ### Using .as_ref() on SmolStr when .as_str() is clearer `SmolStr` implements `AsRef` but also `AsRef<[u8]>`, which can cause ambiguity: ```rust // AMBIGUOUS -- compiler can't infer which AsRef. let s = my_smol.as_ref(); // CLEAR -- use as_str() for concrete SmolStr. let s: &str = my_smol.as_str(); // FINE -- as_ref() works in generic context where S: BosStr. fn generic(val: &Did) -> &str { val.as_ref() } ``` --- ## WASM compatibility Core crates support `wasm32-unknown-unknown` target: - jacquard-common - jacquard-api - jacquard-identity (no DNS resolution) - jacquard-oauth **Pattern**: `#[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))]` **What's different on WASM**: - No `Send` bounds on traits - DNS resolution skipped in handle->DID chain - Tokio-specific features disabled **Test WASM compilation**: ```bash just check-wasm # or cargo build --target wasm32-unknown-unknown -p jacquard-common --no-default-features ``` --- ## Quick reference ### String type constructors | Method | Allocates? | Use when | |--------|-----------|----------| | `new(S)` | Depends on S | Wrapping an already-constructed S | | `new_static(&'static str)` | No | Static string literal | | `new_owned(impl AsRef)` | SmolStr: inline/Arc | Validating from &str | | `borrow(&self) -> Type<&str>` | No | Cheap temporary access | | `convert::(self) -> Type` | Depends on B | Cross-type conversion | | `FromStr::parse()` | SmolStr: inline/Arc | Don't care about explicitness | ### Response parsing | Method | Backing type | Allocates? | Use when | |--------|-------------|-----------|----------| | `.into_output()` | `SmolStr` | Yes (DeserializeOwned) | Most code -- owned, no lifetime concerns | | `.parse::>()` | `CowStr<'_>` | No (zero-copy) | Performance-critical, response stays alive | | `.parse::<&str>()` | `&str` | No (zero-copy) | Cheapest, response stays alive | | `.parse_data()` | `CowStr<'_>` | No | Untyped Data access | | `.parse_raw()` | `'_` | No | Untyped RawData access | ### XRPC traits | Trait | Side | Key types | |-------|------|-----------| | `XrpcRequest` | Client + Server | `type Response: XrpcResp`, requires `Serialize` | | `XrpcResp` | Client + Server | `type Output`, `type Err: DeserializeOwned` | | `XrpcEndpoint` | Server | `type Request`, `type Response: XrpcResp` | | `XrpcClient` | Client | `base_uri()`, `send()` | | `XrpcExt` | Client | Stateless XRPC builder on any HttpClient | ### Session types | Type | Auth method | Auto-refresh | Storage | |------|------------|--------------|---------| | `CredentialSession` | Bearer (app password) | Via refreshSession | SessionStore | | `OAuthSession` | DPoP (OAuth) | Via token endpoint | ClientAuthStore | ### BlockStore implementations | Type | Persistent? | Use case | |------|------------|----------| | `MemoryBlockStore` | No | Testing | | `FileBlockStore` | Yes | Production | | `LayeredBlockStore` | Depends | Read-through cache | --- ## Common operations ### Making an XRPC call ```rust use jacquard::api::app_bsky::feed::get_author_feed::GetAuthorFeed; let request = GetAuthorFeed::new() .actor("alice.bsky.social".into()) .limit(50) .build(); let response = agent.send(request).await?; let output = response.into_output()?; for post in output.feed { println!("{}: {}", post.post.author.handle, post.post.uri); } ``` ### Creating a record ```rust use jacquard::api::app_bsky::feed::post::Post; use jacquard::common::types::string::Datetime; let post = Post::builder() .text("Hello ATProto from Jacquard!") .created_at(Datetime::now()) .build(); agent.create_record(post, None).await?; ``` ### Resolving identity ```rust use jacquard::identity::PublicResolver; let resolver = PublicResolver::default(); // Handle -> DID. let did = resolver.resolve_handle(&handle).await?; // DID -> PDS endpoint. let pds = resolver.pds_for_did(&did).await?; // Combined. let (did, pds) = resolver.pds_for_handle(&handle).await?; ``` ### OAuth login ```rust use jacquard::oauth::client::OAuthClient; use jacquard::client::FileAuthStore; let oauth = OAuthClient::with_default_config( FileAuthStore::new("./auth.json") ); let session = oauth.login_with_local_server( "alice.bsky.social", Default::default(), Default::default(), ).await?; let agent = Agent::from(session); ``` ### Working with different backing types ```rust // Default SmolStr-backed (most common). let did = Did::new_owned("did:plc:abc123")?; // Cheap borrow for function calls. let borrowed: Did<&str> = did.borrow(); some_function(&borrowed); // Zero-copy parsing. let response = agent.send(request).await?; let output: GetPostOutput> = response.parse()?; // Process output while response is alive... // Convert to owned when needed. let owned: GetPostOutput = output.into_static(); ``` ### Server-side XRPC handler ```rust use jacquard_axum::{ExtractXrpc, IntoRouter}; use jacquard::common::DefaultStr; use axum::{Router, Json}; async fn handler( ExtractXrpc(req): ExtractXrpc ) -> Json { // Process request. Json(output) } let app = Router::new() .merge(MyRequest::into_router(handler)); ``` ### MST operations ```rust use jacquard_repo::mst::Mst; use jacquard_repo::storage::MemoryBlockStore; let storage = MemoryBlockStore::new(); let mst = Mst::new(storage.clone()); let mst = mst.add("app.bsky.feed.post/abc123", record_cid).await?; let mst = mst.add("app.bsky.feed.post/xyz789", record_cid2).await?; let root_cid = mst.persist().await?; ``` --- ## Documentation links - [docs.rs/jacquard](https://docs.rs/jacquard/latest/jacquard/) - [docs.rs/jacquard-common](https://docs.rs/jacquard-common/latest/jacquard_common/) - [docs.rs/jacquard-api](https://docs.rs/jacquard-api/latest/jacquard_api/) - [docs.rs/jacquard-oauth](https://docs.rs/jacquard-oauth/latest/jacquard_oauth/) - [docs.rs/jacquard-identity](https://docs.rs/jacquard-identity/latest/jacquard_identity/) - [docs.rs/jacquard-repo](https://docs.rs/jacquard-repo/latest/jacquard_repo/) - [docs.rs/jacquard-axum](https://docs.rs/jacquard-axum/latest/jacquard_axum/) - [Repository](https://tangled.org/@nonbinary.computer/jacquard) --- ## Philosophy summary Jacquard is designed for **correctness**, **performance**, and **ergonomics** in that order. It favors: 1. **Validation at construction time** - Invalid inputs fail fast, not deep in your code 2. **Flexible string backing** - `SmolStr` for owned types with `DeserializeOwned` support, `CowStr`/`&str` for zero-copy when needed 3. **Explicit type control** - You choose the backing type via `S: BosStr`, with sensible defaults 4. **Type safety without boilerplate** - Generated bindings just work, with strong typing and builders 5. **Batteries included, but replaceable** - High-level `Agent` for convenience, low-level primitives for control **When in doubt**: Use the default `SmolStr`-backed types, call `.into_output()` for owned responses, use `.borrow()` for cheap access, and trust the type system. Jacquard is designed to guide you toward correct, performant code.