use std::{fs, io, path::Path};
use serde::{Deserialize, Serialize};
use toml_edit::{DocumentMut, Item, Table};
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("toml_edit error: {0}")]
TomlEdit(#[from] toml_edit::TomlError),
#[error("toml error: {0}")]
Toml(#[from] toml::de::Error),
#[error("Config directory for '{0}' could not be found")]
MissingConfigDir(&'static str),
}
pub trait Config: Sized + Serialize + for<'de> Deserialize<'de> + Default {
const CONFIG_FILE_NAME: &'static str;
fn config_dir() -> Option<&'static Path>;
fn load() -> Result<Self, ConfigError> {
let file = Self::config_dir()
.ok_or(ConfigError::MissingConfigDir(Self::CONFIG_FILE_NAME))?
.join(format!("{}.toml", Self::CONFIG_FILE_NAME));
let config = match std::fs::read_to_string(&file) {
Ok(toml) => toml::from_str(&toml)?,
Err(err) if matches![err.kind(), io::ErrorKind::NotFound] => Self::default(),
Err(err) => return Err(ConfigError::Io(err)),
};
Ok(config)
}
fn save(&self) -> Result<(), ConfigError> {
let config_dir =
Self::config_dir().ok_or(ConfigError::MissingConfigDir(Self::CONFIG_FILE_NAME))?;
let file = config_dir.join(format!("{}.toml", Self::CONFIG_FILE_NAME));
let config_doc = toml_edit::ser::to_document(self).unwrap();
let mut disk_doc: DocumentMut = if file.exists() {
fs::read_to_string(&file)?.parse()?
} else {
DocumentMut::new()
};
let default_table = toml_edit::ser::to_document(&Self::default()).unwrap();
write_needed_config_values(disk_doc.as_table_mut(), &config_doc, &default_table);
fs::create_dir_all(config_dir)?;
fs::write(file, disk_doc.to_string())?;
Ok(())
}
fn print_default() -> String {
toml::to_string_pretty(&Self::default()).unwrap()
}
}
fn write_needed_config_values(disk: &mut Table, current_table: &Table, default_table: &Table) {
for (key, mem_value) in current_table {
match mem_value {
Item::Table(table) => {
let empty = Table::new();
let default_sub = default_table
.get(key)
.and_then(Item::as_table)
.unwrap_or(&empty);
let disk_table = disk
.entry(key)
.or_insert(Item::Table(Table::new()))
.as_table_mut()
.unwrap();
write_needed_config_values(disk_table, table, default_sub);
}
_ => {
if let Some(disk_value) = disk.get(key)
&& !items_equal(mem_value, disk_value)
{
disk.insert(key, mem_value.clone());
} else if let Some(default_value) = default_table.get(key)
&& !items_equal(mem_value, default_value)
{
disk.insert(key, mem_value.clone());
}
}
}
}
}
fn items_equal(a: &Item, b: &Item) -> bool {
match (a, b) {
(Item::Value(a), Item::Value(b)) => a.to_string() == b.to_string(),
_ => false,
}
}