use std::path::{Path, PathBuf};
use std::sync::Arc;
pub struct ConfigStore {
config_dir: PathBuf,
config_file: PathBuf,
data: serde_json::Value,
listeners: Vec<Arc<dyn Fn(&serde_json::Value) + Send + Sync>>,
}
impl ConfigStore {
pub fn new(app_name: &str) -> Self {
let config_dir = Self::get_config_dir(app_name);
let config_file = config_dir.join("config.json");
Self {
config_dir,
config_file,
data: serde_json::Value::Object(serde_json::Map::new()),
listeners: Vec::new(),
}
}
pub fn with_path(config_file: PathBuf) -> Self {
let config_dir = config_file.parent().unwrap_or(Path::new(".")).to_path_buf();
Self {
config_dir,
config_file,
data: serde_json::Value::Object(serde_json::Map::new()),
listeners: Vec::new(),
}
}
fn get_config_dir(app_name: &str) -> PathBuf {
if let Some(config_dir) = dirs::config_dir() {
config_dir.join(app_name)
} else {
PathBuf::from(format!("./{}", app_name))
}
}
pub fn load<T: serde::de::DeserializeOwned + serde::Serialize + Default>(
&mut self,
) -> anyhow::Result<T> {
if self.config_file.exists() {
let content = std::fs::read_to_string(&self.config_file)?;
self.data = serde_json::from_str(&content)?;
Ok(serde_json::from_value(self.data.clone())?)
} else {
let default = T::default();
self.data = serde_json::to_value(&default)?;
Ok(serde_json::from_value(self.data.clone())?)
}
}
pub fn save<T: serde::Serialize>(&mut self, config: &T) -> anyhow::Result<()> {
self.data = serde_json::to_value(config)?;
if !self.config_dir.exists() {
std::fs::create_dir_all(&self.config_dir)?;
}
let content = serde_json::to_string_pretty(&self.data)?;
std::fs::write(&self.config_file, content)?;
for listener in &self.listeners {
listener(&self.data);
}
Ok(())
}
pub fn data(&self) -> &serde_json::Value {
&self.data
}
pub fn config_file(&self) -> &Path {
&self.config_file
}
pub fn on_change<F>(&mut self, callback: F)
where
F: Fn(&serde_json::Value) + Send + Sync + 'static,
{
self.listeners.push(Arc::new(callback));
}
}
impl Default for ConfigStore {
fn default() -> Self {
Self::new("rgpui_app")
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)]
struct TestConfig {
name: String,
value: i32,
}
#[test]
fn test_config_store_save_and_load() {
let temp_dir = TempDir::new().unwrap();
let config_file = temp_dir.path().join("config.json");
let mut store = ConfigStore::with_path(config_file);
let config = TestConfig {
name: "test".to_string(),
value: 42,
};
store.save(&config).unwrap();
let loaded: TestConfig = store.load().unwrap();
assert_eq!(loaded, config);
}
#[test]
fn test_config_store_load_default() {
let temp_dir = TempDir::new().unwrap();
let config_file = temp_dir.path().join("config.json");
let mut store = ConfigStore::with_path(config_file);
let loaded: TestConfig = store.load().unwrap();
assert_eq!(loaded, TestConfig::default());
}
}