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 schemasjacquard-axum- Server-side XRPC handler extractors for Axum framework (not re-exported, depends on jacquard)jacquard-oauth- OAuth/DPoP flow implementation with session managementjacquard-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 schemasjacquard-lexgen- Code generation binariesjacquard-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'staticlifetime parameter is used to refer to the owned version of a type, in the same wayStringis 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) implementsXrpcExt, which providesxrpc(base: Uri<String>) -> XrpcCallfor per-request calls with optionalCallOptions(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>whereS: SessionStore<(Did, CowStr), AtpSession>andT: 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>andOAuthSession<S, T>whereS: ClientAuthStoreandT: 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 theAgentSessionExttrait.
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::SmolStrfor 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
- Byte
Sink - Platform-agnostic byte sink abstraction
- Byte
Stream - Platform-agnostic byte stream abstraction
- Close
Frame - WebSocket close frame
- Object
- Object/map of AT Protocol data values
- Query
Match - A single match from a query operation
- Stream
Error - Error type for streaming operations
- Streaming
Response - HTTP streaming response
- Tungstenite
Client - WebSocket client backed by tokio-tungstenite-wasm
- WebSocket
Connection - WebSocket connection with bidirectional streams
- WsSink
- WebSocket message sink
- WsStream
- WebSocket message stream
- WsText
- UTF-8 validated bytes for WebSocket text messages
Enums§
- AtData
Error - Errors that can occur when working with AT Protocol data
- Authorization
Token - Authorization token types for XRPC requests.
- Close
Code - WebSocket close code
- CowStr
- A copy-on-write immutable string type that uses
SmolStrfor the “owned” variant. - Data
- AT Protocol data model value
- Data
Deserializer Error - Error type for Data/RawData deserializer
- Query
Result - Result of a data query operation
- RawData
- Level 1 deserialization of raw atproto data
- RawData
Serializer Error - Error type for RawData serialization
- Stream
Error Kind - Categories of streaming errors
- WsMessage
- WebSocket message
Traits§
- Into
Static - Allow turning a value into an “owned” variant, which can then be returned, moved, etc.
- WebSocket
Client - 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
Datavalue - from_
data_ owned - Deserialize a typed value from a
Datavalue - 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
RawDatavalue - from_
raw_ data_ owned - Deserialize a typed value from a
RawDatavalue - to_data
- Serialize a typed value into a validated
Datavalue with type inference - to_
raw_ data - Serialize a typed value into a
RawDatavalue
Type Aliases§
- Lazy
- Lazy initialization type for static values.
Attribute Macros§
- lexicon
- Attribute macro that adds an
extra_datafield 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§
- Into
Static - Derive macro for
IntoStatictrait. - Lexicon
Schema - Derive macro for
LexiconSchematrait. - Xrpc
Request - Derive macro for
XrpcRequesttrait.