Compare changes

Choose any two refs to compare.

Changed files
+5 -292
.claude
crates
jacquard
src
jacquard-common
src
-13
.claude/settings.local.json
··· 1 - { 2 - "permissions": { 3 - "allow": [ 4 - "WebSearch", 5 - "WebFetch(domain:atproto.com)", 6 - "WebFetch(domain:github.com)", 7 - "WebFetch(domain:raw.githubusercontent.com)", 8 - "WebFetch(domain:docs.rs)" 9 - ], 10 - "deny": [], 11 - "ask": [] 12 - } 13 - }
-151
CLAUDE.md
··· 1 - # CLAUDE.md 2 - 3 - This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 - 5 - ## Project Overview 6 - 7 - 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: 8 - 9 - - Validated AT Protocol types including typed at:// URIs 10 - - Custom lexicon extension support 11 - - Lexicon `Value` type for working with unknown atproto data (dag-cbor or json) 12 - - Using as much or as little of the crates as needed 13 - 14 - ## Workspace Structure 15 - 16 - This is a Cargo workspace with several crates: 17 - 18 - - **jacquard**: Main library crate with XRPC client and public API surface (re-exports jacquard-api and jacquard-common) 19 - - **jacquard-common**: Core AT Protocol types (DIDs, handles, at-URIs, NSIDs, TIDs, CIDs, etc.) and the `CowStr` type for efficient string handling 20 - - **jacquard-lexicon**: Lexicon parsing and Rust code generation from lexicon schemas 21 - - **jacquard-api**: Generated API bindings from lexicon schemas (implementation detail, not directly used by consumers) 22 - - **jacquard-derive**: Attribute macros (`#[lexicon]`, `#[open_union]`) for lexicon structures 23 - 24 - ## Development Commands 25 - 26 - ### Using Nix (preferred) 27 - ```bash 28 - # Enter dev shell 29 - nix develop 30 - 31 - # Build 32 - nix build 33 - 34 - # Run 35 - nix develop -c cargo run 36 - ``` 37 - 38 - ### Using Cargo/Just 39 - ```bash 40 - # Build 41 - cargo build 42 - 43 - # Run tests 44 - cargo test 45 - 46 - # Run specific test 47 - cargo test <test_name> 48 - 49 - # Run specific package tests 50 - cargo test -p <package_name> 51 - 52 - # Run 53 - cargo run 54 - 55 - # Auto-recompile and run 56 - just watch [ARGS] 57 - 58 - # Format and lint all 59 - just pre-commit-all 60 - 61 - # Generate API bindings from lexicon schemas 62 - cargo run -p jacquard-lexicon --bin jacquard-codegen -- -i <input_dir> -o <output_dir> [-r <root_module>] 63 - # Example: 64 - cargo run -p jacquard-lexicon --bin jacquard-codegen -- -i crates/jacquard-lexicon/tests/fixtures/lexicons/atproto/lexicons -o crates/jacquard-api/src -r crate 65 - ``` 66 - 67 - ## String Type Pattern 68 - 69 - The codebase uses a consistent pattern for validated string types. Each type should have: 70 - 71 - ### Constructors 72 - - `new()`: Construct from a string slice with appropriate lifetime (borrows) 73 - - `new_owned()`: Construct from `impl AsRef<str>`, taking ownership 74 - - `new_static()`: Construct from `&'static str` using `SmolStr`/`CowStr`'s static constructor (no allocation) 75 - - `raw()`: Same as `new()` but panics instead of returning `Result` 76 - - `unchecked()`: Same as `new()` but doesn't validate (marked `unsafe`) 77 - - `as_str()`: Return string slice 78 - 79 - ### Traits 80 - All string types should implement: 81 - - `Serialize` + `Deserialize` (custom impl for latter, sometimes for former) 82 - - `FromStr`, `Display` 83 - - `Debug`, `PartialEq`, `Eq`, `Hash`, `Clone` 84 - - `From<T> for String`, `CowStr`, `SmolStr` 85 - - `From<String>`, `From<CowStr>`, `From<SmolStr>`, or `TryFrom` if likely to fail 86 - - `AsRef<str>` 87 - - `Deref` with `Target = str` (usually) 88 - 89 - ### Implementation Details 90 - - Use `#[repr(transparent)]` when possible (exception: at-uri type and components) 91 - - Use `SmolStr` directly as inner type if most instances will be under 24 bytes 92 - - Use `CowStr` for longer strings to allow borrowing from input 93 - - Implement `IntoStatic` trait to take ownership of string types 94 - 95 - ## Code Style 96 - 97 - - Avoid comments for self-documenting code 98 - - Comments should not detail fixes when refactoring 99 - - Professional writing within source code and comments only 100 - - Prioritize long-term maintainability over implementation speed 101 - 102 - ## Testing 103 - 104 - - Write test cases for all critical code 105 - - Tests can be run per-package or workspace-wide 106 - - Use `cargo test <name>` to run specific tests 107 - - Current test coverage: 89 tests in jacquard-common 108 - 109 - ## Lexicon Code Generation 110 - 111 - The `jacquard-codegen` binary generates Rust types from AT Protocol Lexicon schemas: 112 - 113 - - Generates structs with `#[lexicon]` attribute for forward compatibility (captures unknown fields in `extra_data`) 114 - - Generates enums with `#[open_union]` attribute for handling unknown variants (unless marked `closed` in lexicon) 115 - - Resolves local refs (e.g., `#image` becomes `Image<'a>`) 116 - - Extracts doc comments from lexicon `description` fields 117 - - Adds header comments with `@generated` marker and lexicon NSID 118 - - Handles XRPC queries, procedures, subscriptions, and errors 119 - - Generates proper module tree with Rust 2018 style 120 - - **XrpcRequest trait**: Implemented directly on params/input structs (not marker types), with GATs for Output<'de> and Err<'de> 121 - - **IntoStatic trait**: All generated types implement `IntoStatic` to convert borrowed types to owned ('static) variants 122 - - **Collection trait**: Implemented on record types directly, with const NSID 123 - 124 - ## Current State & Next Steps 125 - 126 - ### Completed 127 - - โœ… Comprehensive validation tests for all core string types (handle, DID, NSID, TID, record key, AT-URI, datetime, language, identifier) 128 - - โœ… Validated implementations against AT Protocol specs and TypeScript reference implementation 129 - - โœ… String type interface standardization (Language now has `new_static()`, Datetime has full conversion traits) 130 - - โœ… Data serialization: Full serialize/deserialize for `Data<'_>`, `Array`, `Object` with format-specific handling (JSON vs CBOR) 131 - - โœ… CidLink wrapper type with automatic `{"$link": "cid"}` serialization in JSON 132 - - โœ… Integration test with real Bluesky thread data validates round-trip correctness 133 - - โœ… Lexicon code generation with forward compatibility and proper lifetime handling 134 - - โœ… IntoStatic implementations for all generated types (structs, enums, unions) 135 - - โœ… XrpcRequest trait with GATs, implemented on params/input types directly 136 - - โœ… HttpClient and XrpcClient traits with generic send_xrpc implementation 137 - - โœ… Response wrapper with parse() (borrowed) and into_output() (owned) methods 138 - - โœ… Structured error types (ClientError, TransportError, EncodeError, DecodeError, HttpError, AuthError) 139 - 140 - ### Next Steps 141 - 1. **Concrete HttpClient Implementation**: Implement HttpClient for reqwest::Client and potentially other HTTP clients 142 - 2. **Error Handling Improvements**: Add XRPC error parsing, better HTTP status code handling, structured error responses 143 - 3. **Authentication**: Session management, token refresh, DPoP support 144 - 4. **Body Encoding**: Support for non-JSON encodings (CBOR, multipart, etc.) in procedures 145 - 5. **Lexicon Resolution**: Fetch lexicons from web sources (atproto authorities, git repositories) and parse into corpus 146 - 6. **Custom Lexicon Support**: Allow users to plug in their own generated lexicons alongside jacquard-api types in the client/server layer 147 - 7. **Public API**: Design the main API surface in `jacquard` that re-exports and wraps generated types 148 - 8. **DID Document Support**: Parsing, validation, and resolution of DID documents 149 - 9. **OAuth Implementation**: OAuth flow support for authentication 150 - 10. **Examples & Documentation**: Create examples and improve documentation 151 - 11. **Testing**: Comprehensive tests for generated code and round-trip serialization
-1
crates/jacquard/src/lib.rs
··· 95 95 #[cfg(feature = "api")] 96 96 /// If enabled, re-export the generated api crate 97 97 pub use jacquard_api as api; 98 - /// Re-export common types 99 98 pub use jacquard_common::*; 100 99 101 100 #[cfg(feature = "derive")]
+5 -7
crates/jacquard-common/src/lib.rs
··· 1 1 //! Common types for the jacquard implementation of atproto 2 2 3 3 #![warn(missing_docs)] 4 + pub use cowstr::CowStr; 5 + pub use into_static::IntoStatic; 6 + pub use smol_str; 7 + pub use url; 4 8 5 9 /// A copy-on-write immutable string type that uses [`SmolStr`] for 6 10 /// the "owned" variant. 7 11 #[macro_use] 8 12 pub mod cowstr; 9 13 #[macro_use] 10 - /// trait for taking ownership of most borrowed types in jacquard. 14 + /// Trait for taking ownership of most borrowed types in jacquard. 11 15 pub mod into_static; 12 - /// Helper macros for common patterns 13 16 pub mod macros; 14 17 /// Baseline fundamental AT Protocol data types. 15 18 pub mod types; 16 - 17 - pub use cowstr::CowStr; 18 - pub use into_static::IntoStatic; 19 - pub use smol_str; 20 - pub use url;
-120
regen.rs
··· 1 - use jacquard_lexicon::codegen::CodeGenerator; 2 - use jacquard_lexicon::corpus::LexiconCorpus; 3 - use prettyplease; 4 - use std::collections::BTreeMap; 5 - use std::fs; 6 - use std::path::Path; 7 - 8 - fn main() -> Result<(), Box<dyn std::error::Error>> { 9 - let lexicons_path = "lexicons/atproto"; 10 - let output_path = "crates/jacquard-api/src"; 11 - let root_module = "crate"; 12 - 13 - println!("Loading lexicons from {}...", lexicons_path); 14 - let corpus = LexiconCorpus::load_from_dir(lexicons_path)?; 15 - println!("Loaded {} lexicons", corpus.len()); 16 - 17 - println!("Generating code..."); 18 - let generator = CodeGenerator::new(&corpus, root_module); 19 - 20 - // Group by module 21 - let mut modules: BTreeMap<String, Vec<(String, String)>> = BTreeMap::new(); 22 - 23 - for (nsid, doc) in corpus.iter() { 24 - let nsid_str = nsid.as_str(); 25 - 26 - // Get module path: app.bsky.feed.post -> app_bsky/feed 27 - let parts: Vec<&str> = nsid_str.split('.').collect(); 28 - let module_path = if parts.len() >= 3 { 29 - let first_two = format!("{}_{}", parts[0], parts[1]); 30 - if parts.len() > 3 { 31 - let middle: Vec<&str> = parts[2..parts.len() - 1].iter().copied().collect(); 32 - format!("{}/{}", first_two, middle.join("/")) 33 - } else { 34 - first_two 35 - } 36 - } else { 37 - parts.join("_") 38 - }; 39 - 40 - let file_name = parts.last().unwrap().to_string(); 41 - 42 - for (def_name, def) in &doc.defs { 43 - match generator.generate_def(nsid_str, def_name, def) { 44 - Ok(tokens) => { 45 - let code = prettyplease::unparse(&syn::parse_file(&tokens.to_string())?); 46 - modules 47 - .entry(format!("{}/{}.rs", module_path, file_name)) 48 - .or_default() 49 - .push((def_name.to_string(), code)); 50 - } 51 - Err(e) => { 52 - eprintln!("Error generating {}.{}: {:?}", nsid_str, def_name, e); 53 - } 54 - } 55 - } 56 - } 57 - 58 - // Write files 59 - for (file_path, defs) in modules { 60 - let full_path = Path::new(output_path).join(&file_path); 61 - 62 - // Create parent directory 63 - if let Some(parent) = full_path.parent() { 64 - fs::create_dir_all(parent)?; 65 - } 66 - 67 - let content = defs.iter().map(|(_, code)| code.as_str()).collect::<Vec<_>>().join("\n"); 68 - fs::write(&full_path, content)?; 69 - println!("Wrote {}", file_path); 70 - } 71 - 72 - // Generate mod.rs files 73 - println!("Generating mod.rs files..."); 74 - generate_mod_files(Path::new(output_path))?; 75 - 76 - println!("Done!"); 77 - Ok(()) 78 - } 79 - 80 - fn generate_mod_files(root: &Path) -> Result<(), Box<dyn std::error::Error>> { 81 - // Find all directories 82 - for entry in fs::read_dir(root)? { 83 - let entry = entry?; 84 - let path = entry.path(); 85 - 86 - if path.is_dir() { 87 - let dir_name = path.file_name().unwrap().to_str().unwrap(); 88 - 89 - // Recursively generate for subdirectories 90 - generate_mod_files(&path)?; 91 - 92 - // Generate mod.rs for this directory 93 - let mut mods = Vec::new(); 94 - for sub_entry in fs::read_dir(&path)? { 95 - let sub_entry = sub_entry?; 96 - let sub_path = sub_entry.path(); 97 - 98 - if sub_path.is_file() { 99 - if let Some(name) = sub_path.file_stem() { 100 - let name_str = name.to_str().unwrap(); 101 - if name_str != "mod" { 102 - mods.push(format!("pub mod {};", name_str)); 103 - } 104 - } 105 - } else if sub_path.is_dir() { 106 - if let Some(name) = sub_path.file_name() { 107 - mods.push(format!("pub mod {};", name.to_str().unwrap())); 108 - } 109 - } 110 - } 111 - 112 - if !mods.is_empty() { 113 - let mod_content = mods.join("\n") + "\n"; 114 - fs::write(path.join("mod.rs"), mod_content)?; 115 - } 116 - } 117 - } 118 - 119 - Ok(()) 120 - }