use clap::{Parser, ValueEnum};
use colored::Colorize;
use std::path::PathBuf;
use std::process::ExitCode;
use metadol::codegen::{JsonSchemaCodegen, RustCodegen, TypeScriptCodegen};
use metadol::parse_file_all;
#[derive(Parser, Debug)]
#[command(name = "dol-codegen")]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(required = true)]
paths: Vec<PathBuf>,
#[arg(short, long, value_enum, default_value = "rust")]
target: TargetLanguage,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(short, long)]
recursive: bool,
#[arg(short, long)]
quiet: bool,
#[arg(long, value_delimiter = ',')]
derive: Vec<String>,
#[arg(long, default_value = "true")]
public: bool,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
enum TargetLanguage {
Rust,
Typescript,
Jsonschema,
}
fn main() -> ExitCode {
let args = Args::parse();
let files = collect_dol_files(&args.paths, args.recursive);
if files.is_empty() {
if !args.quiet {
eprintln!("{}: No .dol files found", "warning".yellow());
}
return ExitCode::SUCCESS;
}
let mut generated_code = String::new();
let mut failed = 0;
if args.target != TargetLanguage::Jsonschema {
let comment_prefix = match args.target {
TargetLanguage::Rust => "//",
TargetLanguage::Typescript => "//",
TargetLanguage::Jsonschema => unreachable!(),
};
generated_code.push_str(&format!(
"{} Generated by dol-codegen from Metal DOL declarations\n",
comment_prefix
));
generated_code.push_str(&format!(
"{} Do not edit manually - regenerate from source .dol files\n\n",
comment_prefix
));
}
for path in &files {
match process_file(path, &args) {
Ok(code) => {
if !args.quiet {
eprintln!("{} {}", "Generated".green(), path.display());
}
if args.target != TargetLanguage::Jsonschema {
generated_code.push_str(&format!("// Source: {}\n", path.display()));
}
generated_code.push_str(&code);
generated_code.push_str("\n\n");
}
Err(e) => {
failed += 1;
eprintln!("{} {}: {}", "Error".red(), path.display(), e);
}
}
}
if failed > 0 {
eprintln!("\n{}: {} file(s) failed to process", "error".red(), failed);
return ExitCode::FAILURE;
}
if args.target == TargetLanguage::Rust {
let type_enum_marker = "pub enum Type {\n";
if let Some(pos) = generated_code.find(type_enum_marker) {
let insert_pos = pos + type_enum_marker.len();
let missing_variants = " // Additional variants for typechecker compatibility\n Unit,\n Never,\n Gene(String, Vec<String>),\n List(Box<Type>),\n Option(Box<Type>),\n Result(Box<Type>, Box<Type>),\n Quoted(Box<Type>),\n";
generated_code.insert_str(insert_pos, missing_variants);
}
let token_enum_marker = "pub enum TokenKind {\n";
if let Some(pos) = generated_code.find(token_enum_marker) {
let insert_pos = pos + token_enum_marker.len();
let missing_variants = " // Additional token kinds\n At,\n";
generated_code.insert_str(insert_pos, missing_variants);
}
}
match &args.output {
Some(output_path) => {
if let Err(e) = std::fs::write(output_path, &generated_code) {
eprintln!("{}: Failed to write output: {}", "error".red(), e);
return ExitCode::FAILURE;
}
if !args.quiet {
eprintln!(
"\n{} {} ({} files)",
"Wrote".green(),
output_path.display(),
files.len()
);
}
}
None => {
print!("{}", generated_code);
}
}
ExitCode::SUCCESS
}
fn collect_dol_files(paths: &[PathBuf], recursive: bool) -> Vec<PathBuf> {
let mut files = Vec::new();
for path in paths {
if path.is_file() {
if path.extension().is_some_and(|ext| ext == "dol") {
files.push(path.clone());
}
} else if path.is_dir() {
if recursive {
collect_dol_files_recursive(path, &mut files);
} else {
if let Ok(entries) = std::fs::read_dir(path) {
for entry in entries.flatten() {
let p = entry.path();
if p.is_file() && p.extension().is_some_and(|ext| ext == "dol") {
files.push(p);
}
}
}
}
}
}
files.sort();
files
}
fn collect_dol_files_recursive(dir: &PathBuf, files: &mut Vec<PathBuf>) {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_dol_files_recursive(&path, files);
} else if path.extension().is_some_and(|ext| ext == "dol") {
files.push(path);
}
}
}
}
fn process_file(path: &PathBuf, args: &Args) -> Result<String, String> {
let source =
std::fs::read_to_string(path).map_err(|e| format!("Failed to read file: {}", e))?;
let decls = parse_file_all(&source).map_err(|e| format!("Parse error: {}", e))?;
let code = match args.target {
TargetLanguage::Rust => RustCodegen::generate_all(&decls),
TargetLanguage::Typescript => TypeScriptCodegen::generate_all(&decls),
TargetLanguage::Jsonschema => JsonSchemaCodegen::generate_all(&decls),
};
Ok(code)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_collect_dol_files_empty() {
let files = collect_dol_files(&[], false);
assert!(files.is_empty());
}
#[test]
fn test_target_language_variants() {
assert_eq!(TargetLanguage::Rust, TargetLanguage::Rust);
}
}