-13
.claude/settings.local.json
-13
.claude/settings.local.json
-151
CLAUDE.md
-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
-1
crates/jacquard/src/lib.rs
+5
-7
crates/jacquard-common/src/lib.rs
+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
-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
-
}