etdl-cli 0.1.3

ETDL CLI: compile and validate .etdl documents with IEC 61025 fault tree and IEC 62502 event tree analysis, generating Rust for microservices
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, &registry);

    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, &registry);

        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
}