diff --git a/CHANGELOG.md b/CHANGELOG.md index bf57ee97..9367543e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,57 @@ # Changelog +## [0.10.0] - 2026-03-20 + +### Breaking changes + +**URL type migration** (`jacquard-common`, `jacquard`, `jacquard-oauth`, `jacquard-identity`, `jacquard-api`) +- Migrated from `url` crate to `fluent_uri` for validated URL/URI types +- All `Url` types are now `Uri` from `fluent_uri` +- Affects any code that constructs, passes, or pattern-matches on endpoint URLs + +**Re-exported crate paths** (`jacquard-api`, `jacquard-common`) +- Re-exported crates (including non-proc-macro dependencies of the generated API crate) are now centralized into a distinct module +- Import paths for re-exported types have changed as a result + +### Added + +**`no_std` groundwork** (`jacquard-common`, `jacquard-api`) +- Initial steps toward `no_std` support for core types +- `jacquard-api` gains feature gating for `std`/`no_std` usage + +**Datetime improvements** (`jacquard-common`) +- [PR from @blyoom.dev](https://tangled.org/nonbinary.computer/jacquard/pulls/6/round/0) exposing timestamps directly on `Datetime` type +- Naming aligned with `chrono` conventions + +**Handle normalization** (`jacquard-common`) +- Handles are now lowercase-normalized on construction + +**Embedded PDS primitives** (`jacquard-repo`) +- Initial lazy disk-spilling collection types for embedded PDS use cases +- Repo firehose types now use generated API types instead of hand-written equivalents + +**Lexicon codegen improvements** (`jacquard-lexicon`, `jacquard-api`) +- `knownValues` generation now aligned with AT Protocol spec and triggers more frequently +- Improved feature dependency tracking for API crate features + +### Fixed + +**Identity resolution** (`jacquard-identity`) +- [PR from @alephcubed.com](https://tangled.org/nonbinary.computer/jacquard/pulls/7/round/0) fixing `DidDocument::handles()` always failing when parsed from `MiniDoc` + +**Error handling** (`jacquard-common`, `jacquard`, `jacquard-oauth`) +- Big error quality-of-life pass with richer, more actionable diagnostics +- More resilient error parsing for auth errors +- Better lexicon parsing error messages + +**WASM** (`jacquard-common`) +- Fixed WASM CI smoke test compilation + +### Changed + +**Lexicons** (`jacquard-api`) +- Large batch of lexicon schema updates with manual cleanup + ## [0.9.6] - 2025-12-19 ### Changed @@ -7,7 +59,7 @@ **Logging** (`jacquard`, `jacquard-axum`) - [PR from @nekomimi.pet](https://tangled.org/nonbinary.computer/jacquard/pulls/5) cleaning up more debug logs, and adding tracing feature gate to jacquard-axum -## Fixed +### Fixed **Repo commit signatures** (`jacquard-repo`) - commit signatures generated by `jacquard-repo` should now be consistent with other implementations diff --git a/Cargo.lock b/Cargo.lock index 07970c29..4f65567e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2371,7 +2371,7 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jacquard" -version = "0.9.5" +version = "0.10.0" dependencies = [ "bytes", "clap", @@ -2405,7 +2405,7 @@ dependencies = [ [[package]] name = "jacquard-api" -version = "0.9.5" +version = "0.10.0" dependencies = [ "jacquard-common", "jacquard-derive", @@ -2417,7 +2417,7 @@ dependencies = [ [[package]] name = "jacquard-axum" -version = "0.9.6" +version = "0.10.0" dependencies = [ "axum", "axum-macros", @@ -2447,7 +2447,7 @@ dependencies = [ [[package]] name = "jacquard-common" -version = "0.9.5" +version = "0.10.0" dependencies = [ "base64 0.22.1", "bon", @@ -2502,7 +2502,7 @@ dependencies = [ [[package]] name = "jacquard-derive" -version = "0.9.5" +version = "0.10.0" dependencies = [ "heck 0.5.0", "inventory", @@ -2518,7 +2518,7 @@ dependencies = [ [[package]] name = "jacquard-identity" -version = "0.9.5" +version = "0.10.0" dependencies = [ "bon", "bytes", @@ -2542,7 +2542,7 @@ dependencies = [ [[package]] name = "jacquard-lexgen" -version = "0.9.5" +version = "0.10.0" dependencies = [ "clap", "clap_complete", @@ -2569,7 +2569,7 @@ dependencies = [ [[package]] name = "jacquard-lexicon" -version = "0.9.5" +version = "0.10.0" dependencies = [ "bytes", "cid", @@ -2598,7 +2598,7 @@ dependencies = [ [[package]] name = "jacquard-oauth" -version = "0.9.6" +version = "0.10.0" dependencies = [ "base64 0.22.1", "bytes", @@ -2629,7 +2629,7 @@ dependencies = [ [[package]] name = "jacquard-repo" -version = "0.9.6" +version = "0.10.0" dependencies = [ "anyhow", "bytes", @@ -2770,7 +2770,7 @@ dependencies = [ [[package]] name = "lazy-collections" -version = "0.9.5" +version = "0.10.0" dependencies = [ "buffer", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 340c8c42..8b8c9032 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = ["crates/*"] [workspace.package] edition = "2024" -version = "0.9.5" +version = "0.10.0" authors = ["Orual "] #repository = "https://github.com/rsform/jacquard" repository = "https://tangled.org/@nonbinary.computer/jacquard" diff --git a/README.md b/README.md index 5d69e4c5..7cb68dfc 100644 --- a/README.md +++ b/README.md @@ -22,55 +22,6 @@ It is also designed around zero-copy/borrowed deserialization: types like [`Post - All the building blocks of the convenient abstractions are available - Use as much or as little from the crates as you need -## 0.9.X Release Highlights: - -**`#[derive(LexiconSchema)]` + `#[lexicon_union]` macros** -- Automatic schema generation for custom lexicons from Rust structs -- Supports all lexicon constraints via attributes (max_length, max_graphemes, min/max, etc.) -- Generates `LexiconDoc` at compile time for runtime validation - -**Runtime lexicon data validation** -- Validation of structural and/or value contraints of data against a lexicon -- caching for value validations -- LexiconSchema trait generated implementations for runtime validation -- detailed validation error results - -**Lexicon resolver** -- Fetch lexicons at runtime for addition to schema registry - -**Query and path DSLs for `Data` and `RawData` value types** -- Pattern-based querying of nested `Data` structures -- `data.query(pattern)` with expressive syntax: - - `field.nested` - exact path navigation - - `[..]` - wildcard over collections (array elements or object values) - - `field..nested` - scoped recursion (find nested within field, expect one) - - `...field` - global recursion (find all occurrences anywhere) -- `get_at_path()` for simple path-based field access on `Data` and `RawData` -- Path syntax: `embed.images[0].alt` for navigating nested structures -- `type_discriminator()` helper methods for AT Protocol union discrimination -- Collection helper methods: `get()`, `contains_key()`, `len()`, `is_empty()`, `iter()`, `keys()`, `values()` -- Index trait implemented: `obj["key"]` and `arr[0]` - -**Caching in identity/lexicon resolver** -- Basic LRU in-memory cache implementation using `mini-moka` -- Reduces number of network requests for certain operations -- Works on both native and WebAssembly via vendored patched version of mini-moka - - -**XRPC client improvements** -- `set_options()` and `set_endpoint()` methods on `XrpcClient` trait -- Default no-op implementations for stateless clients -- Enables runtime reconfiguration of stateful clients -- Better support for custom endpoint and option overrides -- Fixed bug where setting a custom 'Content-Type' header wouldn't be respected - -**Major generated API compilation time improvements** -- Generated code output now includes a typestate builder implementation, similar to the `bon` crate -- Moves the substantial `syn` tax of generating the builders to code generation time, not compile time. - -**New `jacquard-lexgen` crate** -- Moves binaries out of jacquard-lexicon to reduce size further -- Flake app for `lex-fetch` ## Example @@ -134,25 +85,37 @@ async fn main() -> miette::Result<()> { If 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. > [!WARNING] -> A lot of the streaming code is still pretty experimental. The examples work, though.\ -The modules are also less well-documented, and don't have code examples. There are also a lot of utility functions for conveniently working with the streams and transforming them which are lacking. Use [`n0-future`](https://docs.rs/n0-future/latest/n0_future/index.html) to work with them, that is what Jacquard uses internally as much as possible.\ ->I would also note the same for the repository crate until I've had more third parties test it. +> The latest version swaps from the `url` crate to the lighter and quicker `fluent-uri`. It also moves the re-exported crate paths around and renames the `Uri<'_>` value type enum to `UriValue<'_>` to avoid confusion. This is likely to have broken some things. Migrating is pretty straightforward but consider yourself forewarned. This crate is *not* 1.0 for a reason. ### Changelog [CHANGELOG.md](./CHANGELOG.md) - +**`no_std` groundwork** +- Initial work toward allowing jacquard to function on platforms without access to the standard library. +- `std` usage is now feature-gated. the library currently *does not compile* without `std` due to some remaining dependencies. + +### Testimonials + +- ["the most straightforward interface to atproto I've encountered so far."](https://bsky.app/profile/offline.mountainherder.xyz/post/3m3xwewzs3k2v) - @offline.mountainherder.xyz +- "It has saved me a lot of time already! Well worth a few beers and or microcontrollers" - [@baileytownsend.dev](https://bsky.app/profile/baileytownsend.dev) ### Projects using Jacquard - [skywatch-phash-rs](https://tangled.org/skywatch.blue/skywatch-phash-rs) -- [Weaver](https://alpha.weaver.sh/) - [tangled repository](https://tangled.org/nonbinary.computer/weaver) -- [wisp.place CLI tool](https://docs.wisp.place/cli/) +- [Weaver](https://weaver.sh/) - [tangled repository](https://tangled.org/nonbinary.computer/weaver) +- [wisp.place CLI tool](https://docs.wisp.place/cli/) - formerly - [PDS MOOver](https://pdsmoover.com/) - [tangled repository](https://tangled.org/baileytownsend.dev/pds-moover) ## Component crates @@ -189,4 +152,6 @@ 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. + + [![License](https://img.shields.io/crates/l/jacquard.svg)](./LICENSE) diff --git a/crates/jacquard-api/Cargo.toml b/crates/jacquard-api/Cargo.toml index 24706cbf..be3ff86b 100644 --- a/crates/jacquard-api/Cargo.toml +++ b/crates/jacquard-api/Cargo.toml @@ -2,7 +2,7 @@ name = "jacquard-api" description = "Generated AT Protocol API bindings for Jacquard" edition.workspace = true -version = "0.9.5" +version = "0.10.0" authors.workspace = true repository.workspace = true keywords.workspace = true @@ -15,9 +15,9 @@ license.workspace = true features = [ "bluesky", "other", "streaming" ] [dependencies] -jacquard-common = { version = "0.9", path = "../jacquard-common" } -jacquard-derive = { version = "0.9", path = "../jacquard-derive" } -jacquard-lexicon = { version = "0.9", path = "../jacquard-lexicon", default-features = false } +jacquard-common = { version = "0.10", path = "../jacquard-common" } +jacquard-derive = { version = "0.10", path = "../jacquard-derive" } +jacquard-lexicon = { version = "0.10", path = "../jacquard-lexicon", default-features = false } miette.workspace = true serde.workspace = true thiserror.workspace = true diff --git a/crates/jacquard-axum/Cargo.toml b/crates/jacquard-axum/Cargo.toml index 8ba87efa..dd4a3d69 100644 --- a/crates/jacquard-axum/Cargo.toml +++ b/crates/jacquard-axum/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jacquard-axum" edition.workspace = true -version = "0.9.6" +version = "0.10.0" authors.workspace = true repository.workspace = true keywords.workspace = true @@ -22,10 +22,10 @@ path = "../../examples/axum_server.rs" [dependencies] axum = "0.8.6" bytes.workspace = true -jacquard = { version = "0.9", path = "../jacquard", default-features = false, features = ["api"] } -jacquard-common = { version = "0.9", path = "../jacquard-common", features = ["reqwest-client"] } -jacquard-derive = { version = "0.9", path = "../jacquard-derive" } -jacquard-identity = { version = "0.9", path = "../jacquard-identity", optional = true } +jacquard = { version = "0.10", path = "../jacquard", default-features = false, features = ["api"] } +jacquard-common = { version = "0.10", path = "../jacquard-common", features = ["reqwest-client"] } +jacquard-derive = { version = "0.10", path = "../jacquard-derive" } +jacquard-identity = { version = "0.10", path = "../jacquard-identity", optional = true } miette.workspace = true multibase = { version = "0.9.1", optional = true } serde.workspace = true diff --git a/crates/jacquard-common/Cargo.toml b/crates/jacquard-common/Cargo.toml index e6054ba1..d9a92d37 100644 --- a/crates/jacquard-common/Cargo.toml +++ b/crates/jacquard-common/Cargo.toml @@ -2,7 +2,7 @@ name = "jacquard-common" description = "Core AT Protocol types and utilities for Jacquard" edition.workspace = true -version = "0.9.5" +version = "0.10.0" authors.workspace = true repository.workspace = true keywords.workspace = true diff --git a/crates/jacquard-common/src/lib.rs b/crates/jacquard-common/src/lib.rs index 4b00de41..906a1ae8 100644 --- a/crates/jacquard-common/src/lib.rs +++ b/crates/jacquard-common/src/lib.rs @@ -227,18 +227,17 @@ pub mod deps; pub mod error; pub mod http_client; pub mod macros; +pub mod opt_serde_bytes_helper; +pub mod serde_bytes_helper; #[cfg(feature = "service-auth")] pub mod service_auth; pub mod session; +#[cfg(feature = "streaming")] +pub mod stream; /// Compile-time TLD lookup for disambiguating handles from NSIDs. pub(crate) mod tld; /// Baseline fundamental AT Protocol data types. pub mod types; -// XRPC protocol types and traits -pub mod opt_serde_bytes_helper; -pub mod serde_bytes_helper; -#[cfg(feature = "streaming")] -pub mod stream; pub mod xrpc; #[cfg(feature = "streaming")] @@ -289,26 +288,3 @@ where let value = T::deserialize(deserializer)?; Ok(value.into_static()) } - -#[cfg(test)] -mod tests { - use crate::deps::bytes; - use crate::deps::chrono; - use crate::deps::smol_str::SmolStr; - - #[test] - fn deps_smol_str() { - let s = SmolStr::new_static("test"); - assert_eq!(s, "test"); - } - - #[test] - fn deps_bytes() { - let _x = bytes::Bytes::from_static(b"hello"); - } - - #[test] - fn deps_chrono() { - let _now = chrono::Utc::now(); - } -} diff --git a/crates/jacquard-common/src/types/value.rs b/crates/jacquard-common/src/types/value.rs index b8d2a41f..fdbca5e7 100644 --- a/crates/jacquard-common/src/types/value.rs +++ b/crates/jacquard-common/src/types/value.rs @@ -1177,7 +1177,7 @@ impl<'s> QueryResult<'s> { /// A single match from a query operation #[derive(Debug, Clone, PartialEq)] pub struct QueryMatch<'s> { - /// Path where this value was found (e.g., "actors[0].handle") + /// Path where this value was found (e.g., "actors\[0\].handle") pub path: SmolStr, /// The value (None if field was missing during wildcard iteration) pub value: Option<&'s Data<'s>>, diff --git a/crates/jacquard-common/src/xrpc.rs b/crates/jacquard-common/src/xrpc.rs index 3dc18bc4..169ea858 100644 --- a/crates/jacquard-common/src/xrpc.rs +++ b/crates/jacquard-common/src/xrpc.rs @@ -58,7 +58,7 @@ pub use subscription::{ /// Normalize a base URI by removing trailing slashes. /// /// This is useful for XRPC clients where the base URI might be provided with -/// a trailing slash (e.g., "https://bsky.social/") but needs to be normalized +/// a trailing slash (e.g., "") but needs to be normalized /// for consistent path building. Since trimming a trailing slash from a valid URI /// always yields a valid URI, the result is guaranteed to be valid. pub fn normalize_base_uri(uri: Uri) -> Uri { diff --git a/crates/jacquard-common/src/xrpc/subscription.rs b/crates/jacquard-common/src/xrpc/subscription.rs index 9b05f9eb..6ecabda9 100644 --- a/crates/jacquard-common/src/xrpc/subscription.rs +++ b/crates/jacquard-common/src/xrpc/subscription.rs @@ -720,7 +720,7 @@ type StreamMessage<'a, R> = ::Message<'a>; /// This exists primarily for server-side frameworks (like Axum) to extract /// typed subscription parameters without lifetime issues. pub trait SubscriptionEndpoint { - /// Fully-qualified path ('/xrpc/[nsid]') where this subscription endpoint lives + /// Fully-qualified path ('/xrpc/{nsid}') where this subscription endpoint lives const PATH: &'static str; /// Message encoding (JSON or DAG-CBOR) diff --git a/crates/jacquard-derive/Cargo.toml b/crates/jacquard-derive/Cargo.toml index 17a3163f..333cc2b4 100644 --- a/crates/jacquard-derive/Cargo.toml +++ b/crates/jacquard-derive/Cargo.toml @@ -16,15 +16,15 @@ proc-macro = true [dependencies] heck.workspace = true -jacquard-lexicon = { version = "0.9", path = "../jacquard-lexicon", features = ["codegen"] } +jacquard-lexicon = { version = "0.10", path = "../jacquard-lexicon", features = ["codegen"] } proc-macro2.workspace = true quote.workspace = true syn.workspace = true [dev-dependencies] inventory = "0.3" -jacquard-common = { version = "0.9", path = "../jacquard-common" } -jacquard-lexicon = { version = "0.9", path = "../jacquard-lexicon" } +jacquard-common = { version = "0.10", path = "../jacquard-common" } +jacquard-lexicon = { version = "0.10", path = "../jacquard-lexicon" } serde.workspace = true serde_json.workspace = true unicode-segmentation = "1.12" diff --git a/crates/jacquard-identity/Cargo.toml b/crates/jacquard-identity/Cargo.toml index 50086f86..4d9bb54f 100644 --- a/crates/jacquard-identity/Cargo.toml +++ b/crates/jacquard-identity/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jacquard-identity" edition.workspace = true -version = "0.9.5" +version = "0.10.0" authors.workspace = true repository.workspace = true keywords.workspace = true @@ -22,9 +22,9 @@ cache = ["dep:mini-moka"] trait-variant.workspace = true bon.workspace = true bytes.workspace = true -jacquard-common = { version = "0.9", path = "../jacquard-common", features = ["reqwest-client"] } -jacquard-api = { version = "0.9", path = "../jacquard-api", default-features = false, features = ["minimal"] } -jacquard-lexicon = { version = "0.9", path = "../jacquard-lexicon", default-features = false } +jacquard-common = { version = "0.10", path = "../jacquard-common", features = ["reqwest-client"] } +jacquard-api = { version = "0.10", path = "../jacquard-api", default-features = false, features = ["minimal"] } +jacquard-lexicon = { version = "0.10", path = "../jacquard-lexicon", default-features = false } reqwest.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/jacquard-lexgen/Cargo.toml b/crates/jacquard-lexgen/Cargo.toml index 088b4563..8066f8cb 100644 --- a/crates/jacquard-lexgen/Cargo.toml +++ b/crates/jacquard-lexgen/Cargo.toml @@ -32,11 +32,11 @@ path = "../../examples/extract_inventory.rs" clap.workspace = true glob = "0.3" inventory = "0.3" -jacquard-api = { version = "0.9", path = "../jacquard-api", default-features = false, features = [ "minimal" ] } -jacquard-common = { version = "0.9", features = [ "reqwest-client" ], path = "../jacquard-common" } -jacquard-derive = { version = "0.9", path = "../jacquard-derive" } -jacquard-identity = { version = "0.9", path = "../jacquard-identity", features = ["dns"] } -jacquard-lexicon = { version = "0.9", path = "../jacquard-lexicon" } +jacquard-api = { version = "0.10", path = "../jacquard-api", default-features = false, features = [ "minimal" ] } +jacquard-common = { version = "0.10", features = [ "reqwest-client" ], path = "../jacquard-common" } +jacquard-derive = { version = "0.10", path = "../jacquard-derive" } +jacquard-identity = { version = "0.10", path = "../jacquard-identity", features = ["dns"] } +jacquard-lexicon = { version = "0.10", path = "../jacquard-lexicon" } kdl = "6" miette = { workspace = true, features = ["fancy"] } reqwest = { workspace = true, features = ["json", "http2", "system-proxy", "rustls-tls"] } diff --git a/crates/jacquard-lexicon/Cargo.toml b/crates/jacquard-lexicon/Cargo.toml index c9a4039a..ade624a4 100644 --- a/crates/jacquard-lexicon/Cargo.toml +++ b/crates/jacquard-lexicon/Cargo.toml @@ -2,7 +2,7 @@ name = "jacquard-lexicon" description = "Lexicon schema parsing and code generation for Jacquard" edition.workspace = true -version = "0.9.5" +version = "0.10.0" authors.workspace = true repository.workspace = true keywords.workspace = true @@ -20,7 +20,7 @@ cid.workspace = true dashmap.workspace = true heck = { workspace = true, optional = true } inventory = "0.3" -jacquard-common = { version = "0.9", path = "../jacquard-common" } +jacquard-common = { version = "0.10", path = "../jacquard-common" } miette = { workspace = true } multihash.workspace = true prettyplease = { workspace = true, optional = true } @@ -39,5 +39,5 @@ unicode-segmentation = "1.12" [dev-dependencies] bytes = { workspace = true } -jacquard-derive = { version = "0.9", path = "../jacquard-derive"} +jacquard-derive = { version = "0.10", path = "../jacquard-derive"} tempfile = { version = "3.23.0" } diff --git a/crates/jacquard-lexicon/src/codegen/builder_gen/state_mod.rs b/crates/jacquard-lexicon/src/codegen/builder_gen/state_mod.rs index c04d35fd..4a14b48d 100644 --- a/crates/jacquard-lexicon/src/codegen/builder_gen/state_mod.rs +++ b/crates/jacquard-lexicon/src/codegen/builder_gen/state_mod.rs @@ -1,6 +1,6 @@ //! State module generation for builders //! -//! Generates the state trait, Empty state, and SetX transition types +//! Generates the state trait, Empty state, and `SetX` transition types //! that enable type-safe builder patterns. use std::collections::HashSet; diff --git a/crates/jacquard-lexicon/src/derive_impl/lexicon_attr.rs b/crates/jacquard-lexicon/src/derive_impl/lexicon_attr.rs index 11f3f47f..b7053c64 100644 --- a/crates/jacquard-lexicon/src/derive_impl/lexicon_attr.rs +++ b/crates/jacquard-lexicon/src/derive_impl/lexicon_attr.rs @@ -1,4 +1,4 @@ -//! Implementation of #[lexicon] attribute macro +//! Implementation of `#[lexicon]` attribute macro use proc_macro2::TokenStream; use quote::quote; diff --git a/crates/jacquard-lexicon/src/derive_impl/lexicon_union.rs b/crates/jacquard-lexicon/src/derive_impl/lexicon_union.rs index 6805f45a..34fe43eb 100644 --- a/crates/jacquard-lexicon/src/derive_impl/lexicon_union.rs +++ b/crates/jacquard-lexicon/src/derive_impl/lexicon_union.rs @@ -1,4 +1,4 @@ -//! Implementation of #[lexicon_union] attribute macro +//! Implementation of `#[lexicon_union]` attribute macro use proc_macro2::TokenStream; use quote::quote; diff --git a/crates/jacquard-lexicon/src/derive_impl/open_union_attr.rs b/crates/jacquard-lexicon/src/derive_impl/open_union_attr.rs index 0ede86b7..a2a90f06 100644 --- a/crates/jacquard-lexicon/src/derive_impl/open_union_attr.rs +++ b/crates/jacquard-lexicon/src/derive_impl/open_union_attr.rs @@ -1,4 +1,4 @@ -//! Implementation of #[open_union] attribute macro +//! Implementation of `#[open_union]` attribute macro use proc_macro2::TokenStream; use quote::quote; diff --git a/crates/jacquard-lexicon/src/lib.rs b/crates/jacquard-lexicon/src/lib.rs index 1d5284a9..c9d70525 100644 --- a/crates/jacquard-lexicon/src/lib.rs +++ b/crates/jacquard-lexicon/src/lib.rs @@ -10,7 +10,6 @@ //! - [`corpus`] - Lexicon corpus management and namespace organization //! - [`lexicon`] - Schema parsing and validation //! - [`schema`] - Schema generation from Rust types (reverse codegen) -//! - [`union_registry`] - Tracks union types for collision detection //! - [`fs`] - Filesystem utilities for lexicon storage //! - [`derive_impl`] - Implementation functions for derive macros (used by jacquard-derive) //! - [`validation`] - Runtime validation of Data against lexicon schemas diff --git a/crates/jacquard-lexicon/src/schema/from_ast/parse.rs b/crates/jacquard-lexicon/src/schema/from_ast/parse.rs index c4b12a6d..0f22204d 100644 --- a/crates/jacquard-lexicon/src/schema/from_ast/parse.rs +++ b/crates/jacquard-lexicon/src/schema/from_ast/parse.rs @@ -262,7 +262,7 @@ fn extract_xrpc_nsid(attrs: &[Attribute]) -> syn::Result> { Ok(None) } -/// Extract T from Option, return (type, is_required) +/// Extract T from `Option`, return (type, is_required) pub fn extract_option_inner(ty: &syn::Type) -> (&syn::Type, bool) { if let syn::Type::Path(type_path) = ty { if let Some(segment) = type_path.path.segments.last() { @@ -278,7 +278,7 @@ pub fn extract_option_inner(ty: &syn::Type) -> (&syn::Type, bool) { (ty, true) } -/// Check if type has #[open_union] attribute +/// Check if type has `#[open_union]` attribute pub fn has_open_union_attr(attrs: &[Attribute]) -> bool { attrs.iter().any(|attr| attr.path().is_ident("open_union")) } diff --git a/crates/jacquard-lexicon/src/schema/from_ast/types.rs b/crates/jacquard-lexicon/src/schema/from_ast/types.rs index a4188b2f..2c0c0b4f 100644 --- a/crates/jacquard-lexicon/src/schema/from_ast/types.rs +++ b/crates/jacquard-lexicon/src/schema/from_ast/types.rs @@ -40,7 +40,7 @@ pub struct ValidationCheck { pub schema_name: String, /// Rust type path (for diagnostic purposes) pub field_type: String, - /// Is this field required (not Option)? + /// Is this field required (not `Option`)? pub is_required: bool, /// Is this validating an array length (vs string length)? pub is_array: bool, diff --git a/crates/jacquard-oauth/Cargo.toml b/crates/jacquard-oauth/Cargo.toml index 060e9394..14a1195c 100644 --- a/crates/jacquard-oauth/Cargo.toml +++ b/crates/jacquard-oauth/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jacquard-oauth" -version = "0.9.6" +version = "0.10.0" edition.workspace = true description = "AT Protocol OAuth 2.1 core types and helpers for Jacquard" authors.workspace = true @@ -21,8 +21,8 @@ websocket = ["jacquard-common/websocket"] streaming = ["jacquard-common/streaming", "dep:n0-future"] [dependencies] -jacquard-common = { version = "0.9", path = "../jacquard-common", features = ["reqwest-client"] } -jacquard-identity = { version = "0.9", path = "../jacquard-identity" } +jacquard-common = { version = "0.10", path = "../jacquard-common", features = ["reqwest-client"] } +jacquard-identity = { version = "0.10", path = "../jacquard-identity" } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } smol_str = { workspace = true } diff --git a/crates/jacquard-oauth/src/atproto.rs b/crates/jacquard-oauth/src/atproto.rs index 0f5294da..542cd6bd 100644 --- a/crates/jacquard-oauth/src/atproto.rs +++ b/crates/jacquard-oauth/src/atproto.rs @@ -93,29 +93,25 @@ pub struct AtprotoClientMetadata<'m> { pub privacy_policy_uri: Option>, } -impl<'m> AtprotoClientMetadata<'m> { - pub fn new( - client_id: Uri, - client_uri: Option>, - redirect_uris: Vec>, - grant_types: Vec, - scopes: Vec>, - jwks_uri: Option>, - ) -> Self { - Self { - client_id, - client_uri, - redirect_uris, - grant_types, - scopes, - jwks_uri, - client_name: None, - logo_uri: None, - tos_uri: None, +impl<'m> IntoStatic for AtprotoClientMetadata<'m> { + type Output = AtprotoClientMetadata<'static>; + fn into_static(self) -> AtprotoClientMetadata<'static> { + AtprotoClientMetadata { + client_id: self.client_id, + client_uri: self.client_uri, + redirect_uris: self.redirect_uris, + grant_types: self.grant_types, + scopes: self.scopes.into_static(), + jwks_uri: self.jwks_uri, + client_name: self.client_name, + logo_uri: self.logo_uri, + tos_uri: self.tos_uri, privacy_policy_uri: None, } } +} +impl<'m> AtprotoClientMetadata<'m> { pub fn with_prod_info( mut self, client_name: &str, diff --git a/crates/jacquard-oauth/src/lib.rs b/crates/jacquard-oauth/src/lib.rs index 27c77cbd..4d7d3a29 100644 --- a/crates/jacquard-oauth/src/lib.rs +++ b/crates/jacquard-oauth/src/lib.rs @@ -46,6 +46,7 @@ //! //! See [`atproto`] module for AT Protocol-specific metadata helpers. +#![warn(missing_docs)] pub mod atproto; pub mod authstore; pub mod client; diff --git a/crates/jacquard-oauth/src/loopback.rs b/crates/jacquard-oauth/src/loopback.rs index 10bf6eb8..9025e7e3 100644 --- a/crates/jacquard-oauth/src/loopback.rs +++ b/crates/jacquard-oauth/src/loopback.rs @@ -1,5 +1,49 @@ +//! +//! Helpers for the local loopback server method of atproto OAuth. +//! +//! `OAuthClient::login_with_local_server()` is the nice helper. Here is where +//! it and its components live. Below is what it does, so you can have more +//! granular control without having to make your own loopback server. +//! +//! ```ignore +//! let input = "your_handle_here"; +//! let cfg = LoopbackConfig::default(); +//! let opts = AuthorizeOptions::default(); +//! let port = match cfg.port { +//! LoopbackPort::Fixed(p) => p, +//! LoopbackPort::Ephemeral => 0, +//! }; +//! // TODO: fix this to it also accepts ipv6 and properly finds a free port +//! let bind_addr: SocketAddr = format!("0.0.0.0:{}", port) +//! .parse() +//! .expect("invalid loopback host/port"); +//! let oauth = OAuthClient::with_default_config(FileAuthStore::new(&args.store)); +//! +//! let (local_addr, handle) = one_shot_server(bind_addr); +//! println!("Listening on {}", local_addr); +//! +//! let client_data = oauth.build_localhost_client_data(&cfg, &opts, local_addr); +//! // Build client using store and resolver +//! let flow_client = OAuthClient::new_with_shared( +//! self.registry.store.clone(), +//! self.client.clone(), +//! client_data, +//! ); +//! +//! // Start auth and get authorization URL +//! let auth_url = flow_client.start_auth(input.as_ref(), opts).await?; +//! // Print URL for copy/paste +//! println!("To authenticate with your PDS, visit:\n{}\n", auth_url); +//! // Optionally open browser +//! if cfg.open_browser { +//! let _ = try_open_in_browser(&auth_url); +//! } +//! +//! handle_localhost_callback(handle, &flow_client, &cfg).await +//! ``` +//! +//! #![cfg(feature = "loopback")] - use crate::{ atproto::AtprotoClientMetadata, authstore::ClientAuthStore, @@ -41,15 +85,15 @@ impl Default for LoopbackConfig { } #[cfg(feature = "browser-open")] -fn try_open_in_browser(url: &str) -> bool { +pub fn try_open_in_browser(url: &str) -> bool { webbrowser::open(url).is_ok() } #[cfg(not(feature = "browser-open"))] -fn try_open_in_browser(_url: &str) -> bool { +pub fn try_open_in_browser(_url: &str) -> bool { false } -pub fn create_callback_router( +fn create_callback_router( request: &rouille::Request, tx: mpsc::Sender, ) -> rouille::Response { @@ -70,14 +114,22 @@ pub fn create_callback_router( ) } -struct CallbackHandle { +pub struct CallbackHandle { #[allow(dead_code)] server_handle: std::thread::JoinHandle<()>, server_stop: std::sync::mpsc::Sender<()>, callback_rx: mpsc::Receiver>, } -fn one_shot_server(addr: SocketAddr) -> (SocketAddr, CallbackHandle) { +/// One-shot OAuth callback server. +/// +/// Starts an ephemeral in-process web server that listens for the OAuth +/// callback redirect. Returns the server address and a [`CallbackHandle`] +/// that can be used to wait for the callback and stop the server. +/// +/// Use in combination with [`handle_localhost_callback`] to handle the +/// callback for the localhost loopback server. +pub fn one_shot_server(addr: SocketAddr) -> (SocketAddr, CallbackHandle) { let (tx, callback_rx) = mpsc::channel(5); let server = Server::new(addr, move |request| { create_callback_router(request, tx.clone()) @@ -92,12 +144,60 @@ fn one_shot_server(addr: SocketAddr) -> (SocketAddr, CallbackHandle) { (addr, handle) } +/// Handles the OAuth callback for the localhost loopback server. +/// +/// Returns a session if the callback succeeds within the configured timeout +/// and shuts down the server. +pub async fn handle_localhost_callback( + handle: CallbackHandle, + flow_client: &super::client::OAuthClient, + cfg: &LoopbackConfig, +) -> crate::error::Result> +where + T: OAuthResolver + DpopExt + Send + Sync + 'static, + S: ClientAuthStore + Send + Sync + 'static, +{ + // Await callback or timeout + let mut callback_rx = handle.callback_rx; + let cb = tokio::time::timeout( + std::time::Duration::from_millis(cfg.timeout_ms), + callback_rx.recv(), + ) + .await; + // trigger shutdown + let _ = handle.server_stop.send(()); + if let Ok(Some(cb)) = cb { + // Handle callback and create a session + Ok(flow_client.callback(cb).await?) + } else { + Err(OAuthError::Callback(CallbackError::Timeout)) + } +} + impl OAuthClient where T: OAuthResolver + DpopExt + Send + Sync + 'static, S: ClientAuthStore + Send + Sync + 'static, { /// Drive the full OAuth flow using a local loopback server. + /// + /// This uses localhost OAuth and an ephemeral in-process web server to + /// handle the OAuth callback redirect. It has a bunch of nice friendly + /// defaults to help you get started and will basically drive the *entire* + /// callback flow itself. + /// + /// Best used for development and for small CLI applications that don't + /// require long session lengths. For long-running unattended sessions, + /// app passwords (via CredentialSession in the jacquard crate) remain + /// the best option. For more complex OAuth, or if you want more control + /// over the process, use the other methods on OAuthClient. + /// + /// 'input' parameter is what you type in the login box (usually, your handle) + /// for it to look up your PDS and redirect to its authentication interface. + /// + /// If the `browser-open` feature is enabled, this will open a web browser + /// for you to authenticate with your PDS. It will also print the + /// callback url to the console for you to copy. pub async fn login_with_local_server( &self, input: impl AsRef, @@ -114,20 +214,8 @@ where .expect("invalid loopback host/port"); let (local_addr, handle) = one_shot_server(bind_addr); println!("Listening on {}", local_addr); - // build redirect uri - let redirect_uri = format!("http://{}:{}/oauth/callback", cfg.host, local_addr.port(),); - let redirect = Uri::parse(redirect_uri).unwrap(); - - let scopes = if opts.scopes.is_empty() { - Some(self.registry.client_data.config.scopes.clone()) - } else { - Some(opts.scopes.clone().into_static()) - }; - let client_data = crate::session::ClientData { - keyset: self.registry.client_data.keyset.clone(), - config: AtprotoClientMetadata::new_localhost(Some(vec![redirect]), scopes), - }; + let client_data = self.build_localhost_client_data(&cfg, &opts, local_addr); // Build client using store and resolver let flow_client = OAuthClient::new_with_shared( self.registry.store.clone(), @@ -144,20 +232,29 @@ where let _ = try_open_in_browser(&auth_url); } - // Await callback or timeout - let mut callback_rx = handle.callback_rx; - let cb = tokio::time::timeout( - std::time::Duration::from_millis(cfg.timeout_ms), - callback_rx.recv(), - ) - .await; - // trigger shutdown - let _ = handle.server_stop.send(()); - if let Ok(Some(cb)) = cb { - // Handle callback and create a session - Ok(flow_client.callback(cb).await?) + handle_localhost_callback(handle, &flow_client, &cfg).await + } + + /// Builds a [`crate::session::ClientData`] for use with the local loopback server method of OAuth. + pub fn build_localhost_client_data( + &self, + cfg: &LoopbackConfig, + opts: &AuthorizeOptions<'_>, + local_addr: SocketAddr, + ) -> crate::session::ClientData<'static> { + let redirect_uri = format!("http://{}:{}/oauth/callback", cfg.host, local_addr.port(),); + let redirect = Uri::parse(redirect_uri).unwrap(); + + let scopes = if opts.scopes.is_empty() { + Some(self.registry.client_data.config.scopes.clone()) } else { - Err(OAuthError::Callback(CallbackError::Timeout)) + Some(opts.scopes.clone().into_static()) + }; + + crate::session::ClientData { + keyset: self.registry.client_data.keyset.clone(), + config: AtprotoClientMetadata::new_localhost(Some(vec![redirect]), scopes), } + .into_static() } } diff --git a/crates/jacquard-oauth/src/session.rs b/crates/jacquard-oauth/src/session.rs index 40c836cf..fb277347 100644 --- a/crates/jacquard-oauth/src/session.rs +++ b/crates/jacquard-oauth/src/session.rs @@ -239,6 +239,16 @@ pub struct ClientData<'s> { pub config: AtprotoClientMetadata<'s>, } +impl<'s> IntoStatic for ClientData<'s> { + type Output = ClientData<'static>; + fn into_static(self) -> ClientData<'static> { + ClientData { + keyset: self.keyset, + config: self.config.into_static(), + } + } +} + impl<'s> ClientData<'s> { pub fn new(keyset: Option, config: AtprotoClientMetadata<'s>) -> Self { Self { keyset, config } diff --git a/crates/jacquard-repo/Cargo.toml b/crates/jacquard-repo/Cargo.toml index e7b47df4..67a600a2 100644 --- a/crates/jacquard-repo/Cargo.toml +++ b/crates/jacquard-repo/Cargo.toml @@ -2,7 +2,7 @@ name = "jacquard-repo" description = "AT Protocol repository primitives: MST, commits, CAR I/O" edition.workspace = true -version = "0.9.6" +version = "0.10.0" authors.workspace = true repository.workspace = true keywords.workspace = true @@ -16,9 +16,9 @@ default = [] [dependencies] # Internal -jacquard-common = { path = "../jacquard-common", version = "0.9", features = ["crypto-ed25519", "crypto-k256", "crypto-p256"] } -jacquard-derive = { path = "../jacquard-derive", version = "0.9" } -jacquard-api = { path = "../jacquard-api", version = "0.9", features = ["streaming"] } +jacquard-common = { path = "../jacquard-common", version = "0.10", features = ["crypto-ed25519", "crypto-k256", "crypto-p256"] } +jacquard-derive = { path = "../jacquard-derive", version = "0.10" } +jacquard-api = { path = "../jacquard-api", version = "0.10", features = ["streaming"] } # Serialization serde.workspace = true diff --git a/crates/jacquard/Cargo.toml b/crates/jacquard/Cargo.toml index 1bbb5652..2ac5a048 100644 --- a/crates/jacquard/Cargo.toml +++ b/crates/jacquard/Cargo.toml @@ -120,13 +120,13 @@ path = "../../examples/moderated_timeline.rs" required-features = ["api_bluesky", "loopback"] [dependencies] -jacquard-api = { version = "0.9", path = "../jacquard-api" } -jacquard-common = { version = "0.9", path = "../jacquard-common", features = [ +jacquard-api = { version = "0.10", path = "../jacquard-api" } +jacquard-common = { version = "0.10", path = "../jacquard-common", features = [ "reqwest-client", ] } -jacquard-oauth = { version = "0.9", path = "../jacquard-oauth" } -jacquard-derive = { version = "0.9", path = "../jacquard-derive", optional = true } -jacquard-identity = { version = "0.9", path = "../jacquard-identity" } +jacquard-oauth = { version = "0.10", path = "../jacquard-oauth" } +jacquard-derive = { version = "0.10", path = "../jacquard-derive", optional = true } +jacquard-identity = { version = "0.10", path = "../jacquard-identity" } @@ -149,7 +149,7 @@ n0-future = { workspace = true, optional = true } [target.'cfg(not(target_family = "wasm"))'.dependencies] -jacquard-identity = { version = "0.9", path = "../jacquard-identity", features = ["cache"] } +jacquard-identity = { version = "0.10", path = "../jacquard-identity", features = ["cache"] } reqwest = { workspace = true, features = [ "http2", "gzip", diff --git a/crates/jacquard/src/client.rs b/crates/jacquard/src/client.rs index fc276202..604ef933 100644 --- a/crates/jacquard/src/client.rs +++ b/crates/jacquard/src/client.rs @@ -1,7 +1,7 @@ //! XRPC client implementation for AT Protocol //! //! This module provides HTTP and XRPC client traits along with session management -//! for both app-password and OAuth authentication. +//! for both app password and OAuth authentication. //! //! ## Key types //! @@ -15,9 +15,15 @@ //! - [`credential_session`] - App-password session implementation //! - [`token`] - Token storage and persistence //! - [`vec_update`] - Trait for fetch-modify-put patterns on array endpoints +//! +//! +//! "Agent" in this context is derived from Bluesky's own library usage of the term. +//! It represents a (persistent) user session, and includes a number of helpful +//! methods which are available via the `AgentSessionExt` extension trait +//! on anything that implements `AgentSession` + `IdentityResolver`. //pub mod bff_session; -/// App-password session implementation with auto-refresh +/// App password session implementation with auto-refresh pub mod credential_session; /// Agent error type pub mod error; @@ -801,7 +807,7 @@ pub trait AgentSessionExt: AgentSession + IdentityResolver { } /// Untyped, freeform record fetcher. - /// Hits [https://slingshot.microcosm.blue] + /// Hits fn fetch_record_slingshot( &self, uri: &AtUri<'_>, diff --git a/crates/jacquard/src/lib.rs b/crates/jacquard/src/lib.rs index dcd39ac0..3ec3e02a 100644 --- a/crates/jacquard/src/lib.rs +++ b/crates/jacquard/src/lib.rs @@ -62,7 +62,7 @@ //! let session = oauth //! .login_with_local_server( //! args.input.clone(), -//! Default::default(), +//! AuthorizeOptions::default(), //! LoopbackConfig::default(), //! ) //! .await?; diff --git a/crates/jacquard/src/moderation.rs b/crates/jacquard/src/moderation.rs index fe7ff3fa..feed5947 100644 --- a/crates/jacquard/src/moderation.rs +++ b/crates/jacquard/src/moderation.rs @@ -2,13 +2,13 @@ //! //! This is an attempt to semi-generalize the Bluesky moderation system. It avoids //! depending on their lexicons as much as reasonably possible. This works via a -//! trait, [`Labeled`][crate::moderation::Labeled], which represents things that have labels for moderation +//! trait, [`Labeled`], which represents things that have labels for moderation //! applied to them. This way the moderation application functions can operate //! primarily via the trait, and are thus generic over lexicon types, and are //! easy to use with your own types. //! //! For more complex types which might have labels applied to components, -//! there is the [`Moderateable`][crate::moderation::Moderateable] trait. A mostly complete implementation for +//! there is the [`Moderateable`] trait. A mostly complete implementation for //! `FeedViewPost` is available for reference. The trait method outputs a `Vec` //! of tuples, where the first element is a string tag and the second is the //! moderation decision for the tagged element. This lets application developers @@ -16,7 +16,7 @@ //! mostly match Bluesky behaviour (respecting "!hide", and such) by default. //! //! I've taken the time to go through the generated API bindings and implement -//! the [`Labeled`][crate::moderation::Labeled] trait for a number of types. It's a fairly easy trait to +//! the [`Labeled`] trait for a number of types. It's a fairly easy trait to //! implement, just not really automatable. //! //! diff --git a/crates/lazy-collections/src/io.rs b/crates/lazy-collections/src/io.rs index d6691b5b..5010646b 100644 --- a/crates/lazy-collections/src/io.rs +++ b/crates/lazy-collections/src/io.rs @@ -137,7 +137,7 @@ pub trait Read { /// /// All bytes read from this source will be appended to the specified buffer /// `buf`. This function will continuously call [`read()`] to append more data to - /// `buf` until [`read()`] returns either [`Ok(0)`] or an error of + /// `buf` until [`read()`] returns either \[`Ok(0)`\] or an error of /// non-[`ErrorKind::Interrupted`] kind. /// /// If successful, this function will return the total number of bytes read. @@ -323,7 +323,7 @@ pub trait Read { /// Creates an adaptor which will read at most `limit` bytes from it. /// /// This function returns a new instance of `Read` which will read at most - /// `limit` bytes, after which it will always return EOF ([`Ok(0)`]). Any + /// `limit` bytes, after which it will always return EOF (\[`Ok(0)`\]). Any /// read errors will not count towards the number of bytes read and future /// calls to [`read()`](Self::read) may succeed. fn take(self, limit: u64) -> Take diff --git a/crates/lazy-collections/src/lib.rs b/crates/lazy-collections/src/lib.rs index 75ccee47..bf2df590 100644 --- a/crates/lazy-collections/src/lib.rs +++ b/crates/lazy-collections/src/lib.rs @@ -1,5 +1,5 @@ #![cfg_attr(target_os = "none", no_std)] - +#![allow(unused)] #[cfg(all(not(feature = "std"), feature = "alloc"))] extern crate alloc;