use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::time::Duration;
pub const CONFIG_FILE: &str = "config.toml";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ServerConfig {
pub idle_timeout: u64,
pub startup_timeout: u64,
pub parallel_init: bool,
pub watch: bool,
pub watch_debounce_ms: u64,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
idle_timeout: 3600, startup_timeout: 300, parallel_init: true,
watch: true, watch_debounce_ms: 500, }
}
}
impl ServerConfig {
pub fn idle_timeout_duration(&self) -> Option<Duration> {
if self.idle_timeout == 0 {
None
} else {
Some(Duration::from_secs(self.idle_timeout))
}
}
pub fn startup_timeout_duration(&self) -> Duration {
Duration::from_secs(self.startup_timeout)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct CliConfig {
pub verbose: bool,
pub color: String,
pub progress: bool,
}
impl Default for CliConfig {
fn default() -> Self {
Self {
verbose: false,
color: "auto".to_string(),
progress: true,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct GlobalConfig {
pub server: ServerConfig,
pub cli: CliConfig,
}
impl GlobalConfig {
pub fn load(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
let path = path.as_ref();
if !path.exists() {
return Ok(Self::default());
}
let content = std::fs::read_to_string(path)
.map_err(|e| ConfigError::Io(format!("{}: {}", path.display(), e)))?;
toml::from_str(&content)
.map_err(|e| ConfigError::Parse(format!("{}: {}", path.display(), e)))
}
pub fn load_global() -> Result<Self, ConfigError> {
let home = dirs::home_dir()
.ok_or_else(|| ConfigError::Io("Could not find home directory".to_string()))?;
let path = home.join(".ryo").join(CONFIG_FILE);
Self::load(&path)
}
pub fn global_path() -> Option<PathBuf> {
dirs::home_dir().map(|h| h.join(".ryo").join(CONFIG_FILE))
}
pub fn save(&self, path: impl AsRef<Path>) -> Result<(), ConfigError> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| ConfigError::Io(format!("mkdir {}: {}", parent.display(), e)))?;
}
let content =
toml::to_string_pretty(self).map_err(|e| ConfigError::Serialize(e.to_string()))?;
std::fs::write(path, content)
.map_err(|e| ConfigError::Io(format!("{}: {}", path.display(), e)))
}
pub fn save_global(&self) -> Result<(), ConfigError> {
let path = Self::global_path()
.ok_or_else(|| ConfigError::Io("Could not find home directory".to_string()))?;
self.save(&path)
}
pub fn init_global() -> Result<(), ConfigError> {
let path = Self::global_path()
.ok_or_else(|| ConfigError::Io("Could not find home directory".to_string()))?;
if !path.exists() {
Self::default().save(&path)?;
}
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("IO error: {0}")]
Io(String),
#[error("Parse error: {0}")]
Parse(String),
#[error("Serialize error: {0}")]
Serialize(String),
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_default_config() {
let config = GlobalConfig::default();
assert_eq!(config.server.idle_timeout, 3600);
assert_eq!(config.server.startup_timeout, 300);
assert!(config.server.parallel_init);
assert!(config.server.watch); assert_eq!(config.server.watch_debounce_ms, 500);
assert!(!config.cli.verbose);
assert_eq!(config.cli.color, "auto");
}
#[test]
fn test_idle_timeout_duration() {
let mut config = ServerConfig::default();
assert!(config.idle_timeout_duration().is_some());
config.idle_timeout = 0;
assert!(config.idle_timeout_duration().is_none());
}
#[test]
fn test_save_and_load() {
let temp = TempDir::new().unwrap();
let path = temp.path().join("config.toml");
let mut config = GlobalConfig::default();
config.server.idle_timeout = 7200;
config.cli.verbose = true;
config.save(&path).unwrap();
let loaded = GlobalConfig::load(&path).unwrap();
assert_eq!(loaded.server.idle_timeout, 7200);
assert!(loaded.cli.verbose);
}
#[test]
fn test_load_missing_file() {
let config = GlobalConfig::load("/nonexistent/config.toml").unwrap();
assert_eq!(config.server.idle_timeout, 3600);
}
#[test]
fn test_parse_partial_config() {
let temp = TempDir::new().unwrap();
let path = temp.path().join("config.toml");
std::fs::write(
&path,
r#"
[server]
idle_timeout = 1800
"#,
)
.unwrap();
let config = GlobalConfig::load(&path).unwrap();
assert_eq!(config.server.idle_timeout, 1800);
assert_eq!(config.server.startup_timeout, 300);
assert!(config.server.parallel_init);
}
}