A better Rust ATProto crate
1use clap::Parser;
2use jacquard_lexgen::cli::LexFetchArgs;
3use jacquard_lexgen::fetch::{Config, Fetcher};
4use jacquard_lexicon::codegen::CodeGenerator;
5use jacquard_lexicon::corpus::LexiconCorpus;
6use miette::{IntoDiagnostic, Result};
7use std::path::PathBuf;
8
9#[tokio::main]
10async fn main() -> Result<()> {
11 let args = LexFetchArgs::parse();
12
13 if args.verbose {
14 println!("Reading config from {:?}...", args.config);
15 }
16
17 let config_text = std::fs::read_to_string(&args.config).into_diagnostic()?;
18
19 // Parse KDL config
20 let config = Config::from_kdl(&config_text)?;
21
22 // Fetch from all sources
23 if args.verbose {
24 println!("Fetching lexicons from {} sources...", config.sources.len());
25 }
26
27 let fetcher = Fetcher::new(config.clone());
28 let lexicons = fetcher.fetch_all(args.verbose).await?;
29
30 if args.verbose || !args.no_codegen {
31 println!("Fetched {} unique lexicons", lexicons.len());
32 }
33
34 // Ensure output directory exists
35 std::fs::create_dir_all(&config.output.lexicons_dir).into_diagnostic()?;
36
37 // Write each lexicon to a file
38 for (nsid, doc) in &lexicons {
39 let filename = format!("{}.json", nsid.replace('.', "_"));
40 let path = config.output.lexicons_dir.join(&filename);
41
42 let json = serde_json::to_string_pretty(doc).into_diagnostic()?;
43 std::fs::write(&path, json).into_diagnostic()?;
44
45 if args.verbose {
46 println!("Wrote {}", filename);
47 }
48 }
49
50 // Run codegen if requested
51 if !args.no_codegen {
52 if args.verbose {
53 println!("Generating code...");
54 }
55
56 let corpus = LexiconCorpus::load_from_dir(&config.output.lexicons_dir)?;
57 let codegen = CodeGenerator::new(&corpus, "crate".to_string());
58 std::fs::create_dir_all(&config.output.codegen_dir).into_diagnostic()?;
59 codegen.write_to_disk(&config.output.codegen_dir)?;
60
61 println!("Generated code to {:?}", config.output.codegen_dir);
62
63 // Update Cargo.toml features if cargo_toml_path is specified
64 if let Some(cargo_toml_path) = &config.output.cargo_toml_path {
65 if args.verbose {
66 println!("Updating Cargo.toml features...");
67 }
68
69 update_cargo_features(&codegen, cargo_toml_path, &config.output.codegen_dir)?;
70 println!("Updated features in {:?}", cargo_toml_path);
71 }
72 } else {
73 println!("Lexicons written to {:?}", config.output.lexicons_dir);
74 }
75
76 Ok(())
77}
78
79fn update_cargo_features(codegen: &CodeGenerator, cargo_toml_path: &PathBuf, codegen_dir: &PathBuf) -> Result<()> {
80 // Read existing Cargo.toml
81 let content = std::fs::read_to_string(cargo_toml_path).into_diagnostic()?;
82
83 // Find the "# --- generated ---" marker
84 const MARKER: &str = "# --- generated ---";
85
86 let (before, _after) = content.split_once(MARKER)
87 .ok_or_else(|| miette::miette!("Cargo.toml missing '{}' marker", MARKER))?;
88
89 // Generate new features, passing lib.rs path to detect existing modules
90 let lib_rs_path = codegen_dir.join("lib.rs");
91 let features = codegen.generate_cargo_features(Some(&lib_rs_path));
92
93 // Reconstruct file
94 let new_content = format!("{}{}\n{}", before, MARKER, features);
95
96 // Write back
97 std::fs::write(cargo_toml_path, new_content).into_diagnostic()?;
98
99 Ok(())
100}