A better Rust ATProto crate
1[![Crates.io](https://img.shields.io/crates/v/jacquard.svg)](https://crates.io/crates/jacquard) [![Documentation](https://docs.rs/jacquard/badge.svg)](https://docs.rs/jacquard) 2 3# Jacquard 4 5A suite of Rust crates intended to make it much easier to get started with atproto development, without sacrificing flexibility or performance. 6 7[Jacquard is simpler](https://whtwnd.com/nonbinary.computer/3m33efvsylz2s) because it is designed in a way which makes things simple that almost every other atproto library seems to make difficult. 8 9It is also designed around zero-copy/borrowed deserialization: types like [`Post<'_>`](https://tangled.org/@nonbinary.computer/jacquard/blob/main/crates/jacquard-api/src/app_bsky/feed/post.rs) can borrow data (via the [`CowStr<'_>`](https://docs.rs/jacquard/latest/jacquard/cowstr/enum.CowStr.html) 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. 10 11## Features 12 13- Validated, spec-compliant, easy to work with, and performant baseline types 14- Designed such that you can just work with generated API bindings easily 15- Straightforward OAuth 16- Server-side convenience features 17- Lexicon Data value type for working with unknown atproto data (dag-cbor or json) 18- An order of magnitude less boilerplate than some existing crates 19- Batteries-included, but easily replaceable batteries. 20 - Easy to extend with custom lexicons using code generation or handwritten api types 21 - Stateless options (or options where you handle the state) for rolling your own 22 - All the building blocks of the convenient abstractions are available 23 - Use as much or as little from the crates as you need 24 25## 0.9.0 Release Highlights: 26 27**`#[derive(LexiconSchema)]` + `#[lexicon_union]` macros** 28- Automatic schema generation for custom lexicons from Rust structs 29- Supports all lexicon constraints via attributes (max_length, max_graphemes, min/max, etc.) 30- Generates `LexiconDoc` at compile time for runtime validation 31 32**Runtime lexicon data validation** 33- Validation of structural and/or value contraints of data against a lexicon 34- caching for value validations 35- LexiconSchema trait generated implementations for runtime validation 36- detailed validation error results 37 38**Lexicon resolver** 39- Fetch lexicons at runtime for addition to schema registry 40 41**Query and path DSLs for `Data` and `RawData` value types** 42- Pattern-based querying of nested `Data` structures 43- `data.query(pattern)` with expressive syntax: 44 - `field.nested` - exact path navigation 45 - `[..]` - wildcard over collections (array elements or object values) 46 - `field..nested` - scoped recursion (find nested within field, expect one) 47 - `...field` - global recursion (find all occurrences anywhere) 48- `get_at_path()` for simple path-based field access on `Data` and `RawData` 49- Path syntax: `embed.images[0].alt` for navigating nested structures 50- `type_discriminator()` helper methods for AT Protocol union discrimination 51- Collection helper methods: `get()`, `contains_key()`, `len()`, `is_empty()`, `iter()`, `keys()`, `values()` 52- Index trait implemented: `obj["key"]` and `arr[0]` 53 54**Caching in identity/lexicon resolver** 55- Basic LRU in-memory cache implementation using `mini-moka` 56- Reduces number of network requests for certain operations 57- Works on both native and WebAssembly 58- **NOTE** wasm target for `mini-moka` requires a git dependency, use the git version of the crate when compiling for wasm 59 60**XRPC client improvements** 61- `set_options()` and `set_endpoint()` methods on `XrpcClient` trait 62- Default no-op implementations for stateless clients 63- Enables runtime reconfiguration of stateful clients 64- Better support for custom endpoint and option overrides 65- Fixed bug where setting a custom 'Content-Type' header wouldn't be respected 66 67**Major generated API compilation time improvements** 68- Generated code output now includes a typestate builder implementation, similar to the `bon` crate 69- Moves the substantial `syn` tax of generating the builders to code generation time, not compile time. 70 71**New `jacquard-lexgen` crate** 72- Moves binaries out of jacquard-lexicon to reduce size further 73- Flake app for `lex-fetch` 74 75## Example 76 77Dead simple API client. Logs in with OAuth and prints the latest 5 posts from your timeline. 78 79```rust 80// Note: this requires the `loopback` feature enabled (it is currently by default) 81use clap::Parser; 82use jacquard::CowStr; 83use jacquard::api::app_bsky::feed::get_timeline::GetTimeline; 84use jacquard::client::{Agent, FileAuthStore}; 85use jacquard::oauth::client::OAuthClient; 86use jacquard::oauth::loopback::LoopbackConfig; 87use jacquard::types::xrpc::XrpcClient; 88use miette::IntoDiagnostic; 89 90#[derive(Parser, Debug)] 91#[command(author, version, about = "Jacquard - OAuth (DPoP) loopback demo")] 92struct Args { 93 /// Handle (e.g., alice.bsky.social), DID, or PDS URL 94 input: CowStr<'static>, 95 96 /// Path to auth store file (will be created if missing) 97 #[arg(long, default_value = "/tmp/jacquard-oauth-session.json")] 98 store: String, 99} 100 101#[tokio::main] 102async fn main() -> miette::Result<()> { 103 let args = Args::parse(); 104 105 // Build an OAuth client with file-backed auth store and default localhost config 106 let oauth = OAuthClient::with_default_config(FileAuthStore::new(&args.store)); 107 // Authenticate with a PDS, using a loopback server to handle the callback flow 108 let session = oauth 109 .login_with_local_server( 110 args.input.clone(), 111 Default::default(), 112 LoopbackConfig::default(), 113 ) 114 .await?; 115 // Wrap in Agent and fetch the timeline 116 let agent: Agent<_> = Agent::from(session); 117 let timeline = agent 118 .send(&GetTimeline::new().limit(5).build()) 119 .await? 120 .into_output()?; 121 for (i, post) in timeline.feed.iter().enumerate() { 122 println!("\n{}. by {}", i + 1, post.post.author.handle); 123 println!( 124 " {}", 125 serde_json::to_string_pretty(&post.post.record).into_diagnostic()? 126 ); 127 } 128 129 Ok(()) 130} 131 132``` 133 134If you have `just` installed, you can run the [examples](https://tangled.org/@nonbinary.computer/jacquard/tree/main/examples) using `just example {example-name} {ARGS}` or `just examples` to see what's available. 135 136> [!WARNING] 137> A lot of the streaming code is still pretty experimental. The examples work, though.\ 138The modules are also less well-documented, and don't have code examples. There are also a lot of utility functions for conveniently working with the streams and transforming them which are lacking. Use [`n0-future`](https://docs.rs/n0-future/latest/n0_future/index.html) to work with them, that is what Jacquard uses internally as much as possible.\ 139>I would also note the same for the repository crate until I've had more third parties test it. 140 141### Changelog 142 143[CHANGELOG.md](./CHANGELOG.md) 144 145<!--### Testimonials 146 147- ["the most straightforward interface to atproto I've encountered so far."](https://bsky.app/profile/offline.mountainherder.xyz/post/3m3xwewzs3k2v) - @offline.mountainherder.xyz 148 149- "It has saved me a lot of time already! Well worth a few beers and or microcontrollers" - [@baileytownsend.dev](https://bsky.app/profile/baileytownsend.dev)--> 150 151### Projects using Jacquard 152 153- [skywatch-phash-rs](https://tangled.org/@skywatch.blue/skywatch-phash-rs) 154- [PDS MOOver](https://pdsmoover.com/) - [tangled repository](https://tangled.org/@baileytownsend.dev/pds-moover) 155 156## Component crates 157 158Jacquard is broken up into several crates for modularity. The correct one to use is generally `jacquard` itself, as it re-exports most of the others. 159 160| | | | 161| --- | --- | --- | 162| `jacquard` | Main crate | [![Crates.io](https://img.shields.io/crates/v/jacquard.svg)](https://crates.io/crates/jacquard) [![Documentation](https://docs.rs/jacquard/badge.svg)](https://docs.rs/jacquard) | 163|`jacquard-common` | Foundation crate | [![Crates.io](https://img.shields.io/crates/v/jacquard-common.svg)](https://crates.io/crates/jacquard-common) [![Documentation](https://docs.rs/jacquard-common/badge.svg)](https://docs.rs/jacquard-common)| 164| `jacquard-axum` | Axum extractor and other helpers | [![Crates.io](https://img.shields.io/crates/v/jacquard-axum.svg)](https://crates.io/crates/jacquard-axum) [![Documentation](https://docs.rs/jacquard-axum/badge.svg)](https://docs.rs/jacquard-axum) | 165| `jacquard-api` | Autogenerated API bindings | [![Crates.io](https://img.shields.io/crates/v/jacquard-api.svg)](https://crates.io/crates/jacquard-api) [![Documentation](https://docs.rs/jacquard-api/badge.svg)](https://docs.rs/jacquard-api) | 166| `jacquard-oauth` | atproto OAuth implementation | [![Crates.io](https://img.shields.io/crates/v/jacquard-oauth.svg)](https://crates.io/crates/jacquard-oauth) [![Documentation](https://docs.rs/jacquard-oauth/badge.svg)](https://docs.rs/jacquard-oauth) | 167| `jacquard-identity` | Identity resolution | [![Crates.io](https://img.shields.io/crates/v/jacquard-identity.svg)](https://crates.io/crates/jacquard-identity) [![Documentation](https://docs.rs/jacquard-identity/badge.svg)](https://docs.rs/jacquard-identity) | 168| `jacquard-repo` | Repository primitives (MST, commits, CAR I/O) | [![Crates.io](https://img.shields.io/crates/v/jacquard-repo.svg)](https://crates.io/crates/jacquard-repo) [![Documentation](https://docs.rs/jacquard-repo/badge.svg)](https://docs.rs/jacquard-repo) | 169| `jacquard-lexicon` | Lexicon parsing and code generation | [![Crates.io](https://img.shields.io/crates/v/jacquard-lexicon.svg)](https://crates.io/crates/jacquard-lexicon) [![Documentation](https://docs.rs/jacquard-lexicon/badge.svg)](https://docs.rs/jacquard-lexicon) | 170| `jacquard-lexgen` | Code generation binaries | [![Crates.io](https://img.shields.io/crates/v/jacquard-lexgen.svg)](https://crates.io/crates/jacquard-lexgen) [![Documentation](https://docs.rs/jacquard-lexgen/badge.svg)](https://docs.rs/jacquard-lexgen) | 171| `jacquard-derive` | Macros for lexicon types | [![Crates.io](https://img.shields.io/crates/v/jacquard-derive.svg)](https://crates.io/crates/jacquard-derive) [![Documentation](https://docs.rs/jacquard-derive/badge.svg)](https://docs.rs/jacquard-derive) | 172 173## Development 174 175This repo uses [Flakes](https://nixos.asia/en/flakes) 176 177```bash 178# Dev shell 179nix develop 180 181# or run via cargo 182nix develop -c cargo run 183 184# build 185nix build 186``` 187 188There'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. 189 190[![License](https://img.shields.io/crates/l/jacquard.svg)](./LICENSE)