diff --git a/src/functions/array.rs b/src/functions/array.rs index 8341861..b1ab61e 100644 --- a/src/functions/array.rs +++ b/src/functions/array.rs @@ -327,17 +327,19 @@ pub fn array_group_by_fn(kwargs: Kwargs) -> Result { })?; // Get the key value as string - if let Some(obj) = json_value.as_object() - && let Some(key_val) = obj.get(&key) - { - let group_key = match key_val { - serde_json::Value::String(s) => s.clone(), - serde_json::Value::Number(n) => n.to_string(), - serde_json::Value::Bool(b) => b.to_string(), - _ => "null".to_string(), - }; - - groups.entry(group_key).or_default().push(json_value); + // Note: Using nested if-let instead of let-chains for stable Rust compatibility + #[allow(clippy::collapsible_if)] + if let Some(obj) = json_value.as_object() { + if let Some(key_val) = obj.get(&key) { + let group_key = match key_val { + serde_json::Value::String(s) => s.clone(), + serde_json::Value::Number(n) => n.to_string(), + serde_json::Value::Bool(b) => b.to_string(), + _ => "null".to_string(), + }; + + groups.entry(group_key).or_default().push(json_value); + } } } } diff --git a/src/functions/statistics.rs b/src/functions/statistics.rs index 7bfb222..ecabe4f 100644 --- a/src/functions/statistics.rs +++ b/src/functions/statistics.rs @@ -201,7 +201,9 @@ pub fn array_median_fn(kwargs: Kwargs) -> Result { numbers.sort_by(|a, b| a.partial_cmp(b).unwrap()); let len = numbers.len(); - let median = if len.is_multiple_of(2) { + // Note: Using modulo instead of is_multiple_of() for stable Rust compatibility + #[allow(clippy::manual_is_multiple_of)] + let median = if len % 2 == 0 { // Even length: average of two middle values (numbers[len / 2 - 1] + numbers[len / 2]) / 2.0 } else { diff --git a/src/lib.rs b/src/lib.rs index db77d1d..72d805b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,3 +23,42 @@ pub mod validator; pub use cli::Cli; pub use context::TemplateContext; pub use renderer::render_template; + +use clap::Parser; +use std::ffi::OsString; + +/// Run the template tool with the given command line arguments. +/// +/// This function parses command line arguments and renders the template. +/// It's designed to be testable by accepting arguments programmatically. +/// +/// # Arguments +/// +/// * `args` - Iterator of command line arguments (including program name as first element) +/// +/// # Returns +/// +/// Returns `Ok(())` on success, or an error describing what went wrong. +/// +/// # Example +/// +/// ```no_run +/// use tmpltool::run; +/// +/// // Run with custom arguments +/// let result = run(["tmpltool", "template.tmpl", "-o", "output.txt"]); +/// ``` +pub fn run(args: I) -> Result<(), Box> +where + I: IntoIterator, + T: Into + Clone, +{ + let cli = Cli::try_parse_from(args)?; + + render_template( + cli.template.as_deref(), + cli.output.as_deref(), + cli.trust, + cli.validate, + ) +} diff --git a/src/main.rs b/src/main.rs index c1a72f7..24cbdc4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,8 @@ -use clap::Parser; use std::process; -use tmpltool::{Cli, render_template}; +use tmpltool::run; fn main() { - let cli = Cli::parse(); - - if let Err(e) = render_template( - cli.template.as_deref(), - cli.output.as_deref(), - cli.trust, - cli.validate, - ) { + if let Err(e) = run(std::env::args_os()) { eprintln!("Error: {}", e); process::exit(1); } diff --git a/tests/test_run_function.rs b/tests/test_run_function.rs new file mode 100644 index 0000000..ed9997f --- /dev/null +++ b/tests/test_run_function.rs @@ -0,0 +1,281 @@ +//! Tests for the run() function in lib.rs +//! +//! These tests exercise the CLI argument parsing and template rendering +//! through the public run() function, providing coverage for the main +//! entry point logic. + +use std::fs; +use std::io::Write; +use tempfile::TempDir; +use tmpltool::run; + +/// Helper to create a temporary template file +fn create_template(dir: &TempDir, name: &str, content: &str) -> String { + let path = dir.path().join(name); + let mut file = fs::File::create(&path).unwrap(); + write!(file, "{}", content).unwrap(); + path.to_string_lossy().to_string() +} + +#[test] +fn test_run_with_template_file() { + let temp_dir = TempDir::new().unwrap(); + let template_path = create_template(&temp_dir, "test.tmpl", "Hello World"); + + let result = run(["tmpltool", &template_path]); + assert!(result.is_ok()); +} + +#[test] +fn test_run_with_output_file() { + let temp_dir = TempDir::new().unwrap(); + let template_path = create_template(&temp_dir, "test.tmpl", "Hello Output"); + let output_path = temp_dir.path().join("output.txt"); + + let result = run([ + "tmpltool", + &template_path, + "-o", + output_path.to_str().unwrap(), + ]); + + assert!(result.is_ok()); + assert!(output_path.exists()); + + let content = fs::read_to_string(&output_path).unwrap(); + assert_eq!(content, "Hello Output"); +} + +#[test] +fn test_run_with_long_output_flag() { + let temp_dir = TempDir::new().unwrap(); + let template_path = create_template(&temp_dir, "test.tmpl", "Long flag test"); + let output_path = temp_dir.path().join("output.txt"); + + let result = run([ + "tmpltool", + &template_path, + "--output", + output_path.to_str().unwrap(), + ]); + + assert!(result.is_ok()); + assert!(output_path.exists()); +} + +#[test] +fn test_run_missing_template_file() { + let result = run(["tmpltool", "/nonexistent/path/template.tmpl"]); + assert!(result.is_err()); + + let err = result.unwrap_err().to_string(); + // Error message differs by OS: Unix uses "No such file", Windows uses "cannot find" + assert!( + err.contains("No such file") || err.contains("cannot find"), + "Unexpected error message: {}", + err + ); +} + +#[test] +fn test_run_invalid_template_syntax() { + let temp_dir = TempDir::new().unwrap(); + let template_path = create_template(&temp_dir, "invalid.tmpl", "{{ invalid syntax }}"); + + let result = run(["tmpltool", &template_path]); + assert!(result.is_err()); +} + +#[test] +fn test_run_with_trust_flag() { + let temp_dir = TempDir::new().unwrap(); + let template_path = create_template(&temp_dir, "test.tmpl", "Trust mode enabled"); + + let result = run(["tmpltool", "--trust", &template_path]); + assert!(result.is_ok()); +} + +#[test] +fn test_run_with_validate_json() { + let temp_dir = TempDir::new().unwrap(); + let template_path = create_template( + &temp_dir, + "test.tmpl", + r#"{{ to_json(object={"name": "test"}) }}"#, + ); + + let result = run(["tmpltool", &template_path, "--validate", "json"]); + assert!(result.is_ok()); +} + +#[test] +fn test_run_with_validate_json_invalid() { + let temp_dir = TempDir::new().unwrap(); + let template_path = create_template(&temp_dir, "test.tmpl", "not valid json {"); + + let result = run(["tmpltool", &template_path, "--validate", "json"]); + assert!(result.is_err()); + + let err = result.unwrap_err(); + assert!(err.to_string().contains("JSON validation failed")); +} + +#[test] +fn test_run_with_validate_yaml() { + let temp_dir = TempDir::new().unwrap(); + let template_path = create_template(&temp_dir, "test.tmpl", "name: test\nvalue: 42"); + + let result = run(["tmpltool", &template_path, "--validate", "yaml"]); + assert!(result.is_ok()); +} + +#[test] +fn test_run_with_validate_toml() { + let temp_dir = TempDir::new().unwrap(); + let template_path = create_template(&temp_dir, "test.tmpl", "[section]\nkey = \"value\""); + + let result = run(["tmpltool", &template_path, "--validate", "toml"]); + assert!(result.is_ok()); +} + +#[test] +fn test_run_with_env_function() { + let temp_dir = TempDir::new().unwrap(); + let template_path = create_template( + &temp_dir, + "test.tmpl", + r#"{{ get_env(name="PATH", default="no-path") }}"#, + ); + + let result = run(["tmpltool", &template_path]); + assert!(result.is_ok()); +} + +#[test] +fn test_run_help_flag() { + // --help causes clap to print help and return an error + let result = run(["tmpltool", "--help"]); + assert!(result.is_err()); + + // The error should be from clap (help is treated as early exit) + let err = result.unwrap_err(); + // Clap help error contains the help text + assert!(err.to_string().contains("Usage") || err.to_string().contains("tmpltool")); +} + +#[test] +fn test_run_version_flag() { + // --version causes clap to print version and return an error + let result = run(["tmpltool", "--version"]); + assert!(result.is_err()); +} + +#[test] +fn test_run_invalid_flag() { + let result = run(["tmpltool", "--invalid-flag"]); + assert!(result.is_err()); + + let err = result.unwrap_err(); + assert!(err.to_string().contains("unexpected argument")); +} + +#[test] +fn test_run_with_template_using_include() { + let temp_dir = TempDir::new().unwrap(); + + // Create a partial template + create_template(&temp_dir, "partial.tmpl", "Included content"); + + // Create main template that includes the partial + let template_path = create_template( + &temp_dir, + "main.tmpl", + r#"Before {% include "partial.tmpl" %} After"#, + ); + + let result = run(["tmpltool", &template_path]); + assert!(result.is_ok()); +} + +#[test] +fn test_run_combined_flags() { + let temp_dir = TempDir::new().unwrap(); + let template_path = create_template( + &temp_dir, + "test.tmpl", + r#"{{ to_json(object={"valid": true}) }}"#, + ); + let output_path = temp_dir.path().join("output.json"); + + let result = run([ + "tmpltool", + "--trust", + &template_path, + "--output", + output_path.to_str().unwrap(), + "--validate", + "json", + ]); + + assert!(result.is_ok()); + assert!(output_path.exists()); +} + +#[test] +fn test_run_with_multiline_template() { + let temp_dir = TempDir::new().unwrap(); + let template_content = r#"Line 1 +Line 2 +{% for i in range(3) %} +Item {{ i }} +{% endfor %} +End"#; + let template_path = create_template(&temp_dir, "multiline.tmpl", template_content); + + let result = run(["tmpltool", &template_path]); + assert!(result.is_ok()); +} + +#[test] +fn test_run_empty_template() { + let temp_dir = TempDir::new().unwrap(); + let template_path = create_template(&temp_dir, "empty.tmpl", ""); + + let result = run(["tmpltool", &template_path]); + assert!(result.is_ok()); +} + +#[test] +fn test_run_template_with_conditionals() { + let temp_dir = TempDir::new().unwrap(); + let template_path = create_template( + &temp_dir, + "conditional.tmpl", + r#"{% if true %}Yes{% else %}No{% endif %}"#, + ); + + let result = run(["tmpltool", &template_path]); + assert!(result.is_ok()); +} + +#[test] +fn test_run_output_to_nested_directory() { + let temp_dir = TempDir::new().unwrap(); + let template_path = create_template(&temp_dir, "test.tmpl", "Nested output"); + + // Create nested directory + let nested_dir = temp_dir.path().join("nested").join("deep"); + fs::create_dir_all(&nested_dir).unwrap(); + + let output_path = nested_dir.join("output.txt"); + + let result = run([ + "tmpltool", + &template_path, + "-o", + output_path.to_str().unwrap(), + ]); + + assert!(result.is_ok()); + assert!(output_path.exists()); +}