Skip to main content

origin_settings/
settings.rs

1use crate::store::SettingsStore;
2use origin_domain::{AppError, Result};
3use serde::Serialize;
4use serde::de::DeserializeOwned;
5use std::marker::PhantomData;
6use std::sync::Arc;
7
8/// Declaration of one setting: its key and its default, together.
9#[derive(Debug)]
10pub struct Setting<T> {
11    key: &'static str,
12    default: fn() -> T,
13    _type: PhantomData<fn() -> T>,
14}
15
16impl<T> Setting<T> {
17    pub const fn new(key: &'static str, default: fn() -> T) -> Self {
18        Self {
19            key,
20            default,
21            _type: PhantomData,
22        }
23    }
24
25    pub const fn key(&self) -> &'static str {
26        self.key
27    }
28
29    pub fn default_value(&self) -> T {
30        (self.default)()
31    }
32}
33
34/// Typed access to the settings store.
35#[derive(Debug, Clone)]
36pub struct Settings {
37    store: Arc<dyn SettingsStore>,
38}
39
40impl Settings {
41    pub fn new(store: Arc<dyn SettingsStore>) -> Self {
42        Self { store }
43    }
44
45    /// The stored value, or the declared default.
46    ///
47    /// A stored value that no longer decodes — because the setting's type changed
48    /// between releases — falls back to the default and logs a warning. Refusing to
49    /// start over a stale preference would be worse than ignoring it.
50    pub async fn get<T: DeserializeOwned>(&self, setting: &Setting<T>) -> Result<T> {
51        let Some(raw) = self.store.get_raw(setting.key()).await? else {
52            return Ok(setting.default_value());
53        };
54
55        match serde_json::from_str(&raw) {
56            Ok(value) => Ok(value),
57            Err(error) => {
58                tracing::warn!(
59                    setting = setting.key(),
60                    %error,
61                    "stored setting could not be decoded, falling back to default"
62                );
63                Ok(setting.default_value())
64            }
65        }
66    }
67
68    pub async fn set<T: Serialize>(&self, setting: &Setting<T>, value: &T) -> Result<()> {
69        let encoded = serde_json::to_string(value).map_err(|error| {
70            AppError::validation(format!("cannot encode setting {}: {error}", setting.key()))
71        })?;
72        self.store.set_raw(setting.key(), encoded).await
73    }
74
75    /// Drop the stored value so the next read returns the default.
76    pub async fn reset<T>(&self, setting: &Setting<T>) -> Result<()> {
77        self.store.remove(setting.key()).await
78    }
79
80    /// Untyped read for generic settings UIs, which do not know the concrete types.
81    ///
82    /// Prefer [`Settings::get`] wherever the setting is known at compile time.
83    pub async fn get_json(&self, key: &str) -> Result<Option<serde_json::Value>> {
84        let Some(raw) = self.store.get_raw(key).await? else {
85            return Ok(None);
86        };
87        serde_json::from_str(&raw)
88            .map(Some)
89            .map_err(|error| AppError::storage(format!("cannot decode setting {key}: {error}")))
90    }
91
92    /// Untyped write for generic settings UIs.
93    ///
94    /// The value is not validated against the setting's declared type — a generic
95    /// caller has no way to know it. A value that no longer decodes is ignored on read
96    /// (see [`Settings::get`]), so a bad write degrades to the default rather than
97    /// breaking startup.
98    pub async fn set_json(&self, key: &str, value: &serde_json::Value) -> Result<()> {
99        let encoded = serde_json::to_string(value).map_err(|error| {
100            AppError::validation(format!("cannot encode setting {key}: {error}"))
101        })?;
102        self.store.set_raw(key, encoded).await
103    }
104
105    /// Keys that currently have a stored value. Settings left at their default are
106    /// not listed.
107    pub async fn customised_keys(&self) -> Result<Vec<String>> {
108        self.store.keys().await
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use crate::StorageSettingsStore;
116    use origin_domain::testing::FakeClock;
117    use origin_storage::MemoryStorage;
118    use time::macros::datetime;
119
120    const REFRESH_MINUTES: Setting<u32> = Setting::new("sync.refresh_minutes", || 5);
121
122    fn settings() -> Settings {
123        let clock = Arc::new(FakeClock::new(datetime!(2026-08-23 10:00 UTC)));
124        let store = StorageSettingsStore::new(Arc::new(MemoryStorage::new()), clock);
125        Settings::new(Arc::new(store))
126    }
127
128    #[tokio::test]
129    async fn an_unset_setting_returns_its_default() {
130        assert_eq!(settings().get(&REFRESH_MINUTES).await.unwrap(), 5);
131    }
132
133    #[tokio::test]
134    async fn a_stored_value_wins_over_the_default() {
135        let settings = settings();
136        settings.set(&REFRESH_MINUTES, &15).await.unwrap();
137
138        assert_eq!(settings.get(&REFRESH_MINUTES).await.unwrap(), 15);
139        assert_eq!(
140            settings.customised_keys().await.unwrap(),
141            vec!["sync.refresh_minutes"]
142        );
143    }
144
145    #[tokio::test]
146    async fn resetting_restores_the_default() {
147        let settings = settings();
148        settings.set(&REFRESH_MINUTES, &15).await.unwrap();
149        settings.reset(&REFRESH_MINUTES).await.unwrap();
150
151        assert_eq!(settings.get(&REFRESH_MINUTES).await.unwrap(), 5);
152        assert!(settings.customised_keys().await.unwrap().is_empty());
153    }
154
155    #[tokio::test]
156    async fn an_undecodable_stored_value_falls_back_to_the_default() {
157        let settings = settings();
158        // Simulates a setting whose type changed between releases.
159        settings
160            .set(
161                &Setting::<String>::new("sync.refresh_minutes", String::new),
162                &"often".to_string(),
163            )
164            .await
165            .unwrap();
166
167        assert_eq!(settings.get(&REFRESH_MINUTES).await.unwrap(), 5);
168    }
169}