Skip to main content

Crate jacquard

Crate jacquard 

Source
Expand description

§Jacquard

A suite of Rust crates intended to make it much easier to get started with atproto development, without sacrificing flexibility or performance.

Jacquard is simpler because it is designed in a way which makes things simple that almost every other atproto library seems to make difficult.

It is also designed around zero-copy/borrowed deserialization: types like Post<'_> can borrow data (via the CowStr<'_> type and a host of other types built on top of it) directly from the response buffer instead of allocating owned copies. Owned versions are themselves mostly inlined or reference-counted pointers and are therefore still quite efficient. The IntoStatic trait (which is derivable) makes it easy to get an owned version and avoid worrying about lifetimes.

§Features

  • Validated, spec-compliant, easy to work with, and performant baseline types
  • Designed such that you can just work with generated API bindings easily
  • Straightforward OAuth
  • Server-side convenience features
  • Lexicon Data value type for working with unknown atproto data (dag-cbor or json)
  • An order of magnitude less boilerplate than some existing crates
  • Batteries-included, but easily replaceable batteries.
    • Easy to extend with custom lexicons using code generation or handwritten api types
    • Stateless options (or options where you handle the state) for rolling your own
    • All the building blocks of the convenient abstractions are available
    • Use as much or as little from the crates as you need

§Example

Dead simple API client: login with OAuth, then fetch the latest 5 posts.

use jacquard::api::app_bsky::feed::get_timeline::GetTimeline;
use jacquard::client::{Agent, FileAuthStore};
use jacquard::oauth::client::OAuthClient;
use jacquard::xrpc::XrpcClient;
use jacquard::oauth::types::AuthorizeOptions;
use jacquard::oauth::loopback::LoopbackConfig;

#[tokio::main]
async fn main() -> miette::Result<()> {
    let args = Args::parse();

    // Build an OAuth client with file-backed auth store and default localhost config
    let oauth = OAuthClient::with_default_config(FileAuthStore::new(&args.store));
    // Authenticate with a PDS, using a loopback server to handle the callback flow
    let session = oauth
       .login_with_local_server(
           args.input.clone(),
           AuthorizeOptions::default(),
           LoopbackConfig::default(),
       )
       .await?;
    // Wrap in Agent and fetch the timeline
    let agent: Agent<_> = Agent::from(session);
    let timeline = agent
        .send(GetTimeline::new().limit(5).build())
        .await?
        .into_output()?;
    for (i, post) in timeline.feed.iter().enumerate() {
        println!("\n{}. by {}", i + 1, post.post.author.handle);
        println!(
            "   {}",
            serde_json::to_string_pretty(&post.post.record).into_diagnostic()?
        );
    }
    Ok(())
}

§Component crates

