use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PokaYokeSettings {
#[serde(default = "default_true")]
pub warning_headers: bool,
#[serde(default)]
pub gitignore_generated: bool,
#[serde(default)]
pub gitattributes_generated: bool,
#[serde(default)]
pub validate_imports: bool,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GenerationSafetyConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub protected_paths: Vec<String>,
#[serde(default)]
pub regenerate_paths: Vec<String>,
pub generated_header: Option<String>,
#[serde(default)]
pub require_confirmation: bool,
#[serde(default)]
pub backup_before_write: bool,
pub poka_yoke: Option<PokaYokeSettings>,
}
#[derive(Debug, Clone)]
pub struct HeaderInjectionConfig {
pub enabled: bool,
pub header_text: String,
}
impl HeaderInjectionConfig {
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,
}
}
pub fn default_config() -> Self {
Self {
enabled: true,
header_text: "DO NOT EDIT - Generated by ggen. Changes will be overwritten."
.to_string(),
}
}
pub fn disabled() -> Self {
Self {
enabled: false,
header_text: String::new(),
}
}
}
pub fn format_header_for_extension(header: &str, extension: &str) -> String {
let (prefix, suffix) = match extension.to_lowercase().as_str() {
"rs" | "go" | "java" | "js" | "ts" | "jsx" | "tsx" | "c" | "cpp" | "h" | "hpp" | "cs"
| "swift" | "kt" | "kts" | "scala" | "groovy" => ("//", None),
"py" | "rb" | "sh" | "bash" | "zsh" | "yaml" | "yml" | "toml" => ("#", None),
"html" | "htm" | "xml" | "svg" | "xhtml" => ("<!--", Some("-->")),
"css" | "scss" | "sass" | "less" => ("/*", Some("*/")),
"sql" => ("--", None),
"lua" => ("--", None),
"hs" | "lhs" => ("--", None),
"ex" | "exs" | "erl" => ("#", None),
"tf" | "hcl" => ("#", None),
_ => ("//", None),
};
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")
}
pub fn inject_warning_header(
content: &str, output_path: &Path, config: &HeaderInjectionConfig,
) -> String {
if !config.enabled {
return content.to_string();
}
let extension = output_path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
if extension.is_empty() {
return content.to_string();
}
let header = format_header_for_extension(&config.header_text, extension);
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);
}
}
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, gitignore_generated: false,
gitattributes_generated: false,
validate_imports: false,
}),
};
let config = HeaderInjectionConfig::from_config(&safety_config);
assert!(!config.enabled);
}
}