1use std::path::PathBuf;
9
10use serde::{Deserialize, Serialize};
11
12use crate::alias::Alias;
13use crate::error::{Error, Result};
14
15pub const SCHEMA_VERSION: u32 = 1;
22
23const DEFAULT_OUTPUT: &str = "human";
25
26const DEFAULT_COLOR: &str = "auto";
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct Config {
32 pub schema_version: u32,
34
35 #[serde(default)]
37 pub defaults: Defaults,
38
39 #[serde(default)]
41 pub aliases: Vec<Alias>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct Defaults {
47 #[serde(default = "default_output")]
49 pub output: String,
50
51 #[serde(default = "default_color")]
53 pub color: String,
54
55 #[serde(default = "default_true")]
57 pub progress: bool,
58}
59
60fn default_output() -> String {
61 DEFAULT_OUTPUT.to_string()
62}
63
64fn default_color() -> String {
65 DEFAULT_COLOR.to_string()
66}
67
68fn default_true() -> bool {
69 true
70}
71
72impl Default for Defaults {
73 fn default() -> Self {
74 Self {
75 output: default_output(),
76 color: default_color(),
77 progress: true,
78 }
79 }
80}
81
82impl Default for Config {
83 fn default() -> Self {
84 Self {
85 schema_version: SCHEMA_VERSION,
86 defaults: Defaults::default(),
87 aliases: Vec::new(),
88 }
89 }
90}
91
92#[derive(Debug)]
94pub struct ConfigManager {
95 config_path: PathBuf,
96}
97
98impl ConfigManager {
99 pub fn new() -> Result<Self> {
104 let config_dir = if let Ok(dir) = std::env::var("RC_CONFIG_DIR") {
105 PathBuf::from(dir)
106 } else {
107 dirs::config_dir()
108 .ok_or_else(|| Error::Config("Could not determine config directory".into()))?
109 .join("rc")
110 };
111 let config_path = config_dir.join("config.toml");
112 Ok(Self { config_path })
113 }
114
115 pub fn with_path(path: PathBuf) -> Self {
117 Self { config_path: path }
118 }
119
120 pub fn config_path(&self) -> &PathBuf {
122 &self.config_path
123 }
124
125 pub fn load(&self) -> Result<Config> {
130 if !self.config_path.exists() {
131 return Ok(Config::default());
132 }
133
134 let content = std::fs::read_to_string(&self.config_path)?;
135 let mut config: Config = toml::from_str(&content)?;
136
137 if config.schema_version < SCHEMA_VERSION {
139 config = self.migrate(config)?;
140 } else if config.schema_version > SCHEMA_VERSION {
141 return Err(Error::Config(format!(
142 "Configuration file version {} is newer than supported version {}. Please upgrade rc.",
143 config.schema_version, SCHEMA_VERSION
144 )));
145 }
146
147 Ok(config)
148 }
149
150 pub fn save(&self, config: &Config) -> Result<()> {
155 if let Some(parent) = self.config_path.parent() {
157 std::fs::create_dir_all(parent)?;
158 }
159
160 let content = toml::to_string_pretty(config)?;
161 std::fs::write(&self.config_path, content)?;
162
163 #[cfg(unix)]
165 {
166 use std::os::unix::fs::PermissionsExt;
167 let permissions = std::fs::Permissions::from_mode(0o600);
168 std::fs::set_permissions(&self.config_path, permissions)?;
169 }
170
171 Ok(())
172 }
173
174 fn migrate(&self, config: Config) -> Result<Config> {
176 let mut config = config;
177
178 config.schema_version = SCHEMA_VERSION;
185 Ok(config)
186 }
187}
188
189impl Default for ConfigManager {
190 fn default() -> Self {
191 Self::new().expect("Failed to create default ConfigManager")
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use tempfile::TempDir;
199
200 fn temp_config_manager() -> (ConfigManager, TempDir) {
201 let temp_dir = TempDir::new().unwrap();
202 let config_path = temp_dir.path().join("config.toml");
203 let manager = ConfigManager::with_path(config_path);
204 (manager, temp_dir)
205 }
206
207 #[test]
208 fn test_default_config() {
209 let config = Config::default();
210 assert_eq!(config.schema_version, SCHEMA_VERSION);
211 assert_eq!(config.defaults.output, "human");
212 assert_eq!(config.defaults.color, "auto");
213 assert!(config.defaults.progress);
214 assert!(config.aliases.is_empty());
215 }
216
217 #[test]
218 fn test_load_nonexistent_returns_default() {
219 let (manager, _temp_dir) = temp_config_manager();
220 let config = manager.load().unwrap();
221 assert_eq!(config.schema_version, SCHEMA_VERSION);
222 }
223
224 #[test]
225 fn test_save_and_load() {
226 let (manager, _temp_dir) = temp_config_manager();
227
228 let mut config = Config::default();
229 config.aliases.push(Alias {
230 name: "test".to_string(),
231 endpoint: "http://localhost:9000".to_string(),
232 access_key: "accesskey".to_string(),
233 secret_key: "secretkey".to_string(),
234 region: "us-east-1".to_string(),
235 signature: "v4".to_string(),
236 bucket_lookup: "auto".to_string(),
237 insecure: false,
238 ca_bundle: None,
239 retry: None,
240 timeout: None,
241 });
242
243 manager.save(&config).unwrap();
244 let loaded = manager.load().unwrap();
245
246 assert_eq!(loaded.aliases.len(), 1);
247 assert_eq!(loaded.aliases[0].name, "test");
248 }
249
250 #[test]
251 fn test_schema_version_too_new() {
252 let (manager, _temp_dir) = temp_config_manager();
253
254 let content = format!(
255 r#"
256 schema_version = {}
257 "#,
258 SCHEMA_VERSION + 1
259 );
260 std::fs::write(manager.config_path(), content).unwrap();
261
262 let result = manager.load();
263 assert!(result.is_err());
264 assert!(
265 result
266 .unwrap_err()
267 .to_string()
268 .contains("newer than supported")
269 );
270 }
271}