1use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::Path;
6use thiserror::Error;
7
8#[derive(Debug, Error)]
10pub enum ConfigError {
11 #[error("IO error: {0}")]
13 IoError(#[from] std::io::Error),
14
15 #[error("Parse error: {0}")]
17 ParseError(#[from] toml::de::Error),
18
19 #[error("Serialize error: {0}")]
21 SerializeError(#[from] toml::ser::Error),
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct Config {
27 pub rust_bucket_version: String,
29 pub test_timeout: u32,
31 pub project_name: String,
33}
34
35impl Default for Config {
36 fn default() -> Self {
37 Self {
38 rust_bucket_version: env!("CARGO_PKG_VERSION").to_string(),
39 test_timeout: 120,
40 project_name: env!("CARGO_PKG_NAME").to_string(),
41 }
42 }
43}
44
45impl Config {
46 pub fn load(path: &Path) -> Result<Config, ConfigError> {
48 let contents = fs::read_to_string(path)?;
49 let config = toml::from_str(&contents)?;
50 Ok(config)
51 }
52
53 pub fn save(&self, path: &Path) -> Result<(), ConfigError> {
55 let toml_string = toml::to_string_pretty(self)?;
56 let version = env!("CARGO_PKG_VERSION");
57 let header = format!(
58 "# Generated by rust-bucket v{}. DO NOT EDIT BY HAND.\n\n",
59 version
60 );
61 let contents = header + &toml_string;
62 fs::write(path, contents)?;
63 Ok(())
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70 use std::path::PathBuf;
71
72 #[test]
73 fn test_default_config() {
74 let config = Config::default();
75 assert_eq!(config.rust_bucket_version, env!("CARGO_PKG_VERSION"));
76 assert_eq!(config.test_timeout, 120);
77 assert_eq!(config.project_name, env!("CARGO_PKG_NAME"));
78 }
79
80 #[test]
81 fn test_save_and_load_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
82 let temp_dir = tempfile::tempdir()?;
83 let config_path = temp_dir.path().join("rust-bucket.toml");
84
85 let original_config = Config {
87 rust_bucket_version: "0.1.0".to_string(),
88 test_timeout: 300,
89 project_name: "test-project".to_string(),
90 };
91
92 original_config.save(&config_path)?;
94
95 let file_contents = fs::read_to_string(&config_path)?;
97 assert!(file_contents.starts_with("# Generated by rust-bucket v"));
98 assert!(file_contents.contains("DO NOT EDIT BY HAND"));
99
100 let loaded_config = Config::load(&config_path)?;
102
103 assert_eq!(loaded_config.rust_bucket_version, "0.1.0");
105 assert_eq!(loaded_config.test_timeout, 300);
106 assert_eq!(loaded_config.project_name, "test-project");
107 Ok(())
108 }
109
110 #[test]
111 fn test_load_nonexistent_file() {
112 let path = PathBuf::from("/nonexistent/path/config.toml");
113 let result = Config::load(&path);
114 assert!(result.is_err());
115 assert!(matches!(result.unwrap_err(), ConfigError::IoError(_)));
116 }
117
118 #[test]
119 fn test_load_invalid_toml() -> Result<(), Box<dyn std::error::Error>> {
120 let temp_dir = tempfile::tempdir()?;
121 let config_path = temp_dir.path().join("invalid.toml");
122
123 fs::write(&config_path, "this is not valid toml { [ }")?;
125
126 let result = Config::load(&config_path);
127 assert!(result.is_err());
128 assert!(matches!(result.unwrap_err(), ConfigError::ParseError(_)));
129 Ok(())
130 }
131
132 #[test]
133 fn test_save_to_readonly_directory() {
134 }
138}