use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct FormatterConfig {
pub indent_width: usize,
pub use_tabs: bool,
pub quote_variables: bool,
pub use_double_brackets: bool,
pub normalize_functions: bool,
pub inline_then: bool,
pub space_before_brace: bool,
pub preserve_blank_lines: bool,
pub max_blank_lines: usize,
pub ignore_patterns: Vec<String>,
}
impl Default for FormatterConfig {
fn default() -> Self {
Self {
indent_width: 2,
use_tabs: false,
quote_variables: true,
use_double_brackets: true,
normalize_functions: true,
inline_then: true,
space_before_brace: true,
preserve_blank_lines: true,
max_blank_lines: 2,
ignore_patterns: vec![],
}
}
}
impl FormatterConfig {
pub fn new() -> Self {
Self::default()
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, String> {
let contents = std::fs::read_to_string(path.as_ref())
.map_err(|e| format!("Failed to read config file: {}", e))?;
Self::from_toml(&contents)
}
pub fn from_toml(toml_str: &str) -> Result<Self, String> {
toml::from_str(toml_str.trim()).map_err(|e| format!("Failed to parse config TOML: {}", e))
}
pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), String> {
let toml_str = self.to_toml()?;
std::fs::write(path.as_ref(), toml_str)
.map_err(|e| format!("Failed to write config file: {}", e))
}
pub fn to_toml(&self) -> Result<String, String> {
toml::to_string_pretty(self).map_err(|e| format!("Failed to serialize config: {}", e))
}
pub fn should_ignore(&self, path: &str) -> bool {
for pattern in &self.ignore_patterns {
if path.contains(pattern.trim_start_matches("**/").trim_end_matches("/**")) {
return true;
}
}
false
}
pub fn merge(&mut self, other: Self) {
let default = Self::default();
if other.indent_width != default.indent_width {
self.indent_width = other.indent_width;
}
if other.use_tabs != default.use_tabs {
self.use_tabs = other.use_tabs;
}
if other.quote_variables != default.quote_variables {
self.quote_variables = other.quote_variables;
}
if other.use_double_brackets != default.use_double_brackets {
self.use_double_brackets = other.use_double_brackets;
}
if other.normalize_functions != default.normalize_functions {
self.normalize_functions = other.normalize_functions;
}
if other.inline_then != default.inline_then {
self.inline_then = other.inline_then;
}
if other.space_before_brace != default.space_before_brace {
self.space_before_brace = other.space_before_brace;
}
if other.preserve_blank_lines != default.preserve_blank_lines {
self.preserve_blank_lines = other.preserve_blank_lines;
}
if other.max_blank_lines != default.max_blank_lines {
self.max_blank_lines = other.max_blank_lines;
}
if !other.ignore_patterns.is_empty() {
self.ignore_patterns.extend(other.ignore_patterns);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = FormatterConfig::default();
assert_eq!(config.indent_width, 2);
assert!(!config.use_tabs);
assert!(config.quote_variables);
assert!(config.use_double_brackets);
assert!(config.normalize_functions);
}
#[test]
fn test_from_toml() {
let toml = r#"
indent_width = 4
use_tabs = true
quote_variables = false
use_double_brackets = false
normalize_functions = false
inline_then = false
space_before_brace = false
preserve_blank_lines = false
max_blank_lines = 1
ignore_patterns = ["*.test.sh"]
"#;
let config = FormatterConfig::from_toml(toml).unwrap();
assert_eq!(config.indent_width, 4);
assert!(config.use_tabs);
assert!(!config.quote_variables);
assert!(!config.use_double_brackets);
assert!(!config.normalize_functions);
}
#[test]
fn test_to_toml() {
let config = FormatterConfig::default();
let toml = config.to_toml().unwrap();
assert!(toml.contains("indent_width = 2"));
assert!(toml.contains("use_tabs = false"));
assert!(toml.contains("quote_variables = true"));
}
#[test]
fn test_should_ignore() {
let config = FormatterConfig {
ignore_patterns: vec!["**/target/**".to_string(), "**/test/**".to_string()],
..Default::default()
};
assert!(config.should_ignore("src/target/debug/script.sh"));
assert!(config.should_ignore("src/test/integration.sh"));
assert!(!config.should_ignore("src/main.sh"));
}
#[test]
fn test_merge() {
let mut base = FormatterConfig::default();
let override_config = FormatterConfig {
indent_width: 4,
use_tabs: true,
..Default::default()
};
base.merge(override_config);
assert_eq!(base.indent_width, 4);
assert!(base.use_tabs);
assert_eq!(base.max_blank_lines, 2); }
#[test]
fn test_config_round_trip() {
let original = FormatterConfig {
indent_width: 4,
use_tabs: true,
quote_variables: false,
..Default::default()
};
let toml = original.to_toml().unwrap();
let loaded = FormatterConfig::from_toml(&toml).unwrap();
assert_eq!(loaded.indent_width, original.indent_width);
assert_eq!(loaded.use_tabs, original.use_tabs);
assert_eq!(loaded.quote_variables, original.quote_variables);
}
#[test]
fn test_new_equals_default() {
let new_config = FormatterConfig::new();
let default_config = FormatterConfig::default();
assert_eq!(new_config.indent_width, default_config.indent_width);
assert_eq!(new_config.use_tabs, default_config.use_tabs);
}
#[test]
fn test_from_file_and_to_file() {
let temp_dir = tempfile::TempDir::new().unwrap();
let config_path = temp_dir.path().join("test_config.toml");
let config = FormatterConfig {
indent_width: 8,
use_tabs: true,
..Default::default()
};
config.to_file(&config_path).unwrap();
let loaded = FormatterConfig::from_file(&config_path).unwrap();
assert_eq!(loaded.indent_width, 8);
assert!(loaded.use_tabs);
}
#[test]
fn test_from_file_nonexistent() {
let result = FormatterConfig::from_file("/nonexistent/path/config.toml");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Failed to read"));
}
#[test]
fn test_from_toml_invalid() {
let result = FormatterConfig::from_toml("invalid [[[ toml");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Failed to parse"));
}
#[test]
fn test_should_ignore_empty_patterns() {
let config = FormatterConfig::default();
assert!(!config.should_ignore("any/path/file.sh"));
}
#[test]
fn test_merge_all_fields() {
let mut base = FormatterConfig::default();
let override_config = FormatterConfig {
indent_width: 8,
use_tabs: true,
quote_variables: false,
use_double_brackets: false,
normalize_functions: false,
inline_then: false,
space_before_brace: false,
preserve_blank_lines: false,
max_blank_lines: 5,
ignore_patterns: vec!["*.bak".to_string()],
};
base.merge(override_config);
assert_eq!(base.indent_width, 8);
assert!(base.use_tabs);
assert!(!base.quote_variables);
assert!(!base.use_double_brackets);
assert!(!base.normalize_functions);
assert!(!base.inline_then);
assert!(!base.space_before_brace);
assert!(!base.preserve_blank_lines);
assert_eq!(base.max_blank_lines, 5);
assert!(base.ignore_patterns.contains(&"*.bak".to_string()));
}
#[test]
fn test_merge_preserves_base_when_other_is_default() {
let mut base = FormatterConfig {
indent_width: 4,
use_tabs: true,
..Default::default()
};
let other = FormatterConfig::default();
base.merge(other);
assert_eq!(base.indent_width, 4);
assert!(base.use_tabs);
}
#[test]
fn test_partial_toml_uses_defaults() {
let toml = "indent_width = 6";
let config = FormatterConfig::from_toml(toml).unwrap();
assert_eq!(config.indent_width, 6);
assert!(!config.use_tabs);
assert!(config.quote_variables);
}
#[test]
fn test_debug_impl() {
let config = FormatterConfig::default();
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("FormatterConfig"));
assert!(debug_str.contains("indent_width"));
}
#[test]
fn test_clone_impl() {
let config = FormatterConfig {
indent_width: 4,
ignore_patterns: vec!["test".to_string()],
..Default::default()
};
let cloned = config.clone();
assert_eq!(cloned.indent_width, 4);
assert_eq!(cloned.ignore_patterns, vec!["test".to_string()]);
}
}