ggen-domain 5.1.3

Domain logic layer for ggen - pure business logic without CLI dependencies
Documentation
//! Warning header injection for generated files
//!
//! Implements `[generation.poka_yoke].warning_headers` behavior from ggen.toml
//! to add "DO NOT EDIT" headers to generated files.

use serde::{Deserialize, Serialize};
use std::path::Path;

/// Poka-Yoke settings for generation safety
///
/// Local definition to avoid cyclic dependency with ggen-config.
/// Mirrors [generation.poka_yoke] from ggen.toml.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PokaYokeSettings {
    /// Add warning headers to generated files
    #[serde(default = "default_true")]
    pub warning_headers: bool,
    /// Add generated paths to .gitignore
    #[serde(default)]
    pub gitignore_generated: bool,
    /// Add generated paths to .gitattributes
    #[serde(default)]
    pub gitattributes_generated: bool,
    /// Validate imports don't reference generated code
    #[serde(default)]
    pub validate_imports: bool,
}

fn default_true() -> bool {
    true
}

/// Generation safety configuration
///
/// Local definition to avoid cyclic dependency with ggen-config.
/// Mirrors [generation] from ggen.toml.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GenerationSafetyConfig {
    /// Enable generation safety features
    #[serde(default = "default_true")]
    pub enabled: bool,
    /// Paths that MUST NEVER be overwritten
    #[serde(default)]
    pub protected_paths: Vec<String>,
    /// Paths safe to regenerate (overwrite)
    #[serde(default)]
    pub regenerate_paths: Vec<String>,
    /// Custom header text for generated files
    pub generated_header: Option<String>,
    /// Require confirmation before overwriting
    #[serde(default)]
    pub require_confirmation: bool,
    /// Backup files before writing
    #[serde(default)]
    pub backup_before_write: bool,
    /// Poka-Yoke sub-settings
    pub poka_yoke: Option<PokaYokeSettings>,
}

/// Configuration for header injection
#[derive(Debug, Clone)]
pub struct HeaderInjectionConfig {
    /// Whether to inject headers
    pub enabled: bool,
    /// The header text (without comment prefix)
    pub header_text: String,
}

impl HeaderInjectionConfig {
    /// Create from GenerationSafetyConfig
    pub fn from_config(config: &GenerationSafetyConfig) -> Self {
        let enabled = config.poka_yoke.as_ref().is_none_or(|p| p.warning_headers);

        let header_text = config.generated_header.clone().unwrap_or_else(|| {
            "DO NOT EDIT - Generated by ggen. Changes will be overwritten.".to_string()
        });

        Self {
            enabled,
            header_text,
        }
    }

    /// Create with default settings
    pub fn default_config() -> Self {
        Self {
            enabled: true,
            header_text: "DO NOT EDIT - Generated by ggen. Changes will be overwritten."
                .to_string(),
        }
    }

    /// Create a disabled config (for testing or explicit opt-out)
    pub fn disabled() -> Self {
        Self {
            enabled: false,
            header_text: String::new(),
        }
    }
}

/// Format a header comment for a specific file extension
///
/// Returns the header with appropriate comment syntax for the file type.
pub fn format_header_for_extension(header: &str, extension: &str) -> String {
    let (prefix, suffix) = match extension.to_lowercase().as_str() {
        // Rust, Go, Java, JavaScript, TypeScript, C, C++, C#, Swift, Kotlin
        "rs" | "go" | "java" | "js" | "ts" | "jsx" | "tsx" | "c" | "cpp" | "h" | "hpp" | "cs"
        | "swift" | "kt" | "kts" | "scala" | "groovy" => ("//", None),

        // Python, Ruby, Shell, YAML, TOML
        "py" | "rb" | "sh" | "bash" | "zsh" | "yaml" | "yml" | "toml" => ("#", None),

        // HTML, XML, SVG
        "html" | "htm" | "xml" | "svg" | "xhtml" => ("<!--", Some("-->")),

        // CSS, SCSS, Less
        "css" | "scss" | "sass" | "less" => ("/*", Some("*/")),

        // SQL
        "sql" => ("--", None),

        // Lua
        "lua" => ("--", None),

        // Haskell
        "hs" | "lhs" => ("--", None),

        // Elixir, Erlang
        "ex" | "exs" | "erl" => ("#", None),

        // Terraform, HCL
        "tf" | "hcl" => ("#", None),

        // Default to // style comments
        _ => ("//", None),
    };

    // Format each line with the comment prefix
    let lines: Vec<String> = header
        .lines()
        .map(|line| {
            if let Some(end) = suffix {
                format!("{} {} {}", prefix, line, end)
            } else {
                format!("{} {}", prefix, line)
            }
        })
        .collect();

    lines.join("\n")
}