Jacquard is split into several crates for modularity. The main jacquard crate re-exports most of the others, so you typically only need to depend on it directly.

  • jacquard-common - AT Protocol types (DIDs, handles, at-URIs, NSIDs, TIDs, CIDs, etc.)
  • jacquard-api - Generated API bindings from 646+ lexicon schemas
  • jacquard-axum - Server-side XRPC handler extractors for Axum framework (not re-exported, depends on jacquard)
  • jacquard-oauth - OAuth/DPoP flow implementation with session management
  • jacquard-identity - Identity resolution (handle → DID, DID → Doc, OAuth metadata)
  • jacquard-repo - Repository primitives (MST, commits, CAR I/O, block storage)
  • jacquard-lexicon - Lexicon resolution, fetching, parsing and Rust code generation from schemas
  • jacquard-lexgen - Code generation binaries
  • jacquard-derive - Macros (#[lexicon], #[open_union], #[derive(IntoStatic)], #[derive(LexiconSchema)], #[derive(XrpcRequest)])

§A note on lifetimes

You’ll notice a bunch of lifetimes all over Jacquard types, examples, and so on. If you’re newer to Rust or have simply avoided them, they’re part of how Rust knows how long to keep something around before cleaning it up. They’re not unique to Rust (C and C++ have the same concept internally) but Rust is perhaps the one language that makes them explicit, because they’re part of how it validates that things are memory-safe, and being able to give information to the compiler about how long it can expect something to stick around lets the compiler reason out much more sophisticated things. The Rust book has a section on them if you want a refresher.

On Jacquard types like CowStr, a 'static lifetime parameter is used to refer to the owned version of a type, in the same way String is the owned version of &str.

This is somewhat in tension with the ‘make things simpler’ goal of the crate, but it is honestly pretty straightforward once you know the deal, and Jacquard provides a number of escape hatches and easy ways to work.

Because explicit lifetimes are somewhat unique to Rust and are not something you may be used to thinking about, they can seem a bit scary to work with. Normally the compiler is pretty good at them, but Jacquard is built around borrowed deserialization and types. This is for reasons of speed and efficiency, because borrowing from your source buffer saves copying the data around.

However, it does mean that any Jacquard type that can borrow (not all of them do) is annotated with a lifetime, to confirm that all the borrowed bits are “covariant”, i.e. that they all live at least the same amount of time, and that lifetime matches or exceeds the lifetime of the data structure. This also imposes certain restrictions on deserialization. Namely the DeserializeOwned bound does not apply to almost any types in Jacquard. There is a deserialize_owned function which you can use in a serde deserialize_with attribute to help, but the general pattern is to do borrowed deserialization and then call .into_static() if you need ownership.

§Easy mode

Easy mode for jacquard is to mostly just use 'static for your lifetime params and derive/use .into_static() as needed. When writing, first see if you can get away with Thing<'_> and let the compiler infer. second-easiest after that is Thing<'static>, third-easiest is giving everything one lifetime, e.g. fn foo<'a>(&'a self, thing: Thing<'a>) -> /* thing with lifetime 'a */.

When parsing the output of atproto API calls, you can call .into_output() on the Response<R> struct to get an owned version with a 'static lifetime. When deserializing, do not use from_writer() type deserialization functions, or features like Axum’s Json extractor, as they have DeserializeOwned bounds and cannot borrow from their buffer. Either use Jacquard’s features to get an owned version or follow the same patterns it uses in your own code.

§Client options

  • Stateless XRPC: any HttpClient (e.g., reqwest::Client) implements XrpcExt, which provides xrpc(base: Uri<String>) -> XrpcCall for per-request calls with optional CallOptions (auth, proxy, labelers, headers). Useful when you want to pass auth on each call or build advanced flows.
 #[tokio::main]
 async fn main() -> miette::Result<()> {
     let http = reqwest::Client::new();
     let base = Uri::parse("https://public.api.bsky.app").into_diagnostic()?.to_owned();
     let resp = http
         .xrpc(base)
         .send(
             &GetAuthorFeed::new()
                 .actor(AtIdentifier::new_static("pattern.atproto.systems").unwrap())
                 .limit(5)
                 .build(),
         )
         .await?;
     let out = resp.into_output()?;
     println!("{}", serde_json::to_string_pretty(&out).into_diagnostic()?);
     Ok(())
 }
  • Stateful client (app-password): CredentialSession<S, T> where S: SessionStore<(Did, CowStr), AtpSession> and T: IdentityResolver + HttpClient. It auto-attaches bearer authorization, refreshes on expiry, and updates the base endpoint to the user’s PDS on login/restore.
  • Stateful client (OAuth): OAuthClient<S, T> and OAuthSession<S, T> where S: ClientAuthStore and T: OAuthResolver + HttpClient. The client is used to authenticate, returning a session which handles authentication and token refresh internally.
  • Agent<A: AgentSession> Session abstracts over the above two options and provides some useful convenience features via the AgentSessionExt trait.

Per-request overrides (stateless)

#[tokio::main]
async fn main() -> miette::Result<()> {
    let http = reqwest::Client::new();
    let base = Uri::parse("https://public.api.bsky.app").into_diagnostic()?.to_owned();
    let resp = http
        .xrpc(base)
        .auth(AuthorizationToken::Bearer(CowStr::from("ACCESS_JWT")))
        .accept_labelers(vec![CowStr::from("did:plc:labelerid")])
        .header(http::header::USER_AGENT, http::HeaderValue::from_static("jacquard-example"))
        .send(
            &GetAuthorFeed::new()
                .actor(AtIdentifier::new_static("pattern.atproto.systems").unwrap())
                .limit(5)
                .build(),
        )
        .await?;
    let out = resp.into_output()?;
    println!("{}", serde_json::to_string_pretty(&out).into_diagnostic()?);
    Ok(())
}

Re-exports§

pub use jacquard_api as api;
pub use jacquard_common as common;
pub use jacquard_identity as identity;
pub use jacquard_oauth as oauth;

Modules§

client
XRPC client implementation for AT Protocol
cowstr
A copy-on-write immutable string type that uses smol_str::SmolStr for the “owned” variant.
deps
Re-exports of external crate dependencies for consistent access across jacquard. Re-exports of external crate dependencies for consistent access across jacquard.
error
Error types for XRPC client operations
http_client
Minimal HTTP client abstraction shared across crates.
into_static
Trait for taking ownership of most borrowed types in jacquard.
jetstream
Jetstream subscription support
macros
atproto! macro.
moderation
Moderation
opt_serde_bytes_helper
Custom serde helpers for bytes::Bytes using serde_bytes
prelude
Prelude with the extension traits you’re likely to want and some other stuff
richtext
Rich text utilities for Bluesky posts
serde_bytes_helper
Custom serde helpers for bytes::Bytes using serde_bytes
service_auth
Service authentication JWT parsing and verification for AT Protocol.
session
Generic session storage traits and utilities.
stream
Stream abstractions for HTTP request/response bodies
streaming
Streaming endpoints
types
Baseline fundamental AT Protocol data types.
websocket
WebSocket client abstraction
xrpc
Stateless XRPC utilities and request/response mapping

Macros§

atproto
Construct a atproto Data<'_> value from a literal.

Structs§

Array
Array of AT Protocol data values
ByteSink
Platform-agnostic byte sink abstraction
ByteStream
Platform-agnostic byte stream abstraction
CloseFrame
WebSocket close frame
Object
Object/map of AT Protocol data values
QueryMatch
A single match from a query operation
StreamError
Error type for streaming operations
StreamingResponse
HTTP streaming response
TungsteniteClient
WebSocket client backed by tokio-tungstenite-wasm
WebSocketConnection
WebSocket connection with bidirectional streams
WsSink
WebSocket message sink
WsStream
WebSocket message stream
WsText
UTF-8 validated bytes for WebSocket text messages

Enums§

AtDataError
Errors that can occur when working with AT Protocol data
AuthorizationToken
Authorization token types for XRPC requests.
CloseCode
WebSocket close code
CowStr
A copy-on-write immutable string type that uses SmolStr for the “owned” variant.
Data
AT Protocol data model value
DataDeserializerError
Error type for Data/RawData deserializer
QueryResult
Result of a data query operation
RawData
Level 1 deserialization of raw atproto data
RawDataSerializerError
Error type for RawData serialization
StreamErrorKind
Categories of streaming errors
WsMessage
WebSocket message

Traits§

IntoStatic
Allow turning a value into an “owned” variant, which can then be returned, moved, etc.
WebSocketClient
WebSocket client trait

Functions§

deserialize_owned
Serde helper for deserializing stuff when you want an owned version
from_cbor
Deserialize a typed value from cbor bytes
from_data
Deserialize a typed value from a Data value
from_data_owned
Deserialize a typed value from a Data value
from_json_value
Deserialize a typed value from a serde_json::Value
from_postcard
Deserialize a typed value from postcard bytes
from_raw_data
Deserialize a typed value from a RawData value
from_raw_data_owned
Deserialize a typed value from a RawData value
to_data
Serialize a typed value into a validated Data value with type inference
to_raw_data
Serialize a typed value into a RawData value

Type Aliases§

Lazy
Lazy initialization type for static values.

Attribute Macros§

lexicon
Attribute macro that adds an extra_data field to structs to capture unknown fields during deserialization.
lexicon_union
Attribute macro for union enums.
open_union
Attribute macro that adds an Unknown(Data) variant to enums to make them open unions.

Derive Macros§

IntoStatic
Derive macro for IntoStatic trait.
LexiconSchema
Derive macro for LexiconSchema trait.
XrpcRequest
Derive macro for XrpcRequest trait.