use chrono::Utc;
use cloud_terrastodon_pathing::AppDir;
use eyre::Result;
use eyre::eyre;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
use serde_json::{self};
use std::path::PathBuf;
use tokio::fs;
use tracing::debug;
use tracing::warn;
#[async_trait::async_trait]
pub trait Config:
Sized
+ Default
+ std::fmt::Debug
+ Sync
+ for<'de> Deserialize<'de>
+ Serialize
+ Clone
+ Send
+ 'static
+ PartialEq
{
const FILE_SLUG: &'static str;
fn config_path() -> PathBuf {
AppDir::Config.join(format!("{}.json", Self::FILE_SLUG))
}
async fn load() -> Result<Self> {
let path = Self::config_path();
let instance = if path.exists() {
let content = fs::read_to_string(&path).await?;
let user_json: Value = match serde_json::from_str(&content) {
Ok(val) => val,
Err(err) => {
warn!(
"Failed to load config as valid json, will make a backup and will revert to defaults. Error: {}",
err
);
let now = Utc::now().format("%Y%m%dT%H%M%SZ");
let backup_path =
path.with_file_name(format!("{}-{}.json.bak", Self::FILE_SLUG, now));
fs::copy(&path, &backup_path).await?;
serde_json::to_value(Self::default())?
}
};
let default_json = serde_json::to_value(Self::default())?;
let merged_json = merge_json(default_json, user_json);
serde_json::from_value(merged_json)
.map_err(|e| eyre!("Failed to deserialize merged config: {}", e))?
} else {
Self::default()
};
Ok(instance)
}
async fn save(&self) -> Result<()> {
let path = Self::config_path();
if let Some(dir) = path.parent() {
fs::create_dir_all(dir).await?;
}
let content = serde_json::to_string_pretty(self)?;
debug!("Writing config to {:?}", path);
fs::write(&path, content).await?;
Ok(())
}
async fn modify_and_save<F>(&mut self, f: F) -> Result<()>
where
F: FnOnce(&mut Self) + Send,
{
f(self);
self.save().await?;
Ok(())
}
}
fn merge_json(default: Value, user: Value) -> Value {
match (default, user) {
(Value::Object(mut default_map), Value::Object(user_map)) => {
for (key, user_value) in user_map {
let entry = default_map.entry(key).or_insert(Value::Null);
*entry = merge_json(entry.take(), user_value);
}
Value::Object(default_map)
}
(_, user_value) => user_value,
}
}