pub use anyhow;
use serde::{de::DeserializeOwned, Serialize};
use std::{cell::RefCell, ops::Deref, path::PathBuf, rc::Rc};
pub type Result<T = ()> = anyhow::Result<T>;
pub struct AppConfigManager<T>
where
T: Sized + Serialize + DeserializeOwned,
{
data: Rc<RefCell<T>>,
organization_name: String,
app_name: String,
skip_parsing_error_when_loading: bool,
auto_saving: bool,
}
impl<T> AppConfigManager<T>
where
T: Sized + Serialize + DeserializeOwned,
{
pub fn new(
data: Rc<RefCell<T>>,
app_name: impl Into<String>,
organization_name: impl Into<String>,
) -> Self {
Self {
data,
organization_name: organization_name.into(),
app_name: app_name.into(),
auto_saving: true,
skip_parsing_error_when_loading: true,
}
}
pub fn set_skip_parsing_error_when_loading(&mut self, value: bool) -> &mut Self {
self.skip_parsing_error_when_loading = value;
self
}
pub fn with_skip_parsing_error_when_loading(mut self, value: bool) -> Self {
self.set_skip_parsing_error_when_loading(value);
self
}
pub fn set_auto_saving(&mut self, value: bool) -> &mut Self {
self.auto_saving = value;
self
}
pub fn with_auto_saving(mut self, value: bool) -> Self {
self.set_auto_saving(value);
self
}
pub fn set_organization_name(&mut self, value: impl Into<String>) -> &mut Self {
self.organization_name = value.into();
self
}
pub fn with_organization_name(mut self, value: impl Into<String>) -> Self {
self.set_organization_name(value);
self
}
pub fn set_app_name(&mut self, value: impl Into<String>) -> &mut Self {
self.app_name = value.into();
self
}
pub fn with_app_name(mut self, value: impl Into<String>) -> Self {
self.set_app_name(value);
self
}
pub fn load(&self) -> Result {
let path = self.get_user_config_path()?;
let s = std::fs::read_to_string(&path)?;
if self.skip_parsing_error_when_loading {
if let Ok(value) = toml::from_str(&s) {
*self.data.as_ref().borrow_mut() = value;
}
}
else {
*self.data.as_ref().borrow_mut() = toml::from_str(&s)?;
}
Ok(())
}
pub fn save(&self) -> Result {
let path = self.get_user_config_path()?;
let s = toml::to_string_pretty(&*self.data.as_ref().borrow())?;
std::fs::write(&path, &s.as_bytes())?;
Ok(())
}
pub fn data(&self) -> &RefCell<T> {
&self.data
}
fn get_user_config_path(&self) -> Result<PathBuf> {
use std::io;
let mut path = dirs_next::config_dir()
.ok_or(io::Error::new(io::ErrorKind::NotFound, "Config path"))?
.join(&format!("com.{}.{}", self.organization_name, self.app_name));
if !path.exists() {
std::fs::create_dir_all(&path)?;
}
path = path.join("app_config.toml");
Ok(path)
}
}
impl<T> Deref for AppConfigManager<T>
where
T: Sized + Serialize + DeserializeOwned,
{
type Target = RefCell<T>;
fn deref(&self) -> &Self::Target {
self.data()
}
}
impl<T> Drop for AppConfigManager<T>
where
T: Sized + Serialize + DeserializeOwned,
{
fn drop(&mut self) {
if self.auto_saving {
self.save().ok();
}
}
}
#[cfg(test)]
mod tests {
use std::{cell::RefCell, rc::Rc};
use crate::AppConfigManager;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct MyAppConfig {
window_pos: (u32, u32),
}
impl Default for MyAppConfig {
fn default() -> Self {
Self {
window_pos: (320, 280),
}
}
}
#[test]
fn it_works() {
let config = Rc::from(RefCell::from(MyAppConfig::default()));
let manager = AppConfigManager::new(
config.clone(),
std::env!("CARGO_CRATE_NAME"), "sumibi-yakitori",
);
manager.save().unwrap();
manager.load().unwrap();
assert_eq!(*config.borrow(), MyAppConfig::default());
}
}