cloud_terrastodon_config/
config.rs

1use chrono::Utc;
2use cloud_terrastodon_pathing::AppDir;
3use eyre::Result;
4use eyre::eyre;
5use serde::Deserialize;
6use serde::Serialize;
7use serde_json::Value;
8use serde_json::{self};
9use std::path::PathBuf;
10use tokio::fs;
11use tracing::debug;
12use tracing::warn;
13#[async_trait::async_trait]
14pub trait Config:
15    Sized
16    + Default
17    + std::fmt::Debug
18    + Sync
19    + for<'de> Deserialize<'de>
20    + Serialize
21    + Clone
22    + Send
23    + 'static
24    + PartialEq
25{
26    /// Unique slug (used for generating the filename).
27    const FILE_SLUG: &'static str;
28
29    /// Compute the file path where this config is stored.
30    fn config_path() -> PathBuf {
31        AppDir::Config.join(format!("{}.json", Self::FILE_SLUG))
32    }
33
34    /// Asynchronously load the configuration with incremental upgrading.
35    async fn load() -> Result<Self> {
36        let path = Self::config_path();
37        let instance = if path.exists() {
38            let content = fs::read_to_string(&path).await?;
39            // Try to parse file into a serde_json::Value
40            let user_json: Value = match serde_json::from_str(&content) {
41                Ok(val) => val,
42                Err(err) => {
43                    warn!(
44                        "Failed to load config as valid json, will make a backup and will revert to defaults. Error: {}",
45                        err
46                    );
47                    // If we fail, backup the original file and use the default.
48                    let now = Utc::now().format("%Y%m%dT%H%M%SZ");
49                    let backup_path =
50                        path.with_file_name(format!("{}-{}.json.bak", Self::FILE_SLUG, now));
51                    fs::copy(&path, &backup_path).await?;
52                    // For upgrade purposes, we use the default JSON.
53                    serde_json::to_value(Self::default())?
54                }
55            };
56
57            // Get the default config as JSON.
58            let default_json = serde_json::to_value(Self::default())?;
59            // Merge the user config (if present) into the default config.
60            let merged_json = merge_json(default_json, user_json);
61            // Deserialize the merged JSON.
62            serde_json::from_value(merged_json)
63                .map_err(|e| eyre!("Failed to deserialize merged config: {}", e))?
64        } else {
65            Self::default()
66        };
67
68        Ok(instance)
69    }
70
71    /// Asynchronously save the configuration.
72    async fn save(&self) -> Result<()> {
73        let path = Self::config_path();
74        if let Some(dir) = path.parent() {
75            fs::create_dir_all(dir).await?;
76        }
77        let content = serde_json::to_string_pretty(self)?;
78        debug!("Writing config to {:?}", path);
79        fs::write(&path, content).await?;
80        Ok(())
81    }
82
83    async fn modify_and_save<F>(&mut self, f: F) -> Result<()>
84    where
85        F: FnOnce(&mut Self) + Send,
86    {
87        f(self);
88        self.save().await?;
89        Ok(())
90    }
91}
92
93/// A recursive merge function that takes the `default` value and overrides
94/// with any keys found in `user` where the key exists in both objects.
95/// If both values are objects, the merge is done recursively.
96fn merge_json(default: Value, user: Value) -> Value {
97    match (default, user) {
98        // Both default and user are objects: merge key by key.
99        (Value::Object(mut default_map), Value::Object(user_map)) => {
100            for (key, user_value) in user_map {
101                let entry = default_map.entry(key).or_insert(Value::Null);
102                *entry = merge_json(entry.take(), user_value);
103            }
104            Value::Object(default_map)
105        }
106        // In all other cases, take the user value.
107        (_, user_value) => user_value,
108    }
109}