A better Rust ATProto crate
101
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs and changelog updates, version bump to 0.10

Orual af37cbe9 7725398f

+310 -208
+53 -1
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 + ## [0.10.0] - 2026-03-20 4 + 5 + ### Breaking changes 6 + 7 + **URL type migration** (`jacquard-common`, `jacquard`, `jacquard-oauth`, `jacquard-identity`, `jacquard-api`) 8 + - Migrated from `url` crate to `fluent_uri` for validated URL/URI types 9 + - All `Url` types are now `Uri` from `fluent_uri` 10 + - Affects any code that constructs, passes, or pattern-matches on endpoint URLs 11 + 12 + **Re-exported crate paths** (`jacquard-api`, `jacquard-common`) 13 + - Re-exported crates (including non-proc-macro dependencies of the generated API crate) are now centralized into a distinct module 14 + - Import paths for re-exported types have changed as a result 15 + 16 + ### Added 17 + 18 + **`no_std` groundwork** (`jacquard-common`, `jacquard-api`) 19 + - Initial steps toward `no_std` support for core types 20 + - `jacquard-api` gains feature gating for `std`/`no_std` usage 21 + 22 + **Datetime improvements** (`jacquard-common`) 23 + - [PR from @blyoom.dev](https://tangled.org/nonbinary.computer/jacquard/pulls/6/round/0) exposing timestamps directly on `Datetime` type 24 + - Naming aligned with `chrono` conventions 25 + 26 + **Handle normalization** (`jacquard-common`) 27 + - Handles are now lowercase-normalized on construction 28 + 29 + **Embedded PDS primitives** (`jacquard-repo`) 30 + - Initial lazy disk-spilling collection types for embedded PDS use cases 31 + - Repo firehose types now use generated API types instead of hand-written equivalents 32 + 33 + **Lexicon codegen improvements** (`jacquard-lexicon`, `jacquard-api`) 34 + - `knownValues` generation now aligned with AT Protocol spec and triggers more frequently 35 + - Improved feature dependency tracking for API crate features 36 + 37 + ### Fixed 38 + 39 + **Identity resolution** (`jacquard-identity`) 40 + - [PR from @alephcubed.com](https://tangled.org/nonbinary.computer/jacquard/pulls/7/round/0) fixing `DidDocument::handles()` always failing when parsed from `MiniDoc` 41 + 42 + **Error handling** (`jacquard-common`, `jacquard`, `jacquard-oauth`) 43 + - Big error quality-of-life pass with richer, more actionable diagnostics 44 + - More resilient error parsing for auth errors 45 + - Better lexicon parsing error messages 46 + 47 + **WASM** (`jacquard-common`) 48 + - Fixed WASM CI smoke test compilation 49 + 50 + ### Changed 51 + 52 + **Lexicons** (`jacquard-api`) 53 + - Large batch of lexicon schema updates with manual cleanup 54 + 3 55 ## [0.9.6] - 2025-12-19 4 56 5 57 ### Changed ··· 7 59 **Logging** (`jacquard`, `jacquard-axum`) 8 60 - [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 9 61 10 - ## Fixed 62 + ### Fixed 11 63 12 64 **Repo commit signatures** (`jacquard-repo`) 13 65 - commit signatures generated by `jacquard-repo` should now be consistent with other implementations
+11 -11
Cargo.lock
··· 2371 2371 2372 2372 [[package]] 2373 2373 name = "jacquard" 2374 - version = "0.9.5" 2374 + version = "0.10.0" 2375 2375 dependencies = [ 2376 2376 "bytes", 2377 2377 "clap", ··· 2405 2405 2406 2406 [[package]] 2407 2407 name = "jacquard-api" 2408 - version = "0.9.5" 2408 + version = "0.10.0" 2409 2409 dependencies = [ 2410 2410 "jacquard-common", 2411 2411 "jacquard-derive", ··· 2417 2417 2418 2418 [[package]] 2419 2419 name = "jacquard-axum" 2420 - version = "0.9.6" 2420 + version = "0.10.0" 2421 2421 dependencies = [ 2422 2422 "axum", 2423 2423 "axum-macros", ··· 2447 2447 2448 2448 [[package]] 2449 2449 name = "jacquard-common" 2450 - version = "0.9.5" 2450 + version = "0.10.0" 2451 2451 dependencies = [ 2452 2452 "base64 0.22.1", 2453 2453 "bon", ··· 2502 2502 2503 2503 [[package]] 2504 2504 name = "jacquard-derive" 2505 - version = "0.9.5" 2505 + version = "0.10.0" 2506 2506 dependencies = [ 2507 2507 "heck 0.5.0", 2508 2508 "inventory", ··· 2518 2518 2519 2519 [[package]] 2520 2520 name = "jacquard-identity" 2521 - version = "0.9.5" 2521 + version = "0.10.0" 2522 2522 dependencies = [ 2523 2523 "bon", 2524 2524 "bytes", ··· 2542 2542 2543 2543 [[package]] 2544 2544 name = "jacquard-lexgen" 2545 - version = "0.9.5" 2545 + version = "0.10.0" 2546 2546 dependencies = [ 2547 2547 "clap", 2548 2548 "clap_complete", ··· 2569 2569 2570 2570 [[package]] 2571 2571 name = "jacquard-lexicon" 2572 - version = "0.9.5" 2572 + version = "0.10.0" 2573 2573 dependencies = [ 2574 2574 "bytes", 2575 2575 "cid", ··· 2598 2598 2599 2599 [[package]] 2600 2600 name = "jacquard-oauth" 2601 - version = "0.9.6" 2601 + version = "0.10.0" 2602 2602 dependencies = [ 2603 2603 "base64 0.22.1", 2604 2604 "bytes", ··· 2629 2629 2630 2630 [[package]] 2631 2631 name = "jacquard-repo" 2632 - version = "0.9.6" 2632 + version = "0.10.0" 2633 2633 dependencies = [ 2634 2634 "anyhow", 2635 2635 "bytes", ··· 2770 2770 2771 2771 [[package]] 2772 2772 name = "lazy-collections" 2773 - version = "0.9.5" 2773 + version = "0.10.0" 2774 2774 dependencies = [ 2775 2775 "buffer", 2776 2776 "bytes",
+1 -1
Cargo.toml
··· 5 5 6 6 [workspace.package] 7 7 edition = "2024" 8 - version = "0.9.5" 8 + version = "0.10.0" 9 9 authors = ["Orual <orual@nonbinary.computer>"] 10 10 #repository = "https://github.com/rsform/jacquard" 11 11 repository = "https://tangled.org/@nonbinary.computer/jacquard"
+22 -57
README.md
··· 22 22 - All the building blocks of the convenient abstractions are available 23 23 - Use as much or as little from the crates as you need 24 24 25 - ## 0.9.X Release Highlights: 26 - 27 - **`#[derive(LexiconSchema)]` + `#[lexicon_union]` macros** 28 - - Automatic schema generation for custom lexicons from Rust structs 29 - - Supports all lexicon constraints via attributes (max_length, max_graphemes, min/max, etc.) 30 - - Generates `LexiconDoc` at compile time for runtime validation 31 - 32 - **Runtime lexicon data validation** 33 - - Validation of structural and/or value contraints of data against a lexicon 34 - - caching for value validations 35 - - LexiconSchema trait generated implementations for runtime validation 36 - - detailed validation error results 37 - 38 - **Lexicon resolver** 39 - - Fetch lexicons at runtime for addition to schema registry 40 - 41 - **Query and path DSLs for `Data` and `RawData` value types** 42 - - Pattern-based querying of nested `Data` structures 43 - - `data.query(pattern)` with expressive syntax: 44 - - `field.nested` - exact path navigation 45 - - `[..]` - wildcard over collections (array elements or object values) 46 - - `field..nested` - scoped recursion (find nested within field, expect one) 47 - - `...field` - global recursion (find all occurrences anywhere) 48 - - `get_at_path()` for simple path-based field access on `Data` and `RawData` 49 - - Path syntax: `embed.images[0].alt` for navigating nested structures 50 - - `type_discriminator()` helper methods for AT Protocol union discrimination 51 - - Collection helper methods: `get()`, `contains_key()`, `len()`, `is_empty()`, `iter()`, `keys()`, `values()` 52 - - Index trait implemented: `obj["key"]` and `arr[0]` 53 - 54 - **Caching in identity/lexicon resolver** 55 - - Basic LRU in-memory cache implementation using `mini-moka` 56 - - Reduces number of network requests for certain operations 57 - - Works on both native and WebAssembly via vendored patched version of mini-moka 58 - 59 - 60 - **XRPC client improvements** 61 - - `set_options()` and `set_endpoint()` methods on `XrpcClient` trait 62 - - Default no-op implementations for stateless clients 63 - - Enables runtime reconfiguration of stateful clients 64 - - Better support for custom endpoint and option overrides 65 - - Fixed bug where setting a custom 'Content-Type' header wouldn't be respected 66 - 67 - **Major generated API compilation time improvements** 68 - - Generated code output now includes a typestate builder implementation, similar to the `bon` crate 69 - - Moves the substantial `syn` tax of generating the builders to code generation time, not compile time. 70 - 71 - **New `jacquard-lexgen` crate** 72 - - Moves binaries out of jacquard-lexicon to reduce size further 73 - - Flake app for `lex-fetch` 74 25 75 26 ## Example 76 27 ··· 134 85 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. 135 86 136 87 > [!WARNING] 137 - > A lot of the streaming code is still pretty experimental. The examples work, though.\ 138 - 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.\ 139 - >I would also note the same for the repository crate until I've had more third parties test it. 88 + > 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. 140 89 141 90 ### Changelog 142 91 143 92 [CHANGELOG.md](./CHANGELOG.md) 144 93 145 - <!--### Testimonials 94 + #### 0.10 Release Highlights: 95 + 96 + **URL type migration** 97 + - Migrated from `url` crate to `fluent_uri` for validated URL/URI types 98 + - All `Url` types are now `Uri` from `fluent_uri` 99 + - Affects any code that constructs, passes, or pattern-matches on endpoint URLs 100 + 101 + **Re-exported crate paths** 102 + - Re-exported crates (including non-proc-macro dependencies of the generated API crate) are now centralized into a distinct module 103 + - Import paths for re-exported types have changed 104 + 105 + **`no_std` groundwork** 106 + - Initial work toward allowing jacquard to function on platforms without access to the standard library. 107 + - `std` usage is now feature-gated. the library currently *does not compile* without `std` due to some remaining dependencies. 108 + 109 + ### Testimonials 146 110 147 111 - ["the most straightforward interface to atproto I've encountered so far."](https://bsky.app/profile/offline.mountainherder.xyz/post/3m3xwewzs3k2v) - @offline.mountainherder.xyz 148 - 149 - - "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)--> 112 + - "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) 150 113 151 114 ### Projects using Jacquard 152 115 153 116 - [skywatch-phash-rs](https://tangled.org/skywatch.blue/skywatch-phash-rs) 154 - - [Weaver](https://alpha.weaver.sh/) - [tangled repository](https://tangled.org/nonbinary.computer/weaver) 155 - - [wisp.place CLI tool](https://docs.wisp.place/cli/) 117 + - [Weaver](https://weaver.sh/) - [tangled repository](https://tangled.org/nonbinary.computer/weaver) 118 + - [wisp.place CLI tool](https://docs.wisp.place/cli/) - formerly 156 119 - [PDS MOOver](https://pdsmoover.com/) - [tangled repository](https://tangled.org/baileytownsend.dev/pds-moover) 157 120 158 121 ## Component crates ··· 188 151 ``` 189 152 190 153 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. 154 + 155 + 191 156 192 157 [![License](https://img.shields.io/crates/l/jacquard.svg)](./LICENSE)
+4 -4
crates/jacquard-api/Cargo.toml
··· 2 2 name = "jacquard-api" 3 3 description = "Generated AT Protocol API bindings for Jacquard" 4 4 edition.workspace = true 5 - version = "0.9.5" 5 + version = "0.10.0" 6 6 authors.workspace = true 7 7 repository.workspace = true 8 8 keywords.workspace = true ··· 15 15 features = [ "bluesky", "other", "streaming" ] 16 16 17 17 [dependencies] 18 - jacquard-common = { version = "0.9", path = "../jacquard-common" } 19 - jacquard-derive = { version = "0.9", path = "../jacquard-derive" } 20 - jacquard-lexicon = { version = "0.9", path = "../jacquard-lexicon", default-features = false } 18 + jacquard-common = { version = "0.10", path = "../jacquard-common" } 19 + jacquard-derive = { version = "0.10", path = "../jacquard-derive" } 20 + jacquard-lexicon = { version = "0.10", path = "../jacquard-lexicon", default-features = false } 21 21 miette.workspace = true 22 22 serde.workspace = true 23 23 thiserror.workspace = true
+5 -5
crates/jacquard-axum/Cargo.toml
··· 1 1 [package] 2 2 name = "jacquard-axum" 3 3 edition.workspace = true 4 - version = "0.9.6" 4 + version = "0.10.0" 5 5 authors.workspace = true 6 6 repository.workspace = true 7 7 keywords.workspace = true ··· 22 22 [dependencies] 23 23 axum = "0.8.6" 24 24 bytes.workspace = true 25 - jacquard = { version = "0.9", path = "../jacquard", default-features = false, features = ["api"] } 26 - jacquard-common = { version = "0.9", path = "../jacquard-common", features = ["reqwest-client"] } 27 - jacquard-derive = { version = "0.9", path = "../jacquard-derive" } 28 - jacquard-identity = { version = "0.9", path = "../jacquard-identity", optional = true } 25 + jacquard = { version = "0.10", path = "../jacquard", default-features = false, features = ["api"] } 26 + jacquard-common = { version = "0.10", path = "../jacquard-common", features = ["reqwest-client"] } 27 + jacquard-derive = { version = "0.10", path = "../jacquard-derive" } 28 + jacquard-identity = { version = "0.10", path = "../jacquard-identity", optional = true } 29 29 miette.workspace = true 30 30 multibase = { version = "0.9.1", optional = true } 31 31 serde.workspace = true
+1 -1
crates/jacquard-common/Cargo.toml
··· 2 2 name = "jacquard-common" 3 3 description = "Core AT Protocol types and utilities for Jacquard" 4 4 edition.workspace = true 5 - version = "0.9.5" 5 + version = "0.10.0" 6 6 authors.workspace = true 7 7 repository.workspace = true 8 8 keywords.workspace = true
+4 -28
crates/jacquard-common/src/lib.rs
··· 227 227 pub mod error; 228 228 pub mod http_client; 229 229 pub mod macros; 230 + pub mod opt_serde_bytes_helper; 231 + pub mod serde_bytes_helper; 230 232 #[cfg(feature = "service-auth")] 231 233 pub mod service_auth; 232 234 pub mod session; 235 + #[cfg(feature = "streaming")] 236 + pub mod stream; 233 237 /// Compile-time TLD lookup for disambiguating handles from NSIDs. 234 238 pub(crate) mod tld; 235 239 /// Baseline fundamental AT Protocol data types. 236 240 pub mod types; 237 - // XRPC protocol types and traits 238 - pub mod opt_serde_bytes_helper; 239 - pub mod serde_bytes_helper; 240 - #[cfg(feature = "streaming")] 241 - pub mod stream; 242 241 pub mod xrpc; 243 242 244 243 #[cfg(feature = "streaming")] ··· 289 288 let value = T::deserialize(deserializer)?; 290 289 Ok(value.into_static()) 291 290 } 292 - 293 - #[cfg(test)] 294 - mod tests { 295 - use crate::deps::bytes; 296 - use crate::deps::chrono; 297 - use crate::deps::smol_str::SmolStr; 298 - 299 - #[test] 300 - fn deps_smol_str() { 301 - let s = SmolStr::new_static("test"); 302 - assert_eq!(s, "test"); 303 - } 304 - 305 - #[test] 306 - fn deps_bytes() { 307 - let _x = bytes::Bytes::from_static(b"hello"); 308 - } 309 - 310 - #[test] 311 - fn deps_chrono() { 312 - let _now = chrono::Utc::now(); 313 - } 314 - }
+1 -1
crates/jacquard-common/src/types/value.rs
··· 1177 1177 /// A single match from a query operation 1178 1178 #[derive(Debug, Clone, PartialEq)] 1179 1179 pub struct QueryMatch<'s> { 1180 - /// Path where this value was found (e.g., "actors[0].handle") 1180 + /// Path where this value was found (e.g., "actors\[0\].handle") 1181 1181 pub path: SmolStr, 1182 1182 /// The value (None if field was missing during wildcard iteration) 1183 1183 pub value: Option<&'s Data<'s>>,
+1 -1
crates/jacquard-common/src/xrpc.rs
··· 58 58 /// Normalize a base URI by removing trailing slashes. 59 59 /// 60 60 /// This is useful for XRPC clients where the base URI might be provided with 61 - /// a trailing slash (e.g., "https://bsky.social/") but needs to be normalized 61 + /// a trailing slash (e.g., "<https://bsky.social/>") but needs to be normalized 62 62 /// for consistent path building. Since trimming a trailing slash from a valid URI 63 63 /// always yields a valid URI, the result is guaranteed to be valid. 64 64 pub fn normalize_base_uri(uri: Uri<String>) -> Uri<String> {
+1 -1
crates/jacquard-common/src/xrpc/subscription.rs
··· 720 720 /// This exists primarily for server-side frameworks (like Axum) to extract 721 721 /// typed subscription parameters without lifetime issues. 722 722 pub trait SubscriptionEndpoint { 723 - /// Fully-qualified path ('/xrpc/[nsid]') where this subscription endpoint lives 723 + /// Fully-qualified path ('/xrpc/{nsid}') where this subscription endpoint lives 724 724 const PATH: &'static str; 725 725 726 726 /// Message encoding (JSON or DAG-CBOR)
+3 -3
crates/jacquard-derive/Cargo.toml
··· 16 16 17 17 [dependencies] 18 18 heck.workspace = true 19 - jacquard-lexicon = { version = "0.9", path = "../jacquard-lexicon", features = ["codegen"] } 19 + jacquard-lexicon = { version = "0.10", path = "../jacquard-lexicon", features = ["codegen"] } 20 20 proc-macro2.workspace = true 21 21 quote.workspace = true 22 22 syn.workspace = true 23 23 24 24 [dev-dependencies] 25 25 inventory = "0.3" 26 - jacquard-common = { version = "0.9", path = "../jacquard-common" } 27 - jacquard-lexicon = { version = "0.9", path = "../jacquard-lexicon" } 26 + jacquard-common = { version = "0.10", path = "../jacquard-common" } 27 + jacquard-lexicon = { version = "0.10", path = "../jacquard-lexicon" } 28 28 serde.workspace = true 29 29 serde_json.workspace = true 30 30 unicode-segmentation = "1.12"
+4 -4
crates/jacquard-identity/Cargo.toml
··· 1 1 [package] 2 2 name = "jacquard-identity" 3 3 edition.workspace = true 4 - version = "0.9.5" 4 + version = "0.10.0" 5 5 authors.workspace = true 6 6 repository.workspace = true 7 7 keywords.workspace = true ··· 22 22 trait-variant.workspace = true 23 23 bon.workspace = true 24 24 bytes.workspace = true 25 - jacquard-common = { version = "0.9", path = "../jacquard-common", features = ["reqwest-client"] } 26 - jacquard-api = { version = "0.9", path = "../jacquard-api", default-features = false, features = ["minimal"] } 27 - jacquard-lexicon = { version = "0.9", path = "../jacquard-lexicon", default-features = false } 25 + jacquard-common = { version = "0.10", path = "../jacquard-common", features = ["reqwest-client"] } 26 + jacquard-api = { version = "0.10", path = "../jacquard-api", default-features = false, features = ["minimal"] } 27 + jacquard-lexicon = { version = "0.10", path = "../jacquard-lexicon", default-features = false } 28 28 reqwest.workspace = true 29 29 serde.workspace = true 30 30 serde_json.workspace = true
+5 -5
crates/jacquard-lexgen/Cargo.toml
··· 32 32 clap.workspace = true 33 33 glob = "0.3" 34 34 inventory = "0.3" 35 - jacquard-api = { version = "0.9", path = "../jacquard-api", default-features = false, features = [ "minimal" ] } 36 - jacquard-common = { version = "0.9", features = [ "reqwest-client" ], path = "../jacquard-common" } 37 - jacquard-derive = { version = "0.9", path = "../jacquard-derive" } 38 - jacquard-identity = { version = "0.9", path = "../jacquard-identity", features = ["dns"] } 39 - jacquard-lexicon = { version = "0.9", path = "../jacquard-lexicon" } 35 + jacquard-api = { version = "0.10", path = "../jacquard-api", default-features = false, features = [ "minimal" ] } 36 + jacquard-common = { version = "0.10", features = [ "reqwest-client" ], path = "../jacquard-common" } 37 + jacquard-derive = { version = "0.10", path = "../jacquard-derive" } 38 + jacquard-identity = { version = "0.10", path = "../jacquard-identity", features = ["dns"] } 39 + jacquard-lexicon = { version = "0.10", path = "../jacquard-lexicon" } 40 40 kdl = "6" 41 41 miette = { workspace = true, features = ["fancy"] } 42 42 reqwest = { workspace = true, features = ["json", "http2", "system-proxy", "rustls-tls"] }
+3 -3
crates/jacquard-lexicon/Cargo.toml
··· 2 2 name = "jacquard-lexicon" 3 3 description = "Lexicon schema parsing and code generation for Jacquard" 4 4 edition.workspace = true 5 - version = "0.9.5" 5 + version = "0.10.0" 6 6 authors.workspace = true 7 7 repository.workspace = true 8 8 keywords.workspace = true ··· 20 20 dashmap.workspace = true 21 21 heck = { workspace = true, optional = true } 22 22 inventory = "0.3" 23 - jacquard-common = { version = "0.9", path = "../jacquard-common" } 23 + jacquard-common = { version = "0.10", path = "../jacquard-common" } 24 24 miette = { workspace = true } 25 25 multihash.workspace = true 26 26 prettyplease = { workspace = true, optional = true } ··· 39 39 40 40 [dev-dependencies] 41 41 bytes = { workspace = true } 42 - jacquard-derive = { version = "0.9", path = "../jacquard-derive"} 42 + jacquard-derive = { version = "0.10", path = "../jacquard-derive"} 43 43 tempfile = { version = "3.23.0" }
+1 -1
crates/jacquard-lexicon/src/codegen/builder_gen/state_mod.rs
··· 1 1 //! State module generation for builders 2 2 //! 3 - //! Generates the state trait, Empty state, and SetX<S> transition types 3 + //! Generates the state trait, Empty state, and `SetX<S>` transition types 4 4 //! that enable type-safe builder patterns. 5 5 6 6 use std::collections::HashSet;
+1 -1
crates/jacquard-lexicon/src/derive_impl/lexicon_attr.rs
··· 1 - //! Implementation of #[lexicon] attribute macro 1 + //! Implementation of `#[lexicon]` attribute macro 2 2 3 3 use proc_macro2::TokenStream; 4 4 use quote::quote;
+1 -1
crates/jacquard-lexicon/src/derive_impl/lexicon_union.rs
··· 1 - //! Implementation of #[lexicon_union] attribute macro 1 + //! Implementation of `#[lexicon_union]` attribute macro 2 2 3 3 use proc_macro2::TokenStream; 4 4 use quote::quote;
+1 -1
crates/jacquard-lexicon/src/derive_impl/open_union_attr.rs
··· 1 - //! Implementation of #[open_union] attribute macro 1 + //! Implementation of `#[open_union]` attribute macro 2 2 3 3 use proc_macro2::TokenStream; 4 4 use quote::quote;
-1
crates/jacquard-lexicon/src/lib.rs
··· 10 10 //! - [`corpus`] - Lexicon corpus management and namespace organization 11 11 //! - [`lexicon`] - Schema parsing and validation 12 12 //! - [`schema`] - Schema generation from Rust types (reverse codegen) 13 - //! - [`union_registry`] - Tracks union types for collision detection 14 13 //! - [`fs`] - Filesystem utilities for lexicon storage 15 14 //! - [`derive_impl`] - Implementation functions for derive macros (used by jacquard-derive) 16 15 //! - [`validation`] - Runtime validation of Data against lexicon schemas
+2 -2
crates/jacquard-lexicon/src/schema/from_ast/parse.rs
··· 262 262 Ok(None) 263 263 } 264 264 265 - /// Extract T from Option<T>, return (type, is_required) 265 + /// Extract T from `Option<T>`, return (type, is_required) 266 266 pub fn extract_option_inner(ty: &syn::Type) -> (&syn::Type, bool) { 267 267 if let syn::Type::Path(type_path) = ty { 268 268 if let Some(segment) = type_path.path.segments.last() { ··· 278 278 (ty, true) 279 279 } 280 280 281 - /// Check if type has #[open_union] attribute 281 + /// Check if type has `#[open_union]` attribute 282 282 pub fn has_open_union_attr(attrs: &[Attribute]) -> bool { 283 283 attrs.iter().any(|attr| attr.path().is_ident("open_union")) 284 284 }
+1 -1
crates/jacquard-lexicon/src/schema/from_ast/types.rs
··· 40 40 pub schema_name: String, 41 41 /// Rust type path (for diagnostic purposes) 42 42 pub field_type: String, 43 - /// Is this field required (not Option<T>)? 43 + /// Is this field required (not `Option<T>`)? 44 44 pub is_required: bool, 45 45 /// Is this validating an array length (vs string length)? 46 46 pub is_array: bool,
+3 -3
crates/jacquard-oauth/Cargo.toml
··· 1 1 [package] 2 2 name = "jacquard-oauth" 3 - version = "0.9.6" 3 + version = "0.10.0" 4 4 edition.workspace = true 5 5 description = "AT Protocol OAuth 2.1 core types and helpers for Jacquard" 6 6 authors.workspace = true ··· 21 21 streaming = ["jacquard-common/streaming", "dep:n0-future"] 22 22 23 23 [dependencies] 24 - jacquard-common = { version = "0.9", path = "../jacquard-common", features = ["reqwest-client"] } 25 - jacquard-identity = { version = "0.9", path = "../jacquard-identity" } 24 + jacquard-common = { version = "0.10", path = "../jacquard-common", features = ["reqwest-client"] } 25 + jacquard-identity = { version = "0.10", path = "../jacquard-identity" } 26 26 serde = { workspace = true, features = ["derive"] } 27 27 serde_json = { workspace = true } 28 28 smol_str = { workspace = true }
+15 -19
crates/jacquard-oauth/src/atproto.rs
··· 93 93 pub privacy_policy_uri: Option<Uri<String>>, 94 94 } 95 95 96 - impl<'m> AtprotoClientMetadata<'m> { 97 - pub fn new( 98 - client_id: Uri<String>, 99 - client_uri: Option<Uri<String>>, 100 - redirect_uris: Vec<Uri<String>>, 101 - grant_types: Vec<GrantType>, 102 - scopes: Vec<Scope<'m>>, 103 - jwks_uri: Option<Uri<String>>, 104 - ) -> Self { 105 - Self { 106 - client_id, 107 - client_uri, 108 - redirect_uris, 109 - grant_types, 110 - scopes, 111 - jwks_uri, 112 - client_name: None, 113 - logo_uri: None, 114 - tos_uri: None, 96 + impl<'m> IntoStatic for AtprotoClientMetadata<'m> { 97 + type Output = AtprotoClientMetadata<'static>; 98 + fn into_static(self) -> AtprotoClientMetadata<'static> { 99 + AtprotoClientMetadata { 100 + client_id: self.client_id, 101 + client_uri: self.client_uri, 102 + redirect_uris: self.redirect_uris, 103 + grant_types: self.grant_types, 104 + scopes: self.scopes.into_static(), 105 + jwks_uri: self.jwks_uri, 106 + client_name: self.client_name, 107 + logo_uri: self.logo_uri, 108 + tos_uri: self.tos_uri, 115 109 privacy_policy_uri: None, 116 110 } 117 111 } 112 + } 118 113 114 + impl<'m> AtprotoClientMetadata<'m> { 119 115 pub fn with_prod_info( 120 116 mut self, 121 117 client_name: &str,
+1
crates/jacquard-oauth/src/lib.rs
··· 46 46 //! 47 47 //! See [`atproto`] module for AT Protocol-specific metadata helpers. 48 48 49 + #![warn(missing_docs)] 49 50 pub mod atproto; 50 51 pub mod authstore; 51 52 pub mod client;
+129 -32
crates/jacquard-oauth/src/loopback.rs
··· 1 + //! 2 + //! Helpers for the local loopback server method of atproto OAuth. 3 + //! 4 + //! `OAuthClient::login_with_local_server()` is the nice helper. Here is where 5 + //! it and its components live. Below is what it does, so you can have more 6 + //! granular control without having to make your own loopback server. 7 + //! 8 + //! ```ignore 9 + //! let input = "your_handle_here"; 10 + //! let cfg = LoopbackConfig::default(); 11 + //! let opts = AuthorizeOptions::default(); 12 + //! let port = match cfg.port { 13 + //! LoopbackPort::Fixed(p) => p, 14 + //! LoopbackPort::Ephemeral => 0, 15 + //! }; 16 + //! // TODO: fix this to it also accepts ipv6 and properly finds a free port 17 + //! let bind_addr: SocketAddr = format!("0.0.0.0:{}", port) 18 + //! .parse() 19 + //! .expect("invalid loopback host/port"); 20 + //! let oauth = OAuthClient::with_default_config(FileAuthStore::new(&args.store)); 21 + //! 22 + //! let (local_addr, handle) = one_shot_server(bind_addr); 23 + //! println!("Listening on {}", local_addr); 24 + //! 25 + //! let client_data = oauth.build_localhost_client_data(&cfg, &opts, local_addr); 26 + //! // Build client using store and resolver 27 + //! let flow_client = OAuthClient::new_with_shared( 28 + //! self.registry.store.clone(), 29 + //! self.client.clone(), 30 + //! client_data, 31 + //! ); 32 + //! 33 + //! // Start auth and get authorization URL 34 + //! let auth_url = flow_client.start_auth(input.as_ref(), opts).await?; 35 + //! // Print URL for copy/paste 36 + //! println!("To authenticate with your PDS, visit:\n{}\n", auth_url); 37 + //! // Optionally open browser 38 + //! if cfg.open_browser { 39 + //! let _ = try_open_in_browser(&auth_url); 40 + //! } 41 + //! 42 + //! handle_localhost_callback(handle, &flow_client, &cfg).await 43 + //! ``` 44 + //! 45 + //! 1 46 #![cfg(feature = "loopback")] 2 - 3 47 use crate::{ 4 48 atproto::AtprotoClientMetadata, 5 49 authstore::ClientAuthStore, ··· 41 85 } 42 86 43 87 #[cfg(feature = "browser-open")] 44 - fn try_open_in_browser(url: &str) -> bool { 88 + pub fn try_open_in_browser(url: &str) -> bool { 45 89 webbrowser::open(url).is_ok() 46 90 } 47 91 #[cfg(not(feature = "browser-open"))] 48 - fn try_open_in_browser(_url: &str) -> bool { 92 + pub fn try_open_in_browser(_url: &str) -> bool { 49 93 false 50 94 } 51 95 52 - pub fn create_callback_router( 96 + fn create_callback_router( 53 97 request: &rouille::Request, 54 98 tx: mpsc::Sender<CallbackParams>, 55 99 ) -> rouille::Response { ··· 70 114 ) 71 115 } 72 116 73 - struct CallbackHandle { 117 + pub struct CallbackHandle { 74 118 #[allow(dead_code)] 75 119 server_handle: std::thread::JoinHandle<()>, 76 120 server_stop: std::sync::mpsc::Sender<()>, 77 121 callback_rx: mpsc::Receiver<CallbackParams<'static>>, 78 122 } 79 123 80 - fn one_shot_server(addr: SocketAddr) -> (SocketAddr, CallbackHandle) { 124 + /// One-shot OAuth callback server. 125 + /// 126 + /// Starts an ephemeral in-process web server that listens for the OAuth 127 + /// callback redirect. Returns the server address and a [`CallbackHandle`] 128 + /// that can be used to wait for the callback and stop the server. 129 + /// 130 + /// Use in combination with [`handle_localhost_callback`] to handle the 131 + /// callback for the localhost loopback server. 132 + pub fn one_shot_server(addr: SocketAddr) -> (SocketAddr, CallbackHandle) { 81 133 let (tx, callback_rx) = mpsc::channel(5); 82 134 let server = Server::new(addr, move |request| { 83 135 create_callback_router(request, tx.clone()) ··· 92 144 (addr, handle) 93 145 } 94 146 147 + /// Handles the OAuth callback for the localhost loopback server. 148 + /// 149 + /// Returns a session if the callback succeeds within the configured timeout 150 + /// and shuts down the server. 151 + pub async fn handle_localhost_callback<T, S>( 152 + handle: CallbackHandle, 153 + flow_client: &super::client::OAuthClient<T, S>, 154 + cfg: &LoopbackConfig, 155 + ) -> crate::error::Result<super::client::OAuthSession<T, S>> 156 + where 157 + T: OAuthResolver + DpopExt + Send + Sync + 'static, 158 + S: ClientAuthStore + Send + Sync + 'static, 159 + { 160 + // Await callback or timeout 161 + let mut callback_rx = handle.callback_rx; 162 + let cb = tokio::time::timeout( 163 + std::time::Duration::from_millis(cfg.timeout_ms), 164 + callback_rx.recv(), 165 + ) 166 + .await; 167 + // trigger shutdown 168 + let _ = handle.server_stop.send(()); 169 + if let Ok(Some(cb)) = cb { 170 + // Handle callback and create a session 171 + Ok(flow_client.callback(cb).await?) 172 + } else { 173 + Err(OAuthError::Callback(CallbackError::Timeout)) 174 + } 175 + } 176 + 95 177 impl<T, S> OAuthClient<T, S> 96 178 where 97 179 T: OAuthResolver + DpopExt + Send + Sync + 'static, 98 180 S: ClientAuthStore + Send + Sync + 'static, 99 181 { 100 182 /// Drive the full OAuth flow using a local loopback server. 183 + /// 184 + /// This uses localhost OAuth and an ephemeral in-process web server to 185 + /// handle the OAuth callback redirect. It has a bunch of nice friendly 186 + /// defaults to help you get started and will basically drive the *entire* 187 + /// callback flow itself. 188 + /// 189 + /// Best used for development and for small CLI applications that don't 190 + /// require long session lengths. For long-running unattended sessions, 191 + /// app passwords (via CredentialSession in the jacquard crate) remain 192 + /// the best option. For more complex OAuth, or if you want more control 193 + /// over the process, use the other methods on OAuthClient. 194 + /// 195 + /// 'input' parameter is what you type in the login box (usually, your handle) 196 + /// for it to look up your PDS and redirect to its authentication interface. 197 + /// 198 + /// If the `browser-open` feature is enabled, this will open a web browser 199 + /// for you to authenticate with your PDS. It will also print the 200 + /// callback url to the console for you to copy. 101 201 pub async fn login_with_local_server( 102 202 &self, 103 203 input: impl AsRef<str>, ··· 114 214 .expect("invalid loopback host/port"); 115 215 let (local_addr, handle) = one_shot_server(bind_addr); 116 216 println!("Listening on {}", local_addr); 117 - // build redirect uri 118 - let redirect_uri = format!("http://{}:{}/oauth/callback", cfg.host, local_addr.port(),); 119 - let redirect = Uri::parse(redirect_uri).unwrap(); 120 - 121 - let scopes = if opts.scopes.is_empty() { 122 - Some(self.registry.client_data.config.scopes.clone()) 123 - } else { 124 - Some(opts.scopes.clone().into_static()) 125 - }; 126 217 127 - let client_data = crate::session::ClientData { 128 - keyset: self.registry.client_data.keyset.clone(), 129 - config: AtprotoClientMetadata::new_localhost(Some(vec![redirect]), scopes), 130 - }; 218 + let client_data = self.build_localhost_client_data(&cfg, &opts, local_addr); 131 219 // Build client using store and resolver 132 220 let flow_client = OAuthClient::new_with_shared( 133 221 self.registry.store.clone(), ··· 144 232 let _ = try_open_in_browser(&auth_url); 145 233 } 146 234 147 - // Await callback or timeout 148 - let mut callback_rx = handle.callback_rx; 149 - let cb = tokio::time::timeout( 150 - std::time::Duration::from_millis(cfg.timeout_ms), 151 - callback_rx.recv(), 152 - ) 153 - .await; 154 - // trigger shutdown 155 - let _ = handle.server_stop.send(()); 156 - if let Ok(Some(cb)) = cb { 157 - // Handle callback and create a session 158 - Ok(flow_client.callback(cb).await?) 235 + handle_localhost_callback(handle, &flow_client, &cfg).await 236 + } 237 + 238 + /// Builds a [`crate::session::ClientData`] for use with the local loopback server method of OAuth. 239 + pub fn build_localhost_client_data( 240 + &self, 241 + cfg: &LoopbackConfig, 242 + opts: &AuthorizeOptions<'_>, 243 + local_addr: SocketAddr, 244 + ) -> crate::session::ClientData<'static> { 245 + let redirect_uri = format!("http://{}:{}/oauth/callback", cfg.host, local_addr.port(),); 246 + let redirect = Uri::parse(redirect_uri).unwrap(); 247 + 248 + let scopes = if opts.scopes.is_empty() { 249 + Some(self.registry.client_data.config.scopes.clone()) 159 250 } else { 160 - Err(OAuthError::Callback(CallbackError::Timeout)) 251 + Some(opts.scopes.clone().into_static()) 252 + }; 253 + 254 + crate::session::ClientData { 255 + keyset: self.registry.client_data.keyset.clone(), 256 + config: AtprotoClientMetadata::new_localhost(Some(vec![redirect]), scopes), 161 257 } 258 + .into_static() 162 259 } 163 260 }
+10
crates/jacquard-oauth/src/session.rs
··· 239 239 pub config: AtprotoClientMetadata<'s>, 240 240 } 241 241 242 + impl<'s> IntoStatic for ClientData<'s> { 243 + type Output = ClientData<'static>; 244 + fn into_static(self) -> ClientData<'static> { 245 + ClientData { 246 + keyset: self.keyset, 247 + config: self.config.into_static(), 248 + } 249 + } 250 + } 251 + 242 252 impl<'s> ClientData<'s> { 243 253 pub fn new(keyset: Option<Keyset>, config: AtprotoClientMetadata<'s>) -> Self { 244 254 Self { keyset, config }
+4 -4
crates/jacquard-repo/Cargo.toml
··· 2 2 name = "jacquard-repo" 3 3 description = "AT Protocol repository primitives: MST, commits, CAR I/O" 4 4 edition.workspace = true 5 - version = "0.9.6" 5 + version = "0.10.0" 6 6 authors.workspace = true 7 7 repository.workspace = true 8 8 keywords.workspace = true ··· 16 16 17 17 [dependencies] 18 18 # Internal 19 - jacquard-common = { path = "../jacquard-common", version = "0.9", features = ["crypto-ed25519", "crypto-k256", "crypto-p256"] } 20 - jacquard-derive = { path = "../jacquard-derive", version = "0.9" } 21 - jacquard-api = { path = "../jacquard-api", version = "0.9", features = ["streaming"] } 19 + jacquard-common = { path = "../jacquard-common", version = "0.10", features = ["crypto-ed25519", "crypto-k256", "crypto-p256"] } 20 + jacquard-derive = { path = "../jacquard-derive", version = "0.10" } 21 + jacquard-api = { path = "../jacquard-api", version = "0.10", features = ["streaming"] } 22 22 23 23 # Serialization 24 24 serde.workspace = true
+6 -6
crates/jacquard/Cargo.toml
··· 120 120 required-features = ["api_bluesky", "loopback"] 121 121 122 122 [dependencies] 123 - jacquard-api = { version = "0.9", path = "../jacquard-api" } 124 - jacquard-common = { version = "0.9", path = "../jacquard-common", features = [ 123 + jacquard-api = { version = "0.10", path = "../jacquard-api" } 124 + jacquard-common = { version = "0.10", path = "../jacquard-common", features = [ 125 125 "reqwest-client", 126 126 ] } 127 - jacquard-oauth = { version = "0.9", path = "../jacquard-oauth" } 128 - jacquard-derive = { version = "0.9", path = "../jacquard-derive", optional = true } 129 - jacquard-identity = { version = "0.9", path = "../jacquard-identity" } 127 + jacquard-oauth = { version = "0.10", path = "../jacquard-oauth" } 128 + jacquard-derive = { version = "0.10", path = "../jacquard-derive", optional = true } 129 + jacquard-identity = { version = "0.10", path = "../jacquard-identity" } 130 130 131 131 132 132 ··· 149 149 150 150 151 151 [target.'cfg(not(target_family = "wasm"))'.dependencies] 152 - jacquard-identity = { version = "0.9", path = "../jacquard-identity", features = ["cache"] } 152 + jacquard-identity = { version = "0.10", path = "../jacquard-identity", features = ["cache"] } 153 153 reqwest = { workspace = true, features = [ 154 154 "http2", 155 155 "gzip",
+9 -3
crates/jacquard/src/client.rs
··· 1 1 //! XRPC client implementation for AT Protocol 2 2 //! 3 3 //! This module provides HTTP and XRPC client traits along with session management 4 - //! for both app-password and OAuth authentication. 4 + //! for both app password and OAuth authentication. 5 5 //! 6 6 //! ## Key types 7 7 //! ··· 15 15 //! - [`credential_session`] - App-password session implementation 16 16 //! - [`token`] - Token storage and persistence 17 17 //! - [`vec_update`] - Trait for fetch-modify-put patterns on array endpoints 18 + //! 19 + //! 20 + //! "Agent" in this context is derived from Bluesky's own library usage of the term. 21 + //! It represents a (persistent) user session, and includes a number of helpful 22 + //! methods which are available via the `AgentSessionExt` extension trait 23 + //! on anything that implements `AgentSession` + `IdentityResolver`. 18 24 19 25 //pub mod bff_session; 20 - /// App-password session implementation with auto-refresh 26 + /// App password session implementation with auto-refresh 21 27 pub mod credential_session; 22 28 /// Agent error type 23 29 pub mod error; ··· 801 807 } 802 808 803 809 /// Untyped, freeform record fetcher. 804 - /// Hits [https://slingshot.microcosm.blue] 810 + /// Hits <https://slingshot.microcosm.blue> 805 811 fn fetch_record_slingshot( 806 812 &self, 807 813 uri: &AtUri<'_>,
+1 -1
crates/jacquard/src/lib.rs
··· 62 62 //! let session = oauth 63 63 //! .login_with_local_server( 64 64 //! args.input.clone(), 65 - //! Default::default(), 65 + //! AuthorizeOptions::default(), 66 66 //! LoopbackConfig::default(), 67 67 //! ) 68 68 //! .await?;
+3 -3
crates/jacquard/src/moderation.rs
··· 2 2 //! 3 3 //! This is an attempt to semi-generalize the Bluesky moderation system. It avoids 4 4 //! depending on their lexicons as much as reasonably possible. This works via a 5 - //! trait, [`Labeled`][crate::moderation::Labeled], which represents things that have labels for moderation 5 + //! trait, [`Labeled`], which represents things that have labels for moderation 6 6 //! applied to them. This way the moderation application functions can operate 7 7 //! primarily via the trait, and are thus generic over lexicon types, and are 8 8 //! easy to use with your own types. 9 9 //! 10 10 //! For more complex types which might have labels applied to components, 11 - //! there is the [`Moderateable`][crate::moderation::Moderateable] trait. A mostly complete implementation for 11 + //! there is the [`Moderateable`] trait. A mostly complete implementation for 12 12 //! `FeedViewPost` is available for reference. The trait method outputs a `Vec` 13 13 //! of tuples, where the first element is a string tag and the second is the 14 14 //! moderation decision for the tagged element. This lets application developers ··· 16 16 //! mostly match Bluesky behaviour (respecting "!hide", and such) by default. 17 17 //! 18 18 //! I've taken the time to go through the generated API bindings and implement 19 - //! the [`Labeled`][crate::moderation::Labeled] trait for a number of types. It's a fairly easy trait to 19 + //! the [`Labeled`] trait for a number of types. It's a fairly easy trait to 20 20 //! implement, just not really automatable. 21 21 //! 22 22 //!
+2 -2
crates/lazy-collections/src/io.rs
··· 137 137 /// 138 138 /// All bytes read from this source will be appended to the specified buffer 139 139 /// `buf`. This function will continuously call [`read()`] to append more data to 140 - /// `buf` until [`read()`] returns either [`Ok(0)`] or an error of 140 + /// `buf` until [`read()`] returns either \[`Ok(0)`\] or an error of 141 141 /// non-[`ErrorKind::Interrupted`] kind. 142 142 /// 143 143 /// If successful, this function will return the total number of bytes read. ··· 323 323 /// Creates an adaptor which will read at most `limit` bytes from it. 324 324 /// 325 325 /// This function returns a new instance of `Read` which will read at most 326 - /// `limit` bytes, after which it will always return EOF ([`Ok(0)`]). Any 326 + /// `limit` bytes, after which it will always return EOF (\[`Ok(0)`\]). Any 327 327 /// read errors will not count towards the number of bytes read and future 328 328 /// calls to [`read()`](Self::read) may succeed. 329 329 fn take(self, limit: u64) -> Take<Self>
+1 -1
crates/lazy-collections/src/lib.rs
··· 1 1 #![cfg_attr(target_os = "none", no_std)] 2 - 2 + #![allow(unused)] 3 3 #[cfg(all(not(feature = "std"), feature = "alloc"))] 4 4 extern crate alloc; 5 5