From 184386a96b5de359b78eb337cd01fcc8821bf096 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Wed, 18 Mar 2026 11:26:40 +0100 Subject: [PATCH] updated rule based logic for finding and outputting files --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/main.rs | 145 ++---------------------------------- src/resolver.rs | 193 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+), 142 deletions(-) create mode 100644 src/resolver.rs diff --git a/Cargo.lock b/Cargo.lock index 1968e7b..182d2e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -853,7 +853,7 @@ dependencies = [ [[package]] name = "mould" -version = "0.4.2" +version = "0.4.3" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 97e0a8c..1b0627f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,4 +30,4 @@ tempfile = "3.27.0" authors = ["Nils Pukropp "] edition = "2024" name = "mould" -version = "0.4.2" +version = "0.4.3" diff --git a/src/main.rs b/src/main.rs index 3b59d64..f175ac4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod error; mod format; mod runner; mod ui; +mod resolver; use app::App; use config::load_config; @@ -13,7 +14,6 @@ use format::{detect_format, get_handler}; use log::{error, info, warn}; use runner::AppRunner; use std::io; -use std::path::{Path, PathBuf}; use crossterm::{ event::{DisableMouseCapture, EnableMouseCapture}, @@ -22,114 +22,6 @@ use crossterm::{ }; use ratatui::{Terminal, backend::CrosstermBackend}; -/// Helper to automatically determine the output file path based on common naming conventions. -fn determine_output_path(input: &Path) -> PathBuf { - let file_name = input.file_name().unwrap_or_default().to_string_lossy(); - - // Standard mappings - if file_name == ".env.example" || file_name == ".env.template" { - return input.with_file_name(".env"); - } - - if file_name == "docker-compose.yml" || file_name == "compose.yml" { - return input.with_file_name("compose.override.yml"); - } - if file_name == "docker-compose.yaml" || file_name == "compose.yaml" { - return input.with_file_name("compose.override.yaml"); - } - - // Pattern-based mappings - if let Some(base) = file_name.strip_suffix(".env.example") { - return input.with_file_name(format!("{}.env", base)); - } - if let Some(base) = file_name.strip_suffix(".env.template") { - return input.with_file_name(format!("{}.env", base)); - } - if let Some(base) = file_name.strip_suffix(".example.json") { - return input.with_file_name(format!("{}.json", base)); - } - if let Some(base) = file_name.strip_suffix(".template.json") { - return input.with_file_name(format!("{}.json", base)); - } - if let Some(base) = file_name.strip_suffix(".example.yml") { - return input.with_file_name(format!("{}.yml", base)); - } - if let Some(base) = file_name.strip_suffix(".template.yml") { - return input.with_file_name(format!("{}.yml", base)); - } - if let Some(base) = file_name.strip_suffix(".example.yaml") { - return input.with_file_name(format!("{}.yaml", base)); - } - if let Some(base) = file_name.strip_suffix(".template.yaml") { - return input.with_file_name(format!("{}.yaml", base)); - } - if let Some(base) = file_name.strip_suffix(".example.toml") { - return input.with_file_name(format!("{}.toml", base)); - } - if let Some(base) = file_name.strip_suffix(".template.toml") { - return input.with_file_name(format!("{}.toml", base)); - } - - input.with_extension(format!( - "{}.out", - input.extension().unwrap_or_default().to_string_lossy() - )) -} - -/// Discovers common configuration template files in the current directory. -fn find_input_file() -> Option { - let candidates = [ - ".env.example", - "compose.yml", - "docker-compose.yml", - ".env.template", - "compose.yaml", - "docker-compose.yaml", - ]; - - // Priority 1: Exact matches for well-known defaults - for name in &candidates { - let path = PathBuf::from(name); - if path.exists() { - return Some(path); - } - } - - // Priority 2: Pattern matches - if let Ok(entries) = std::fs::read_dir(".") { - let mut fallback = None; - for entry in entries.flatten() { - let name = entry.file_name(); - let name_str = name.to_string_lossy(); - - if name_str.ends_with(".env.example") - || name_str.ends_with(".env.template") - || name_str.ends_with(".example.json") - || name_str.ends_with(".template.json") - || name_str.ends_with(".example.yml") - || name_str.ends_with(".template.yml") - || name_str.ends_with(".example.yaml") - || name_str.ends_with(".template.yaml") - || name_str.ends_with(".example.toml") - || name_str.ends_with(".template.toml") - { - // Prefer .env.* or compose.* if multiple matches - if name_str.contains(".env") || name_str.contains("compose") { - return Some(entry.path()); - } - if fallback.is_none() { - fallback = Some(entry.path()); - } - } - } - if let Some(path) = fallback { - return Some(path); - } - } - - None -} - fn main() -> anyhow::Result<()> { let args = cli::parse(); @@ -152,7 +44,7 @@ fn main() -> anyhow::Result<()> { } path } - None => match find_input_file() { + None => match resolver::find_input_file() { Some(path) => { info!("Discovered template: {}", path.display()); path @@ -172,38 +64,11 @@ fn main() -> anyhow::Result<()> { let handler = get_handler(format_type); // Smart Comparison Logic - let input_name = input_path.file_name().unwrap_or_default().to_string_lossy(); - let is_template_input = input_name.contains(".example") || input_name.contains(".template") || input_name == "compose.yml" || input_name == "docker-compose.yml"; - - let mut template_path = None; - let mut active_path = None; - - if is_template_input { - template_path = Some(input_path.clone()); - let expected_active = determine_output_path(&input_path); - if expected_active.exists() { - active_path = Some(expected_active); - } - } else { - // Input is likely an active config (e.g., .env) - active_path = Some(input_path.clone()); - // Try to find a template - let possible_templates = [ - format!("{}.example", input_name), - format!("{}.template", input_name), - ]; - for t in possible_templates { - let p = input_path.with_file_name(t); - if p.exists() { - template_path = Some(p); - break; - } - } - } + let (active_path, template_path) = resolver::resolve_paths(&input_path); let output_path = args .output - .unwrap_or_else(|| active_path.clone().unwrap_or_else(|| determine_output_path(&input_path))); + .unwrap_or_else(|| active_path.clone().unwrap_or_else(|| resolver::determine_output_path(&input_path))); info!("Output: {}", output_path.display()); @@ -283,4 +148,4 @@ fn main() -> anyhow::Result<()> { Err(e.into()) } } -} +} \ No newline at end of file diff --git a/src/resolver.rs b/src/resolver.rs new file mode 100644 index 0000000..eb5b438 --- /dev/null +++ b/src/resolver.rs @@ -0,0 +1,193 @@ +use std::path::{Path, PathBuf}; + +pub struct Rule { + pub template_suffix: &'static str, + pub active_suffix: &'static str, + pub is_exact_match: bool, +} + +pub const RULES: &[Rule] = &[ + // Exact matches + Rule { template_suffix: "compose.yml", active_suffix: "compose.override.yml", is_exact_match: true }, + Rule { template_suffix: "compose.yaml", active_suffix: "compose.override.yaml", is_exact_match: true }, + Rule { template_suffix: "docker-compose.yml", active_suffix: "docker-compose.override.yml", is_exact_match: true }, + Rule { template_suffix: "docker-compose.yaml", active_suffix: "docker-compose.override.yaml", is_exact_match: true }, + + // Pattern matches + Rule { template_suffix: ".env.example", active_suffix: ".env", is_exact_match: false }, + Rule { template_suffix: ".env.template", active_suffix: ".env", is_exact_match: false }, + Rule { template_suffix: ".example.json", active_suffix: ".json", is_exact_match: false }, + Rule { template_suffix: ".template.json", active_suffix: ".json", is_exact_match: false }, + Rule { template_suffix: ".example.yml", active_suffix: ".yml", is_exact_match: false }, + Rule { template_suffix: ".template.yml", active_suffix: ".yml", is_exact_match: false }, + Rule { template_suffix: ".example.yaml", active_suffix: ".yaml", is_exact_match: false }, + Rule { template_suffix: ".template.yaml", active_suffix: ".yaml", is_exact_match: false }, + Rule { template_suffix: ".example.toml", active_suffix: ".toml", is_exact_match: false }, + Rule { template_suffix: ".template.toml", active_suffix: ".toml", is_exact_match: false }, +]; + +pub const DEFAULT_CANDIDATES: &[&str] = &[ + ".env.example", + "compose.yml", + "docker-compose.yml", + ".env.template", + "compose.yaml", + "docker-compose.yaml", +]; + +/// Helper to automatically determine the output file path based on common naming conventions. +pub fn determine_output_path(input: &Path) -> PathBuf { + let file_name = input.file_name().unwrap_or_default().to_string_lossy(); + + for rule in RULES { + if rule.is_exact_match { + if file_name == rule.template_suffix { + return input.with_file_name(rule.active_suffix); + } + } else { + if file_name == rule.template_suffix { + return input.with_file_name(rule.active_suffix); + } else if let Some(base) = file_name.strip_suffix(rule.template_suffix) { + return input.with_file_name(format!("{}{}", base, rule.active_suffix)); + } + } + } + + input.with_extension(format!( + "{}.out", + input.extension().unwrap_or_default().to_string_lossy() + )) +} + +/// Discovers common configuration template files in the current directory. +pub fn find_input_file() -> Option { + // Priority 1: Exact matches for well-known defaults + for &name in DEFAULT_CANDIDATES { + let path = PathBuf::from(name); + if path.exists() { + return Some(path); + } + } + + // Priority 2: Pattern matches + if let Ok(entries) = std::fs::read_dir(".") { + let mut fallback = None; + for entry in entries.flatten() { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + + for rule in RULES { + if !rule.is_exact_match && name_str.ends_with(rule.template_suffix) { + if name_str.contains(".env") || name_str.contains("compose") { + return Some(entry.path()); + } + if fallback.is_none() { + fallback = Some(entry.path()); + } + break; + } + } + } + if let Some(path) = fallback { + return Some(path); + } + } + + None +} + +/// Resolves the active and template paths given an input path. +/// Returns `(active_path, template_path)`. +pub fn resolve_paths(input: &Path) -> (Option, Option) { + let file_name = input.file_name().unwrap_or_default().to_string_lossy(); + + // Check if the input matches any known template pattern + let mut is_template = false; + for rule in RULES { + if rule.is_exact_match { + if file_name == rule.template_suffix { + is_template = true; + break; + } + } else if file_name.ends_with(rule.template_suffix) { + is_template = true; + break; + } + } + + // Fallback template detection + if !is_template && (file_name.contains(".example") || file_name.contains(".template")) { + is_template = true; + } + + if is_template { + let expected_active = determine_output_path(input); + let active = if expected_active.exists() { + Some(expected_active) + } else { + None + }; + (active, Some(input.to_path_buf())) + } else { + // Input is treated as the active config + let active = Some(input.to_path_buf()); + let mut template = None; + + // Try to reverse match rules to find a template + for rule in RULES { + if rule.is_exact_match { + if file_name == rule.active_suffix { + let t = input.with_file_name(rule.template_suffix); + if t.exists() { + template = Some(t); + break; + } + } + } else if file_name.ends_with(rule.active_suffix) { + if file_name == rule.active_suffix { + let t = input.with_file_name(rule.template_suffix); + if t.exists() { + template = Some(t); + break; + } + } else if let Some(base) = file_name.strip_suffix(rule.active_suffix) { + let t = input.with_file_name(format!("{}{}", base, rule.template_suffix)); + if t.exists() { + template = Some(t); + break; + } + } + } + } + + // Fallback reverse detection + if template.is_none() { + let possible_templates = [ + format!("{}.example", file_name), + format!("{}.template", file_name), + ]; + for t in possible_templates { + let p = input.with_file_name(t); + if p.exists() { + template = Some(p); + break; + } + } + } + + (active, template) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_determine_output_path() { + assert_eq!(determine_output_path(Path::new(".env.example")), PathBuf::from(".env")); + assert_eq!(determine_output_path(Path::new("compose.yml")), PathBuf::from("compose.override.yml")); + assert_eq!(determine_output_path(Path::new("config.template.json")), PathBuf::from("config.json")); + assert_eq!(determine_output_path(Path::new("config.example")), PathBuf::from("config.example.out")); + } +} -- 2.49.1