diff --git a/crates/jacquard-axum/src/service_auth.rs b/crates/jacquard-axum/src/service_auth.rs index cc0e82e67..4e7277889 100644 --- a/crates/jacquard-axum/src/service_auth.rs +++ b/crates/jacquard-axum/src/service_auth.rs @@ -249,7 +249,7 @@ pub enum ServiceAuthError { #[error("missing Authorization header")] MissingAuthHeader, - /// Authorization header is malformed (not "Bearer ") + /// Authorization header is malformed (not "Bearer `token`") #[error("invalid Authorization header format")] InvalidAuthHeader, diff --git a/crates/jacquard-common/src/cowstr.rs b/crates/jacquard-common/src/cowstr.rs index d47bc7beb..7e3c7c34f 100644 --- a/crates/jacquard-common/src/cowstr.rs +++ b/crates/jacquard-common/src/cowstr.rs @@ -1,4 +1,4 @@ -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; use smol_str::SmolStr; use std::{ borrow::Cow, @@ -9,12 +9,13 @@ use std::{ use crate::IntoStatic; -/// Shamelessly copied from [](https://github.com/bearcove/merde) /// A copy-on-write immutable string type that uses [`SmolStr`] for /// the "owned" variant. /// /// The standard [`Cow`] type cannot be used, since /// `::Owned` is `String`, and not `SmolStr`. +/// +/// Shamelessly ported from [merde](https://github.com/bearcove/merde) #[derive(Clone)] pub enum CowStr<'s> { /// &str varaiant @@ -332,16 +333,6 @@ where } } -/// Serde helper for deserializing stuff when you want an owned version -pub fn deserialize_owned<'de, T, D>(deserializer: D) -> Result<::Output, D::Error> -where - T: Deserialize<'de> + IntoStatic, - D: Deserializer<'de>, -{ - let value = T::deserialize(deserializer)?; - Ok(value.into_static()) -} - /// Convert to a CowStr. pub trait ToCowStr { /// Convert to a CowStr. diff --git a/crates/jacquard-common/src/into_static.rs b/crates/jacquard-common/src/into_static.rs index 5f150d2c3..e106674ca 100644 --- a/crates/jacquard-common/src/into_static.rs +++ b/crates/jacquard-common/src/into_static.rs @@ -7,11 +7,12 @@ use std::hash::BuildHasher; use std::hash::Hash; use std::sync::Arc; -/// Shamelessly copied from [](https://github.com/bearcove/merde) /// Allow turning a value into an "owned" variant, which can then be /// returned, moved, etc. /// /// This usually involves allocating buffers for `Cow<'a, str>`, etc. +/// +/// Shamelessly copied from [merde](https://github.com/bearcove/merde) pub trait IntoStatic: Sized { /// The "owned" variant of the type. For `Cow<'a, str>`, this is `Cow<'static, str>`, for example. type Output: 'static; diff --git a/crates/jacquard-common/src/lib.rs b/crates/jacquard-common/src/lib.rs index b6040c432..caeeb6b58 100644 --- a/crates/jacquard-common/src/lib.rs +++ b/crates/jacquard-common/src/lib.rs @@ -211,11 +211,11 @@ pub mod error; /// HTTP client abstraction used by jacquard crates. pub mod http_client; pub mod macros; -/// Generic session storage traits and utilities. -pub mod session; /// Service authentication JWT parsing and verification. #[cfg(feature = "service-auth")] pub mod service_auth; +/// Generic session storage traits and utilities. +pub mod session; /// Baseline fundamental AT Protocol data types. pub mod types; // XRPC protocol types and traits @@ -239,3 +239,13 @@ impl<'s> IntoStatic for AuthorizationToken<'s> { } } } + +/// Serde helper for deserializing stuff when you want an owned version +pub fn deserialize_owned<'de, T, D>(deserializer: D) -> Result<::Output, D::Error> +where + T: serde::Deserialize<'de> + IntoStatic, + D: serde::Deserializer<'de>, +{ + let value = T::deserialize(deserializer)?; + Ok(value.into_static()) +} diff --git a/crates/jacquard-common/src/types/collection.rs b/crates/jacquard-common/src/types/collection.rs index a8fcc3e6e..c96dc8b41 100644 --- a/crates/jacquard-common/src/types/collection.rs +++ b/crates/jacquard-common/src/types/collection.rs @@ -21,7 +21,7 @@ pub trait Collection: fmt::Debug + Serialize { const NSID: &'static str; /// A marker type implementing [`XrpcResp`] that allows typed deserialization of records - /// from this collection. Used by [`Agent::get_record`] to return properly typed responses. + /// from this collection. Used by [`AgentSessionExt::get_record`](https://docs.rs/jacquard/latest/jacquard/client/trait.AgentSessionExt.html) to return properly typed responses. type Record: XrpcResp; /// Returns the [`Nsid`] for the Lexicon that defines the schema of records in this diff --git a/crates/jacquard/src/lib.rs b/crates/jacquard/src/lib.rs index 15bc8dbb2..9fdf8d546 100644 --- a/crates/jacquard/src/lib.rs +++ b/crates/jacquard/src/lib.rs @@ -1,19 +1,27 @@ //! # Jacquard //! -//! A suite of Rust crates for the AT Protocol. +//! 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](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. +//! +//! It is also designed around zero-copy/borrowed deserialization: types like [`Post<'_>`](https://docs.rs/jacquard-api/latest/jacquard_api/app_bsky/feed/post/struct.Post.html) 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. //! //! //! ## 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 -//! - 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 +//! - 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 +//! - 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 +//! - Use as much or as little from the crates as you need +//! //! //! //! @@ -77,7 +85,65 @@ //!} //! ``` //! -//! ## Client options: +//! +//! ## 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`](https://docs.rs/jacquard-common/latest/jacquard_common/index.html) - AT Protocol types (DIDs, handles, at-URIs, NSIDs, TIDs, CIDs, etc.) +//! - [`jacquard-api`](https://docs.rs/jacquard-api/latest/jacquard_api/index.html) - Generated API bindings from 646+ lexicon schemas +//! - [`jacquard-axum`](https://docs.rs/jacquard-axum/latest/jacquard_axum/index.html) - Server-side XRPC handler extractors for Axum framework (not re-exported, depends on jacquard) +//! - [`jacquard-oauth`](https://docs.rs/jacquard-oauth/latest/jacquard_oauth/index.html) - OAuth/DPoP flow implementation with session management +//! - [`jacquard-identity`](https://docs.rs/jacquard-identity/latest/jacquard_identity/index.html) - Identity resolution (handle → DID, DID → Doc, OAuth metadata) +//! - [`jacquard-lexicon`](https://docs.rs/jacquard-lexicon/latest/jacquard_lexicon/index.html) - Lexicon resolution, fetching, parsing and Rust code generation from schemas +//! - [`jacquard-derive`](https://docs.rs/jacquard-derive/latest/jacquard_derive/index.html) - Macros (`#[lexicon]`, `#[open_union]`, `#[derive(IntoStatic)]`) +//! +//! +//! ### 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](https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html) has a section on them if you want a refresher. +//! +//! > On Jacquard types like [`CowStr`], a `'static` lifetime parameter is used to refer to the owned +//! version of a type, in the same way `String` is 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](https://docs.rs/jacquard-common/latest/jacquard_common/#working-with-lifetimes-and-zero-copy-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"](https://doc.rust-lang.org/nomicon/subtyping.html), 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`](https://serde.rs/lifetimes.html) +//! 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` +//! 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](https://whtwnd.com/nonbinary.computer/3m33efvsylz2s) it uses in your own code. +//! +//! ## Client options //! //! - Stateless XRPC: any `HttpClient` (e.g., `reqwest::Client`) implements `XrpcExt`, //! which provides `xrpc(base: Url) -> XrpcCall` for per-request calls with @@ -112,7 +178,7 @@ //! base endpoint to the user's PDS on login/restore. //! - Stateful client (OAuth): `OAuthClient` and `OAuthSession` where `S: ClientAuthStore` and //! `T: OAuthResolver + HttpClient`. The client is used to authenticate, returning a session which handles authentication and token refresh internally. -//! - `Agent` Session abstracts over the above two options. Currently it is a thin wrapper, but this will be the thing that gets all the convenience helpers. +//! - `Agent` Session abstracts over the above two options and provides some useful convenience features via the [`AgentSessionExt`] trait. //! //! Per-request overrides (stateless) //! ```no_run @@ -145,18 +211,9 @@ //! } //! ``` //! -//! ## 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 schemas -//! - [`jacquard-axum`] - Server-side XRPC handler extractors for Axum framework (not re-exported, depends on jacquard) -//! - [`jacquard-oauth`] - OAuth/DPoP flow implementation with session management -//! - [`jacquard-identity`] - Identity resolution (handle→DID, DID→Doc, OAuth metadata) -//! - [`jacquard-lexicon`] - Lexicon resolution, fetching, parsing and Rust code generation from schemas -//! - [`jacquard-derive`] - Macros (`#[lexicon]`, `#[open_union]`, `#[derive(IntoStatic)]`) +//! [`deserialize_owned`]: crate::deserialize_owned +//! [`AgentSessionExt`]: crate::client::AgentSessionExt +//! [`.into_static()`]: IntoStatic #![warn(missing_docs)] @@ -164,15 +221,12 @@ pub mod client; pub use common::*; #[cfg(feature = "api")] -/// If enabled, re-export the generated api crate pub use jacquard_api as api; pub use jacquard_common as common; #[cfg(feature = "derive")] -/// if enabled, reexport the attribute macros pub use jacquard_derive::*; pub use jacquard_identity as identity; -/// OAuth usage helpers (discovery, PAR, token exchange) pub use jacquard_oauth as oauth;