A better Rust ATProto crate

workspace discovery works

Orual efa35bdf 4eb26a04

+111 -225
+5 -5
Cargo.lock
··· 2288 2288 [[package]] 2289 2289 name = "jacquard-api" 2290 2290 version = "0.8.0" 2291 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#988f0eedfc499d0e2cdd667f6adab086465984e2" 2291 + source = "git+https://tangled.org/@nonbinary.computer/jacquard#4eb26a04129dd5a71c7a2282d43bef50f713e335" 2292 2292 dependencies = [ 2293 2293 "bon", 2294 2294 "bytes", ··· 2378 2378 [[package]] 2379 2379 name = "jacquard-common" 2380 2380 version = "0.8.0" 2381 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#988f0eedfc499d0e2cdd667f6adab086465984e2" 2381 + source = "git+https://tangled.org/@nonbinary.computer/jacquard#4eb26a04129dd5a71c7a2282d43bef50f713e335" 2382 2382 dependencies = [ 2383 2383 "base64 0.22.1", 2384 2384 "bon", ··· 2431 2431 [[package]] 2432 2432 name = "jacquard-derive" 2433 2433 version = "0.8.0" 2434 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#988f0eedfc499d0e2cdd667f6adab086465984e2" 2434 + source = "git+https://tangled.org/@nonbinary.computer/jacquard#4eb26a04129dd5a71c7a2282d43bef50f713e335" 2435 2435 dependencies = [ 2436 2436 "heck 0.5.0", 2437 2437 "jacquard-lexicon 0.8.0 (git+https://tangled.org/@nonbinary.computer/jacquard)", ··· 2468 2468 [[package]] 2469 2469 name = "jacquard-identity" 2470 2470 version = "0.8.0" 2471 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#988f0eedfc499d0e2cdd667f6adab086465984e2" 2471 + source = "git+https://tangled.org/@nonbinary.computer/jacquard#4eb26a04129dd5a71c7a2282d43bef50f713e335" 2472 2472 dependencies = [ 2473 2473 "bon", 2474 2474 "bytes", ··· 2542 2542 [[package]] 2543 2543 name = "jacquard-lexicon" 2544 2544 version = "0.8.0" 2545 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#988f0eedfc499d0e2cdd667f6adab086465984e2" 2545 + source = "git+https://tangled.org/@nonbinary.computer/jacquard#4eb26a04129dd5a71c7a2282d43bef50f713e335" 2546 2546 dependencies = [ 2547 2547 "glob", 2548 2548 "heck 0.5.0",
+79
crates/jacquard-lexgen/examples/extract_inventory.rs
··· 1 + //! Extract AT Protocol lexicon schemas from compiled Rust types via inventory 2 + //! 3 + //! This example discovers types with `#[derive(LexiconSchema)]` via inventory 4 + //! and generates lexicon JSON files. This approach requires types to be linked 5 + //! into the binary at compile time. 6 + //! 7 + //! For workspace-wide schema extraction without linking requirements, 8 + //! use the `extract-schemas` binary or `WorkspaceDiscovery` API instead. 9 + 10 + use clap::Parser; 11 + use jacquard_lexgen::schema_extraction::{self, ExtractOptions, SchemaExtractor}; 12 + use miette::Result; 13 + 14 + /// Extract lexicon schemas from compiled Rust types via inventory 15 + #[derive(Parser, Debug)] 16 + #[command(name = "extract-inventory")] 17 + #[command(about = "Extract AT Protocol lexicon schemas from linked Rust types")] 18 + #[command(long_about = r#" 19 + Discovers types implementing LexiconSchema via inventory and generates 20 + lexicon JSON files. The binary only discovers types that are linked, 21 + so you need to import your schema types in this binary or a custom one. 22 + 23 + For workspace-wide extraction, use the extract-schemas binary instead. 24 + 25 + See: https://docs.rs/jacquard-lexgen/latest/jacquard_lexgen/schema_extraction/ 26 + "#)] 27 + struct Args { 28 + /// Output directory for generated schema files 29 + #[arg(short, long, default_value = "lexicons")] 30 + output: String, 31 + 32 + /// Verbose output 33 + #[arg(short, long)] 34 + verbose: bool, 35 + 36 + /// Filter by NSID prefix (e.g., "app.bsky") 37 + #[arg(short, long)] 38 + filter: Option<String>, 39 + 40 + /// Validate schemas before writing 41 + #[arg(short = 'V', long, default_value = "true")] 42 + validate: bool, 43 + 44 + /// Pretty-print JSON output 45 + #[arg(short, long, default_value = "true")] 46 + pretty: bool, 47 + 48 + /// Watch mode - regenerate on changes 49 + #[arg(short, long)] 50 + watch: bool, 51 + } 52 + 53 + fn main() -> Result<()> { 54 + let args = Args::parse(); 55 + 56 + // Simple case: use convenience function 57 + if !args.watch && args.filter.is_none() && args.validate && args.pretty { 58 + return schema_extraction::run(&args.output, args.verbose); 59 + } 60 + 61 + // Advanced case: use full options 62 + let options = ExtractOptions { 63 + output_dir: args.output.into(), 64 + verbose: args.verbose, 65 + filter: args.filter, 66 + validate: args.validate, 67 + pretty: args.pretty, 68 + }; 69 + 70 + let extractor = SchemaExtractor::new(options); 71 + 72 + if args.watch { 73 + extractor.watch()?; 74 + } else { 75 + extractor.extract_all()?; 76 + } 77 + 78 + Ok(()) 79 + }
-50
crates/jacquard-lexgen/examples/workspace_discovery.rs
··· 1 - #!/usr/bin/env cargo 2 - //! Example: Discover schemas across the workspace without link-time discovery 3 - //! 4 - //! Run with: cargo run --example workspace_discovery 5 - 6 - use jacquard_lexgen::schema_discovery::WorkspaceDiscovery; 7 - 8 - fn main() -> miette::Result<()> { 9 - println!("Workspace Schema Discovery Example\n"); 10 - 11 - // Create workspace discovery 12 - let discovery = WorkspaceDiscovery::new().verbose(true); 13 - 14 - // Scan workspace 15 - let schemas = discovery.scan()?; 16 - 17 - println!("\n━━━ Results ━━━"); 18 - println!("Discovered {} schema types:\n", schemas.len()); 19 - 20 - // Group by crate 21 - use std::collections::HashMap; 22 - let mut by_crate: HashMap<String, Vec<_>> = HashMap::new(); 23 - 24 - for schema in &schemas { 25 - let crate_name = schema 26 - .source_path 27 - .components() 28 - .find_map(|c| { 29 - let s = c.as_os_str().to_str()?; 30 - if s.starts_with("jacquard-") || s == "jacquard" { 31 - Some(s.to_string()) 32 - } else { 33 - None 34 - } 35 - }) 36 - .unwrap_or_else(|| "unknown".to_string()); 37 - 38 - by_crate.entry(crate_name).or_default().push(schema); 39 - } 40 - 41 - for (crate_name, crate_schemas) in by_crate { 42 - println!("📦 {} ({} schemas)", crate_name, crate_schemas.len()); 43 - for schema in crate_schemas { 44 - println!(" • {} ({})", schema.nsid, schema.type_name); 45 - } 46 - println!(); 47 - } 48 - 49 - Ok(()) 50 - }
+16 -47
crates/jacquard-lexgen/src/bin/extract_schemas.rs
··· 1 - //! Extract AT Protocol lexicon schemas from compiled Rust types 1 + //! Extract AT Protocol lexicon schemas via workspace discovery 2 2 //! 3 - //! This binary discovers types with `#[derive(LexiconSchema)]` via inventory 4 - //! and generates lexicon JSON files. See the `schema_extraction` module docs 5 - //! for usage patterns and integration examples. 3 + //! This binary scans the workspace for types with `#[derive(LexiconSchema)]` 4 + //! and generates lexicon JSON files. Unlike inventory-based extraction, this 5 + //! discovers schemas across the entire workspace without requiring linking. 6 6 7 7 use clap::Parser; 8 - use jacquard_lexgen::schema_extraction::{self, ExtractOptions, SchemaExtractor}; 8 + use jacquard_lexgen::schema_discovery::WorkspaceDiscovery; 9 9 use miette::Result; 10 10 11 - /// Extract lexicon schemas from compiled Rust types 11 + /// Extract lexicon schemas from workspace source files 12 12 #[derive(Parser, Debug)] 13 13 #[command(name = "extract-schemas")] 14 - #[command(about = "Extract AT Protocol lexicon schemas from Rust types")] 14 + #[command(about = "Extract AT Protocol lexicon schemas from workspace")] 15 15 #[command(long_about = r#" 16 - Discovers types implementing LexiconSchema via inventory and generates 17 - lexicon JSON files. The binary only discovers types that are linked, 18 - so you need to import your schema types in this binary or a custom one. 16 + Scans workspace source files for types with #[derive(LexiconSchema)] and 17 + generates lexicon JSON files. This discovers all schemas in the workspace 18 + without requiring types to be linked into the binary. 19 19 20 - See: https://docs.rs/jacquard-lexgen/latest/jacquard_lexgen/schema_extraction/ 20 + For inventory-based extraction (link-time discovery), see the extract_inventory example. 21 + 22 + See: https://docs.rs/jacquard-lexgen/latest/jacquard_lexgen/schema_discovery/ 21 23 "#)] 22 24 struct Args { 23 25 /// Output directory for generated schema files ··· 27 29 /// Verbose output 28 30 #[arg(short, long)] 29 31 verbose: bool, 30 - 31 - /// Filter by NSID prefix (e.g., "app.bsky") 32 - #[arg(short, long)] 33 - filter: Option<String>, 34 - 35 - /// Validate schemas before writing 36 - #[arg(short = 'V', long, default_value = "true")] 37 - validate: bool, 38 - 39 - /// Pretty-print JSON output 40 - #[arg(short, long, default_value = "true")] 41 - pretty: bool, 42 - 43 - /// Watch mode - regenerate on changes 44 - #[arg(short, long)] 45 - watch: bool, 46 32 } 47 33 48 34 fn main() -> Result<()> { 49 35 let args = Args::parse(); 50 36 51 - // Simple case: use convenience function 52 - if !args.watch && args.filter.is_none() && args.validate && args.pretty { 53 - return schema_extraction::run(&args.output, args.verbose); 54 - } 37 + let discovery = WorkspaceDiscovery::new() 38 + .verbose(args.verbose); 55 39 56 - // Advanced case: use full options 57 - let options = ExtractOptions { 58 - output_dir: args.output.into(), 59 - verbose: args.verbose, 60 - filter: args.filter, 61 - validate: args.validate, 62 - pretty: args.pretty, 63 - }; 64 - 65 - let extractor = SchemaExtractor::new(options); 66 - 67 - if args.watch { 68 - extractor.watch()?; 69 - } else { 70 - extractor.extract_all()?; 71 - } 40 + discovery.generate_and_write(args.output)?; 72 41 73 42 Ok(()) 74 43 }
-2
crates/jacquard-lexgen/src/lib.rs
··· 36 36 pub mod fetch; 37 37 pub mod schema_discovery; 38 38 pub mod schema_extraction; 39 - #[cfg(any(test, debug_assertions))] 40 - pub mod test_schemas; 41 39 42 40 pub use fetch::{Config, Fetcher};
+11 -8
crates/jacquard-lexgen/src/schema_discovery.rs
··· 155 155 156 156 // Use schema builder based on kind 157 157 let built = match schema_info.kind { 158 - SchemaKind::Struct => { 159 - jacquard_lexicon::schema::from_ast::build_struct_schema(&ast)? 160 - } 158 + SchemaKind::Struct => jacquard_lexicon::schema::from_ast::build_struct_schema(&ast) 159 + .into_diagnostic()?, 161 160 SchemaKind::Enum => { 162 - jacquard_lexicon::schema::from_ast::build_enum_schema(&ast)? 161 + jacquard_lexicon::schema::from_ast::build_enum_schema(&ast).into_diagnostic()? 163 162 } 164 163 }; 165 164 ··· 210 209 } 211 210 212 211 /// Group schemas by base NSID (strip fragment suffix) 213 - fn group_by_base_nsid(&self, schemas: &[GeneratedSchema]) -> BTreeMap<String, Vec<&GeneratedSchema>> { 214 - let mut groups: BTreeMap<String, Vec<&GeneratedSchema>> = BTreeMap::new(); 212 + fn group_by_base_nsid<'a>( 213 + &self, 214 + schemas: &'a [GeneratedSchema], 215 + ) -> BTreeMap<String, Vec<&'a GeneratedSchema>> { 216 + let mut groups: BTreeMap<String, Vec<&'a GeneratedSchema>> = BTreeMap::new(); 215 217 216 218 for schema in schemas { 217 219 // Split on # to get base NSID ··· 297 299 298 300 /// Serialize a lexicon doc with "main" def first 299 301 fn serialize_with_main_first(&self, doc: &LexiconDoc) -> Result<String> { 300 - use serde_json::{json, Map, Value}; 302 + use serde_json::{Map, Value, json}; 301 303 302 304 // Build defs map with main first 303 305 let mut defs_map = Map::new(); ··· 533 535 lex_attrs.key = Some(lit.value()); 534 536 } 535 537 Ok(()) 536 - }).into_diagnostic()?; 538 + }) 539 + .into_diagnostic()?; 537 540 } 538 541 } 539 542
-31
crates/jacquard-lexgen/src/test_schemas.rs
··· 1 - // Test schemas for verifying extraction works 2 - // These are only compiled in tests/dev builds 3 - 4 - use jacquard_common::CowStr; 5 - use jacquard_derive::LexiconSchema; 6 - 7 - #[derive(LexiconSchema)] 8 - #[lexicon(nsid = "com.example.testRecord", record, key = "tid")] 9 - pub struct TestRecord<'a> { 10 - #[lexicon(max_length = 100)] 11 - pub text: CowStr<'a>, 12 - pub count: i64, 13 - } 14 - 15 - #[derive(LexiconSchema)] 16 - #[lexicon(nsid = "com.example.testRecord#fragment")] 17 - pub struct TestFragment { 18 - pub field: i64, 19 - } 20 - 21 - #[derive(LexiconSchema)] 22 - #[lexicon(nsid = "com.example.testDefs.defs#defOne")] 23 - pub struct DefOne { 24 - pub value: String, 25 - } 26 - 27 - #[derive(LexiconSchema)] 28 - #[lexicon(nsid = "com.example.testDefs.defs#defTwo")] 29 - pub struct DefTwo { 30 - pub number: i64, 31 - }
-82
crates/jacquard-lexgen/tests/schema_extraction.rs
··· 1 - use jacquard_lexgen::schema_extraction::{ExtractOptions, SchemaExtractor}; 2 - use tempfile::TempDir; 3 - 4 - #[test] 5 - fn test_extract_all_creates_output_dir() { 6 - let temp_dir = TempDir::new().unwrap(); 7 - 8 - let options = ExtractOptions { 9 - output_dir: temp_dir.path().to_path_buf(), 10 - verbose: false, 11 - filter: None, 12 - validate: true, 13 - pretty: true, 14 - }; 15 - 16 - let extractor = SchemaExtractor::new(options); 17 - 18 - // This will discover any schemas registered via inventory in the binary 19 - // In a minimal test environment, this might be 0 20 - let result = extractor.extract_all(); 21 - 22 - // Should succeed even if no schemas found 23 - assert!(result.is_ok()); 24 - 25 - // Directory should exist 26 - assert!(temp_dir.path().exists()); 27 - } 28 - 29 - #[test] 30 - fn test_extract_with_filter() { 31 - let temp_dir = TempDir::new().unwrap(); 32 - 33 - let options = ExtractOptions { 34 - output_dir: temp_dir.path().to_path_buf(), 35 - verbose: false, 36 - filter: Some("com.example.nonexistent".into()), 37 - validate: true, 38 - pretty: true, 39 - }; 40 - 41 - let extractor = SchemaExtractor::new(options); 42 - let result = extractor.extract_all(); 43 - 44 - // Should succeed (just won't write any files) 45 - assert!(result.is_ok()); 46 - } 47 - 48 - #[test] 49 - fn test_extract_with_verbose() { 50 - let temp_dir = TempDir::new().unwrap(); 51 - 52 - let options = ExtractOptions { 53 - output_dir: temp_dir.path().to_path_buf(), 54 - verbose: true, 55 - filter: None, 56 - validate: true, 57 - pretty: true, 58 - }; 59 - 60 - let extractor = SchemaExtractor::new(options); 61 - let result = extractor.extract_all(); 62 - 63 - assert!(result.is_ok()); 64 - } 65 - 66 - #[test] 67 - fn test_extract_compact_json() { 68 - let temp_dir = TempDir::new().unwrap(); 69 - 70 - let options = ExtractOptions { 71 - output_dir: temp_dir.path().to_path_buf(), 72 - verbose: false, 73 - filter: None, 74 - validate: true, 75 - pretty: false, // Compact JSON 76 - }; 77 - 78 - let extractor = SchemaExtractor::new(options); 79 - let result = extractor.extract_all(); 80 - 81 - assert!(result.is_ok()); 82 - }