···11-# CLAUDE.md
22-33-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
44-55-## Project Overview
66-77-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. Key design goals:
88-99-- Validated AT Protocol types including typed at:// URIs
1010-- Custom lexicon extension support
1111-- Lexicon `Value` type for working with unknown atproto data (dag-cbor or json)
1212-- Using as much or as little of the crates as needed
1313-1414-## Workspace Structure
1515-1616-This is a Cargo workspace with several crates:
1717-1818-- **jacquard**: Main library crate with XRPC client and public API surface (re-exports jacquard-api and jacquard-common)
1919-- **jacquard-common**: Core AT Protocol types (DIDs, handles, at-URIs, NSIDs, TIDs, CIDs, etc.) and the `CowStr` type for efficient string handling
2020-- **jacquard-lexicon**: Lexicon parsing and Rust code generation from lexicon schemas
2121-- **jacquard-api**: Generated API bindings from lexicon schemas (implementation detail, not directly used by consumers)
2222-- **jacquard-derive**: Attribute macros (`#[lexicon]`, `#[open_union]`) for lexicon structures
2323-2424-## Development Commands
2525-2626-### Using Nix (preferred)
2727-```bash
2828-# Enter dev shell
2929-nix develop
3030-3131-# Build
3232-nix build
3333-3434-# Run
3535-nix develop -c cargo run
3636-```
3737-3838-### Using Cargo/Just
3939-```bash
4040-# Build
4141-cargo build
4242-4343-# Run tests
4444-cargo test
4545-4646-# Run specific test
4747-cargo test <test_name>
4848-4949-# Run specific package tests
5050-cargo test -p <package_name>
5151-5252-# Run
5353-cargo run
5454-5555-# Auto-recompile and run
5656-just watch [ARGS]
5757-5858-# Format and lint all
5959-just pre-commit-all
6060-6161-# Generate API bindings from lexicon schemas
6262-cargo run -p jacquard-lexicon --bin jacquard-codegen -- -i <input_dir> -o <output_dir> [-r <root_module>]
6363-# Example:
6464-cargo run -p jacquard-lexicon --bin jacquard-codegen -- -i crates/jacquard-lexicon/tests/fixtures/lexicons/atproto/lexicons -o crates/jacquard-api/src -r crate
6565-```
6666-6767-## String Type Pattern
6868-6969-The codebase uses a consistent pattern for validated string types. Each type should have:
7070-7171-### Constructors
7272-- `new()`: Construct from a string slice with appropriate lifetime (borrows)
7373-- `new_owned()`: Construct from `impl AsRef<str>`, taking ownership
7474-- `new_static()`: Construct from `&'static str` using `SmolStr`/`CowStr`'s static constructor (no allocation)
7575-- `raw()`: Same as `new()` but panics instead of returning `Result`
7676-- `unchecked()`: Same as `new()` but doesn't validate (marked `unsafe`)
7777-- `as_str()`: Return string slice
7878-7979-### Traits
8080-All string types should implement:
8181-- `Serialize` + `Deserialize` (custom impl for latter, sometimes for former)
8282-- `FromStr`, `Display`
8383-- `Debug`, `PartialEq`, `Eq`, `Hash`, `Clone`
8484-- `From<T> for String`, `CowStr`, `SmolStr`
8585-- `From<String>`, `From<CowStr>`, `From<SmolStr>`, or `TryFrom` if likely to fail
8686-- `AsRef<str>`
8787-- `Deref` with `Target = str` (usually)
8888-8989-### Implementation Details
9090-- Use `#[repr(transparent)]` when possible (exception: at-uri type and components)
9191-- Use `SmolStr` directly as inner type if most instances will be under 24 bytes
9292-- Use `CowStr` for longer strings to allow borrowing from input
9393-- Implement `IntoStatic` trait to take ownership of string types
9494-9595-## Code Style
9696-9797-- Avoid comments for self-documenting code
9898-- Comments should not detail fixes when refactoring
9999-- Professional writing within source code and comments only
100100-- Prioritize long-term maintainability over implementation speed
101101-102102-## Testing
103103-104104-- Write test cases for all critical code
105105-- Tests can be run per-package or workspace-wide
106106-- Use `cargo test <name>` to run specific tests
107107-- Current test coverage: 89 tests in jacquard-common
108108-109109-## Lexicon Code Generation
110110-111111-The `jacquard-codegen` binary generates Rust types from AT Protocol Lexicon schemas:
112112-113113-- Generates structs with `#[lexicon]` attribute for forward compatibility (captures unknown fields in `extra_data`)
114114-- Generates enums with `#[open_union]` attribute for handling unknown variants (unless marked `closed` in lexicon)
115115-- Resolves local refs (e.g., `#image` becomes `Image<'a>`)
116116-- Extracts doc comments from lexicon `description` fields
117117-- Adds header comments with `@generated` marker and lexicon NSID
118118-- Handles XRPC queries, procedures, subscriptions, and errors
119119-- Generates proper module tree with Rust 2018 style
120120-- **XrpcRequest trait**: Implemented directly on params/input structs (not marker types), with GATs for Output<'de> and Err<'de>
121121-- **IntoStatic trait**: All generated types implement `IntoStatic` to convert borrowed types to owned ('static) variants
122122-- **Collection trait**: Implemented on record types directly, with const NSID
123123-124124-## Current State & Next Steps
125125-126126-### Completed
127127-- ✅ Comprehensive validation tests for all core string types (handle, DID, NSID, TID, record key, AT-URI, datetime, language, identifier)
128128-- ✅ Validated implementations against AT Protocol specs and TypeScript reference implementation
129129-- ✅ String type interface standardization (Language now has `new_static()`, Datetime has full conversion traits)
130130-- ✅ Data serialization: Full serialize/deserialize for `Data<'_>`, `Array`, `Object` with format-specific handling (JSON vs CBOR)
131131-- ✅ CidLink wrapper type with automatic `{"$link": "cid"}` serialization in JSON
132132-- ✅ Integration test with real Bluesky thread data validates round-trip correctness
133133-- ✅ Lexicon code generation with forward compatibility and proper lifetime handling
134134-- ✅ IntoStatic implementations for all generated types (structs, enums, unions)
135135-- ✅ XrpcRequest trait with GATs, implemented on params/input types directly
136136-- ✅ HttpClient and XrpcClient traits with generic send_xrpc implementation
137137-- ✅ Response wrapper with parse() (borrowed) and into_output() (owned) methods
138138-- ✅ Structured error types (ClientError, TransportError, EncodeError, DecodeError, HttpError, AuthError)
139139-140140-### Next Steps
141141-1. **Concrete HttpClient Implementation**: Implement HttpClient for reqwest::Client and potentially other HTTP clients
142142-2. **Error Handling Improvements**: Add XRPC error parsing, better HTTP status code handling, structured error responses
143143-3. **Authentication**: Session management, token refresh, DPoP support
144144-4. **Body Encoding**: Support for non-JSON encodings (CBOR, multipart, etc.) in procedures
145145-5. **Lexicon Resolution**: Fetch lexicons from web sources (atproto authorities, git repositories) and parse into corpus
146146-6. **Custom Lexicon Support**: Allow users to plug in their own generated lexicons alongside jacquard-api types in the client/server layer
147147-7. **Public API**: Design the main API surface in `jacquard` that re-exports and wraps generated types
148148-8. **DID Document Support**: Parsing, validation, and resolution of DID documents
149149-9. **OAuth Implementation**: OAuth flow support for authentication
150150-10. **Examples & Documentation**: Create examples and improve documentation
151151-11. **Testing**: Comprehensive tests for generated code and round-trip serialization
-120
regen.rs
···11-use jacquard_lexicon::codegen::CodeGenerator;
22-use jacquard_lexicon::corpus::LexiconCorpus;
33-use prettyplease;
44-use std::collections::BTreeMap;
55-use std::fs;
66-use std::path::Path;
77-88-fn main() -> Result<(), Box<dyn std::error::Error>> {
99- let lexicons_path = "lexicons/atproto";
1010- let output_path = "crates/jacquard-api/src";
1111- let root_module = "crate";
1212-1313- println!("Loading lexicons from {}...", lexicons_path);
1414- let corpus = LexiconCorpus::load_from_dir(lexicons_path)?;
1515- println!("Loaded {} lexicons", corpus.len());
1616-1717- println!("Generating code...");
1818- let generator = CodeGenerator::new(&corpus, root_module);
1919-2020- // Group by module
2121- let mut modules: BTreeMap<String, Vec<(String, String)>> = BTreeMap::new();
2222-2323- for (nsid, doc) in corpus.iter() {
2424- let nsid_str = nsid.as_str();
2525-2626- // Get module path: app.bsky.feed.post -> app_bsky/feed
2727- let parts: Vec<&str> = nsid_str.split('.').collect();
2828- let module_path = if parts.len() >= 3 {
2929- let first_two = format!("{}_{}", parts[0], parts[1]);
3030- if parts.len() > 3 {
3131- let middle: Vec<&str> = parts[2..parts.len() - 1].iter().copied().collect();
3232- format!("{}/{}", first_two, middle.join("/"))
3333- } else {
3434- first_two
3535- }
3636- } else {
3737- parts.join("_")
3838- };
3939-4040- let file_name = parts.last().unwrap().to_string();
4141-4242- for (def_name, def) in &doc.defs {
4343- match generator.generate_def(nsid_str, def_name, def) {
4444- Ok(tokens) => {
4545- let code = prettyplease::unparse(&syn::parse_file(&tokens.to_string())?);
4646- modules
4747- .entry(format!("{}/{}.rs", module_path, file_name))
4848- .or_default()
4949- .push((def_name.to_string(), code));
5050- }
5151- Err(e) => {
5252- eprintln!("Error generating {}.{}: {:?}", nsid_str, def_name, e);
5353- }
5454- }
5555- }
5656- }
5757-5858- // Write files
5959- for (file_path, defs) in modules {
6060- let full_path = Path::new(output_path).join(&file_path);
6161-6262- // Create parent directory
6363- if let Some(parent) = full_path.parent() {
6464- fs::create_dir_all(parent)?;
6565- }
6666-6767- let content = defs.iter().map(|(_, code)| code.as_str()).collect::<Vec<_>>().join("\n");
6868- fs::write(&full_path, content)?;
6969- println!("Wrote {}", file_path);
7070- }
7171-7272- // Generate mod.rs files
7373- println!("Generating mod.rs files...");
7474- generate_mod_files(Path::new(output_path))?;
7575-7676- println!("Done!");
7777- Ok(())
7878-}
7979-8080-fn generate_mod_files(root: &Path) -> Result<(), Box<dyn std::error::Error>> {
8181- // Find all directories
8282- for entry in fs::read_dir(root)? {
8383- let entry = entry?;
8484- let path = entry.path();
8585-8686- if path.is_dir() {
8787- let dir_name = path.file_name().unwrap().to_str().unwrap();
8888-8989- // Recursively generate for subdirectories
9090- generate_mod_files(&path)?;
9191-9292- // Generate mod.rs for this directory
9393- let mut mods = Vec::new();
9494- for sub_entry in fs::read_dir(&path)? {
9595- let sub_entry = sub_entry?;
9696- let sub_path = sub_entry.path();
9797-9898- if sub_path.is_file() {
9999- if let Some(name) = sub_path.file_stem() {
100100- let name_str = name.to_str().unwrap();
101101- if name_str != "mod" {
102102- mods.push(format!("pub mod {};", name_str));
103103- }
104104- }
105105- } else if sub_path.is_dir() {
106106- if let Some(name) = sub_path.file_name() {
107107- mods.push(format!("pub mod {};", name.to_str().unwrap()));
108108- }
109109- }
110110- }
111111-112112- if !mods.is_empty() {
113113- let mod_content = mods.join("\n") + "\n";
114114- fs::write(path.join("mod.rs"), mod_content)?;
115115- }
116116- }
117117- }
118118-119119- Ok(())
120120-}