ryo_storage/storage/
config.rs1use serde::{Deserialize, Serialize};
21use std::path::{Path, PathBuf};
22use std::time::Duration;
23
24pub const CONFIG_FILE: &str = "config.toml";
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29#[serde(default)]
30pub struct ServerConfig {
31 pub idle_timeout: u64,
34
35 pub startup_timeout: u64,
38
39 pub parallel_init: bool,
42
43 pub watch: bool,
46
47 pub watch_debounce_ms: u64,
50}
51
52impl Default for ServerConfig {
53 fn default() -> Self {
54 Self {
55 idle_timeout: 3600, startup_timeout: 300, parallel_init: true,
58 watch: true, watch_debounce_ms: 500, }
61 }
62}
63
64impl ServerConfig {
65 pub fn idle_timeout_duration(&self) -> Option<Duration> {
67 if self.idle_timeout == 0 {
68 None
69 } else {
70 Some(Duration::from_secs(self.idle_timeout))
71 }
72 }
73
74 pub fn startup_timeout_duration(&self) -> Duration {
76 Duration::from_secs(self.startup_timeout)
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(default)]
83pub struct CliConfig {
84 pub verbose: bool,
86
87 pub color: String,
89
90 pub progress: bool,
92}
93
94impl Default for CliConfig {
95 fn default() -> Self {
96 Self {
97 verbose: false,
98 color: "auto".to_string(),
99 progress: true,
100 }
101 }
102}
103
104#[derive(Debug, Clone, Default, Serialize, Deserialize)]
106#[serde(default)]
107pub struct GlobalConfig {
108 pub server: ServerConfig,
110
111 pub cli: CliConfig,
113}
114
115impl GlobalConfig {
116 pub fn load(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
118 let path = path.as_ref();
119 if !path.exists() {
120 return Ok(Self::default());
121 }
122
123 let content = std::fs::read_to_string(path)
124 .map_err(|e| ConfigError::Io(format!("{}: {}", path.display(), e)))?;
125
126 toml::from_str(&content)
127 .map_err(|e| ConfigError::Parse(format!("{}: {}", path.display(), e)))
128 }
129
130 pub fn load_global() -> Result<Self, ConfigError> {
132 let home = dirs::home_dir()
133 .ok_or_else(|| ConfigError::Io("Could not find home directory".to_string()))?;
134 let path = home.join(".ryo").join(CONFIG_FILE);
135 Self::load(&path)
136 }
137
138 pub fn global_path() -> Option<PathBuf> {
140 dirs::home_dir().map(|h| h.join(".ryo").join(CONFIG_FILE))
141 }
142
143 pub fn save(&self, path: impl AsRef<Path>) -> Result<(), ConfigError> {
145 let path = path.as_ref();
146
147 if let Some(parent) = path.parent() {
149 std::fs::create_dir_all(parent)
150 .map_err(|e| ConfigError::Io(format!("mkdir {}: {}", parent.display(), e)))?;
151 }
152
153 let content =
154 toml::to_string_pretty(self).map_err(|e| ConfigError::Serialize(e.to_string()))?;
155
156 std::fs::write(path, content)
157 .map_err(|e| ConfigError::Io(format!("{}: {}", path.display(), e)))
158 }
159
160 pub fn save_global(&self) -> Result<(), ConfigError> {
162 let path = Self::global_path()
163 .ok_or_else(|| ConfigError::Io("Could not find home directory".to_string()))?;
164 self.save(&path)
165 }
166
167 pub fn init_global() -> Result<(), ConfigError> {
169 let path = Self::global_path()
170 .ok_or_else(|| ConfigError::Io("Could not find home directory".to_string()))?;
171
172 if !path.exists() {
173 Self::default().save(&path)?;
174 }
175 Ok(())
176 }
177}
178
179#[derive(Debug, thiserror::Error)]
181pub enum ConfigError {
182 #[error("IO error: {0}")]
184 Io(String),
185
186 #[error("Parse error: {0}")]
188 Parse(String),
189
190 #[error("Serialize error: {0}")]
192 Serialize(String),
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use tempfile::TempDir;
199
200 #[test]
201 fn test_default_config() {
202 let config = GlobalConfig::default();
203 assert_eq!(config.server.idle_timeout, 3600);
204 assert_eq!(config.server.startup_timeout, 300);
205 assert!(config.server.parallel_init);
206 assert!(config.server.watch); assert_eq!(config.server.watch_debounce_ms, 500);
208 assert!(!config.cli.verbose);
209 assert_eq!(config.cli.color, "auto");
210 }
211
212 #[test]
213 fn test_idle_timeout_duration() {
214 let mut config = ServerConfig::default();
215 assert!(config.idle_timeout_duration().is_some());
216
217 config.idle_timeout = 0;
218 assert!(config.idle_timeout_duration().is_none());
219 }
220
221 #[test]
222 fn test_save_and_load() {
223 let temp = TempDir::new().unwrap();
224 let path = temp.path().join("config.toml");
225
226 let mut config = GlobalConfig::default();
227 config.server.idle_timeout = 7200;
228 config.cli.verbose = true;
229
230 config.save(&path).unwrap();
231 let loaded = GlobalConfig::load(&path).unwrap();
232
233 assert_eq!(loaded.server.idle_timeout, 7200);
234 assert!(loaded.cli.verbose);
235 }
236
237 #[test]
238 fn test_load_missing_file() {
239 let config = GlobalConfig::load("/nonexistent/config.toml").unwrap();
240 assert_eq!(config.server.idle_timeout, 3600);
242 }
243
244 #[test]
245 fn test_parse_partial_config() {
246 let temp = TempDir::new().unwrap();
247 let path = temp.path().join("config.toml");
248
249 std::fs::write(
251 &path,
252 r#"
253[server]
254idle_timeout = 1800
255"#,
256 )
257 .unwrap();
258
259 let config = GlobalConfig::load(&path).unwrap();
260 assert_eq!(config.server.idle_timeout, 1800);
261 assert_eq!(config.server.startup_timeout, 300);
263 assert!(config.server.parallel_init);
264 }
265}