1use crate::{Error, Result};
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6use tokio::fs;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Config {
11 pub initialized: bool,
13 pub version: String,
15 pub update_channel: String,
17 pub auto_update: bool,
19 pub clippy_rules: Vec<String>,
21 pub max_file_lines: usize,
23 pub max_function_lines: usize,
25 pub enforce_edition_2024: bool,
27 pub ban_underscore_bandaid: bool,
29 pub require_documentation: bool,
31 pub custom_rules: Vec<CustomRule>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct CustomRule {
38 pub name: String,
40 pub pattern: String,
42 pub message: String,
44 pub enabled: bool,
46}
47
48impl Default for Config {
49 fn default() -> Self {
50 Self {
51 initialized: false,
52 version: "0.1.0".to_string(),
53 update_channel: "stable".to_string(),
54 auto_update: true,
55 clippy_rules: vec![
56 "-D warnings".to_string(),
57 "-D clippy::unwrap_used".to_string(),
58 "-D clippy::expect_used".to_string(),
59 "-D clippy::panic".to_string(),
60 "-D clippy::unimplemented".to_string(),
61 "-D clippy::todo".to_string(),
62 ],
63 max_file_lines: 300,
64 max_function_lines: 50,
65 enforce_edition_2024: true,
66 ban_underscore_bandaid: true,
67 require_documentation: true,
68 custom_rules: vec![],
69 }
70 }
71}
72
73impl Config {
74 pub async fn load_or_default() -> Result<Self> {
76 match Self::load().await {
77 Ok(config) => Ok(config),
78 Err(_) => Ok(Self::default()),
79 }
80 }
81
82 pub async fn load() -> Result<Self> {
84 let config_path = Self::config_file_path()?;
85 let contents = fs::read_to_string(&config_path)
86 .await
87 .map_err(|e| Error::config(format!("Failed to read config file: {}", e)))?;
88
89 let config: Config = toml::from_str(&contents)
90 .map_err(|e| Error::config(format!("Failed to parse config file: {}", e)))?;
91
92 Ok(config)
93 }
94
95 pub async fn save(&self) -> Result<()> {
97 let config_path = Self::config_file_path()?;
98
99 if let Some(parent) = config_path.parent() {
101 fs::create_dir_all(parent).await?;
102 }
103
104 let contents = toml::to_string_pretty(self)
105 .map_err(|e| Error::config(format!("Failed to serialize config: {}", e)))?;
106
107 fs::write(&config_path, contents)
108 .await
109 .map_err(|e| Error::config(format!("Failed to write config file: {}", e)))?;
110
111 Ok(())
112 }
113
114 pub fn config_file_path() -> Result<PathBuf> {
116 let config_dir =
117 dirs::config_dir().ok_or_else(|| Error::config("Could not find config directory"))?;
118
119 Ok(config_dir.join("ferrous-forge").join("config.toml"))
120 }
121
122 pub fn config_dir_path() -> Result<PathBuf> {
124 let config_dir =
125 dirs::config_dir().ok_or_else(|| Error::config("Could not find config directory"))?;
126
127 Ok(config_dir.join("ferrous-forge"))
128 }
129
130 pub async fn ensure_directories(&self) -> Result<()> {
132 let config_dir = Self::config_dir_path()?;
133 fs::create_dir_all(&config_dir).await?;
134
135 fs::create_dir_all(config_dir.join("templates")).await?;
137 fs::create_dir_all(config_dir.join("rules")).await?;
138 fs::create_dir_all(config_dir.join("backups")).await?;
139
140 Ok(())
141 }
142
143 pub fn is_initialized(&self) -> bool {
145 self.initialized
146 }
147
148 pub fn mark_initialized(&mut self) {
150 self.initialized = true;
151 }
152
153 pub fn get(&self, key: &str) -> Option<String> {
155 match key {
156 "update_channel" => Some(self.update_channel.clone()),
157 "auto_update" => Some(self.auto_update.to_string()),
158 "max_file_lines" => Some(self.max_file_lines.to_string()),
159 "max_function_lines" => Some(self.max_function_lines.to_string()),
160 "enforce_edition_2024" => Some(self.enforce_edition_2024.to_string()),
161 "ban_underscore_bandaid" => Some(self.ban_underscore_bandaid.to_string()),
162 "require_documentation" => Some(self.require_documentation.to_string()),
163 _ => None,
164 }
165 }
166
167 pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
169 match key {
170 "update_channel" => {
171 if !["stable", "beta", "nightly"].contains(&value) {
172 return Err(Error::config(
173 "Invalid update channel. Must be: stable, beta, or nightly",
174 ));
175 }
176 self.update_channel = value.to_string();
177 }
178 "auto_update" => {
179 self.auto_update = value
180 .parse()
181 .map_err(|_| Error::config("Invalid boolean value for auto_update"))?;
182 }
183 "max_file_lines" => {
184 self.max_file_lines = value
185 .parse()
186 .map_err(|_| Error::config("Invalid number for max_file_lines"))?;
187 }
188 "max_function_lines" => {
189 self.max_function_lines = value
190 .parse()
191 .map_err(|_| Error::config("Invalid number for max_function_lines"))?;
192 }
193 "enforce_edition_2024" => {
194 self.enforce_edition_2024 = value
195 .parse()
196 .map_err(|_| Error::config("Invalid boolean value for enforce_edition_2024"))?;
197 }
198 "ban_underscore_bandaid" => {
199 self.ban_underscore_bandaid = value.parse().map_err(|_| {
200 Error::config("Invalid boolean value for ban_underscore_bandaid")
201 })?;
202 }
203 "require_documentation" => {
204 self.require_documentation = value.parse().map_err(|_| {
205 Error::config("Invalid boolean value for require_documentation")
206 })?;
207 }
208 _ => return Err(Error::config(format!("Unknown configuration key: {}", key))),
209 }
210
211 Ok(())
212 }
213
214 pub fn list(&self) -> Vec<(String, String)> {
216 vec![
217 ("update_channel".to_string(), self.update_channel.clone()),
218 ("auto_update".to_string(), self.auto_update.to_string()),
219 (
220 "max_file_lines".to_string(),
221 self.max_file_lines.to_string(),
222 ),
223 (
224 "max_function_lines".to_string(),
225 self.max_function_lines.to_string(),
226 ),
227 (
228 "enforce_edition_2024".to_string(),
229 self.enforce_edition_2024.to_string(),
230 ),
231 (
232 "ban_underscore_bandaid".to_string(),
233 self.ban_underscore_bandaid.to_string(),
234 ),
235 (
236 "require_documentation".to_string(),
237 self.require_documentation.to_string(),
238 ),
239 ]
240 }
241
242 pub fn reset(&mut self) {
244 *self = Self::default();
245 self.initialized = true; }
247}
248
249#[cfg(test)]
250#[allow(clippy::expect_used, clippy::unwrap_used)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn test_config_default() {
256 let config = Config::default();
257
258 assert!(!config.initialized);
259 assert_eq!(config.version, "0.1.0");
260 assert_eq!(config.update_channel, "stable");
261 assert!(config.auto_update);
262 assert_eq!(config.max_file_lines, 300);
263 assert_eq!(config.max_function_lines, 50);
264 assert!(config.enforce_edition_2024);
265 assert!(config.ban_underscore_bandaid);
266 assert!(config.require_documentation);
267 assert!(!config.clippy_rules.is_empty());
268 assert!(config.custom_rules.is_empty());
269 }
270
271 #[test]
272 fn test_config_initialization() {
273 let mut config = Config::default();
274
275 assert!(!config.is_initialized());
276 config.mark_initialized();
277 assert!(config.is_initialized());
278 }
279
280 #[test]
281 fn test_config_get() {
282 let config = Config::default();
283
284 assert_eq!(config.get("update_channel"), Some("stable".to_string()));
285 assert_eq!(config.get("auto_update"), Some("true".to_string()));
286 assert_eq!(config.get("max_file_lines"), Some("300".to_string()));
287 assert_eq!(config.get("max_function_lines"), Some("50".to_string()));
288 assert_eq!(config.get("enforce_edition_2024"), Some("true".to_string()));
289 assert_eq!(
290 config.get("ban_underscore_bandaid"),
291 Some("true".to_string())
292 );
293 assert_eq!(
294 config.get("require_documentation"),
295 Some("true".to_string())
296 );
297 assert_eq!(config.get("nonexistent"), None);
298 }
299
300 #[test]
301 fn test_config_set_update_channel() {
302 let mut config = Config::default();
303
304 assert!(config.set("update_channel", "stable").is_ok());
306 assert_eq!(config.update_channel, "stable");
307
308 assert!(config.set("update_channel", "beta").is_ok());
309 assert_eq!(config.update_channel, "beta");
310
311 assert!(config.set("update_channel", "nightly").is_ok());
312 assert_eq!(config.update_channel, "nightly");
313
314 assert!(config.set("update_channel", "invalid").is_err());
316 }
317
318 #[test]
319 fn test_config_set_boolean_values() {
320 let mut config = Config::default();
321
322 assert!(config.set("auto_update", "false").is_ok());
324 assert!(!config.auto_update);
325 assert!(config.set("auto_update", "true").is_ok());
326 assert!(config.auto_update);
327 assert!(config.set("auto_update", "invalid").is_err());
328
329 assert!(config.set("enforce_edition_2024", "false").is_ok());
331 assert!(!config.enforce_edition_2024);
332
333 assert!(config.set("ban_underscore_bandaid", "false").is_ok());
335 assert!(!config.ban_underscore_bandaid);
336
337 assert!(config.set("require_documentation", "false").is_ok());
339 assert!(!config.require_documentation);
340 }
341
342 #[test]
343 fn test_config_set_numeric_values() {
344 let mut config = Config::default();
345
346 assert!(config.set("max_file_lines", "500").is_ok());
348 assert_eq!(config.max_file_lines, 500);
349 assert!(config.set("max_file_lines", "invalid").is_err());
350
351 assert!(config.set("max_function_lines", "100").is_ok());
353 assert_eq!(config.max_function_lines, 100);
354 assert!(config.set("max_function_lines", "invalid").is_err());
355 }
356
357 #[test]
358 fn test_config_set_unknown_key() {
359 let mut config = Config::default();
360 assert!(config.set("unknown_key", "value").is_err());
361 }
362
363 #[test]
364 fn test_config_list() {
365 let config = Config::default();
366 let list = config.list();
367
368 assert_eq!(list.len(), 7);
369 assert!(list
370 .iter()
371 .any(|(k, v)| k == "update_channel" && v == "stable"));
372 assert!(list.iter().any(|(k, v)| k == "auto_update" && v == "true"));
373 assert!(list
374 .iter()
375 .any(|(k, v)| k == "max_file_lines" && v == "300"));
376 assert!(list
377 .iter()
378 .any(|(k, v)| k == "max_function_lines" && v == "50"));
379 }
380
381 #[test]
382 fn test_config_reset() {
383 let mut config = Config::default();
384 config.mark_initialized();
385 config.update_channel = "beta".to_string();
386 config.auto_update = false;
387
388 config.reset();
389
390 assert!(config.is_initialized()); assert_eq!(config.update_channel, "stable"); assert!(config.auto_update); }
394
395 #[test]
396 fn test_custom_rule() {
397 let rule = CustomRule {
398 name: "test_rule".to_string(),
399 pattern: r"test_.*".to_string(),
400 message: "Test message".to_string(),
401 enabled: true,
402 };
403
404 assert_eq!(rule.name, "test_rule");
405 assert_eq!(rule.pattern, r"test_.*");
406 assert_eq!(rule.message, "Test message");
407 assert!(rule.enabled);
408 }
409
410 #[test]
415 fn test_config_file_path() {
416 let result = Config::config_file_path();
417 assert!(result.is_ok());
418 let path = result.expect("Should get config file path");
419 assert!(path.to_string_lossy().contains("ferrous-forge"));
420 assert!(path.to_string_lossy().ends_with("config.toml"));
421 }
422
423 #[test]
424 fn test_config_dir_path() {
425 let result = Config::config_dir_path();
426 assert!(result.is_ok());
427 let path = result.expect("Should get config dir path");
428 assert!(path.to_string_lossy().contains("ferrous-forge"));
429 }
430
431 #[cfg(test)]
433 mod property_tests {
434 use super::*;
435 use proptest::prelude::*;
436
437 proptest! {
438 #[test]
439 fn test_config_get_set_roundtrip(
440 channel in prop::sample::select(vec!["stable", "beta", "nightly"]),
441 auto_update in any::<bool>(),
442 max_file_lines in 1usize..10000,
443 max_function_lines in 1usize..1000,
444 ) {
445 let mut config = Config::default();
446
447 prop_assert!(config.set("update_channel", channel).is_ok());
448 prop_assert_eq!(config.get("update_channel"), Some(channel.to_string()));
449
450 prop_assert!(config.set("auto_update", &auto_update.to_string()).is_ok());
451 prop_assert_eq!(config.get("auto_update"), Some(auto_update.to_string()));
452
453 prop_assert!(config.set("max_file_lines", &max_file_lines.to_string()).is_ok());
454 prop_assert_eq!(config.get("max_file_lines"), Some(max_file_lines.to_string()));
455
456 prop_assert!(config.set("max_function_lines", &max_function_lines.to_string()).is_ok());
457 prop_assert_eq!(config.get("max_function_lines"), Some(max_function_lines.to_string()));
458 }
459 }
460 }
461}