A better Rust ATProto crate

lexicon.kdl example

Orual 966ebdf0 ee92b831

+154 -56
+32 -11
crates/jacquard-lexicon/build.rs
··· 10 10 mod cli; 11 11 12 12 fn main() -> Result<()> { 13 - let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap_or_else(|_| ".".to_string())); 14 - let mut cmd = cli::LexFetchArgs::command(); 13 + let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set")); 14 + 15 + // Generate docs for lex-fetch 16 + generate_docs_for_binary( 17 + &out_dir, 18 + cli::LexFetchArgs::command(), 19 + "lex-fetch", 20 + )?; 21 + 22 + // Generate docs for jacquard-codegen 23 + generate_docs_for_binary( 24 + &out_dir, 25 + cli::CodegenArgs::command(), 26 + "jacquard-codegen", 27 + )?; 28 + 29 + println!( 30 + "cargo:warning=Generated man pages and completions to {:?}", 31 + out_dir 32 + ); 33 + 34 + Ok(()) 35 + } 15 36 37 + fn generate_docs_for_binary( 38 + out_dir: &PathBuf, 39 + mut cmd: clap::Command, 40 + bin_name: &str, 41 + ) -> Result<()> { 16 42 // Generate man page 17 43 let man_dir = out_dir.join("man"); 18 44 fs::create_dir_all(&man_dir)?; ··· 20 46 let man = Man::new(cmd.clone()); 21 47 let mut man_buffer = Vec::new(); 22 48 man.render(&mut man_buffer)?; 23 - fs::write(man_dir.join("lex-fetch.1"), man_buffer)?; 49 + fs::write(man_dir.join(format!("{}.1", bin_name)), man_buffer)?; 24 50 25 51 // Generate shell completions 26 52 let comp_dir = out_dir.join("completions"); 27 53 fs::create_dir_all(&comp_dir)?; 28 54 29 - generate_to(shells::Bash, &mut cmd, "lex-fetch", &comp_dir)?; 30 - generate_to(shells::Fish, &mut cmd, "lex-fetch", &comp_dir)?; 31 - generate_to(shells::Zsh, &mut cmd, "lex-fetch", &comp_dir)?; 32 - 33 - println!( 34 - "cargo:warning=Generated man page and completions to {:?}", 35 - out_dir 36 - ); 55 + generate_to(shells::Bash, &mut cmd, bin_name, &comp_dir)?; 56 + generate_to(shells::Fish, &mut cmd, bin_name, &comp_dir)?; 57 + generate_to(shells::Zsh, &mut cmd, bin_name, &comp_dir)?; 37 58 38 59 Ok(()) 39 60 }
+71
crates/jacquard-lexicon/lexicons.kdl.example
··· 1 + // Example lex-fetch configuration file 2 + // This demonstrates the available source types and configuration options 3 + 4 + // Output configuration (required) 5 + output { 6 + // Where to save fetched lexicon JSON files 7 + lexicons "lexicons" 8 + 9 + // Where to generate Rust code 10 + codegen "src" 11 + 12 + // Path to Cargo.toml for feature generation (optional) 13 + // cargo-toml "Cargo.toml" 14 + 15 + // NOTE: root-module option is currently disabled due to issues when set to non-"crate" values 16 + // It will always use "crate" as the root module name 17 + } 18 + 19 + // Fetch ATProto and Bluesky lexicons from official repo 20 + // Higher priority (101) means these override conflicts from other sources 21 + source "atproto" type="git" priority=101 { 22 + repo "https://github.com/bluesky-social/atproto" 23 + pattern "lexicons/**/*.json" 24 + } 25 + 26 + // Fetch lexicons from a Git repository 27 + source "my-lexicons" type="git" priority=100 { 28 + repo "https://github.com/example/my-lexicons" 29 + 30 + // Optional: specific branch/tag/commit 31 + // ref "main" 32 + 33 + // Glob pattern for finding lexicon files (defaults to **/*.json) 34 + pattern "**/*.json" 35 + } 36 + 37 + // Use local directory for custom/in-development lexicons 38 + source "local-dev" type="local" priority=200 { 39 + path "./my-lexicons" 40 + 41 + // Optional pattern (omit to use **/*.json) 42 + pattern "*.json" 43 + } 44 + 45 + // Fetch from an AT Protocol endpoint (user's repo) 46 + source "custom-user" type="atproto" { 47 + endpoint "did:plc:example123456789" 48 + 49 + // Optional: specific slice within their repo 50 + // slice "app.example.slice" 51 + } 52 + 53 + // Fetch from HTTP endpoint returning lexicon array 54 + // source "http-lexicons" type="http" { 55 + // url "https://api.example.com/lexicons" 56 + // } 57 + 58 + // Fetch from network.slices 59 + // source "slices-example" type="slices" { 60 + // slice "at://did:plc:example/network.slices.slice/record123" 61 + // } 62 + 63 + // Load a single JSON file containing multiple lexicons 64 + // source "single-file" type="jsonfile" { 65 + // path "./lexicons-bundle.json" 66 + // } 67 + 68 + // Priority notes: 69 + // - Higher numbers override lower numbers when lexicons conflict 70 + // - Useful for preferring official schemas over community forks 71 + // - Default priority if not specified: 50
+3 -19
crates/jacquard-lexicon/src/bin/codegen.rs
··· 1 1 use clap::Parser; 2 + use jacquard_lexicon::cli::CodegenArgs; 2 3 use jacquard_lexicon::codegen::CodeGenerator; 3 4 use jacquard_lexicon::corpus::LexiconCorpus; 4 - use std::path::PathBuf; 5 - 6 - #[derive(Parser, Debug)] 7 - #[command(author, version, about = "Generate Rust code from Lexicon schemas")] 8 - struct Args { 9 - /// Directory containing Lexicon JSON files 10 - #[arg(short = 'i', long)] 11 - input: PathBuf, 12 - 13 - /// Output directory for generated Rust code 14 - #[arg(short = 'o', long)] 15 - output: PathBuf, 16 - 17 - /// Root module name (default: "crate") 18 - #[arg(short = 'r', long, default_value = "crate")] 19 - root_module: String, 20 - } 21 5 22 6 fn main() -> miette::Result<()> { 23 - let args = Args::parse(); 7 + let args = CodegenArgs::parse(); 24 8 25 9 println!("Loading lexicons from {:?}...", args.input); 26 10 let corpus = LexiconCorpus::load_from_dir(&args.input)?; ··· 28 12 println!("Loaded {} lexicon documents", corpus.iter().count()); 29 13 30 14 println!("Generating code..."); 31 - let codegen = CodeGenerator::new(&corpus, args.root_module); 15 + let codegen = CodeGenerator::new(&corpus, "crate".to_string()); 32 16 codegen.write_to_disk(&args.output)?; 33 17 34 18 println!("Generated code to {:?}", args.output);
+1 -5
crates/jacquard-lexicon/src/bin/lex_fetch.rs
··· 54 54 } 55 55 56 56 let corpus = LexiconCorpus::load_from_dir(&config.output.lexicons_dir)?; 57 - let root_module = config 58 - .output 59 - .root_module 60 - .unwrap_or_else(|| "crate".to_string()); 61 - let codegen = CodeGenerator::new(&corpus, root_module); 57 + let codegen = CodeGenerator::new(&corpus, "crate".to_string()); 62 58 std::fs::create_dir_all(&config.output.codegen_dir).into_diagnostic()?; 63 59 codegen.write_to_disk(&config.output.codegen_dir)?; 64 60
+17
crates/jacquard-lexicon/src/cli.rs
··· 16 16 #[arg(short = 'v', long)] 17 17 pub verbose: bool, 18 18 } 19 + 20 + #[derive(Parser, Debug)] 21 + #[command(author, version, about = "Generate Rust code from Lexicon schemas")] 22 + pub struct CodegenArgs { 23 + /// Directory containing Lexicon JSON files 24 + #[arg(short = 'i', long)] 25 + pub input: PathBuf, 26 + 27 + /// Output directory for generated Rust code 28 + #[arg(short = 'o', long)] 29 + pub output: PathBuf, 30 + 31 + // TODO: root_module causes issues when set to anything other than "crate", needs rework 32 + // /// Root module name (default: "crate") 33 + // #[arg(short = 'r', long, default_value = "crate")] 34 + // pub root_module: String, 35 + }
+11 -11
crates/jacquard-lexicon/src/fetch/config.rs
··· 15 15 pub struct OutputConfig { 16 16 pub lexicons_dir: PathBuf, 17 17 pub codegen_dir: PathBuf, 18 - pub root_module: Option<String>, 18 + // TODO: root_module causes issues when set to anything other than "crate", needs rework 19 + // pub root_module: Option<String>, 19 20 pub cargo_toml_path: Option<PathBuf>, 20 21 } 21 22 ··· 58 59 59 60 let mut lexicons_dir: Option<PathBuf> = None; 60 61 let mut codegen_dir: Option<PathBuf> = None; 61 - let mut root_module: Option<String> = None; 62 62 let mut cargo_toml_path: Option<PathBuf> = None; 63 63 64 64 for child in children.nodes() { ··· 79 79 .ok_or_else(|| miette!("codegen expects a string value"))?; 80 80 codegen_dir = Some(PathBuf::from(val)); 81 81 } 82 - "root-module" => { 83 - let val = child 84 - .entries() 85 - .get(0) 86 - .and_then(|e| e.value().as_string()) 87 - .ok_or_else(|| miette!("root-module expects a string value"))?; 88 - root_module = Some(val.to_string()); 89 - } 82 + // TODO: root-module causes issues, disabled for now 83 + // "root-module" => { 84 + // let val = child 85 + // .entries() 86 + // .get(0) 87 + // .and_then(|e| e.value().as_string()) 88 + // .ok_or_else(|| miette!("root-module expects a string value"))?; 89 + // root_module = Some(val.to_string()); 90 + // } 90 91 "cargo-toml" => { 91 92 let val = child 92 93 .entries() ··· 104 105 Ok(OutputConfig { 105 106 lexicons_dir: lexicons_dir.ok_or_else(|| miette!("Missing lexicons directory"))?, 106 107 codegen_dir: codegen_dir.ok_or_else(|| miette!("Missing codegen directory"))?, 107 - root_module, 108 108 cargo_toml_path, 109 109 }) 110 110 }
+19 -10
nix/modules/rust.nix
··· 78 78 crane = { 79 79 args = { 80 80 buildInputs = commonBuildInputs; 81 + nativeBuildInputs = [pkgs.installShellFiles]; 81 82 doCheck = false; # Tests require lexicon corpus files not available in nix build 82 83 postInstall = '' 83 - # Install man pages 84 - if [ -d "$OUT_DIR/man" ]; then 85 - install -Dm644 $OUT_DIR/man/*.1 -t $out/share/man/man1/ 86 - fi 84 + # Install man pages and completions from build script output 85 + for outdir in target/release/build/jacquard-lexicon-*/out; do 86 + if [ -d "$outdir/man" ]; then 87 + installManPage $outdir/man/*.1 88 + fi 89 + if [ -d "$outdir/completions" ]; then 90 + # Install completions for both binaries 91 + for completion in $outdir/completions/*; do 92 + case "$(basename "$completion")" in 93 + *.bash) installShellCompletion --bash "$completion" ;; 94 + *.fish) installShellCompletion --fish "$completion" ;; 95 + _*) installShellCompletion --zsh "$completion" ;; 96 + esac 97 + done 98 + fi 99 + done 87 100 88 - # Install shell completions 89 - if [ -d "$OUT_DIR/completions" ]; then 90 - install -Dm644 $OUT_DIR/completions/lex-fetch.bash $out/share/bash-completion/completions/lex-fetch 91 - install -Dm644 $OUT_DIR/completions/lex-fetch.fish $out/share/fish/vendor_completions.d/lex-fetch.fish 92 - install -Dm644 $OUT_DIR/completions/_lex-fetch $out/share/zsh/site-functions/_lex-fetch 93 - fi 101 + # Install example lexicons.kdl config 102 + install -Dm644 ${./../../crates/jacquard-lexicon/lexicons.kdl.example} $out/share/doc/jacquard-lexicon/lexicons.kdl.example 94 103 ''; 95 104 }; 96 105 };