use crate::error::SwitchdevError;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Serialize, Deserialize)]
pub struct Project {
pub name: String,
pub path: String,
pub commands: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
pub projects: Vec<Project>,
}
fn get_config_path_from(base: &Path) -> PathBuf {
base.join(".switchdev").join("config.json")
}
pub fn get_config_path() -> Result<PathBuf, SwitchdevError> {
let home_dir = dirs::home_dir().ok_or(SwitchdevError::ConfigLoadFailed)?;
Ok(get_config_path_from(&home_dir))
}
pub fn load_config() -> Result<Config, SwitchdevError> {
let config_path = get_config_path()?;
load_config_at(&config_path)
}
fn load_config_at(config_path: &Path) -> Result<Config, SwitchdevError> {
if !config_path.exists() {
let config_dir = config_path
.parent()
.ok_or(SwitchdevError::ConfigLoadFailed)?;
fs::create_dir_all(config_dir).map_err(|_| SwitchdevError::ConfigLoadFailed)?;
let empty_config = empty_config();
save_config_at(config_path, &empty_config)?;
}
let contents = fs::read_to_string(config_path).map_err(|_| SwitchdevError::ConfigLoadFailed)?;
match serde_json::from_str(&contents) {
Ok(config) => Ok(config),
Err(_) => {
println!("[✗] Config file is corrupted");
let backup_path = config_path.with_extension("json.bak");
if fs::rename(config_path, &backup_path).is_ok() {
println!("[i] Backup created at {}", backup_path.to_string_lossy());
}
let clean_config = empty_config();
save_config_at(config_path, &clean_config)?;
println!("[i] New config initialized");
Ok(clean_config)
}
}
}
pub fn save_config(config: &Config) -> Result<(), SwitchdevError> {
let config_path = get_config_path()?;
save_config_at(&config_path, config)
}
fn save_config_at(config_path: &Path, config: &Config) -> Result<(), SwitchdevError> {
let config_dir = config_path
.parent()
.ok_or(SwitchdevError::ConfigSaveFailed)?;
fs::create_dir_all(config_dir).map_err(|_| SwitchdevError::ConfigSaveFailed)?;
let serialized =
serde_json::to_string_pretty(config).map_err(|_| SwitchdevError::ConfigSaveFailed)?;
let temp_path = config_path.with_extension("json.tmp");
fs::write(&temp_path, serialized).map_err(|_| SwitchdevError::ConfigSaveFailed)?;
fs::rename(&temp_path, config_path).map_err(|_| SwitchdevError::ConfigSaveFailed)?;
Ok(())
}
fn empty_config() -> Config {
Config {
projects: Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{SystemTime, UNIX_EPOCH};
fn test_base_dir(test_name: &str) -> PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after unix epoch")
.as_nanos();
std::env::temp_dir().join(format!("switchdev-{test_name}-{unique}"))
}
#[test]
fn load_config_creates_file_if_missing() {
let base_dir = test_base_dir("load-config");
let config_path = get_config_path_from(&base_dir);
let config = load_config_at(&config_path).expect("load_config_at should create config");
assert!(config_path.exists());
assert!(config.projects.is_empty());
}
#[test]
fn save_config_writes_data_correctly() {
let base_dir = test_base_dir("save-config");
let config_path = get_config_path_from(&base_dir);
let config = Config {
projects: vec![Project {
name: String::from("api"),
path: String::from("/tmp/api"),
commands: Some(vec![String::from("npm run dev"), String::from("npm test")]),
}],
};
save_config_at(&config_path, &config).expect("save_config_at should write config");
let loaded = load_config_at(&config_path).expect("load_config_at should read saved config");
assert_eq!(loaded.projects.len(), 1);
assert_eq!(loaded.projects[0].name, "api");
assert_eq!(loaded.projects[0].path, "/tmp/api");
assert_eq!(
loaded.projects[0].commands,
Some(vec![String::from("npm run dev"), String::from("npm test")])
);
}
#[test]
fn load_config_recovers_from_corrupted_file() {
let base_dir = test_base_dir("corrupted-config");
let config_path = get_config_path_from(&base_dir);
let config_dir = config_path
.parent()
.expect("config path should have a parent directory");
fs::create_dir_all(config_dir).expect("test config directory should be created");
fs::write(&config_path, "{ invalid json").expect("test config file should be written");
let config = load_config_at(&config_path).expect("corrupted config should be recovered");
assert!(config.projects.is_empty());
assert!(config_path.exists());
assert!(config_path.with_extension("json.bak").exists());
}
}