use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "etdl", version, about = "ETDL parser, validator, and compiler")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Compile {
#[arg(help = "Path to .etdl document")]
file: PathBuf,
#[arg(long, default_value = "rust", help = "Target language for code generation")]
target: String,
#[arg(long, default_value = ".", help = "Output directory for generated code")]
out_dir: PathBuf,
},
Validate {
#[arg(help = "Path(s) to .etdl document(s)")]
files: Vec<PathBuf>,
},
Version,
}
fn main() {
let cli = Cli::parse();
match cli.command {
Command::Compile {
file,
target,
out_dir,
} => {
if let Err(e) = cmd_compile(&file, &target, &out_dir) {
eprintln!("error: {}", e);
std::process::exit(1);
}
}
Command::Validate { files } => {
let exit_code = cmd_validate(&files);
std::process::exit(exit_code);
}
Command::Version => {
println!("etdl {}", env!("CARGO_PKG_VERSION"));
}
}
}
fn cmd_compile(file: &PathBuf, target: &str, out_dir: &PathBuf) -> Result<(), String> {
if target != "rust" {
return Err(format!("unsupported target language '{}'; supported: rust", target));
}
let doc = etdl_parser::parse_document_from_file(file)?;
let base_dir = file.parent().unwrap_or(std::path::Path::new("."));
let registry = etdl_parser::load_asyncapi_imports(&doc, base_dir)?;
let compiler = etdl_compiler::Compiler::new();
let result = compiler.compile(&doc, ®istry);
let error_count = result.diagnostics.iter().filter(|d| d.is_error()).count();
let warning_count = result.diagnostics.iter().filter(|d| !d.is_error()).count();
for diag in &result.diagnostics {
let level = if diag.is_error() { "ERROR" } else { "WARNING" };
eprintln!("[{}] {}: {}", level, diag.code, diag.message);
}
if let Some(ref output) = result.rust_output {
let stem = file.file_stem().unwrap_or_default().to_string_lossy();
let out_path = out_dir.join(format!("{}.rs", stem));
let out_dir_exists = out_dir.exists();
if !out_dir_exists {
std::fs::create_dir_all(out_dir).map_err(|e| {
format!("cannot create output directory: {}", e)
})?;
}
std::fs::write(&out_path, output).map_err(|e| {
format!("cannot write generated code to {}: {}", out_path.display(), e)
})?;
println!(
"compiled '{}' to '{}' ({} errors, {} warnings)",
file.display(),
out_path.display(),
error_count,
warning_count
);
} else {
eprintln!(
"compilation failed with {} errors and {} warnings",
error_count, warning_count
);
std::process::exit(1);
}
Ok(())
}
fn cmd_validate(files: &[PathBuf]) -> i32 {
let mut worst_exit = 0;
for file in files {
let doc = match etdl_parser::parse_document_from_file(file) {
Ok(doc) => doc,
Err(e) => {
eprintln!("[ERROR] {}: {}", file.display(), e);
worst_exit = 1;
continue;
}
};
let base_dir = file.parent().unwrap_or(std::path::Path::new("."));
let registry = match etdl_parser::load_asyncapi_imports(&doc, base_dir) {
Ok(registry) => registry,
Err(e) => {
eprintln!("[ERROR] {}: {}", file.display(), e);
worst_exit = 1;
continue;
}
};
let compiler = etdl_compiler::Compiler::new();
let diagnostics = compiler.validate(&doc, ®istry);
let error_count = diagnostics.iter().filter(|d| d.is_error()).count();
let warning_count = diagnostics.iter().filter(|d| !d.is_error()).count();
for diag in &diagnostics {
let level = if diag.is_error() { "ERROR" } else { "WARNING" };
println!("[{}] {}: {}", level, diag.code, diag.message);
}
if error_count == 0 {
println!(
"document '{}' is valid ({} errors, {} warnings)",
file.display(),
error_count,
warning_count
);
} else {
eprintln!(
"document '{}' has {} validation errors",
file.display(),
error_count
);
worst_exit = 1;
}
}
worst_exit
}