···6677[Jacquard is simpler](https://alpha.weaver.sh/nonbinary.computer/jacquard/jacquard_magic) because it is designed in a way which makes things simple that almost every other atproto library seems to make difficult.
8899-It is also designed around zero-copy/borrowed deserialization: types like [`Post<'_>`](https://tangled.org/nonbinary.computer/jacquard/blob/main/crates/jacquard-api/src/app_bsky/feed/post.rs) 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.
99+Jacquard generated types are generic over their string backing, but ordinary client code can usually ignore that detail. Use the generated request builders, pass normal strings, and call `.send(...).into_output()?` to get owned output that is easy to store, move independently of the response buffer, and pass through frameworks or APIs that require `DeserializeOwned`. If you need tighter control later, Jacquard still supports borrowing and zero-copy parsing with backing types such as `&str` and `CowStr<'_>`.
101011111212## Features
···26262727## Example
28282929-Dead simple API client. Logs in with OAuth and prints the latest 5 posts from your timeline.
2929+Dead simple API client. Resumes a stored OAuth session or opens a browser login, then prints the latest 5 posts from your timeline. This is the default path for local scripts and CLIs where browser login is acceptable; app-password credential sessions are mainly for unattended workflows that must re-authenticate non-interactively.
30303131```rust
3232-// Note: this requires the `loopback` feature enabled (it is currently by default)
3333-use clap::Parser;
3434-use jacquard::CowStr;
3232+// Note: this requires the `loopback` feature enabled (it is currently by default).
3533use jacquard::api::app_bsky::feed::get_timeline::GetTimeline;
3634use jacquard::client::{Agent, FileAuthStore};
3535+use jacquard::common::session::SessionHint;
3736use jacquard::oauth::client::OAuthClient;
3837use jacquard::oauth::loopback::LoopbackConfig;
3939-use jacquard::types::xrpc::XrpcClient;
3838+use jacquard::oauth::types::AuthorizeOptions;
3939+use jacquard::xrpc::XrpcClient;
4040use miette::IntoDiagnostic;
41414242-#[derive(Parser, Debug)]
4343-#[command(author, version, about = "Jacquard - OAuth (DPoP) loopback demo")]
4444-struct Args {
4545- /// Handle (e.g., alice.bsky.social), DID, or PDS URL
4646- input: CowStr<'static>,
4747-4848- /// Path to auth store file (will be created if missing)
4949- #[arg(long, default_value = "/tmp/jacquard-oauth-session.json")]
5050- store: String,
5151-}
4242+const STORE_PATH: &str = "/tmp/jacquard-oauth-session.json";
52435344#[tokio::main]
5445async fn main() -> miette::Result<()> {
5555- let args = Args::parse();
4646+ let login_hint = std::env::args().nth(1);
4747+ let oauth = OAuthClient::with_default_config(FileAuthStore::new(STORE_PATH));
4848+ let hint = SessionHint::from_optional_input(login_hint.as_deref());
56495757- // Build an OAuth client with file-backed auth store and default localhost config
5858- let oauth = OAuthClient::with_default_config(FileAuthStore::new(&args.store));
5959- // Authenticate with a PDS, using a loopback server to handle the callback flow
6060- let session = oauth
6161- .login_with_local_server(
6262- args.input.clone(),
6363- Default::default(),
5050+ let Some(session) = oauth
5151+ .resume_or_login_with_local_server(
5252+ &hint,
5353+ AuthorizeOptions::default(),
6454 LoopbackConfig::default(),
6555 )
6666- .await?;
6767- // Wrap in Agent and fetch the timeline
5656+ .await?
5757+ else {
5858+ miette::bail!(
5959+ "no stored OAuth session found in {STORE_PATH}; pass a handle, DID, or PDS URL to log in"
6060+ );
6161+ };
6262+6863 let agent: Agent<_> = Agent::from(session);
6964 let timeline = agent
7070- .send(&GetTimeline::new().limit(5).build())
6565+ .send(GetTimeline::new().limit(5).build())
7166 .await?
7267 .into_output()?;
6868+7369 for (i, post) in timeline.feed.iter().enumerate() {
7470 println!("\n{}. by {}", i + 1, post.post.author.handle);
7571 println!(
···80768177 Ok(())
8278}
8383-8479```
85808681If you have `just` installed, you can run the [examples](https://tangled.org/nonbinary.computer/jacquard/tree/main/examples) using `just example {example-name} {ARGS}` or `just examples` to see what's available.
+50-99
crates/jacquard-common/src/lib.rs
···6767//! const NSID: &'static str;
6868//! /// Output encoding (MIME type)
6969//! const ENCODING: &'static str;
7070-//! type Output<'de>: Deserialize<'de> + IntoStatic;
7171-//! type Err<'de>: Error + Deserialize<'de> + IntoStatic;
7070+//! type Output<S: BosStr>;
7171+//! type Err: Error + Serialize + DeserializeOwned;
7272//! }
7373//! ```
7474//! Here are the implementations for [`GetTimeline`](https://tangled.org/@nonbinary.computer/jacquard/blob/main/crates/jacquard-api/src/app_bsky/feed/get_timeline.rs). You'll also note that `send()` doesn't return the fully decoded response on success. It returns a Response struct which has a generic parameter that must implement the XrpcResp trait above. Here's its definition. It's essentially just a cheaply cloneable byte buffer and a type marker.
···8181//! }
8282//!
8383//! impl<R: XrpcResp> Response<R> {
8484-//! pub fn parse<'s>(
8585-//! &'s self
8686-//! ) -> Result<<Resp as XrpcResp>::Output<'s>, XrpcError<<Resp as XrpcResp>::Err<'s>>> {
8787-//! // Borrowed parsing into Output or Err
8484+//! pub fn parse<'s, S>(&'s self) -> Result<R::Output<S>, XrpcError<R::Err>>
8585+//! where
8686+//! S: BosStr + Deserialize<'s>,
8787+//! R::Output<S>: Deserialize<'s>,
8888+//! {
8989+//! // Parse with the caller's chosen string backing.
8890//! }
8989-//! pub fn into_output(
9090-//! self
9191-//! ) -> Result<<Resp as XrpcResp>::Output<'static>, XrpcError<<Resp as XrpcResp>::Err<'static>>>
9292-//! where ...
9393-//! { /* Owned parsing into Output or Err */ }
9191+//! pub fn into_output(self) -> Result<R::Output<SmolStr>, XrpcError<R::Err>>
9292+//! where
9393+//! R::Output<SmolStr>: DeserializeOwned,
9494+//! {
9595+//! // Parse into owned/default-backed output.
9696+//! }
9497//! }
9598//! ```
9696-//! You decode the response (or the endpoint-specific error) out of this, borrowing from the buffer or taking ownership so you can drop the buffer. There are two reasons for this. One is separation of concerns. By two-staging the parsing, it's easier to distinguish network and authentication problems from application-level errors. The second is lifetimes and borrowed deserialization.
9999+//! You decode the response (or the endpoint-specific error) out of this when you are ready. There
100100+//! are two reasons for this. One is separation of concerns: by two-staging the parsing, it is easier
101101+//! to distinguish network and authentication problems from application-level errors. The second is
102102+//! string backing: callers can choose ordinary owned output or explicit borrowed parsing.
97103//!
9898-//! ## Working with Lifetimes and Zero-Copy Deserialization
104104+//! ## String backing, borrowing, and response parsing
99105//!
100100-//! Jacquard is designed around zero-copy/borrowed deserialization: types like [`Post<'a>`](https://tangled.org/@nonbinary.computer/jacquard/blob/main/crates/jacquard-api/src/app_bsky/feed/post.rs) can borrow strings and other data directly from the response buffer instead of allocating owned copies. This is great for performance, but it creates some interesting challenges, especially in async contexts. So how do you specify the lifetime of the borrow?
106106+//! Most generated output types are parameterized over a string backing type: `Output<S: BosStr>`.
107107+//! The usual path is owned output:
101108//!
102102-//! The naive approach would be to put a lifetime parameter on the trait itself:
109109+//! ```ignore
110110+//! let output = response.into_output()?;
111111+//! ```
103112//!
104104-//!```ignore
105105-//!// This looks reasonable but creates problems in generic/async contexts
106106-//!trait NaiveXrpcRequest<'de> {
107107-//! type Output: Deserialize<'de>;
108108-//! // ...
109109-//!}
110110-//!```
113113+//! `into_output()` parses into `SmolStr`-backed output. This is the convenient default when values
114114+//! need to be stored, moved independently of the response buffer, or passed through frameworks and
115115+//! APIs that require `DeserializeOwned`.
111116//!
112112-//! This looks reasonable until you try to use it in a generic context. If you have a function that works with *any* lifetime, you need a Higher-ranked trait bound:
117117+//! When you specifically want to borrow from the response buffer, choose a borrowed or
118118+//! borrow-or-own backing at parse time:
113119//!
114114-//!```ignore
115115-//!fn parse<R>(response: &[u8]) ... // return type
116116-//!where
117117-//! R: for<'any> XrpcRequest<'any>
118118-//!{ /* deserialize from response... */ }
119119-//!```
120120+//! ```ignore
121121+//! let output = response.parse::<CowStr<'_>>()?;
122122+//! let output = response.parse::<&str>()?;
123123+//! ```
120124//!
121121-//! The `for<'any>` bound says "this type must implement `XrpcRequest` for *every possible lifetime*", which, for `Deserialize`, is effectively the same as requiring `DeserializeOwned`. You've probably just thrown away your zero-copy optimization, and furthermore that trait bound just straight-up won't work on most of the types in Jacquard. The vast majority of them have either a custom Deserialize implementation which will borrow if it can, a `#[serde(borrow)]` attribute on one or more fields, or an equivalent lifetime bound attribute, associated with the Deserialize derive macro. You will get "Deserialize implementation not general enough" if you try. And no, you cannot have an additional deserialize implementation for the `'static` lifetime due to how serde works.
125125+//! Borrowed output works fine across async code as long as the value remains tied to the
126126+//! buffer-owning `Response`. The `.send()` method itself can stay lifetime-free because it returns
127127+//! that buffer-owning response, and the caller decides whether to parse into owned data or borrow
128128+//! from the buffer.
122129//!
123123-//! If you instead try something like the below function signature and specify a specific lifetime, it will compile in isolation, but when you go to use it, the Rust compiler will not generally be able to figure out the lifetimes at the call site, and will complain about things being dropped while still borrowed, even if you convert the response to an owned/ `'static` lifetime version of the type.
130130+//! `XrpcResp` stays lifetime-free too. Its success output is a GAT over the backing string type,
131131+//! and its error type is a plain owned type:
124132//!
125125-//!```ignore
126126-//!fn parse<'s, R: XrpcRequest<'s>>(response: &'s [u8]) ... // return type with the same lifetime
127127-//!{ /* deserialize from response... */ }
128128-//!```
129129-//!
130130-//! It gets worse with async. If you want to return borrowed data from an async method, where does the lifetime come from? The response buffer needs to outlive the borrow, but the buffer is consumed or potentially has to have an unbounded lifetime. You end up with confusing and frustrating errors because the compiler can't prove the buffer will stay alive or that you have taken ownership of the parts of it you care about. You *could* do some lifetime laundering with `unsafe`, but that road leads to potential soundness issues, and besides, you don't actually *need* to tell `rustc` to "trust me, bro", you can, with some cleverness, explain this to the compiler in a way that it can reason about perfectly well.
131131-//!
132132-//! ### Explaining where the buffer goes to `rustc`
133133-//!
134134-//! The fix is to use Generic Associated Types (GATs) on the trait's associated types, while keeping the trait itself lifetime-free:
135135-//!
136136-//!```ignore
137137-//!pub trait XrpcResp {
138138-//! const NSID: &'static str;
139139-//! /// Output encoding (MIME type)
140140-//! const ENCODING: &'static str;
141141-//! type Output<'de>: Deserialize<'de> + IntoStatic;
142142-//! type Err<'de>: Error + Deserialize<'de> + IntoStatic;
143143-//!}
144144-//!```
145145-//!
146146-//!Now you can write trait bounds without HRTBs, and with lifetime bounds that are actually possible for Jacquard's borrowed deserializing types to meet:
147147-//!
148148-//!```ignore
149149-//!fn parse<'s, R: XrpcResp>(response: &'s [u8]) /* return type with same lifetime */ {
150150-//! // Compiler can pick a concrete lifetime for R::Output<'_> or have it specified easily
151151-//!}
152152-//!```
153153-//!
154154-//!Methods that need lifetimes use method-level generic parameters:
155155-//!
156156-//!```ignore
157157-//!// This is part of a trait from jacquard itself, used to genericize updates to things like the Bluesky
158158-//!// preferences union, so that if you implement a similar lexicon type in your app, you don't have
159159-//!// to special-case it. Instead you can do a relatively simple trait implementation and then call
160160-//!// .update_vec() with a modifier function or .update_vec_item() with a single item you want to set.
161161-//
162162-//!pub trait VecUpdate {
163163-//! type GetRequest: XrpcRequest;
164164-//! type PutRequest: XrpcRequest;
165165-//! //... more stuff
166166-//
167167-//! //Method-level lifetime, GAT on response type
168168-//! fn extract_vec<'s>(
169169-//! output: <<Self::GetRequest as XrpcRequest>::Response as XrpcResp>::Output<'s>
170170-//! ) -> Vec<Self::Item>;
171171-//! //... more stuff
172172-//!}
173173-//!```
133133+//! ```ignore
134134+//! pub trait XrpcResp {
135135+//! const NSID: &'static str;
136136+//! const ENCODING: &'static str;
137137+//! type Output<S: BosStr>;
138138+//! type Err: Error + Serialize + DeserializeOwned;
139139+//! }
140140+//! ```
174141//!
175175-//!The compiler can monomorphize for concrete lifetimes instead of trying to prove bounds hold for *all* lifetimes at once, or struggle to figure out when you're done with a buffer. `XrpcResp` being separate and lifetime-free lets async methods like `.send()` return a `Response` that owns the response buffer, and then the *caller* decides the lifetime strategy:
176176-//!
177177-//!```ignore
178178-//!// Zero-copy: borrow from the owned buffer
179179-//!let output: R::Output<'_> = response.parse()?;
180180-//
181181-//!// Owned: convert to 'static via IntoStatic
182182-//!let output: R::Output<'static> = response.into_output()?;
183183-//!```
184184-//!
185185-//! The async method doesn't need to know or care about lifetimes for the most part - it just returns the `Response`. The caller gets full control over whether to use borrowed or owned data. It can even decide after the fact that it doesn't want to parse out the API response type that it asked for. Instead it can call `.parse_data()` or `.parse_raw()` on the response to get loosely typed, validated data or minimally typed maximally accepting data values out.
186186-//!
187187-//! When you see types like `Response<R: XrpcResp>` or methods with lifetime parameters,
188188-//! this is the pattern at work. It looks a bit funky, but it's solving a specific problem
189189-//! in a way that doesn't require unsafe code or much actual work from you, if you're using it.
190190-//! It's also not too bad to write, once you're aware of the pattern and why it works. If you run
191191-//! into a lifetime/borrowing inference issue in jacquard, please contact the crate author. She'd
192192-//! be happy to debug, and if it's using a method from one of the jacquard crates and seems like
193193-//! it *should* just work, that is a bug in jacquard, and you should [file an issue](https://tangled.org/nonbinary.computer/jacquard/).
142142+//! Keeping endpoint errors owned avoids lifetime gymnastics on the unhappy path. If you do not want
143143+//! to parse the endpoint-specific success type, `Response` also supports `.parse_data()` and
144144+//! `.parse_raw()` for loosely typed atproto data.
194145195146#![no_std]
196147#![warn(missing_docs)]
+121-16
crates/jacquard-common/src/macros.rs
···11//! `atproto!` macro.
22-/// Construct a atproto `Data<'_>` value from a literal.
22+33+use crate::{DefaultStr, FromStaticStr, types::value::Data};
44+55+/// Hidden conversion hook used by the [`atproto!`] macro.
66+#[doc(hidden)]
77+pub trait AtprotoMacroLiteral {
88+ /// Convert this literal into default-backed AT Protocol data.
99+ fn into_atproto_data(self) -> Data;
1010+}
1111+1212+impl AtprotoMacroLiteral for &'static str {
1313+ fn into_atproto_data(self) -> Data {
1414+ Data::String(crate::types::string::AtprotoStr::new(
1515+ DefaultStr::from_static(self),
1616+ ))
1717+ }
1818+}
1919+2020+macro_rules! impl_atproto_macro_integer_literal {
2121+ ($($ty:ty),* $(,)?) => {
2222+ $(
2323+ impl AtprotoMacroLiteral for $ty {
2424+ fn into_atproto_data(self) -> Data {
2525+ Data::Integer(i64::from(self))
2626+ }
2727+ }
2828+ )*
2929+ };
3030+}
3131+3232+macro_rules! impl_atproto_macro_checked_integer_literal {
3333+ ($($ty:ty),* $(,)?) => {
3434+ $(
3535+ impl AtprotoMacroLiteral for $ty {
3636+ fn into_atproto_data(self) -> Data {
3737+ Data::Integer(self.try_into().expect("integer literal exceeds the AT Protocol i64 range"))
3838+ }
3939+ }
4040+ )*
4141+ };
4242+}
4343+4444+impl_atproto_macro_integer_literal!(i8, i16, i32, u8, u16, u32);
4545+impl_atproto_macro_checked_integer_literal!(i64, i128, isize, u64, u128, usize);
4646+4747+/// Construct a default-backed atproto [`Data`] value from a literal.
4848+///
4949+/// [`Data`]: crate::types::value::Data
350///
451/// ```
552/// # use jacquard_common::atproto;
···1865///
1966/// Variables or expressions can be interpolated into the ATProto literal. Any type
2067/// interpolated into an array element or object value must implement Serde's
2121-/// `Serialize` trait, while any type interpolated into a object key must
2222-/// implement `Into<String>`. If the `Serialize` implementation of the
2323-/// interpolated type decides to fail, or if the interpolated type contains a
2424-/// map with non-string keys, the `atproto!` macro will panic.
6868+/// `Serialize` trait, while any type interpolated into an object key must
6969+/// convert into the default string backing. If the `Serialize` implementation
7070+/// of the interpolated type decides to fail, or if the interpolated type
7171+/// contains a map with non-string keys, the `atproto!` macro will panic.
2572///
2673/// ```
2774/// # use jacquard_common::atproto;
···138185139186 // Insert the current entry followed by trailing comma.
140187 (@object $object:ident [$($key:tt)+] ($value:expr) , $($rest:tt)*) => {
141141- let _ = $object.insert(($($key)+).into(), $value);
188188+ let _ = $object.insert(atproto_internal_key!($($key)+), $value);
142189 atproto_internal!(@object $object () ($($rest)*) ($($rest)*));
143190 };
144191···149196150197 // Insert the last entry without trailing comma.
151198 (@object $object:ident [$($key:tt)+] ($value:expr)) => {
152152- let _ = $object.insert(($($key)+).into(), $value);
199199+ let _ = $object.insert(atproto_internal_key!($($key)+), $value);
153200 };
154201155202 // Next value is `null`.
···230277 //////////////////////////////////////////////////////////////////////////
231278232279 (null) => {
233233- $crate::types::value::Data::Null
280280+ $crate::types::value::Data::<$crate::DefaultStr>::Null
234281 };
235282236283 (true) => {
237237- $crate::types::value::Data::Boolean(true)
284284+ $crate::types::value::Data::<$crate::DefaultStr>::Boolean(true)
238285 };
239286240287 (false) => {
241241- $crate::types::value::Data::Boolean(false)
288288+ $crate::types::value::Data::<$crate::DefaultStr>::Boolean(false)
242289 };
243290244291 ([]) => {
245245- $crate::types::value::Data::Array($crate::types::value::Array(atproto_internal_vec![]))
292292+ $crate::types::value::Data::<$crate::DefaultStr>::Array($crate::types::value::Array(atproto_internal_vec![]))
246293 };
247294248295 ([ $($tt:tt)+ ]) => {
249249- $crate::types::value::Data::Array($crate::types::value::Array(atproto_internal!(@array [] $($tt)+)))
296296+ $crate::types::value::Data::<$crate::DefaultStr>::Array($crate::types::value::Array(atproto_internal!(@array [] $($tt)+)))
250297 };
251298252299 ({}) => {
253253- $crate::types::value::Data::Object($crate::types::value::Object(::std::collections::BTreeMap::new()))
300300+ $crate::types::value::Data::<$crate::DefaultStr>::Object($crate::types::value::Object(::std::collections::BTreeMap::new()))
254301 };
255302256303 ({ $($tt:tt)+ }) => {
257257- $crate::types::value::Data::Object($crate::types::value::Object({
304304+ $crate::types::value::Data::<$crate::DefaultStr>::Object($crate::types::value::Object({
258305 let mut object = ::std::collections::BTreeMap::new();
259306 atproto_internal!(@object object () ($($tt)+) ($($tt)+));
260307 object
261308 }))
262309 };
263310264264- // Any Serialize type: numbers, strings, struct literals, variables etc.
311311+ // Literal values go through a helper so string literals can use static
312312+ // storage while integer literals remain integers.
313313+ ($literal:literal) => {
314314+ $crate::macros::AtprotoMacroLiteral::into_atproto_data($literal)
315315+ };
316316+317317+ // Any Serialize type: variables, struct literals, dynamic strings etc.
265318 // Must be below every other rule.
266319 ($other:expr) => {
267320 {
268268- $crate::types::value::Data::from($other)
321321+ $crate::types::value::Data::<$crate::DefaultStr>::from($other)
269322 }
270323 };
271324}
···283336284337#[macro_export]
285338#[doc(hidden)]
339339+macro_rules! atproto_internal_key {
340340+ ($key:literal) => {
341341+ <$crate::DefaultStr as $crate::FromStaticStr>::from_static($key)
342342+ };
343343+344344+ ($key:expr) => {
345345+ ($key).into()
346346+ };
347347+}
348348+349349+#[macro_export]
350350+#[doc(hidden)]
286351macro_rules! atproto_unexpected {
287352 () => {};
288353}
354354+355355+#[cfg(test)]
356356+mod tests {
357357+ use crate::{DefaultStr, types::value::Data};
358358+359359+ const LONG_KEY: &str = "a-static-key-that-is-longer-than-inline-capacity";
360360+ const LONG_VALUE: &str = "a static string value that is longer than inline capacity";
361361+362362+ #[test]
363363+ fn string_literals_use_static_default_backing() {
364364+ let value = atproto!({
365365+ "a-static-key-that-is-longer-than-inline-capacity":
366366+ "a static string value that is longer than inline capacity"
367367+ });
368368+369369+ let Data::Object(object) = value else {
370370+ panic!("expected object");
371371+ };
372372+ let (key, value) = object.0.iter().next().expect("object has one field");
373373+374374+ assert_eq!(key.as_str(), LONG_KEY);
375375+ assert!(!key.is_heap_allocated());
376376+377377+ let Data::String(string) = value else {
378378+ panic!("expected string value");
379379+ };
380380+ assert_eq!(string.as_str(), LONG_VALUE);
381381+ if let crate::types::string::AtprotoStr::String(backing) = string {
382382+ assert!(!backing.is_heap_allocated());
383383+ } else {
384384+ panic!("test value should not be inferred as a richer atproto string type");
385385+ }
386386+ }
387387+388388+ #[test]
389389+ fn macro_result_defaults_to_default_backing_without_context() {
390390+ let value = atproto!(["hello", 200, true, null]);
391391+ let _: Data<DefaultStr> = value;
392392+ }
393393+}
+3-3
crates/jacquard-common/src/types/value.rs
···708708///
709709/// # Example
710710/// ```
711711-/// # use jacquard_common::types::value::{Data, from_data};
711711+/// # use jacquard_common::{atproto, Data};
712712+/// # use jacquard_common::types::value::from_data;
712713/// # use serde::Deserialize;
713714/// #
714715/// #[derive(Deserialize)]
···720721/// }
721722///
722723/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
723723-/// # let json = serde_json::json!({"text": "hello", "author": "alice"});
724724-/// # let data = Data::from_json(&json)?;
724724+/// # let data: Data = atproto!({"text": "hello", "author": "alice"});
725725/// let post: Post = from_data(&data)?;
726726/// # Ok(())
727727/// # }
+4-4
crates/jacquard-common/src/xrpc.rs
···469469/// # #[tokio::main]
470470/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
471471/// use jacquard_common::xrpc::XrpcExt;
472472-/// use jacquard_common::{AuthorizationToken, CowStr};
472472+/// use jacquard_common::AuthorizationToken;
473473/// use jacquard_common::deps::fluent_uri::Uri;
474474///
475475/// let http = reqwest::Client::new();
476476-/// let base = Uri::parse("https://public.api.bsky.app").unwrap().to_owned();
476476+/// let base = Uri::parse("https://public.api.bsky.app").unwrap();
477477/// let call = http
478478/// .xrpc(base)
479479-/// .auth(AuthorizationToken::Bearer(CowStr::from("ACCESS_JWT")))
480480-/// .accept_labelers(vec![CowStr::from("did:plc:labelerid")])
479479+/// .auth(AuthorizationToken::Bearer("ACCESS_JWT".into()))
480480+/// .accept_labelers(vec!["did:plc:labelerid".into()])
481481/// .header(http::header::USER_AGENT, http::HeaderValue::from_static("jacquard-example"));
482482/// // let resp = call.send(&request).await?;
483483/// # Ok(())
+16-15
crates/jacquard/src/client.rs
···144144 /// # #[tokio::main]
145145 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
146146 /// let client = BasicClient::unauthenticated();
147147- /// let uri = AtUri::new_static("at://did:plc:xyz/app.bsky.feed.post/3l5abc").unwrap();
147147+ /// let uri: AtUri = AtUri::new_static("at://did:plc:xyz/app.bsky.feed.post/3l5abc").unwrap();
148148 /// let response = client.get_record::<Post, _>(&uri).await?;
149149 /// # Ok(())
150150 /// # }
···655655///
656656/// // Read it back
657657/// let response = agent.get_record::<Post, _>(&output.uri).await?;
658658-/// let record = response.parse()?;
658658+/// let record = response.into_output()?;
659659/// println!("Post: {}", record.value.text);
660660/// # Ok(())
661661/// # }
···744744 /// Get a record from the repository using an at:// URI.
745745 ///
746746 /// Returns a typed `Response` that deserializes directly to the record type.
747747- /// Use `.parse()` to borrow from the response buffer, or `.into_output()` for owned data.
747747+ /// Use `.into_output()` for owned data, or `.parse::<S>()` to choose another string backing.
748748 ///
749749 /// # Example
750750 ///
···752752 /// # use jacquard::client::BasicClient;
753753 /// # use jacquard_api::app_bsky::feed::post::Post;
754754 /// # use jacquard_common::types::string::AtUri;
755755- /// # use jacquard_common::IntoStatic;
755755+ /// # use jacquard_common::CowStr;
756756 /// use jacquard::client::AgentSessionExt;
757757 /// # #[tokio::main]
758758 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
759759 /// # let agent: BasicClient = todo!();
760760- /// let uri = AtUri::new_static("at://did:plc:xyz/app.bsky.feed.post/3l5bqm7lepk2c").unwrap();
760760+ /// let uri: AtUri = AtUri::new_static("at://did:plc:xyz/app.bsky.feed.post/3l5bqm7lepk2c").unwrap();
761761 /// let response = agent.get_record::<Post, _>(&uri).await?;
762762- /// let output = response.parse()?; // PostGetRecordOutput<'_> borrowing from buffer
762762+ /// let output = response.into_output()?;
763763 /// println!("Post text: {}", output.value.text);
764764 ///
765765- /// // Or get owned data
766766- /// let output_owned = response.into_output()?;
765765+ /// // Or choose borrowed parsing explicitly.
766766+ /// let response = agent.get_record::<Post, _>(&uri).await?;
767767+ /// let borrowed = response.parse::<CowStr<'_>>()?;
768768+ /// let borrowed_strs = response.parse::<&str>()?;
767769 /// # Ok(())
768770 /// # }
769771 /// ```
···945947 /// ```no_run
946948 /// # use jacquard::client::BasicClient;
947949 /// # use jacquard_api::app_bsky::actor::profile::Profile;
948948- /// # use jacquard_common::CowStr;
949950 /// # use jacquard_common::types::string::AtUri;
950951 /// use jacquard::client::AgentSessionExt;
951952 /// # #[tokio::main]
952953 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
953954 /// # let agent: BasicClient = todo!();
954954- /// let uri = AtUri::new_static("at://did:plc:xyz/app.bsky.actor.profile/self").unwrap();
955955+ /// let uri: AtUri = AtUri::new_static("at://did:plc:xyz/app.bsky.actor.profile/self").unwrap();
955956 /// // Update profile record in-place
956957 /// agent.update_record::<Profile, _>(&uri, |profile| {
957957- /// profile.display_name = Some(CowStr::from("New Name"));
958958- /// profile.description = Some(CowStr::from("Updated bio"));
958958+ /// profile.display_name = Some("New Name".into());
959959+ /// profile.description = Some("Updated bio".into());
959960 /// }).await?;
960961 /// # Ok(())
961962 /// # }
···11241125 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
11251126 /// # let agent: BasicClient = todo!();
11261127 /// let data = std::fs::read("image.png")?;
11271127- /// let mime_type = MimeType::new_static("image/png");
11281128+ /// let mime_type = MimeType::new("image/png");
11281129 /// let blob_ref = agent.upload_blob(data, mime_type).await?;
11291130 /// # Ok(())
11301131 /// # }
···12741275 async move {
12751276 CredentialSession::<S, T, W>::session_info(self)
12761277 .await
12771277- // Convert the SmolStr session id to CowStr<'static>.
12781278+ // The session id is already owned as SmolStr.
12781279 .map(|key| (key.did, Some(key.session_id)))
12791280 }
12801281 }
···13051306 fn session_info(&self) -> impl Future<Output = Option<(Did, Option<SmolStr>)>> {
13061307 async {
13071308 let (did, sid) = OAuthSession::<T, S, W>::session_info(self).await;
13081308- // did is already Did<SmolStr>; convert SmolStr sid to CowStr<'static>.
13091309+ // Both the DID and session id are already owned.
13091310 Some((did, Some(sid)))
13101311 }
13111312 }
+47-83
crates/jacquard/src/lib.rs
···66//! [Jacquard is simpler](https://whtwnd.com/nonbinary.computer/3m33efvsylz2s) because it is
77//! designed in a way which makes things simple that almost every other atproto library seems to make difficult.
88//!
99-//! 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.
1010-//!
119//!
1210//! ## Features
1311//!
1414-//! - Validated, spec-compliant, easy to work with, and performant baseline types
1515-//! - Designed such that you can just work with generated API bindings easily
1616-//! - Straightforward OAuth
1717-//! - Server-side convenience features
1818-//! - Lexicon Data value type for working with unknown atproto data (dag-cbor or json)
1919-//! - An order of magnitude less boilerplate than some existing crates
1212+//! - Validated, spec-compliant, easy to work with, and performant baseline types.
1313+//! - Designed such that you can just work with generated API bindings easily.
1414+//! - Straightforward OAuth.
1515+//! - Server-side convenience features.
1616+//! - Lexicon Data value type for working with unknown atproto data (dag-cbor or json).
1717+//! - An order of magnitude less boilerplate than some existing crates.
2018//! - Batteries-included, but easily replaceable batteries.
2121-//! - Easy to extend with custom lexicons using code generation or handwritten api types
2222-//! - Stateless options (or options where you handle the state) for rolling your own
2323-//! - All the building blocks of the convenient abstractions are available
2424-//! - Use as much or as little from the crates as you need
2525-//!
2626-//!
1919+//! - Easy to extend with custom lexicons using code generation or handwritten api types.
2020+//! - Stateless options (or options where you handle the state) for rolling your own.
2121+//! - All the building blocks of the convenient abstractions are available.
2222+//! - Use as much or as little from the crates as you need.
2723//!
2824//! ## Example
2925//!
3030-//! Dead simple API client: login with OAuth, then fetch the latest 5 posts.
2626+//! Dead simple API client: resume a stored OAuth session or open a browser login, then fetch the
2727+//! latest 5 posts. OAuth loopback is the default path for local scripts and CLIs where browser login
2828+//! is acceptable; app-password credential sessions are mainly for unattended workflows that must
2929+//! re-authenticate non-interactively.
3130//!
3231//! ```no_run
3333-//! # use clap::Parser;
3434-//! # use jacquard::CowStr;
3532//! use jacquard::api::app_bsky::feed::get_timeline::GetTimeline;
3633//! use jacquard::client::{Agent, FileAuthStore};
3434+//! use jacquard::common::session::SessionHint;
3735//! use jacquard::oauth::client::OAuthClient;
3836//! use jacquard::xrpc::XrpcClient;
3937//! use jacquard::oauth::types::AuthorizeOptions;
···4139//! use jacquard::oauth::loopback::LoopbackConfig;
4240//! # use miette::IntoDiagnostic;
4341//!
4444-//! # #[derive(Parser, Debug)]
4545-//! # #[command(author, version, about = "Jacquard - OAuth (DPoP) loopback demo")]
4646-//! # struct Args {
4747-//! # /// Handle (e.g., alice.bsky.social), DID, or PDS URL
4848-//! # input: CowStr<'static>,
4949-//! #
5050-//! # /// Path to auth store file (will be created if missing)
5151-//! # #[arg(long, default_value = "/tmp/jacquard-oauth-session.json")]
5252-//! # store: String,
5353-//! # }
5454-//! #
4242+//! const STORE_PATH: &str = "/tmp/jacquard-oauth-session.json";
4343+//!
5544//! #[tokio::main]
5645//! async fn main() -> miette::Result<()> {
5757-//! let args = Args::parse();
4646+//! let login_hint = std::env::args().nth(1);
4747+//! let oauth = OAuthClient::with_default_config(FileAuthStore::new(STORE_PATH));
4848+//! let hint = SessionHint::from_optional_input(login_hint.as_deref());
5849//!
5959-//! // Build an OAuth client with file-backed auth store and default localhost config
6060-//! let oauth = OAuthClient::with_default_config(FileAuthStore::new(&args.store));
6161-//! // Authenticate with a PDS, using a loopback server to handle the callback flow
6250//! # #[cfg(feature = "loopback")]
6363-//! let session = oauth
6464-//! .login_with_local_server(
6565-//! args.input.clone(),
5151+//! let Some(session) = oauth
5252+//! .resume_or_login_with_local_server(
5353+//! &hint,
6654//! AuthorizeOptions::default(),
6755//! LoopbackConfig::default(),
6856//! )
6969-//! .await?;
5757+//! .await?
5858+//! else {
5959+//! miette::bail!(
6060+//! "no stored OAuth session found in {STORE_PATH}; pass a handle, DID, or PDS URL to log in"
6161+//! );
6262+//! };
7063//! # #[cfg(not(feature = "loopback"))]
7164//! # compile_error!("loopback feature must be enabled to run this example");
7272-//! // Wrap in Agent and fetch the timeline
6565+//!
7366//! let agent: Agent<_> = Agent::from(session);
7467//! let timeline = agent
7568//! .send(GetTimeline::new().limit(5).build())
7669//! .await?
7770//! .into_output()?;
7171+//!
7872//! for (i, post) in timeline.feed.iter().enumerate() {
7973//! println!("\n{}. by {}", i + 1, post.post.author.handle);
8074//! println!(
···10397//! - [`jacquard-derive`](https://docs.rs/jacquard-derive/latest/jacquard_derive/index.html) - Macros (`#[lexicon]`, `#[open_union]`, `#[derive(IntoStatic)]`, `#[derive(LexiconSchema)]`, `#[derive(XrpcRequest)]`)
10498//!
10599//!
106106-//! ### A note on lifetimes
100100+//! ### String backing types
107101//!
108108-//! You'll notice a bunch of lifetimes all over Jacquard types, examples, and so on. If you're newer
109109-//! to Rust or have simply avoided them, they're part of how Rust knows how long to keep something
110110-//! around before cleaning it up. They're not unique to Rust (C and C++ have the same concept
111111-//! internally) but Rust is perhaps the one language that makes them explicit, because they're part
112112-//! of how it validates that things are memory-safe, and being able to give information to the compiler
113113-//! about how long it can expect something to stick around lets the compiler reason out much more
114114-//! 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.
115115-//!
116116-//! > On Jacquard types like [`CowStr`], a `'static` lifetime parameter is used to refer to the owned
117117-//! version of a type, in the same way `String` is the owned version of `&str`.
102102+//! Most generated Jacquard types are parameterized over a string backing type: `Type<S = DefaultStr>`.
103103+//! The default backing is owned and efficient. It is especially convenient when values need to be
104104+//! stored, moved independently of a response buffer, or passed through frameworks and APIs with
105105+//! `DeserializeOwned` bounds. In most examples you will not write the `S` parameter at all because
106106+//! builders, constructors, and `.into_output()` infer or choose the owned default for you.
118107//!
119119-//! This is somewhat in tension with the 'make things simpler' goal of the crate, but it is honestly
120120-//! pretty straightforward once you know the deal, and Jacquard provides a number of escape hatches
121121-//! and easy ways to work.
108108+//! When you are writing generic helpers or optimizing parsing, you can choose another backing such
109109+//! as `String`, `&str`, or [`CowStr<'_>`] with the [`BosStr`] trait. For API responses, use
110110+//! `.into_output()` as the normal path for owned/default-backed output. Use
111111+//! `.parse::<CowStr<'_>>()` or `.parse::<&str>()` when you specifically want to borrow from the
112112+//! response buffer.
122113//!
123123-//! Because explicit lifetimes are somewhat unique to Rust and are not something you may be used to
124124-//! thinking about, they can seem a bit scary to work with. Normally the compiler is pretty good at
125125-//! 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
126126-//! speed and efficiency, because borrowing from your source buffer saves copying the data around.
127127-//!
128128-//! However, it does mean that any Jacquard type that can borrow (not all of them do) is annotated
129129-//! 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
130130-//! at least the same amount of time, and that lifetime matches or exceeds the lifetime of the data
131131-//! structure. This also imposes certain restrictions on deserialization. Namely the [`DeserializeOwned`](https://serde.rs/lifetimes.html)
132132-//! bound does not apply to almost any types in Jacquard. There is a [`deserialize_owned`] function
133133-//! which you can use in a serde `deserialize_with` attribute to help, but the general pattern is
134134-//! to do borrowed deserialization and then call [`.into_static()`] if you need ownership.
135135-//!
136136-//! ### Easy mode
137137-//!
138138-//! Easy mode for jacquard is to mostly just use `'static` for your lifetime params and derive/use
139139-//! [`.into_static()`] as needed. When writing, first see if you can get away with `Thing<'_>`
140140-//! and let the compiler infer. second-easiest after that is `Thing<'static>`, third-easiest is giving
141141-//! everything one lifetime, e.g. `fn foo<'a>(&'a self, thing: Thing<'a>) -> /* thing with lifetime 'a */`.
142142-//!
143143-//! When parsing the output of atproto API calls, you can call `.into_output()` on the `Response<R>`
144144-//! struct to get an owned version with a `'static` lifetime. When deserializing, do not use
145145-//! `from_writer()` type deserialization functions, or features like Axum's `Json` extractor, as they
146146-//! have DeserializeOwned bounds and cannot borrow from their buffer. Either use Jacquard's features
147147-//! to get an owned version or follow the same [patterns](https://whtwnd.com/nonbinary.computer/3m33efvsylz2s) it uses in your own code.
114114+//! [`BosStr`]: crate::BosStr
148115//!
149116//! ## Client options
150117//!
···162129//! #[tokio::main]
163130//! async fn main() -> miette::Result<()> {
164131//! let http = reqwest::Client::new();
165165-//! let base = Uri::parse("https://public.api.bsky.app").into_diagnostic()?.to_owned();
132132+//! let base = Uri::parse("https://public.api.bsky.app").into_diagnostic()?;
166133//! let resp = http
167134//! .xrpc(base)
168135//! .send(
···190157//! # use jacquard::xrpc::XrpcExt;
191158//! # use jacquard::api::app_bsky::feed::get_author_feed::GetAuthorFeed;
192159//! # use jacquard::types::ident::AtIdentifier;
193193-//! # use jacquard::CowStr;
194160//! # use jacquard::deps::fluent_uri::Uri;
195161//! # use miette::IntoDiagnostic;
196162//! #
197163//! #[tokio::main]
198164//! async fn main() -> miette::Result<()> {
199165//! let http = reqwest::Client::new();
200200-//! let base = Uri::parse("https://public.api.bsky.app").into_diagnostic()?.to_owned();
166166+//! let base = Uri::parse("https://public.api.bsky.app").into_diagnostic()?;
201167//! let resp = http
202168//! .xrpc(base)
203203-//! .auth(AuthorizationToken::Bearer(CowStr::from("ACCESS_JWT")))
204204-//! .accept_labelers(vec![CowStr::from("did:plc:labelerid")])
169169+//! .auth(AuthorizationToken::Bearer("ACCESS_JWT".into()))
170170+//! .accept_labelers(vec!["did:plc:labelerid".into()])
205171//! .header(http::header::USER_AGENT, http::HeaderValue::from_static("jacquard-example"))
206172//! .send(
207173//! &GetAuthorFeed::new()
···216182//! }
217183//! ```
218184//!
219219-//! [`deserialize_owned`]: crate::deserialize_owned
220185//! [`AgentSessionExt`]: crate::client::AgentSessionExt
221221-//! [`.into_static()`]: IntoStatic
222186223187#![warn(missing_docs)]
224188