A better Rust ATProto crate
101
fork

Configure Feed

Select the types of activity you want to include in your feed.

reworked workspace deps, descriptions, added example to readme

Orual 590fc90f 30ecb131

+180 -104
+31 -2
Cargo.toml
··· 15 15 exclude = [".direnv"] 16 16 17 17 18 - description = "A simple Rust project using Nix" 19 - 18 + description = "Simple and powerful AT Protocol client library for Rust" 20 19 21 20 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 22 21 23 22 [workspace.dependencies] 23 + # CLI 24 24 clap = { version = "4.5", features = ["derive"] } 25 + 26 + # Serialization 27 + serde = { version = "1.0", features = ["derive"] } 28 + serde_json = "1.0" 29 + serde_with = "3.14" 30 + serde_html_form = "0.2" 31 + serde_ipld_dagcbor = "0.6" 32 + serde_repr = "0.1" 33 + 34 + # Error handling 35 + miette = "7.6" 36 + thiserror = "2.0" 37 + 38 + # Data types 39 + bytes = "1.10" 40 + smol_str = { version = "0.3", features = ["serde"] } 41 + url = "2.5" 42 + 43 + # Proc macros 44 + proc-macro2 = "1.0" 45 + quote = "1.0" 46 + syn = "2.0" 47 + heck = "0.5" 48 + itertools = "0.14" 49 + prettyplease = "0.2" 50 + 51 + # HTTP 52 + http = "1.3" 53 + reqwest = { version = "0.12", default-features = false }
+71 -26
README.md
··· 2 2 3 3 A suite of Rust crates for the AT Protocol. 4 4 5 + ## Example 6 + 7 + Dead simple api client. Logs in, prints the latest 5 posts from your timeline. 8 + 9 + ```rust 10 + use clap::Parser; 11 + use jacquard::CowStr; 12 + use jacquard::api::app_bsky::feed::get_timeline::GetTimeline; 13 + use jacquard::api::com_atproto::server::create_session::CreateSession; 14 + use jacquard::client::{AuthenticatedClient, Session, XrpcClient}; 15 + use miette::IntoDiagnostic; 16 + 17 + #[derive(Parser, Debug)] 18 + #[command(author, version, about = "Jacquard - AT Protocol client demo")] 19 + struct Args { 20 + /// Username/handle (e.g., alice.mosphere.at) 21 + #[arg(short, long)] 22 + username: CowStr<'static>, 23 + 24 + /// PDS URL (e.g., https://bsky.social) 25 + #[arg(long, default_value = "https://bsky.social")] 26 + pds: CowStr<'static>, 27 + 28 + /// App password 29 + #[arg(short, long)] 30 + password: CowStr<'static>, 31 + } 32 + 33 + #[tokio::main] 34 + async fn main() -> miette::Result<()> { 35 + let args = Args::parse(); 36 + 37 + // Create HTTP client 38 + let mut client = AuthenticatedClient::new(reqwest::Client::new(), args.pds); 39 + 40 + // Create session 41 + let session = Session::from( 42 + client 43 + .send( 44 + CreateSession::new() 45 + .identifier(args.username) 46 + .password(args.password) 47 + .build(), 48 + ) 49 + .await? 50 + .into_output()?, 51 + ); 52 + 53 + println!("logged in as {} ({})", session.handle, session.did); 54 + client.set_session(session); 55 + 56 + // Fetch timeline 57 + println!("\nfetching timeline..."); 58 + let timeline = client 59 + .send(GetTimeline::new().limit(5).build()) 60 + .await? 61 + .into_output()?; 62 + 63 + println!("\ntimeline ({} posts):", timeline.feed.len()); 64 + for (i, post) in timeline.feed.iter().enumerate() { 65 + println!("\n{}. by {}", i + 1, post.post.author.handle); 66 + println!( 67 + " {}", 68 + serde_json::to_string_pretty(&post.post.record).into_diagnostic()? 69 + ); 70 + } 71 + 72 + Ok(()) 73 + } 74 + ``` 75 + 5 76 ## Goals 6 77 7 78 - Validated, spec-compliant, easy to work with, and performant baseline types (including typed at:// uris) ··· 29 100 ``` 30 101 31 102 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. 32 - 33 - 34 - 35 - ### String types 36 - Something of a note to self. Developing a pattern with the string types (may macro-ify at some point). Each needs: 37 - - new(): constructing from a string slice with the right lifetime that borrows 38 - - new_owned(): constructing from an impl AsRef<str>, taking ownership 39 - - new_static(): construction from a &'static str, using SmolStr's/CowStr's new_static() constructor to not allocate 40 - - raw(): same as new() but panics instead of erroring 41 - - unchecked(): same as new() but doesn't validate. marked unsafe. 42 - - as_str(): does what it says on the tin 43 - #### Traits: 44 - - Serialize + Deserialize (custom impl for latter, sometimes for former) 45 - - FromStr 46 - - Display 47 - - Debug, PartialEq, Eq, Hash, Clone 48 - - From<T> for String, CowStr, SmolStr, 49 - - From<String>, From<CowStr>, From<SmolStr>, or TryFrom if likely enough to fail in practice to make panics common 50 - - AsRef<str> 51 - - Deref with Target = str (usually) 52 - 53 - Use `#[repr(transparent)]` as much as possible. Main exception is at-uri type and components. 54 - Use SmolStr directly as the inner type if most or all of the instances will be under 24 bytes, save lifetime headaches. 55 - Use CowStr for longer to allow for borrowing from input. 56 - 57 - TODO: impl IntoStatic trait to take ownership of string types
+8 -8
crates/jacquard-api/Cargo.toml
··· 1 1 [package] 2 2 name = "jacquard-api" 3 + description = "Generated AT Protocol API bindings for Jacquard" 3 4 edition.workspace = true 4 5 version.workspace = true 5 6 authors.workspace = true ··· 7 8 keywords.workspace = true 8 9 categories.workspace = true 9 10 readme.workspace = true 10 - documentation.workspace = true 11 + documentation = "https://docs.rs/jacquard-api" 11 12 exclude.workspace = true 12 - description.workspace = true 13 13 14 14 [features] 15 15 default = [ "com_atproto"] ··· 20 20 21 21 [dependencies] 22 22 bon = "3" 23 - bytes = { version = "1.10.1", features = ["serde"] } 24 - jacquard-common = { version = "0.1.0", path = "../jacquard-common" } 25 - jacquard-derive = { version = "0.1.0", path = "../jacquard-derive" } 26 - miette = "7.6.0" 27 - serde = { version = "1.0.228", features = ["derive"] } 28 - thiserror = "2.0.17" 23 + bytes = { workspace = true, features = ["serde"] } 24 + jacquard-common = { path = "../jacquard-common" } 25 + jacquard-derive = { path = "../jacquard-derive" } 26 + miette.workspace = true 27 + serde.workspace = true 28 + thiserror.workspace = true
+11 -11
crates/jacquard-common/Cargo.toml
··· 1 1 [package] 2 2 name = "jacquard-common" 3 + description = "Core AT Protocol types and utilities for Jacquard" 3 4 edition.workspace = true 4 5 version.workspace = true 5 6 authors.workspace = true ··· 7 8 keywords.workspace = true 8 9 categories.workspace = true 9 10 readme.workspace = true 10 - documentation.workspace = true 11 + documentation = "https://docs.rs/jacquard-common" 11 12 exclude.workspace = true 12 - description.workspace = true 13 13 14 14 15 15 16 16 [dependencies] 17 17 base64 = "0.22.1" 18 - bytes = "1.10.1" 18 + bytes.workspace = true 19 19 chrono = "0.4.42" 20 20 cid = { version = "0.11.1", features = ["serde", "std"] } 21 21 enum_dispatch = "0.3.13" 22 22 ipld-core = { version = "0.4.2", features = ["serde"] } 23 23 langtag = { version = "0.4.0", features = ["serde"] } 24 - miette = "7.6.0" 24 + miette.workspace = true 25 25 multibase = "0.9.1" 26 26 multihash = "0.19.3" 27 27 num-traits = "0.2.19" 28 28 ouroboros = "0.18.5" 29 29 rand = "0.9.2" 30 30 regex = "1.11.3" 31 - serde = { version = "1.0.227", features = ["derive"] } 32 - serde_html_form = "0.2.8" 33 - serde_json = "1.0.145" 34 - serde_with = "3.14.1" 35 - smol_str = { version = "0.3.2", features = ["serde"] } 36 - thiserror = "2.0.16" 37 - url = "2.5.7" 31 + serde.workspace = true 32 + serde_html_form.workspace = true 33 + serde_json.workspace = true 34 + serde_with.workspace = true 35 + smol_str.workspace = true 36 + thiserror.workspace = true 37 + url.workspace = true
+13 -13
crates/jacquard-derive/Cargo.toml
··· 1 1 [package] 2 2 name = "jacquard-derive" 3 + description = "Procedural macros for Jacquard lexicon types" 3 4 edition.workspace = true 4 5 version.workspace = true 5 6 authors.workspace = true ··· 7 8 keywords.workspace = true 8 9 categories.workspace = true 9 10 readme.workspace = true 10 - documentation.workspace = true 11 + documentation = "https://docs.rs/jacquard-derive" 11 12 exclude.workspace = true 12 - description.workspace = true 13 13 14 14 [lib] 15 15 proc-macro = true 16 16 17 17 [dependencies] 18 - heck = "0.5.0" 19 - itertools = "0.14.0" 20 - prettyplease = "0.2.37" 21 - proc-macro2 = "1.0.101" 22 - quote = "1.0.41" 23 - serde = { version = "1.0.228", features = ["derive"] } 24 - serde_json = "1.0.145" 25 - serde_repr = "0.1.20" 26 - serde_with = "3.14.1" 27 - syn = "2.0.106" 18 + heck.workspace = true 19 + itertools.workspace = true 20 + prettyplease.workspace = true 21 + proc-macro2.workspace = true 22 + quote.workspace = true 23 + serde.workspace = true 24 + serde_json.workspace = true 25 + serde_repr.workspace = true 26 + serde_with.workspace = true 27 + syn.workspace = true 28 28 29 29 30 30 [dev-dependencies] 31 - jacquard-common = { version = "0.1.0", path = "../jacquard-common" } 31 + jacquard-common = { path = "../jacquard-common" }
+15 -15
crates/jacquard-lexicon/Cargo.toml
··· 1 1 [package] 2 2 name = "jacquard-lexicon" 3 + description = "Lexicon schema parsing and code generation for Jacquard" 3 4 edition.workspace = true 4 5 version.workspace = true 5 6 authors.workspace = true ··· 9 10 readme.workspace = true 10 11 documentation.workspace = true 11 12 exclude.workspace = true 12 - description.workspace = true 13 13 14 14 [[bin]] 15 15 name = "jacquard-codegen" 16 16 path = "src/bin/codegen.rs" 17 17 18 18 [dependencies] 19 - clap = { workspace = true } 20 - heck = "0.5.0" 21 - itertools = "0.14.0" 22 - jacquard-common = { version = "0.1.0", path = "../jacquard-common" } 23 - miette = { version = "7.6.0", features = ["fancy"] } 24 - prettyplease = "0.2.37" 25 - proc-macro2 = "1.0.101" 26 - quote = "1.0.41" 27 - serde = { version = "1.0.228", features = ["derive"] } 28 - serde_json = "1.0.145" 29 - serde_repr = "0.1.20" 30 - serde_with = "3.14.1" 31 - syn = "2.0.106" 32 - thiserror = "2.0.17" 19 + clap.workspace = true 20 + heck.workspace = true 21 + itertools.workspace = true 22 + jacquard-common = { path = "../jacquard-common" } 23 + miette = { workspace = true, features = ["fancy"] } 24 + prettyplease.workspace = true 25 + proc-macro2.workspace = true 26 + quote.workspace = true 27 + serde.workspace = true 28 + serde_json.workspace = true 29 + serde_repr.workspace = true 30 + serde_with.workspace = true 31 + syn.workspace = true 32 + thiserror.workspace = true
+12 -12
crates/jacquard/Cargo.toml
··· 1 1 [package] 2 2 name = "jacquard" 3 - description = "Simple and powerful AT Procotol implementation" 3 + description.workspace = true 4 4 edition.workspace = true 5 5 version.workspace = true 6 6 authors.workspace = true ··· 26 26 path = "src/main.rs" 27 27 28 28 [dependencies] 29 - bytes = "1.10" 30 - clap = { workspace = true } 31 - http = "1.3.1" 32 - jacquard-api = { version = "0.1.0", path = "../jacquard-api" } 29 + bytes.workspace = true 30 + clap.workspace = true 31 + http.workspace = true 32 + jacquard-api = { path = "../jacquard-api" } 33 33 jacquard-common = { path = "../jacquard-common" } 34 34 jacquard-derive = { path = "../jacquard-derive", optional = true } 35 - miette = "7.6.0" 36 - reqwest = { version = "0.12.23", default-features = false, features = ["charset", "http2", "json", "system-proxy", "gzip", "rustls-tls"] } 37 - serde = { version = "1.0", features = ["derive"] } 38 - serde_html_form = "0.2" 39 - serde_ipld_dagcbor = "0.6.4" 40 - serde_json = "1.0" 41 - thiserror = "2.0" 35 + miette.workspace = true 36 + reqwest = { workspace = true, features = ["charset", "http2", "json", "system-proxy", "gzip", "rustls-tls"] } 37 + serde.workspace = true 38 + serde_html_form.workspace = true 39 + serde_ipld_dagcbor.workspace = true 40 + serde_json.workspace = true 41 + thiserror.workspace = true 42 42 tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
+19 -17
crates/jacquard/src/main.rs
··· 1 1 use clap::Parser; 2 + use jacquard::CowStr; 3 + use jacquard::api::app_bsky::feed::get_timeline::GetTimeline; 4 + use jacquard::api::com_atproto::server::create_session::CreateSession; 2 5 use jacquard::client::{AuthenticatedClient, Session, XrpcClient}; 3 - use jacquard_api::app_bsky::feed::get_timeline::GetTimeline; 4 - use jacquard_api::com_atproto::server::create_session::CreateSession; 5 - use jacquard_common::CowStr; 6 6 use miette::IntoDiagnostic; 7 7 8 8 #[derive(Parser, Debug)] ··· 20 20 #[arg(short, long)] 21 21 password: CowStr<'static>, 22 22 } 23 - 24 23 #[tokio::main] 25 24 async fn main() -> miette::Result<()> { 26 25 let args = Args::parse(); 27 26 28 27 // Create HTTP client 29 - let http = reqwest::Client::new(); 30 - let mut client = AuthenticatedClient::new(http, args.pds); 28 + let mut client = AuthenticatedClient::new(reqwest::Client::new(), args.pds); 31 29 32 30 // Create session 33 - println!("logging in as {}...", args.username); 34 - let create_session = CreateSession::new() 35 - .identifier(args.username) 36 - .password(args.password) 37 - .build(); 38 - 39 - let session_output = client.send(create_session).await?.into_output()?; 40 - let session = Session::from(session_output); 31 + let session = Session::from( 32 + client 33 + .send( 34 + CreateSession::new() 35 + .identifier(args.username) 36 + .password(args.password) 37 + .build(), 38 + ) 39 + .await? 40 + .into_output()?, 41 + ); 41 42 42 43 println!("logged in as {} ({})", session.handle, session.did); 43 44 client.set_session(session); 44 45 45 46 // Fetch timeline 46 47 println!("\nfetching timeline..."); 47 - let timeline_req = GetTimeline::new().limit(5).build(); 48 - 49 - let timeline = client.send(timeline_req).await?.into_output()?; 48 + let timeline = client 49 + .send(GetTimeline::new().limit(5).build()) 50 + .await? 51 + .into_output()?; 50 52 51 53 println!("\ntimeline ({} posts):", timeline.feed.len()); 52 54 for (i, post) in timeline.feed.iter().enumerate() {