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