audiobook_forge/utils/
config.rs

1//! Configuration file management
2
3use crate::models::Config;
4use anyhow::{Context, Result};
5use std::fs;
6use std::path::PathBuf;
7
8/// Configuration manager for loading and saving config files
9pub struct ConfigManager;
10
11impl ConfigManager {
12    /// Get the default config file path
13    pub fn default_config_path() -> Result<PathBuf> {
14        let config_dir = dirs::config_dir()
15            .context("Cannot determine config directory")?
16            .join("audiobook-forge");
17
18        Ok(config_dir.join("config.yaml"))
19    }
20
21    /// Ensure config directory exists
22    pub fn ensure_config_dir() -> Result<PathBuf> {
23        let config_dir = dirs::config_dir()
24            .context("Cannot determine config directory")?
25            .join("audiobook-forge");
26
27        if !config_dir.exists() {
28            fs::create_dir_all(&config_dir)
29                .context("Failed to create config directory")?;
30        }
31
32        Ok(config_dir)
33    }
34
35    /// Load configuration from file or create default
36    pub fn load_or_default(path: Option<&PathBuf>) -> Result<Config> {
37        let config_path = match path {
38            Some(p) => p.clone(),
39            None => Self::default_config_path()?,
40        };
41
42        if config_path.exists() {
43            Self::load(&config_path)
44        } else {
45            Ok(Config::default())
46        }
47    }
48
49    /// Load configuration from file
50    pub fn load(path: &PathBuf) -> Result<Config> {
51        let contents = fs::read_to_string(path)
52            .with_context(|| format!("Failed to read config file: {}", path.display()))?;
53
54        let config: Config = serde_yaml::from_str(&contents)
55            .with_context(|| format!("Failed to parse config file: {}", path.display()))?;
56
57        Ok(config)
58    }
59
60    /// Save configuration to file
61    pub fn save(config: &Config, path: Option<&PathBuf>) -> Result<()> {
62        let config_path = match path {
63            Some(p) => p.clone(),
64            None => Self::default_config_path()?,
65        };
66
67        // Ensure parent directory exists
68        if let Some(parent) = config_path.parent() {
69            if !parent.exists() {
70                fs::create_dir_all(parent)
71                    .context("Failed to create config directory")?;
72            }
73        }
74
75        let yaml = serde_yaml::to_string(config)
76            .context("Failed to serialize config to YAML")?;
77
78        fs::write(&config_path, yaml)
79            .with_context(|| format!("Failed to write config file: {}", config_path.display()))?;
80
81        Ok(())
82    }
83
84    /// Initialize config file with defaults and comprehensive comments
85    pub fn init(force: bool) -> Result<PathBuf> {
86        let config_path = Self::default_config_path()?;
87
88        if config_path.exists() && !force {
89            anyhow::bail!(
90                "Config file already exists at: {}\nUse --force to overwrite",
91                config_path.display()
92            );
93        }
94
95        // Create documented config
96        let config_content = include_str!("../../templates/config.yaml");
97
98        // Ensure parent directory exists
99        if let Some(parent) = config_path.parent() {
100            if !parent.exists() {
101                fs::create_dir_all(parent)?;
102            }
103        }
104
105        fs::write(&config_path, config_content)
106            .with_context(|| format!("Failed to write config file: {}", config_path.display()))?;
107
108        Ok(config_path)
109    }
110
111    /// Validate configuration
112    pub fn validate(config: &Config) -> Result<Vec<String>> {
113        let mut warnings = Vec::new();
114
115        // Validate parallel workers
116        if config.processing.parallel_workers < 1 || config.processing.parallel_workers > 8 {
117            warnings.push(format!(
118                "parallel_workers ({}) should be between 1 and 8",
119                config.processing.parallel_workers
120            ));
121        }
122
123        // Validate chapter source
124        let valid_chapter_sources = ["auto", "files", "cue", "id3", "none"];
125        if !valid_chapter_sources.contains(&config.quality.chapter_source.as_str()) {
126            warnings.push(format!(
127                "chapter_source '{}' is not recognized. Valid options: {}",
128                config.quality.chapter_source,
129                valid_chapter_sources.join(", ")
130            ));
131        }
132
133        // Validate log level
134        let valid_log_levels = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"];
135        if !valid_log_levels.contains(&config.logging.log_level.to_uppercase().as_str()) {
136            warnings.push(format!(
137                "log_level '{}' is not recognized. Valid options: {}",
138                config.logging.log_level,
139                valid_log_levels.join(", ")
140            ));
141        }
142
143        // Check if custom paths exist
144        if let Some(ref path) = config.directories.source {
145            if !path.exists() {
146                warnings.push(format!(
147                    "source directory does not exist: {}",
148                    path.display()
149                ));
150            }
151        }
152
153        Ok(warnings)
154    }
155
156    /// Show current configuration
157    pub fn show(path: Option<&PathBuf>) -> Result<String> {
158        let config = Self::load_or_default(path)?;
159        let yaml = serde_yaml::to_string(&config)
160            .context("Failed to serialize config")?;
161        Ok(yaml)
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use tempfile::tempdir;
169
170    #[test]
171    fn test_load_save_config() {
172        let dir = tempdir().unwrap();
173        let config_path = dir.path().join("config.yaml");
174
175        let config = Config::default();
176        ConfigManager::save(&config, Some(&config_path)).unwrap();
177
178        let loaded = ConfigManager::load(&config_path).unwrap();
179        assert_eq!(loaded.processing.parallel_workers, 2);
180    }
181
182    #[test]
183    fn test_validate_config() {
184        let mut config = Config::default();
185        config.processing.parallel_workers = 10; // Invalid
186
187        let warnings = ConfigManager::validate(&config).unwrap();
188        assert!(!warnings.is_empty());
189    }
190}