A better Rust ATProto crate

docs updates, improved explanation of lifetimes and patterns, linked to docs.rs

Orual c33ec212 c27a050b

Changed files
+98 -42
crates
jacquard
src
jacquard-axum
jacquard-common
+1 -1
crates/jacquard-axum/src/service_auth.rs
···
#[error("missing Authorization header")]
MissingAuthHeader,
-
/// Authorization header is malformed (not "Bearer <token>")
+
/// Authorization header is malformed (not "Bearer `token`")
#[error("invalid Authorization header format")]
InvalidAuthHeader,
+3 -12
crates/jacquard-common/src/cowstr.rs
···
-
use serde::{Deserialize, Deserializer, Serialize};
+
use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
use std::{
borrow::Cow,
···
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
/// `<str as ToOwned>::Owned` is `String`, and not `SmolStr`.
+
///
+
/// Shamelessly ported from [merde](https://github.com/bearcove/merde)
#[derive(Clone)]
pub enum CowStr<'s> {
/// &str varaiant
···
{
deserializer.deserialize_str(CowStrVisitor)
}
-
}
-
-
/// Serde helper for deserializing stuff when you want an owned version
-
pub fn deserialize_owned<'de, T, D>(deserializer: D) -> Result<<T as IntoStatic>::Output, D::Error>
-
where
-
T: Deserialize<'de> + IntoStatic,
-
D: Deserializer<'de>,
-
{
-
let value = T::deserialize(deserializer)?;
-
Ok(value.into_static())
}
/// Convert to a CowStr.
+2 -1
crates/jacquard-common/src/into_static.rs
···
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;
+12 -2
crates/jacquard-common/src/lib.rs
···
/// 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
···
}
}
}
+
+
/// Serde helper for deserializing stuff when you want an owned version
+
pub fn deserialize_owned<'de, T, D>(deserializer: D) -> Result<<T as IntoStatic>::Output, D::Error>
+
where
+
T: serde::Deserialize<'de> + IntoStatic,
+
D: serde::Deserializer<'de>,
+
{
+
let value = T::deserialize(deserializer)?;
+
Ok(value.into_static())
+
}
+1 -1
crates/jacquard-common/src/types/collection.rs
···
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
+79 -25
crates/jacquard/src/lib.rs
···
//! # 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
+
//!
//!
//!
//!
···
//!}
//! ```
//!
-
//! ## 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<R>`
+
//! 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
···
//! base endpoint to the user's PDS on login/restore.
//! - Stateful client (OAuth): `OAuthClient<S, T>` and `OAuthSession<S, T>` where `S: ClientAuthStore` and
//! `T: OAuthResolver + HttpClient`. The client is used to authenticate, returning a session which handles authentication and token refresh internally.
-
//! - `Agent<A: AgentSession>` 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<A: AgentSession>` Session abstracts over the above two options and provides some useful convenience features via the [`AgentSessionExt`] trait.
//!
//! Per-request overrides (stateless)
//! ```no_run
···
//! }
//! ```
//!
-
//! ## 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)]
···
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;