# Jacquard
A suite of Rust crates for the AT Protocol. [Docs](https://docs.rs/jacquard/latest/jacquard/)
## Goals and Features
- Validated, spec-compliant, easy to work with, and performant baseline types
- Batteries-included, but easily replaceable batteries.
- Easy to extend with custom lexicons using code generation or handwritten api types
- Straightforward OAuth
- stateless options (or options where you handle the state) for rolling your own
- all the building blocks of the convenient abstractions are available
- lexicon Value type for working with unknown atproto data (dag-cbor or json)
- order of magnitude less boilerplate than some existing crates
- use as much or as little from the crates as you need
## Example
Dead simple API client. Logs in with OAuth and prints the latest 5 posts from your timeline.
```rust
// Note: this requires the `loopback` feature enabled (it is currently by default)
use clap::Parser;
use jacquard::CowStr;
use jacquard::api::app_bsky::feed::get_timeline::GetTimeline;
use jacquard::client::{Agent, FileAuthStore};
use jacquard::oauth::atproto::AtprotoClientMetadata;
use jacquard::oauth::client::OAuthClient;
use jacquard::oauth::loopback::LoopbackConfig;
use jacquard::oauth::scopes::Scope;
use jacquard::types::xrpc::XrpcClient;
use miette::IntoDiagnostic;
#[derive(Parser, Debug)]
#[command(author, version, about = "Jacquard - OAuth (DPoP) loopback demo")]
struct Args {
/// Handle (e.g., alice.bsky.social), DID, or PDS URL
input: CowStr<'static>,
/// Path to auth store file (will be created if missing)
#[arg(long, default_value = "/tmp/jacquard-oauth-session.json")]
store: String,
}
#[tokio::main]
async fn main() -> miette::Result<()> {
let args = Args::parse();
// File-backed auth store for testing
let store = FileAuthStore::new(&args.store);
let client_data = jacquard_oauth::session::ClientData {
keyset: None,
// Default sets normal localhost redirect URIs and "atproto transition:generic" scopes.
// The localhost helper will ensure you have at least "atproto" and will fix urls
config: AtprotoClientMetadata::default_localhost()
};
// Build an OAuth client
let oauth = OAuthClient::new(store, client_data);
// Authenticate with a PDS, using a loopback server to handle the callback flow
let session = oauth
.login_with_local_server(
args.input.clone(),
Default::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 broken up into several crates for modularity. The correct one to use is generally `jacquard` itself, as it re-exports the others.
- `jacquard`: Main crate [](https://crates.io/crates/jacquard) [](https://docs.rs/jacquard)
- `jacquard-common`: Foundation crate [](https://crates.io/crates/jacquard-common) [](https://docs.rs/jacquard-common)
- `jacquard-api`: Autogenerated API bindings [](https://crates.io/crates/jacquard-api) [](https://docs.rs/jacquard-api)
- `jacquard-oauth`: atproto OAuth implementation [](https://crates.io/crates/jacquard-oauth) [](https://docs.rs/jacquard-oauth)
- `jacquard-identity`: Identity resolution [](https://crates.io/crates/jacquard-identity) [](https://docs.rs/jacquard-identity)
- `jacquard-lexicon`: Lexicon parsing and code generation [](https://crates.io/crates/jacquard-lexicon) [](https://docs.rs/jacquard-lexicon)
- `jacquard-derive`: Derive macros for lexicon types [](https://crates.io/crates/jacquard-derive) [](https://docs.rs/jacquard-derive)
## Changelog
[CHANGELOG.md](./CHANGELOG.md)
Highlights:
- A ton of new lexicons included in `jacquard-api`
- `jacquard-axum` Axum extractor and a number of improvements to lifetimes and (de)serialization required to make that work (thanks [@thoth.ptnote.dev](https://tangled.org/@thoth.ptnote.dev) for helping provide feedback and sample code to test against)
- `from_data`, `from_raw_data`, `to_data`, and `to_raw_data` to serialize to and deserialize from the loosely typed value data formats (think `serde_json::from_value` and company). Particularly useful for second-stage deserialization of type "unknown" fields in lexicons, such as `PostView.record`.
- better API code generation
## Development
This repo uses [Flakes](https://nixos.asia/en/flakes) from the get-go.
```bash
# Dev shell
nix develop
# or run via cargo
nix develop -c cargo run
# build
nix build
```
There's also a [`justfile`](https://just.systems/) for Makefile-esque commands to be run inside of the devShell, and you can generally `cargo ...` or `just ...` whatever just fine if you don't want to use Nix and have the prerequisites installed.
[](./LICENSE)