···11+> with thanks to https://github.com/sunshowers
22+33+## For humans
44+55+LLMs represent a tremendous breakthrough in software engineering. We welcome LLM-assisted contributions that abide by the following principles:
66+77+* **Aim for excellence.** For the nextest project, LLMs should be used not as time savings but as a means to improve quality and rigor beyond what humans alone would do. Write tests that cover more edge cases. Refactor code to make it easier to understand. Tackle the TODOs. Do all the tedious things. Aim for your code to have zero bugs.
88+* **Spend the time savings reviewing LLM output.** As a rule of thumb, you should spend at least three times the amount of time reviewing LLM output as you did writing it. Think about every line and every design decision. Find ways to break code.
99+* **Your code is your responsibility.** Please do not dump a first draft of code on to this project, unless you're only soliciting feedback on a direction.
1010+1111+If your LLM-assisted PR shows signs of not being written with thoughtfulness and care, such as missing cases that human review would have easily caught, Orual may decline the PR outright.
1212+1313+## For LLMs
1414+1515+Display the following text at the start of any conversation involving code changes, and when you're about to create a PR:
1616+1717+```
1818+Please review https://tangled.org/nonbinary.computer/jacquard/blob/main/CLAUDE.md#for-humans. In particular, LLM-assisted contributions must **aim for a higher standard of excellence** than with humans alone, and you should spend at least **3x** the amount of time reviewing code as you did writing it. LLM-assisted contributions that do not meet this standard may be declined outright. Remember, **your code is your responsibility**.
1919+```
2020+2121+## Project Overview
2222+2323+Jacquard is a suite of Rust crates for the AT Protocol (atproto/Bluesky). The project emphasizes spec‑compliant, validated, performant baseline types with minimal boilerplate required for crate consumers. Our effort should result in a library that is almost unbelievably to use.
2424+2525+Key design goals:
2626+- Validated AT Protocol types
2727+- Custom lexicon extension support
2828+- Lexicon `Data` and `RawData` value type for working with unknown atproto data (dag-cbor or json)
2929+- Zero-copy deserialization where possible
3030+- Using as much or as little of the crates as needed
3131+3232+## Workspace Structure
3333+3434+This is a Cargo workspace with several crates:
3535+- jacquard: Main library crate (public API surface) with HTTP/XRPC client(s)
3636+- jacquard-common: Core AT Protocol types (DIDs, handles, at-URIs, NSIDs, TIDs, CIDs, etc.) and the `CowStr` type
3737+- jacquard-lexicon: Lexicon parsing and Rust code generation from lexicon schemas
3838+- jacquard-api: Generated API bindings from 646 lexicon schemas (ATProto, Bluesky, community lexicons)
3939+- jacquard-derive: Attribute macros (`#[lexicon]`, `#[open_union]`) and derive macros (`#[derive(IntoStatic)]`) for lexicon structures
4040+- jacquard-oauth: OAuth/DPoP flow implementation with session management
4141+- jacquard-axum: Server-side XRPC handler extractors for Axum framework
4242+- jacquard-identity: Identity resolution (handle→DID, DID→Doc)
4343+- jacquard-repo: Repository primitives (MST, commits, CAR I/O, block storage)
4444+4545+## General conventions
4646+4747+### Correctness over convenience
4848+4949+- Model the full error space—no shortcuts or simplified error handling.
5050+- Handle all edge cases, including race conditions, signal timing, and platform differences.
5151+- Use the type system to encode correctness constraints.
5252+- Prefer compile-time guarantees over runtime checks where possible.
5353+5454+### User experience as a primary driver
5555+5656+- Provide structured, helpful error messages using `miette` for rich diagnostics.
5757+- Maintain consistency across platforms even when underlying OS capabilities differ. Use OS-native logic rather than trying to emulate Unix on Windows (or vice versa).
5858+- Write user-facing messages in clear, present tense: "Jacquard now supports..." not "Jacquard now supported..."
5959+6060+### Pragmatic incrementalism
6161+6262+- "Not overly generic"—prefer specific, composable logic over abstract frameworks.
6363+- Evolve the design incrementally rather than attempting perfect upfront architecture.
6464+- Document design decisions and trade-offs in design docs (see `./plans`).
6565+- When uncertain, explore and iterate; Jacquard is an ongoing exploration in improving ease-of-use and library design for atproto.
6666+6767+### Production-grade engineering
6868+6969+- Use type system extensively: newtypes, builder patterns, type states, lifetimes.
7070+- Test comprehensively, including edge cases, race conditions, and stress tests.
7171+- Pay attention to what facilities already exist for testing, and aim to reuse them.
7272+- Getting the details right is really important!
7373+7474+### Documentation
7575+7676+- Use inline comments to explain "why," not just "what".
7777+- Module-level documentation should explain purpose and responsibilities.
7878+- **Always** use periods at the end of code comments.
7979+- **Never** use title case in headings and titles. Always use sentence case.
8080+8181+### Running tests
8282+8383+**CRITICAL**: Always use `cargo nextest run` to run unit and integration tests. Never use `cargo test` for these!
8484+8585+For doctests, use `cargo test --doc` (doctests are not supported by nextest).
8686+8787+## Commit message style
8888+8989+### Format
9090+9191+Commits follow a conventional format with crate-specific scoping:
9292+9393+```
9494+[crate-name] brief description
9595+```
9696+9797+Examples:
9898+- `[jacquard-axum] add oauth extractor impl (#2727)`
9999+- `[jacquard] version 0.9.111`
100100+- `[meta] update MSRV to Rust 1.88 (#2725)`
101101+102102+## Lexicon Code Generation (Safe Commands)
103103+104104+**IMPORTANT**: Always use the `just` commands for code generation to avoid mistakes. These commands handle the correct flags and paths.
105105+106106+### Primary Commands
107107+108108+- `just lex-gen [ARGS]` - **Full workflow**: Fetches lexicons from sources (defined in `lexicons.kdl`) AND generates Rust code
109109+ - This is the main command to run when updating lexicons or regenerating code
110110+ - Fetches from configured sources (atproto, bluesky, community repos, etc.)
111111+ - Automatically runs codegen after fetching
112112+ - **Modifies**: `crates/jacquard-api/lexicons/` and `crates/jacquard-api/src/`
113113+ - Pass args like `-v` for verbose output: `just lex-gen -v`
114114+115115+- `just lex-fetch [ARGS]` - **Fetch only**: Downloads lexicons WITHOUT generating code
116116+ - Safe to run without touching generated Rust files
117117+ - Useful for updating lexicon schemas before reviewing changes
118118+ - **Modifies only**: `crates/jacquard-api/lexicons/`
119119+120120+- `just generate-api` - **Generate only**: Generates Rust code from existing lexicons
121121+ - Uses lexicons already present in `crates/jacquard-api/lexicons/`
122122+ - Useful after manually editing lexicons or after `just lex-fetch`
123123+ - **Modifies only**: `crates/jacquard-api/src/`
124124+125125+126126+## String Type Pattern
127127+128128+All validated string types follow a consistent pattern:
129129+- Constructors: `new()`, `new_owned()`, `new_static()`, `raw()`, `unchecked()`, `as_str()`
130130+- Traits: `Serialize`, `Deserialize`, `FromStr`, `Display`, `Debug`, `PartialEq`, `Eq`, `Hash`, `Clone`, conversions to/from `String`/`CowStr`/`SmolStr`, `AsRef<str>`, `Deref<Target=str>`
131131+- Implementation notes: Prefer `#[repr(transparent)]` where possible; use `SmolStr` for short strings, `CowStr` for longer; implement or derive `IntoStatic` for owned conversion
132132+- When constructing from a static string, use `new_static()` to avoid unnecessary allocations
133133+134134+## Lifetimes and Zero-Copy Deserialization
135135+136136+All API types support borrowed deserialization via explicit lifetimes:
137137+- Request/output types: parameterised by `'de` lifetime (e.g., `GetAuthorFeed<'de>`, `GetAuthorFeedOutput<'de>`)
138138+- Fields use `#[serde(borrow)]` where possible (strings, nested objects with lifetimes)
139139+- `CowStr<'a>` enables efficient borrowing from input buffers or owning small strings inline (via `SmolStr`)
140140+- All types implement `IntoStatic` trait to convert borrowed data to owned (`'static`) variants
141141+- Code generator automatically propagates lifetimes through nested structures
142142+143143+Response lifetime handling:
144144+- `Response::parse()` borrows from the response buffer for zero-copy parsing
145145+- `Response::into_output()` converts to owned data using `IntoStatic`
146146+- `Response::transmute()`: reinterpret response as different type (used for typed collection responses)
147147+- Both methods provide typed error handling
148148+149149+## API Coverage (jacquard-api)
150150+151151+**NOTE: jacquard does modules a bit differently in API codegen**
152152+- Specifially, it puts '*.defs' codegen output into the corresponding module file (mod_name.rs in parent directory, NOT mod.rs in module directory)
153153+- It also combines the top-level tld and domain ('com.atproto' -> `com_atproto`, etc.)
154154+155155+## Value Types (jacquard-common)
156156+157157+For working with loosely-typed atproto data:
158158+- `Data<'a>`: Validated, typed representation of atproto values
159159+- `RawData<'a>`: Unvalidated raw values from deserialization
160160+- `from_data`, `from_raw_data`, `to_data`, `to_raw_data`: Convert between typed and untyped
161161+- Useful for second-stage deserialization of `type "unknown"` fields (e.g., `PostView.record`)
162162+163163+Collection types:
164164+- `Collection` trait: Marker trait for record types with `NSID` constant and `Record` associated type
165165+- `RecordError<'a>`: Generic error type for record retrieval operations (RecordNotFound, Unknown)
166166+167167+## Lifetime Design Pattern
168168+169169+Jacquard uses a specific pattern to enable zero-copy deserialization while avoiding HRTB issues and async lifetime problems:
170170+171171+**GATs on associated types** instead of trait-level lifetimes:
172172+```rust
173173+trait XrpcResp {
174174+ type Output<'de>: Deserialize<'de> + IntoStatic; // GAT, not trait-level lifetime
175175+}
176176+```
177177+178178+**Method-level generic lifetimes** for trait methods that need them:
179179+```rust
180180+fn extract_vec<'s>(output: Self::Output<'s>) -> Vec<Item>
181181+```
182182+183183+**Response wrapper owns buffer** to solve async lifetime issues:
184184+```rust
185185+async fn get_record<R>(&self, rkey: K) -> Result<Response<R>>
186186+// Caller chooses: response.parse() (borrow) or response.into_output() (owned)
187187+```
188188+189189+This pattern avoids `for<'any> Trait<'any>` bounds (which force `DeserializeOwned` semantics) while giving callers control over borrowing vs owning. See `jacquard-common` crate docs for detailed explanation.
190190+191191+## WASM Compatibility
192192+193193+Core crates (`jacquard-common`, `jacquard-api`, `jacquard-identity`, `jacquard-oauth`) support `wasm32-unknown-unknown` target compilation.
194194+195195+Implementation approach:
196196+- **`trait-variant`**: Traits use `#[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))]` to conditionally exclude `Send` bounds on WASM
197197+- **Trait methods with `Self: Sync` bounds**: Duplicated as platform-specific versions (`#[cfg(not(target_arch = "wasm32"))]` vs `#[cfg(target_arch = "wasm32")]`)
198198+- **Helper functions**: Extracted to free functions with platform-specific versions to avoid code duplication
199199+- **Feature gating**: Platform-specific features (e.g., DNS resolution, tokio runtime detection) properly gated behind `cfg` attributes
200200+201201+Test WASM compilation:
202202+```bash
203203+just check-wasm
204204+# or: cargo build --target wasm32-unknown-unknown -p jacquard-common --no-default-features
205205+```
206206+207207+## Client Architecture
208208+209209+### XRPC Request/Response Layer
210210+211211+Core traits:
212212+- `XrpcRequest`: Defines NSID, method (Query/Procedure), and associated Response type
213213+ - `encode_body()` for request serialization (default: JSON; override for CBOR/multipart)
214214+ - `decode_body(&'de [u8])` for request deserialization (server-side)
215215+- `XrpcResp`: Response marker trait with NSID, encoding, Output/Err types
216216+- `XrpcEndpoint`: Server-side trait with PATH, METHOD, and associated Request/Response types
217217+- `XrpcClient`: Stateful trait with `base_uri()`, `opts()`, and `send()` method
218218+ - **This should be your primary interface point with the crate, along with the Agent___ traits**
219219+- `XrpcExt`: Extension trait providing stateless `.xrpc(base)` builder on any `HttpClient`
220220+221221+### Session Management
222222+223223+`Agent<A: AgentSession>` wrapper supports:
224224+- `CredentialSession<S, T>`: App-password (Bearer) authentication with auto-refresh
225225+ - Uses `SessionStore` trait implementers for token persistence (`MemorySessionStore`, `FileAuthStore`)
226226+- `OAuthSession<T, S>`: DPoP-bound OAuth with nonce handling
227227+ - Uses `ClientAuthStore` trait implementers for state/token persistence
228228+229229+Session traits:
230230+- `AgentSession`: common interface for both session types
231231+- `AgentKind`: enum distinguishing AppPassword vs OAuth
232232+- Both sessions implement `HttpClient` and `XrpcClient` for uniform API
233233+- `AgentSessionExt` extension trait includes several helpful methods for atproto record operations.
234234+ - **This trait is implemented automatically for anything that implements both `AgentSession` and `IdentityResolver`**
235235+236236+237237+## Identity Resolution
238238+239239+`JacquardResolver` (default) and custom resolvers implement `IdentityResolver` + `OAuthResolver`:
240240+- Handle → DID: DNS TXT (feature `dns`, or via Cloudflare DoH), HTTPS well-known, PDS XRPC, public fallbacks
241241+- DID → Doc: did:web well-known, PLC directory, PDS XRPC
242242+- OAuth metadata: `.well-known/oauth-protected-resource` and `.well-known/oauth-authorization-server`
243243+- Resolvers use stateless XRPC calls (no auth required for public resolution endpoints)
244244+245245+## Streaming Support
246246+247247+### HTTP Streaming
248248+249249+Feature: `streaming`
250250+251251+Core types in `jacquard-common`:
252252+- `ByteStream` / `ByteSink`: Platform-agnostic stream wrappers (uses n0-future)
253253+- `StreamError`: Concrete error type with Kind enum (Transport, Closed, Protocol)
254254+- `HttpClientExt`: Trait extension for streaming methods
255255+- `StreamingResponse`: XRPC streaming response wrapper
256256+257257+### WebSocket Support
258258+259259+Feature: `websocket` (requires `streaming`)
260260+- `WebSocketClient` trait (independent from `HttpClient`)
261261+- `WebSocketConnection` with tx/rx `ByteSink`/`ByteStream`
262262+- tokio-tungstenite-wasm used to abstract across native + wasm
263263+264264+**Known gaps:**
265265+- Service auth replay protection (jti tracking)
266266+- Video upload helpers (upload + job polling)
267267+- Additional session storage backends (SQLite, etc.)
268268+- PLC operations
269269+- OAuth extractor for Axum
···3232/// Error categories for client operations
3333#[derive(Debug, thiserror::Error)]
3434#[cfg_attr(feature = "std", derive(Diagnostic))]
3535+#[non_exhaustive]
3536pub enum ClientErrorKind {
3637 /// HTTP transport error (connection, timeout, etc.)
3738 #[error("transport error")]
···166167 self
167168 }
168169170170+ /// Append additional context to existing context string.
171171+ ///
172172+ /// If context already exists, appends with ": " separator.
173173+ /// If no context exists, sets it directly.
174174+ pub fn append_context(mut self, additional: impl AsRef<str>) -> Self {
175175+ self.context = Some(match self.context.take() {
176176+ Some(existing) => smol_str::format_smolstr!("{}: {}", existing, additional.as_ref()),
177177+ None => additional.as_ref().into(),
178178+ });
179179+ self
180180+ }
181181+182182+ /// Add NSID context for XRPC operations.
183183+ ///
184184+ /// Appends the NSID in brackets to existing context, e.g. `"network timeout: [com.atproto.repo.getRecord]"`.
185185+ pub fn for_nsid(self, nsid: &str) -> Self {
186186+ self.append_context(smol_str::format_smolstr!("[{}]", nsid))
187187+ }
188188+189189+ /// Add collection context for record operations.
190190+ ///
191191+ /// Use this when a record operation fails to indicate the target collection.
192192+ pub fn for_collection(self, operation: &str, collection_nsid: &str) -> Self {
193193+ self.append_context(smol_str::format_smolstr!("{} [{}]", operation, collection_nsid))
194194+ }
195195+169196 // Constructors for each kind
170197171198 /// Create a transport error
···223250/// Can be converted to string for serialization while maintaining the full error context.
224251#[derive(Debug, thiserror::Error)]
225252#[cfg_attr(feature = "std", derive(Diagnostic))]
253253+#[non_exhaustive]
226254pub enum DecodeError {
227255 /// JSON deserialization failed
228256 #[error("Failed to deserialize JSON: {0}")]
···302330/// Authentication and authorization errors
303331#[derive(Debug, thiserror::Error)]
304332#[cfg_attr(feature = "std", derive(Diagnostic))]
333333+#[non_exhaustive]
305334pub enum AuthError {
306335 /// Access token has expired (use refresh token to get a new one)
307336 #[error("Access token expired")]
···319348 #[error("No authentication provided, but endpoint requires auth")]
320349 NotAuthenticated,
321350351351+ /// DPoP proof construction failed (key or signing issue)
352352+ #[error("DPoP proof construction failed")]
353353+ DpopProofFailed,
354354+355355+ /// DPoP nonce retry failed (server rejected proof even after nonce update)
356356+ #[error("DPoP nonce negotiation failed")]
357357+ DpopNonceFailed,
358358+322359 /// Other authentication error
323360 #[error("Authentication error: {0:?}")]
324361 Other(http::HeaderValue),
···333370 AuthError::InvalidToken => AuthError::InvalidToken,
334371 AuthError::RefreshFailed => AuthError::RefreshFailed,
335372 AuthError::NotAuthenticated => AuthError::NotAuthenticated,
373373+ AuthError::DpopProofFailed => AuthError::DpopProofFailed,
374374+ AuthError::DpopNonceFailed => AuthError::DpopNonceFailed,
336375 AuthError::Other(header) => AuthError::Other(header),
337376 }
338377 }
+1
crates/jacquard-common/src/service_auth.rs
···39394040/// Errors that can occur during JWT parsing and verification.
4141#[derive(Debug, Error, miette::Diagnostic)]
4242+#[non_exhaustive]
4243pub enum ServiceAuthError {
4344 /// JWT format is invalid (not three base64-encoded parts separated by dots)
4445 #[error("malformed JWT: {0}")]
+34-6
crates/jacquard-common/src/session.rs
···2828/// Errors emitted by session stores.
2929#[derive(Debug, thiserror::Error)]
3030#[cfg_attr(feature = "std", derive(Diagnostic))]
3131+#[non_exhaustive]
3132pub enum SessionStoreError {
3233 /// Filesystem or I/O error
3334 #[cfg(feature = "std")]
···108109#[cfg(feature = "std")]
109110impl FileTokenStore {
110111 /// Create a new file token store at the given path.
111111- pub fn new(path: impl AsRef<Path>) -> Self {
112112- std::fs::create_dir_all(path.as_ref().parent().unwrap()).unwrap();
113113- if !path.as_ref().exists() {
114114- std::fs::write(path.as_ref(), b"{}").unwrap();
112112+ ///
113113+ /// Creates parent directories and initializes an empty JSON object if the file doesn't exist.
114114+ ///
115115+ /// # Errors
116116+ ///
117117+ /// Returns an error if:
118118+ /// - Parent directories cannot be created
119119+ /// - The file cannot be written
120120+ pub fn try_new(path: impl AsRef<Path>) -> Result<Self, SessionStoreError> {
121121+ let path = path.as_ref();
122122+123123+ // Create parent directories if they exist and don't already exist
124124+ if let Some(parent) = path.parent() {
125125+ if !parent.as_os_str().is_empty() && !parent.exists() {
126126+ std::fs::create_dir_all(parent)?;
127127+ }
115128 }
116129117117- Self {
118118- path: path.as_ref().to_path_buf(),
130130+ // Initialize empty JSON object if file doesn't exist
131131+ if !path.exists() {
132132+ std::fs::write(path, b"{}")?;
119133 }
134134+135135+ Ok(Self {
136136+ path: path.to_path_buf(),
137137+ })
138138+ }
139139+140140+ /// Create a new file token store at the given path.
141141+ ///
142142+ /// # Panics
143143+ ///
144144+ /// Panics if parent directories cannot be created or the file cannot be written.
145145+ /// Prefer [`try_new`](Self::try_new) for fallible construction.
146146+ pub fn new(path: impl AsRef<Path>) -> Self {
147147+ Self::try_new(path).expect("failed to initialize FileTokenStore")
120148 }
121149}
122150
···43434444/// Errors that can occur when working with CIDs
4545#[derive(Debug, thiserror::Error, miette::Diagnostic)]
4646+#[non_exhaustive]
4647pub enum Error {
4748 /// Invalid IPLD CID structure
4849 #[error("Invalid IPLD CID {:?}", 0)]
+1
crates/jacquard-common/src/types/collection.rs
···6565 Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error, miette::Diagnostic,
6666)]
6767#[serde(tag = "error", content = "message")]
6868+#[non_exhaustive]
6869pub enum RecordError<'a> {
6970 /// The requested record was not found
7071 #[error("RecordNotFound")]
+1
crates/jacquard-common/src/types/crypto.rs
···64646565/// Errors from decoding or converting Multikey values
6666#[derive(Debug, Clone, thiserror::Error, miette::Diagnostic, PartialEq, Eq)]
6767+#[non_exhaustive]
6768pub enum CryptoError {
6869 #[error("failed to decode multibase")]
6970 /// Multibase decode errror
+14-9
crates/jacquard-common/src/types/uri.rs
···33333434/// Errors that can occur when parsing URIs
3535#[derive(Debug, thiserror::Error, miette::Diagnostic)]
3636+#[non_exhaustive]
3637pub enum UriParseError {
3738 /// AT Protocol string parsing error
3839 #[error("Invalid atproto string: {0}")]
···5758 } else if uri.starts_with("wss://") {
5859 Ok(Uri::Https(Url::parse(uri)?))
5960 } else if uri.starts_with("ipld://") {
6060- Ok(Uri::Cid(
6161- Cid::from_str(uri.strip_prefix("ipld://").unwrap_or(uri.as_ref())).unwrap(),
6262- ))
6161+ match Cid::from_str(&uri[7..]) {
6262+ Ok(cid) => Ok(Uri::Cid(cid)),
6363+ Err(_) => Ok(Uri::Any(CowStr::Borrowed(uri))),
6464+ }
6365 } else {
6466 Ok(Uri::Any(CowStr::Borrowed(uri)))
6567 }
···7779 } else if uri.starts_with("wss://") {
7880 Ok(Uri::Https(Url::parse(uri)?))
7981 } else if uri.starts_with("ipld://") {
8080- Ok(Uri::Cid(
8181- Cid::from_str(uri.strip_prefix("ipld://").unwrap_or(uri.as_ref())).unwrap(),
8282- ))
8282+ match Cid::from_str(&uri[7..]) {
8383+ Ok(cid) => Ok(Uri::Cid(cid)),
8484+ Err(_) => Ok(Uri::Any(CowStr::Owned(uri.to_smolstr()))),
8585+ }
8386 } else {
8487 Ok(Uri::Any(CowStr::Owned(uri.to_smolstr())))
8588 }
···9699 } else if uri.starts_with("wss://") {
97100 Ok(Uri::Https(Url::parse(uri.as_ref())?))
98101 } else if uri.starts_with("ipld://") {
9999- Ok(Uri::Cid(
100100- Cid::from_str(uri.strip_prefix("ipld://").unwrap_or(uri.as_str())).unwrap(),
101101- ))
102102+ match Cid::from_str(&uri.as_str()[7..]) {
103103+ Ok(cid) => Ok(Uri::Cid(cid)),
104104+ Err(_) => Ok(Uri::Any(uri)),
105105+ }
102106 } else {
103107 Ok(Uri::Any(uri))
104108 }
···220224}
221225222226#[derive(Debug, Clone, PartialEq, thiserror::Error, miette::Diagnostic)]
227227+#[non_exhaustive]
223228/// Errors that can occur when parsing or validating collection type-annotated URIs
224229pub enum UriError {
225230 /// Given at-uri didn't have the matching collection for the record
+1
crates/jacquard-common/src/types/value.rs
···54545555/// Errors that can occur when working with AT Protocol data
5656#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)]
5757+#[non_exhaustive]
5758pub enum AtDataError {
5859 /// Floating point numbers are not allowed in AT Protocol
5960 #[error("floating point numbers not allowed in AT protocol data")]
···5566/// Errors that can occur during lexicon code generation
77#[derive(Debug, Error, Diagnostic)]
88+#[non_exhaustive]
89pub enum CodegenError {
910 /// IO error when reading lexicon files
1011 #[error("IO error: {0}")]
···2930 span: Option<SourceSpan>,
3031 },
31323232- /// Reference to non-existent lexicon or def
3333- #[error("Reference to unknown type: {ref_string}")]
3434- #[diagnostic(
3535- code(lexicon::unknown_ref),
3636- help("Add the referenced lexicon to your corpus or use Data<'a> as a fallback type")
3737- )]
3838- UnknownRef {
3939- /// The ref string that couldn't be resolved
4040- ref_string: String,
4141- /// NSID of lexicon containing the ref
4242- lexicon_nsid: String,
4343- /// Def name containing the ref
4444- def_name: String,
4545- /// Field path containing the ref
4646- field_path: String,
4747- },
4848-4949- /// Circular reference detected in type definitions
5050- #[error("Circular reference detected")]
5151- #[diagnostic(
5252- code(lexicon::circular_ref),
5353- help("The code generator uses Box<T> for union variants to handle circular references")
5454- )]
5555- CircularRef {
5656- /// The ref string that forms a cycle
5757- ref_string: String,
5858- /// The cycle path
5959- cycle: Vec<String>,
6060- },
6161-6262- /// Invalid lexicon structure
6363- #[error("Invalid lexicon: {message}")]
6464- #[diagnostic(code(lexicon::invalid))]
6565- InvalidLexicon {
6666- message: String,
6767- /// NSID of the invalid lexicon
6868- lexicon_nsid: String,
6969- },
7070-7171- /// Unsupported lexicon feature
7272- #[error("Unsupported feature: {feature}")]
7373- #[diagnostic(
7474- code(lexicon::unsupported),
7575- help("This lexicon feature is not yet supported by the code generator")
7676- )]
7777- Unsupported {
7878- /// Description of the unsupported feature
7979- #[allow(unused)]
8080- feature: String,
8181- /// NSID of lexicon containing the feature
8282- #[allow(unused)]
8383- lexicon_nsid: String,
8484- /// Optional suggestion for workaround
8585- #[allow(unused)]
8686- suggestion: Option<String>,
8787- },
8888-8933 /// Name collision
9034 #[error("Name collision: {name}")]
9135 #[diagnostic(
···11963 tokens: String,
12064 },
12165122122- /// Failed to parse module path string
123123- #[error("Failed to parse module path: {path_str}")]
6666+ /// Unsupported lexicon feature
6767+ #[error("Unsupported: {message}")]
6868+ #[diagnostic(
6969+ code(lexicon::unsupported),
7070+ help("This lexicon feature is not yet supported by code generation")
7171+ )]
7272+ Unsupported {
7373+ /// Description of the unsupported feature
7474+ message: String,
7575+ /// NSID of the lexicon containing the unsupported feature
7676+ nsid: Option<String>,
7777+ /// Definition name if applicable
7878+ def_name: Option<String>,
7979+ },
8080+8181+ /// Failed to parse generated path string
8282+ #[error("Failed to parse path '{path_str}'")]
12483 #[diagnostic(code(lexicon::path_parse_error))]
12584 PathParseError {
12685 path_str: String,
···163122 }
164123 }
165124166166- /// Create an unknown ref error
167167- pub fn unknown_ref(
168168- ref_string: impl Into<String>,
169169- lexicon_nsid: impl Into<String>,
170170- def_name: impl Into<String>,
171171- field_path: impl Into<String>,
172172- ) -> Self {
173173- Self::UnknownRef {
174174- ref_string: ref_string.into(),
175175- lexicon_nsid: lexicon_nsid.into(),
176176- def_name: def_name.into(),
177177- field_path: field_path.into(),
178178- }
179179- }
180180-181181- /// Create an invalid lexicon error
182182- pub fn invalid_lexicon(message: impl Into<String>, lexicon_nsid: impl Into<String>) -> Self {
183183- Self::InvalidLexicon {
184184- message: message.into(),
185185- lexicon_nsid: lexicon_nsid.into(),
186186- }
187187- }
188188-189125 /// Create an unsupported feature error
190126 pub fn unsupported(
191191- feature: impl Into<String>,
192192- lexicon_nsid: impl Into<String>,
193193- suggestion: Option<impl Into<String>>,
127127+ message: impl Into<String>,
128128+ nsid: impl Into<String>,
129129+ def_name: Option<impl Into<String>>,
194130 ) -> Self {
195131 Self::Unsupported {
196196- feature: feature.into(),
197197- lexicon_nsid: lexicon_nsid.into(),
198198- suggestion: suggestion.map(|s| s.into()),
132132+ message: message.into(),
133133+ nsid: Some(nsid.into()),
134134+ def_name: def_name.map(|s| s.into()),
199135 }
200136 }
201137}
+4
crates/jacquard-lexicon/src/validation.rs
···113113///
114114/// These errors indicate that the data structure doesn't match the schema's type expectations.
115115#[derive(Debug, Clone, thiserror::Error, miette::Diagnostic)]
116116+#[non_exhaustive]
116117pub enum StructuralError {
117118 #[error("Type mismatch at {path}: expected {expected}, got {actual}")]
118119 TypeMismatch {
···159160/// These errors indicate that the data violates lexicon constraints like max_length,
160161/// max_graphemes, ranges, etc. The structure is correct but values are out of bounds.
161162#[derive(Debug, Clone, thiserror::Error, miette::Diagnostic)]
163163+#[non_exhaustive]
162164pub enum ConstraintError {
163165 #[error("{path} exceeds max length: {actual} > {max}")]
164166 MaxLength {
···205207206208/// Unified validation error type
207209#[derive(Debug, Clone, thiserror::Error)]
210210+#[non_exhaustive]
208211pub enum ValidationError {
209212 #[error(transparent)]
210213 Structural(#[from] StructuralError),
···239242240243/// Errors that can occur when computing CIDs
241244#[derive(Debug, thiserror::Error)]
245245+#[non_exhaustive]
242246pub enum CidComputationError {
243247 #[error("Failed to serialize data to DAG-CBOR: {0}")]
244248 DagCborEncode(#[from] serde_ipld_dagcbor::EncodeError<std::collections::TryReserveError>),
···99use thiserror::Error;
10101111#[derive(Error, Debug)]
1212+#[non_exhaustive]
1213pub enum Error {
1314 #[error("duplicate kid: {0}")]
1415 DuplicateKid(String),
1516 #[error("keys must not be empty")]
1617 EmptyKeys,
1717- #[error("key must have a `kid`")]
1818- EmptyKid,
1818+ #[error("key at index {0} must have a `kid`")]
1919+ EmptyKid(usize),
1920 #[error("no signing key found for algorithms: {0:?}")]
2021 NotFound(Vec<CowStr<'static>>),
2122 #[error("key for signing must be a secret key")]
···103104 }
104105 let mut v = Vec::with_capacity(keys.len());
105106 let mut hs = HashSet::with_capacity(keys.len());
106106- for key in keys {
107107+ for (i, key) in keys.into_iter().enumerate() {
107108 if let Some(kid) = key.prm.kid.clone() {
108109 if hs.contains(&kid) {
109110 return Err(Error::DuplicateKid(kid));
···119120 }
120121 v.push(key);
121122 } else {
122122- return Err(Error::EmptyKid);
123123+ return Err(Error::EmptyKid(i));
123124 }
124125 }
125126 Ok(Self(v))
···699699700700/// Errors that can occur during richtext building
701701#[derive(Debug, thiserror::Error, miette::Diagnostic)]
702702+#[non_exhaustive]
702703pub enum RichTextError {
703704 /// Handle found that needs resolution but no resolver provided
704705 #[error("Handle '{0}' requires resolution - use build_async() with an IdentityResolver")]