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
··· 249 249 #[error("missing Authorization header")] 250 250 MissingAuthHeader, 251 251 252 - /// Authorization header is malformed (not "Bearer <token>") 252 + /// Authorization header is malformed (not "Bearer `token`") 253 253 #[error("invalid Authorization header format")] 254 254 InvalidAuthHeader, 255 255
+3 -12
crates/jacquard-common/src/cowstr.rs
··· 1 - use serde::{Deserialize, Deserializer, Serialize}; 1 + use serde::{Deserialize, Serialize}; 2 2 use smol_str::SmolStr; 3 3 use std::{ 4 4 borrow::Cow, ··· 9 9 10 10 use crate::IntoStatic; 11 11 12 - /// Shamelessly copied from [](https://github.com/bearcove/merde) 13 12 /// A copy-on-write immutable string type that uses [`SmolStr`] for 14 13 /// the "owned" variant. 15 14 /// 16 15 /// The standard [`Cow`] type cannot be used, since 17 16 /// `<str as ToOwned>::Owned` is `String`, and not `SmolStr`. 17 + /// 18 + /// Shamelessly ported from [merde](https://github.com/bearcove/merde) 18 19 #[derive(Clone)] 19 20 pub enum CowStr<'s> { 20 21 /// &str varaiant ··· 330 331 { 331 332 deserializer.deserialize_str(CowStrVisitor) 332 333 } 333 - } 334 - 335 - /// Serde helper for deserializing stuff when you want an owned version 336 - pub fn deserialize_owned<'de, T, D>(deserializer: D) -> Result<<T as IntoStatic>::Output, D::Error> 337 - where 338 - T: Deserialize<'de> + IntoStatic, 339 - D: Deserializer<'de>, 340 - { 341 - let value = T::deserialize(deserializer)?; 342 - Ok(value.into_static()) 343 334 } 344 335 345 336 /// Convert to a CowStr.
+2 -1
crates/jacquard-common/src/into_static.rs
··· 7 7 use std::hash::Hash; 8 8 use std::sync::Arc; 9 9 10 - /// Shamelessly copied from [](https://github.com/bearcove/merde) 11 10 /// Allow turning a value into an "owned" variant, which can then be 12 11 /// returned, moved, etc. 13 12 /// 14 13 /// This usually involves allocating buffers for `Cow<'a, str>`, etc. 14 + /// 15 + /// Shamelessly copied from [merde](https://github.com/bearcove/merde) 15 16 pub trait IntoStatic: Sized { 16 17 /// The "owned" variant of the type. For `Cow<'a, str>`, this is `Cow<'static, str>`, for example. 17 18 type Output: 'static;
+12 -2
crates/jacquard-common/src/lib.rs
··· 211 211 /// HTTP client abstraction used by jacquard crates. 212 212 pub mod http_client; 213 213 pub mod macros; 214 - /// Generic session storage traits and utilities. 215 - pub mod session; 216 214 /// Service authentication JWT parsing and verification. 217 215 #[cfg(feature = "service-auth")] 218 216 pub mod service_auth; 217 + /// Generic session storage traits and utilities. 218 + pub mod session; 219 219 /// Baseline fundamental AT Protocol data types. 220 220 pub mod types; 221 221 // XRPC protocol types and traits ··· 239 239 } 240 240 } 241 241 } 242 + 243 + /// Serde helper for deserializing stuff when you want an owned version 244 + pub fn deserialize_owned<'de, T, D>(deserializer: D) -> Result<<T as IntoStatic>::Output, D::Error> 245 + where 246 + T: serde::Deserialize<'de> + IntoStatic, 247 + D: serde::Deserializer<'de>, 248 + { 249 + let value = T::deserialize(deserializer)?; 250 + Ok(value.into_static()) 251 + }
+1 -1
crates/jacquard-common/src/types/collection.rs
··· 21 21 const NSID: &'static str; 22 22 23 23 /// A marker type implementing [`XrpcResp`] that allows typed deserialization of records 24 - /// from this collection. Used by [`Agent::get_record`] to return properly typed responses. 24 + /// from this collection. Used by [`AgentSessionExt::get_record`](https://docs.rs/jacquard/latest/jacquard/client/trait.AgentSessionExt.html) to return properly typed responses. 25 25 type Record: XrpcResp; 26 26 27 27 /// Returns the [`Nsid`] for the Lexicon that defines the schema of records in this
+79 -25
crates/jacquard/src/lib.rs
··· 1 1 //! # Jacquard 2 2 //! 3 - //! A suite of Rust crates for the AT Protocol. 3 + //! A suite of Rust crates intended to make it much easier to get started with atproto development, 4 + //! without sacrificing flexibility or performance. 5 + //! 6 + //! [Jacquard is simpler](https://whtwnd.com/nonbinary.computer/3m33efvsylz2s) because it is 7 + //! designed in a way which makes things simple that almost every other atproto library seems to make difficult. 8 + //! 9 + //! 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. 4 10 //! 5 11 //! 6 12 //! ## Goals and Features 7 13 //! 8 14 //! - Validated, spec-compliant, easy to work with, and performant baseline types 9 15 //! - Batteries-included, but easily replaceable batteries. 10 - //! - Easy to extend with custom lexicons 11 - //! - Straightforward OAuth 12 - //! - stateless options (or options where you handle the state) for rolling your own 13 - //! - all the building blocks of the convenient abstractions are available 14 - //! - lexicon Value type for working with unknown atproto data (dag-cbor or json) 15 - //! - order of magnitude less boilerplate than some existing crates 16 - //! - use as much or as little from the crates as you need 16 + //! - Easy to extend with custom lexicons using code generation or handwritten api types 17 + //! - Straightforward OAuth 18 + //! - Stateless options (or options where you handle the state) for rolling your own 19 + //! - All the building blocks of the convenient abstractions are available 20 + //! - Server-side convenience features 21 + //! - Lexicon Data value type for working with unknown atproto data (dag-cbor or json) 22 + //! - An order of magnitude less boilerplate than some existing crates 23 + //! - Use as much or as little from the crates as you need 24 + //! 17 25 //! 18 26 //! 19 27 //! ··· 77 85 //!} 78 86 //! ``` 79 87 //! 80 - //! ## Client options: 88 + //! 89 + //! ## Component crates 90 + //! 91 + //! Jacquard is split into several crates for modularity. The main `jacquard` crate 92 + //! re-exports most of the others, so you typically only need to depend on it directly. 93 + //! 94 + //! - [`jacquard-common`](https://docs.rs/jacquard-common/latest/jacquard_common/index.html) - AT Protocol types (DIDs, handles, at-URIs, NSIDs, TIDs, CIDs, etc.) 95 + //! - [`jacquard-api`](https://docs.rs/jacquard-api/latest/jacquard_api/index.html) - Generated API bindings from 646+ lexicon schemas 96 + //! - [`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) 97 + //! - [`jacquard-oauth`](https://docs.rs/jacquard-oauth/latest/jacquard_oauth/index.html) - OAuth/DPoP flow implementation with session management 98 + //! - [`jacquard-identity`](https://docs.rs/jacquard-identity/latest/jacquard_identity/index.html) - Identity resolution (handle → DID, DID → Doc, OAuth metadata) 99 + //! - [`jacquard-lexicon`](https://docs.rs/jacquard-lexicon/latest/jacquard_lexicon/index.html) - Lexicon resolution, fetching, parsing and Rust code generation from schemas 100 + //! - [`jacquard-derive`](https://docs.rs/jacquard-derive/latest/jacquard_derive/index.html) - Macros (`#[lexicon]`, `#[open_union]`, `#[derive(IntoStatic)]`) 101 + //! 102 + //! 103 + //! ### A note on lifetimes 104 + //! 105 + //! You'll notice a bunch of lifetimes all over Jacquard types, examples, and so on. If you're newer 106 + //! to Rust or have simply avoided them, they're part of how Rust knows how long to keep something 107 + //! around before cleaning it up. They're not unique to Rust (C and C++ have the same concept 108 + //! internally) but Rust is perhaps the one language that makes them explicit, because they're part 109 + //! of how it validates that things are memory-safe, and being able to give information to the compiler 110 + //! about how long it can expect something to stick around lets the compiler reason out much more 111 + //! 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. 112 + //! 113 + //! > On Jacquard types like [`CowStr`], a `'static` lifetime parameter is used to refer to the owned 114 + //! version of a type, in the same way `String` is the owned version of `&str`. 115 + //! 116 + //! This is somewhat in tension with the 'make things simpler' goal of the crate, but it is honestly 117 + //! pretty straightforward once you know the deal, and Jacquard provides a number of escape hatches 118 + //! and easy ways to work. 119 + //! 120 + //! Because explicit lifetimes are somewhat unique to Rust and are not something you may be used to 121 + //! thinking about, they can seem a bit scary to work with. Normally the compiler is pretty good at 122 + //! 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 123 + //! speed and efficiency, because borrowing from your source buffer saves copying the data around. 124 + //! 125 + //! However, it does mean that any Jacquard type that can borrow (not all of them do) is annotated 126 + //! 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 127 + //! at least the same amount of time, and that lifetime matches or exceeds the lifetime of the data 128 + //! structure. This also imposes certain restrictions on deserialization. Namely the [`DeserializeOwned`](https://serde.rs/lifetimes.html) 129 + //! bound does not apply to almost any types in Jacquard. There is a [`deserialize_owned`] function 130 + //! which you can use in a serde `deserialize_with` attribute to help, but the general pattern is 131 + //! to do borrowed deserialization and then call [`.into_static()`] if you need ownership. 132 + //! 133 + //! ### Easy mode 134 + //! 135 + //! Easy mode for jacquard is to mostly just use `'static` for your lifetime params and derive/use 136 + //! [`.into_static()`] as needed. When writing, first see if you can get away with `Thing<'_>` 137 + //! and let the compiler infer. second-easiest after that is `Thing<'static>`, third-easiest is giving 138 + //! everything one lifetime, e.g. `fn foo<'a>(&'a self, thing: Thing<'a>) -> /* thing with lifetime 'a */`. 139 + //! 140 + //! When parsing the output of atproto API calls, you can call `.into_output()` on the `Response<R>` 141 + //! struct to get an owned version with a `'static` lifetime. When deserializing, do not use 142 + //! `from_writer()` type deserialization functions, or features like Axum's `Json` extractor, as they 143 + //! have DeserializeOwned bounds and cannot borrow from their buffer. Either use Jacquard's features 144 + //! to get an owned version or follow the same [patterns](https://whtwnd.com/nonbinary.computer/3m33efvsylz2s) it uses in your own code. 145 + //! 146 + //! ## Client options 81 147 //! 82 148 //! - Stateless XRPC: any `HttpClient` (e.g., `reqwest::Client`) implements `XrpcExt`, 83 149 //! which provides `xrpc(base: Url) -> XrpcCall` for per-request calls with ··· 112 178 //! base endpoint to the user's PDS on login/restore. 113 179 //! - Stateful client (OAuth): `OAuthClient<S, T>` and `OAuthSession<S, T>` where `S: ClientAuthStore` and 114 180 //! `T: OAuthResolver + HttpClient`. The client is used to authenticate, returning a session which handles authentication and token refresh internally. 115 - //! - `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. 181 + //! - `Agent<A: AgentSession>` Session abstracts over the above two options and provides some useful convenience features via the [`AgentSessionExt`] trait. 116 182 //! 117 183 //! Per-request overrides (stateless) 118 184 //! ```no_run ··· 145 211 //! } 146 212 //! ``` 147 213 //! 148 - //! ## Component Crates 149 - //! 150 - //! Jacquard is split into several crates for modularity. The main `jacquard` crate 151 - //! re-exports most of the others, so you typically only need to depend on it directly. 152 - //! 153 - //! - [`jacquard-common`] - AT Protocol types (DIDs, handles, at-URIs, NSIDs, TIDs, CIDs, etc.) 154 - //! - [`jacquard-api`] - Generated API bindings from 646+ lexicon schemas 155 - //! - [`jacquard-axum`] - Server-side XRPC handler extractors for Axum framework (not re-exported, depends on jacquard) 156 - //! - [`jacquard-oauth`] - OAuth/DPoP flow implementation with session management 157 - //! - [`jacquard-identity`] - Identity resolution (handle→DID, DID→Doc, OAuth metadata) 158 - //! - [`jacquard-lexicon`] - Lexicon resolution, fetching, parsing and Rust code generation from schemas 159 - //! - [`jacquard-derive`] - Macros (`#[lexicon]`, `#[open_union]`, `#[derive(IntoStatic)]`) 214 + //! [`deserialize_owned`]: crate::deserialize_owned 215 + //! [`AgentSessionExt`]: crate::client::AgentSessionExt 216 + //! [`.into_static()`]: IntoStatic 160 217 161 218 #![warn(missing_docs)] 162 219 ··· 164 221 165 222 pub use common::*; 166 223 #[cfg(feature = "api")] 167 - /// If enabled, re-export the generated api crate 168 224 pub use jacquard_api as api; 169 225 pub use jacquard_common as common; 170 226 171 227 #[cfg(feature = "derive")] 172 - /// if enabled, reexport the attribute macros 173 228 pub use jacquard_derive::*; 174 229 175 230 pub use jacquard_identity as identity; 176 231 177 - /// OAuth usage helpers (discovery, PAR, token exchange) 178 232 pub use jacquard_oauth as oauth;