origin_settings/
settings.rs1use 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#[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#[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 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 pub async fn reset<T>(&self, setting: &Setting<T>) -> Result<()> {
77 self.store.remove(setting.key()).await
78 }
79
80 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 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 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 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}