Skip to main content

digdigdig3_station/settings/
store.rs

1//! Native file-backed `SettingsStore`.
2//!
3//! Persists a `HashMap<String, serde_json::Value>` as a pretty-printed JSON
4//! file at `{root}/{namespace}.json`. Writes are atomic: a `.tmp` file is
5//! written first, then renamed over the target path.
6
7use std::collections::HashMap;
8use std::path::PathBuf;
9
10use serde_json::Value;
11
12use super::SettingsError;
13
14/// Native JSON settings store.
15///
16/// All get/set/remove/contains/keys operations are synchronous (in-memory).
17/// Only `open` and `save` are async (matching the wasm32 sibling API).
18#[derive(Debug)]
19pub struct SettingsStore {
20    path: PathBuf,
21    map: HashMap<String, Value>,
22}
23
24impl SettingsStore {
25    /// Open (or create) a settings store.
26    ///
27    /// `root` — directory that will contain the settings file.
28    /// `namespace` — base name of the file (`{namespace}.json`).
29    ///
30    /// Creates parent directories if missing. If the file exists it is parsed
31    /// into the in-memory map; a corrupt or unreadable file returns `Err`
32    /// rather than silently wiping it.
33    pub async fn open(
34        root: impl AsRef<std::path::Path>,
35        namespace: &str,
36    ) -> Result<Self, SettingsError> {
37        let path = root.as_ref().join(format!("{namespace}.json"));
38
39        // Create parent directory if needed.
40        if let Some(parent) = path.parent() {
41            std::fs::create_dir_all(parent)
42                .map_err(|e| SettingsError::Io(e.to_string()))?;
43        }
44
45        let map = if path.exists() {
46            let content = std::fs::read_to_string(&path)
47                .map_err(|e| SettingsError::Io(e.to_string()))?;
48            serde_json::from_str::<HashMap<String, Value>>(&content)
49                .map_err(|e| SettingsError::Serde(e.to_string()))?
50        } else {
51            HashMap::new()
52        };
53
54        Ok(Self { path, map })
55    }
56
57    /// Look up a key and deserialize it as `T`.
58    ///
59    /// Returns `None` when the key is absent or the value cannot be
60    /// deserialized as `T` (wrong type — not a panic).
61    pub fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
62        let value = self.map.get(key)?;
63        serde_json::from_value(value.clone()).ok()
64    }
65
66    /// Serialize `value` as JSON and insert it under `key`.
67    ///
68    /// The store is only modified in memory; call `save()` to persist.
69    pub fn set<T: serde::Serialize>(
70        &mut self,
71        key: &str,
72        value: &T,
73    ) -> Result<(), SettingsError> {
74        let json_value =
75            serde_json::to_value(value).map_err(|e| SettingsError::Serde(e.to_string()))?;
76        self.map.insert(key.to_string(), json_value);
77        Ok(())
78    }
79
80    /// Remove a key. Returns `true` if the key was present.
81    pub fn remove(&mut self, key: &str) -> bool {
82        self.map.remove(key).is_some()
83    }
84
85    /// Return all keys in the store (order unspecified).
86    pub fn keys(&self) -> Vec<String> {
87        self.map.keys().cloned().collect()
88    }
89
90    /// Return `true` if `key` exists in the store.
91    pub fn contains(&self, key: &str) -> bool {
92        self.map.contains_key(key)
93    }
94
95    /// Persist the in-memory map to disk atomically.
96    ///
97    /// Writes to `{path}.tmp` first, then renames it over `path`. This ensures
98    /// the previous file is never partially overwritten on a crash.
99    pub async fn save(&self) -> Result<(), SettingsError> {
100        let json = serde_json::to_string_pretty(&self.map)
101            .map_err(|e| SettingsError::Serde(e.to_string()))?;
102
103        let tmp_path = {
104            let mut p = self.path.clone();
105            let fname = p
106                .file_name()
107                .and_then(|n| n.to_str())
108                .unwrap_or("settings")
109                .to_string();
110            p.set_file_name(format!("{fname}.tmp"));
111            p
112        };
113
114        std::fs::write(&tmp_path, &json)
115            .map_err(|e| SettingsError::Io(e.to_string()))?;
116
117        std::fs::rename(&tmp_path, &self.path)
118            .map_err(|e| SettingsError::Io(e.to_string()))?;
119
120        Ok(())
121    }
122}