/// Inject a warning header into generated content
///
/// Returns the content with the header prepended (if enabled).
pub fn inject_warning_header(
    content: &str, output_path: &Path, config: &HeaderInjectionConfig,
) -> String {
    if !config.enabled {
        return content.to_string();
    }

    // Get file extension
    let extension = output_path
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("");

    // Skip binary or unknown extensions
    if extension.is_empty() {
        return content.to_string();
    }

    // Format header for this file type
    let header = format_header_for_extension(&config.header_text, extension);

    // Handle shebang lines (keep them at top)
    if content.starts_with("#!") {
        if let Some(newline_pos) = content.find('\n') {
            let shebang = &content[..=newline_pos];
            let rest = &content[newline_pos + 1..];
            return format!("{}{}\n\n{}", shebang, header, rest);
        }
    }

    // Prepend header with blank line
    format!("{}\n\n{}", header, content)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_format_header_rust() {
        let header = "DO NOT EDIT";
        let result = format_header_for_extension(header, "rs");
        assert_eq!(result, "// DO NOT EDIT");
    }

    #[test]
    fn test_format_header_python() {
        let header = "DO NOT EDIT";
        let result = format_header_for_extension(header, "py");
        assert_eq!(result, "# DO NOT EDIT");
    }

    #[test]
    fn test_format_header_html() {
        let header = "DO NOT EDIT";
        let result = format_header_for_extension(header, "html");
        assert_eq!(result, "<!-- DO NOT EDIT -->");
    }

    #[test]
    fn test_format_header_multiline() {
        let header = "DO NOT EDIT\nGenerated by ggen";
        let result = format_header_for_extension(header, "rs");
        assert_eq!(result, "// DO NOT EDIT\n// Generated by ggen");
    }

    #[test]
    fn test_inject_header_rust() {
        let content = "pub fn hello() {}";
        let config = HeaderInjectionConfig::default_config();
        let result = inject_warning_header(content, Path::new("test.rs"), &config);

        assert!(result.starts_with("// DO NOT EDIT"));
        assert!(result.contains("pub fn hello() {}"));
    }

    #[test]
    fn test_inject_header_preserves_shebang() {
        let content = "#!/usr/bin/env python3\nprint('hello')";
        let config = HeaderInjectionConfig::default_config();
        let result = inject_warning_header(content, Path::new("script.py"), &config);

        assert!(result.starts_with("#!/usr/bin/env python3\n"));
        assert!(result.contains("# DO NOT EDIT"));
        assert!(result.contains("print('hello')"));
    }

    #[test]
    fn test_inject_header_disabled() {
        let content = "pub fn hello() {}";
        let config = HeaderInjectionConfig::disabled();
        let result = inject_warning_header(content, Path::new("test.rs"), &config);

        assert_eq!(result, content);
    }

    #[test]
    fn test_config_from_generation_safety() {
        let safety_config = GenerationSafetyConfig {
            enabled: true,
            protected_paths: vec![],
            regenerate_paths: vec![],
            generated_header: Some("Custom Header".to_string()),
            require_confirmation: false,
            backup_before_write: false,
            poka_yoke: Some(PokaYokeSettings {
                warning_headers: true,
                gitignore_generated: false,
                gitattributes_generated: false,
                validate_imports: false,
            }),
        };

        let config = HeaderInjectionConfig::from_config(&safety_config);
        assert!(config.enabled);
        assert_eq!(config.header_text, "Custom Header");
    }

    #[test]
    fn test_config_disabled_via_poka_yoke() {
        let safety_config = GenerationSafetyConfig {
            enabled: true,
            protected_paths: vec![],
            regenerate_paths: vec![],
            generated_header: Some("Header".to_string()),
            require_confirmation: false,
            backup_before_write: false,
            poka_yoke: Some(PokaYokeSettings {
                warning_headers: false, // Disabled
                gitignore_generated: false,
                gitattributes_generated: false,
                validate_imports: false,
            }),
        };

        let config = HeaderInjectionConfig::from_config(&safety_config);
        assert!(!config.enabled);
    }
}