#![allow(clippy::print_stderr, clippy::print_stdout)]
use std::process::ExitCode;
use zerodds_idl::config::ParserConfig;
use zerodds_idl::grammar::deltas::RTI_CONNEXT;
use zerodds_idl::parser::{parse, parse_with_deltas};
const VERSION: &str = env!("CARGO_PKG_VERSION");
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
match run(&args) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("zerodds-idlc: {e}");
ExitCode::from(e.exit_code())
}
}
}
#[derive(Debug)]
enum CliError {
Usage(String),
Io(String),
Parse(String),
NotImplemented(String),
}
impl CliError {
fn exit_code(&self) -> u8 {
match self {
Self::Usage(_) | Self::Io(_) => 2,
Self::Parse(_) => 1,
Self::NotImplemented(_) => 3,
}
}
}
impl core::fmt::Display for CliError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Usage(m) | Self::Io(m) | Self::Parse(m) | Self::NotImplemented(m) => {
f.write_str(m)
}
}
}
}
#[derive(Default)]
struct CliOptions {
parse_only: bool,
rti: bool,
file: Option<String>,
}
fn run(args: &[String]) -> Result<(), CliError> {
let mut opts = CliOptions::default();
for arg in args {
match arg.as_str() {
"-h" | "--help" => {
print_help();
return Ok(());
}
"-V" | "--version" => {
println!("zerodds-idlc {VERSION}");
return Ok(());
}
"--parse-only" => opts.parse_only = true,
"--rti" => opts.rti = true,
other if other.starts_with('-') => {
return Err(CliError::Usage(format!("unknown flag: {other}")));
}
other => {
if opts.file.is_some() {
return Err(CliError::Usage(
"only one input file supported in Phase 0".to_string(),
));
}
opts.file = Some(other.to_string());
}
}
}
if !opts.parse_only {
return Err(CliError::NotImplemented(
"Phase 0 supports only --parse-only; code-gen backends follow in Phase 1".to_string(),
));
}
let path = opts
.file
.ok_or_else(|| CliError::Usage("missing input file (try --help)".to_string()))?;
let src = std::fs::read_to_string(&path)
.map_err(|e| CliError::Io(format!("cannot read {path}: {e}")))?;
let cfg = ParserConfig::default();
let ast = if opts.rti {
parse_with_deltas(&src, &cfg, &[&RTI_CONNEXT])
} else {
parse(&src, &cfg)
}
.map_err(|e| CliError::Parse(format!("parse failed: {e}")))?;
println!("{ast}");
Ok(())
}
fn print_help() {
println!(
"zerodds-idlc {VERSION}\n\
IDL4 compiler (Phase 0: parse-only)\n\n\
USAGE:\n\
\x20 zerodds-idlc --parse-only [--rti] <file.idl>\n\n\
OPTIONS:\n\
\x20 --parse-only Parse + print AST (Phase 0 default)\n\
\x20 --rti Aktiviere RTI Connext-Grammar-Delta\n\
\x20 -h, --help Diese Hilfe\n\
\x20 -V, --version Versions-Info\n\n\
EXIT-CODES:\n\
\x20 0 Erfolg\n\
\x20 1 Parse-Fehler\n\
\x20 2 CLI/IO-Fehler\n\
\x20 3 Feature noch nicht implementiert\n"
);
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::*;
#[test]
fn run_with_no_args_errors_with_usage() {
let result = run(&[]);
assert!(matches!(result, Err(CliError::NotImplemented(_))));
}
#[test]
fn run_with_unknown_flag_errors() {
let result = run(&["--bogus".to_string()]);
assert!(matches!(result, Err(CliError::Usage(_))));
}
#[test]
fn run_with_help_succeeds() {
let result = run(&["--help".to_string()]);
assert!(result.is_ok());
}
#[test]
fn run_with_version_succeeds() {
let result = run(&["--version".to_string()]);
assert!(result.is_ok());
}
#[test]
fn run_parse_only_without_file_errors() {
let result = run(&["--parse-only".to_string()]);
assert!(matches!(result, Err(CliError::Usage(_))));
}
#[test]
fn run_parse_only_with_missing_file_errors_io() {
let result = run(&[
"--parse-only".to_string(),
"/nonexistent/file.idl".to_string(),
]);
assert!(matches!(result, Err(CliError::Io(_))));
}
#[test]
fn cli_error_exit_codes_are_distinct() {
assert_eq!(CliError::Parse("x".into()).exit_code(), 1);
assert_eq!(CliError::Usage("x".into()).exit_code(), 2);
assert_eq!(CliError::Io("x".into()).exit_code(), 2);
assert_eq!(CliError::NotImplemented("x".into()).exit_code(), 3);
}
}