diff --git a/turtle-lib-macros/src/lib.rs b/turtle-lib-macros/src/lib.rs index e2724ba..0b88344 100644 --- a/turtle-lib-macros/src/lib.rs +++ b/turtle-lib-macros/src/lib.rs @@ -66,17 +66,11 @@ use syn::ItemFn; /// use turtle_lib::*; /// /// fn main() { +/// let mut build_commands = |turtle: &mut turtle_lib::TurtlePlan| { +/// my_drawing(turtle); +/// }; /// // Handle optional SVG export headlessly without opening a window -/// if let Some(filename) = turtle_lib::export::parse_svg_export_arg() { -/// let mut build_commands = |turtle: &mut turtle_lib::TurtlePlan| { -/// my_drawing(turtle); -/// }; -/// if let Err(e) = turtle_lib::export::run_headless_svg_export(&mut build_commands, &filename) { -/// eprintln!("Error exporting SVG: {:?}", e); -/// std::process::exit(1); -/// } -/// return; -/// } +/// turtle_lib::export::handle_svg_export(&mut build_commands); /// /// // Normal interactive GUI mode with window /// turtle_lib::macroquad::Window::new("My Turtle Drawing", async { @@ -252,18 +246,7 @@ fn turtle_main_impl( }; // If --export-svg flag is present, export headlessly without opening a window - if let Some(filename) = turtle_lib::export::parse_svg_export_arg() { - match turtle_lib::export::run_headless_svg_export(&mut build_commands, &filename) { - Ok(()) => { - println!("SVG exported successfully to: {}", filename); - return; - } - Err(e) => { - eprintln!("Error exporting SVG: {}", e); - std::process::exit(1); - } - } - } + turtle_lib::export::handle_svg_export(&mut build_commands); // Normal rendering mode (interactive window) turtle_lib::macroquad::Window::new(#window_title, async { diff --git a/turtle-lib/Cargo.toml b/turtle-lib/Cargo.toml index 9b485c3..5ae159c 100644 --- a/turtle-lib/Cargo.toml +++ b/turtle-lib/Cargo.toml @@ -14,6 +14,7 @@ tracing = { version = "0.1", features = [ ], default-features = false } turtle-lib-macros = { path = "../turtle-lib-macros" } crossbeam = "0.8" +pico-args = { version = "0.5", features = ["eq-separator"] } [dev-dependencies] # For examples and testing diff --git a/turtle-lib/src/export.rs b/turtle-lib/src/export.rs index 1a20016..68f6e19 100644 --- a/turtle-lib/src/export.rs +++ b/turtle-lib/src/export.rs @@ -8,6 +8,7 @@ pub enum ExportError { Io(std::io::Error), Format(String), Execution(String), + MissingFeature(String), // Additional formats can be added here. } @@ -17,6 +18,7 @@ impl std::fmt::Display for ExportError { Self::Io(err) => write!(f, "I/O error: {err}"), Self::Format(msg) => write!(f, "format error: {msg}"), Self::Execution(msg) => write!(f, "execution error: {msg}"), + Self::MissingFeature(msg) => write!(f, "{msg}"), } } } @@ -25,7 +27,7 @@ impl std::error::Error for ExportError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::Io(err) => Some(err), - Self::Format(_) | Self::Execution(_) => None, + Self::Format(_) | Self::Execution(_) | Self::MissingFeature(_) => None, } } } @@ -47,18 +49,27 @@ pub(crate) trait DrawingExporter { fn export(&self, world: &TurtleWorld, filename: &str) -> Result<(), ExportError>; } +/// Parse the `--export-svg` parameter from a `pico_args::Arguments` instance. +/// +/// # Errors +/// +/// Returns `pico_args::Error` if `--export-svg` is provided without a valid filename. +pub fn parse_svg_export_from_args( + args: &mut pico_args::Arguments, +) -> Result, pico_args::Error> { + match args.opt_value_from_str::<_, String>("--export-svg") { + Ok(Some(s)) if s.trim().is_empty() || s.starts_with('-') => { + Err(pico_args::Error::OptionWithoutAValue("--export-svg")) + } + other => other, + } +} + /// Check command-line arguments for the `--export-svg ` flag. #[must_use] pub fn parse_svg_export_arg() -> Option { - let args: Vec = std::env::args().collect(); - let mut i = 1; - while i < args.len() { - if args[i] == "--export-svg" && i + 1 < args.len() { - return Some(args[i + 1].clone()); - } - i += 1; - } - None + let mut args = pico_args::Arguments::from_env(); + parse_svg_export_from_args(&mut args).ok().flatten() } #[cfg(feature = "svg")] @@ -166,7 +177,7 @@ where { let _ = &mut build_commands; let _ = filename; - Err(ExportError::Format( + Err(ExportError::MissingFeature( "SVG export feature is not enabled. Please rebuild with --features svg".to_string(), )) } @@ -179,16 +190,117 @@ pub fn handle_svg_export(build_commands: F) where F: FnMut(&mut TurtlePlan), { - if let Some(filename) = parse_svg_export_arg() { - match run_headless_svg_export(build_commands, &filename) { - Ok(()) => { - println!("SVG exported successfully to: {filename}"); - std::process::exit(0); - } - Err(e) => { - eprintln!("Error exporting SVG: {e}"); + let mut args = pico_args::Arguments::from_env(); + let export_arg = parse_svg_export_from_args(&mut args); + + #[cfg(not(feature = "svg"))] + { + let _ = build_commands; + if export_arg.is_err() || matches!(export_arg, Ok(Some(_))) { + eprintln!("Error: SVG export feature is not enabled. Please rebuild with --features svg"); + std::process::exit(1); + } + } + + #[cfg(feature = "svg")] + { + match export_arg { + Ok(None) => {} + Err(_) => { + eprintln!("Error: --export-svg: option requires an argument"); std::process::exit(1); } + Ok(Some(filename)) => { + match run_headless_svg_export(build_commands, &filename) { + Ok(()) => { + println!("SVG exported successfully to: {filename}"); + std::process::exit(0); + } + Err(e) => { + eprintln!("Error: {e}"); + std::process::exit(1); + } + } + } } } } + +#[cfg(test)] +mod tests { + use super::*; + use std::ffi::OsString; + + fn make_args(args: &[&str]) -> pico_args::Arguments { + let os_args: Vec = args.iter().map(OsString::from).collect(); + pico_args::Arguments::from_vec(os_args) + } + + #[test] + fn test_parse_svg_export_with_filename() { + let mut args = make_args(&["--export-svg", "output.svg"]); + let res = parse_svg_export_from_args(&mut args); + assert_eq!(res.unwrap(), Some("output.svg".to_string())); + } + + #[test] + fn test_parse_svg_export_with_equals() { + let mut args = make_args(&["--export-svg=output.svg"]); + let res = parse_svg_export_from_args(&mut args); + assert_eq!(res.unwrap(), Some("output.svg".to_string())); + } + + #[test] + fn test_parse_svg_export_missing_argument_at_end() { + let mut args = make_args(&["--export-svg"]); + let res = parse_svg_export_from_args(&mut args); + assert!(matches!(res, Err(pico_args::Error::OptionWithoutAValue(_)))); + } + + #[test] + fn test_parse_svg_export_missing_argument_followed_by_flag() { + let mut args = make_args(&["--export-svg", "--verbose"]); + let res = parse_svg_export_from_args(&mut args); + assert!(matches!(res, Err(pico_args::Error::OptionWithoutAValue(_)))); + } + + #[test] + fn test_parse_svg_export_empty_string() { + let mut args = make_args(&["--export-svg", ""]); + let res = parse_svg_export_from_args(&mut args); + assert!(matches!(res, Err(pico_args::Error::OptionWithoutAValue(_)))); + } + + #[test] + fn test_parse_svg_export_empty_equals() { + let mut args = make_args(&["--export-svg="]); + let res = parse_svg_export_from_args(&mut args); + assert!(matches!(res, Err(pico_args::Error::OptionWithoutAValue(_)))); + } + + #[test] + fn test_parse_svg_export_not_present() { + let mut args = make_args(&["--verbose", "input.txt"]); + let res = parse_svg_export_from_args(&mut args); + assert_eq!(res.unwrap(), None); + } + + #[test] + fn test_parse_svg_export_preserves_unknown_args() { + let mut args = make_args(&["--other-flag", "--export-svg", "out.svg", "positional"]); + let res = parse_svg_export_from_args(&mut args); + assert_eq!(res.unwrap(), Some("out.svg".to_string())); + assert!(args.contains("--other-flag")); + } + + #[test] + fn test_export_error_display_missing_feature() { + let err = ExportError::MissingFeature( + "SVG export feature is not enabled. Please rebuild with --features svg".to_string(), + ); + assert_eq!( + err.to_string(), + "SVG export feature is not enabled. Please rebuild with --features svg" + ); + } +}