From 8dc96d4605fda12073e6df38163be07fd75fcdf4 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Fri, 20 Mar 2026 10:54:38 +0100 Subject: [PATCH] fixed resolver issue --- Cargo.lock | 2 +- src/resolver.rs | 334 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 271 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7979626..3ffebd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -914,7 +914,7 @@ dependencies = [ [[package]] name = "mould" -version = "0.5.0" +version = "0.5.1" dependencies = [ "anyhow", "clap", diff --git a/src/resolver.rs b/src/resolver.rs index cc8306d..56a1c30 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -1,88 +1,294 @@ //! Automatically resolves relationships between template and active configuration files. //! -//! The resolver allows `mould` to be run without explicit output arguments -//! by intelligently guessing the counterpart of a given input file based +//! The resolver allows `mould` to be run without explicit output arguments +//! by intelligently guessing the counterpart of a given input file based //! on common naming conventions. use std::path::{Path, PathBuf}; -/// Logic for determining which files to parse and where to save the results. -pub struct TemplateResolver; +pub struct Rule { + pub template_suffix: &'static str, + pub active_suffix: &'static str, + pub is_exact_match: bool, +} -impl TemplateResolver { - /// Determines the template and output paths based on the provided input. - /// - /// If an output path is explicitly provided via CLI arguments, it is used. - /// Otherwise, the resolver applies a set of heuristic rules to find a matching pairing. - pub fn resolve( - input: &Path, - output_override: Option, - ) -> (PathBuf, PathBuf) { - if let Some(out) = output_override { - return (input.to_path_buf(), out); - } +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, + }, + Rule { + template_suffix: ".example.xml", + active_suffix: ".xml", + is_exact_match: false, + }, + Rule { + template_suffix: ".template.xml", + active_suffix: ".xml", + is_exact_match: false, + }, + Rule { + template_suffix: ".example.ini", + active_suffix: ".ini", + is_exact_match: false, + }, + Rule { + template_suffix: ".template.ini", + active_suffix: ".ini", + is_exact_match: false, + }, + Rule { + template_suffix: ".example.properties", + active_suffix: ".properties", + is_exact_match: false, + }, + Rule { + template_suffix: ".template.properties", + active_suffix: ".properties", + is_exact_match: false, + }, +]; - // Apply automatic discovery rules based on file name patterns. - if let Some((template, output)) = Self::discover_pairing(input) { - (template, output) - } else { - // Fallback: If no pairing is found, use the input as both - // the template source and the save target. - (input.to_path_buf(), input.to_path_buf()) +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)); } } - /// Attempts to find a known template/active pairing for a given file path. - /// - /// Naming Rules Applied: - /// 1. `.env.example` <-> `.env` (Standard environment file pattern). - /// 2. `compose.yml` -> `compose.override.yml` (Docker Compose convention). - /// 3. `.template.` -> `.` (General template pattern). - /// 4. `..example` -> `.` (General example pattern). - fn discover_pairing(path: &Path) -> Option<(PathBuf, PathBuf)> { - let file_name = path.file_name()?.to_str()?; + input.with_extension(format!( + "{}.out", + input.extension().unwrap_or_default().to_string_lossy() + )) +} - // Rule 1: Standard .env pairing - if file_name == ".env" || file_name == ".env.example" { - let dir = path.parent().unwrap_or_else(|| Path::new(".")); - return Some((dir.join(".env.example"), dir.join(".env"))); +/// 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); } + } - // Rule 2: Docker Compose pairing - if file_name == "docker-compose.yml" || file_name == "docker-compose.yaml" || file_name == "compose.yml" { - let dir = path.parent().unwrap_or_else(|| Path::new(".")); - let override_file = if file_name == "compose.yml" { - "compose.override.yml" - } else { - "docker-compose.override.yml" - }; - return Some((path.to_path_buf(), dir.join(override_file))); + // 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; + } + } } - - // Rule 3: .template or .example suffix removal - if file_name.contains(".template.") { - let output_name = file_name.replace(".template.", "."); - return Some((path.to_path_buf(), path.with_file_name(output_name))); + if let Some(path) = fallback { + return Some(path); } - - if file_name.ends_with(".example") { - let output_name = &file_name[..file_name.len() - 8]; - return Some((path.to_path_buf(), path.with_file_name(output_name))); + } + + 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; } + } - // Inverse Rule 3: If running against the active file, look for the template counterpart. - let template_candidates = [ - format!("{}.example", file_name), - file_name.replace('.', ".template."), - ]; + // Fallback template detection + if !is_template && (file_name.contains(".example") || file_name.contains(".template")) { + is_template = true; + } - for t in template_candidates { - let p = path.with_file_name(t); - if p.exists() { - return Some((p, path.to_path_buf())); + 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; + } + } } } - None + // 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