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 resolve_diagnostic_positions(
diagnostics: &mut [etdl_compiler::validate::Diagnostic],
content: &str,
) {
let index = etdl_parser::spanned::build_span_index(content).ok();
for d in diagnostics.iter_mut() {
if d.line.is_none() {
if let (Some(key), Some(index)) = (&d.key, &index) {
if let Some(el) = index.resolve(key) {
let span = el.key_span.unwrap_or(el.span);
d.line = Some(span.line);
d.column = Some(span.column);
d.end_line = Some(span.end_line);
d.end_column = Some(span.end_column);
}
}
}
}
}
fn append_duplicate_warnings(
diagnostics: &mut Vec<etdl_compiler::validate::Diagnostic>,
content: &str,
) {
if let Ok(duplicates) = etdl_parser::spanned::detect_duplicate_ids(content) {
for dup in duplicates {
let mut d = etdl_compiler::validate::Diagnostic::warning(
"V-001",
format!(
"duplicate {} id '{}' in tree '{}'",
dup.kind, dup.id, dup.tree
),
)
.with_position(dup.span.line, dup.span.column);
d.end_line = Some(dup.span.end_line);
d.end_column = Some(dup.span.end_column);
diagnostics.push(d);
}
}
}
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 content =
std::fs::read_to_string(file).map_err(|e| format!("cannot read file: {}", e))?;
let doc = etdl_parser::parse_document(&content)?;
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 mut result = compiler.compile(&doc, ®istry);
append_duplicate_warnings(&mut result.diagnostics, &content);
resolve_diagnostic_positions(&mut result.diagnostics, &content);
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" };
let position = match (diag.line, diag.column) {
(Some(l), Some(c)) => format!(" ({}:{})", l + 1, c + 1),
_ => String::new(),
};
eprintln!("[{}] {}{}: {}", level, diag.code, position, 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 content = match std::fs::read_to_string(file) {
Ok(c) => c,
Err(e) => {
eprintln!("[ERROR] {}: {}", file.display(), e);
worst_exit = 1;
continue;
}
};
let doc = match etdl_parser::parse_document(&content) {
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 mut diagnostics = compiler.validate(&doc, ®istry);
append_duplicate_warnings(&mut diagnostics, &content);
resolve_diagnostic_positions(&mut diagnostics, &content);
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" };
let position = match (diag.line, diag.column) {
(Some(l), Some(c)) => format!(" ({}:{})", l + 1, c + 1),
_ => String::new(),
};
println!("[{}] {}{}: {}", level, diag.code, position, 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
}