use crate::store::SettingsStore;
use origin_domain::{AppError, Result};
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::marker::PhantomData;
use std::sync::Arc;
#[derive(Debug)]
pub struct Setting<T> {
key: &'static str,
default: fn() -> T,
_type: PhantomData<fn() -> T>,
}
impl<T> Setting<T> {
pub const fn new(key: &'static str, default: fn() -> T) -> Self {
Self {
key,
default,
_type: PhantomData,
}
}
pub const fn key(&self) -> &'static str {
self.key
}
pub fn default_value(&self) -> T {
(self.default)()
}
}
#[derive(Debug, Clone)]
pub struct Settings {
store: Arc<dyn SettingsStore>,
}
impl Settings {
pub fn new(store: Arc<dyn SettingsStore>) -> Self {
Self { store }
}
pub async fn get<T: DeserializeOwned>(&self, setting: &Setting<T>) -> Result<T> {
let Some(raw) = self.store.get_raw(setting.key()).await? else {
return Ok(setting.default_value());
};
match serde_json::from_str(&raw) {
Ok(value) => Ok(value),
Err(error) => {
tracing::warn!(
setting = setting.key(),
%error,
"stored setting could not be decoded, falling back to default"
);
Ok(setting.default_value())
}
}
}
pub async fn set<T: Serialize>(&self, setting: &Setting<T>, value: &T) -> Result<()> {
let encoded = serde_json::to_string(value).map_err(|error| {
AppError::validation(format!("cannot encode setting {}: {error}", setting.key()))
})?;
self.store.set_raw(setting.key(), encoded).await
}
pub async fn reset<T>(&self, setting: &Setting<T>) -> Result<()> {
self.store.remove(setting.key()).await
}
pub async fn get_json(&self, key: &str) -> Result<Option<serde_json::Value>> {
let Some(raw) = self.store.get_raw(key).await? else {
return Ok(None);
};
serde_json::from_str(&raw)
.map(Some)
.map_err(|error| AppError::storage(format!("cannot decode setting {key}: {error}")))
}
pub async fn set_json(&self, key: &str, value: &serde_json::Value) -> Result<()> {
let encoded = serde_json::to_string(value).map_err(|error| {
AppError::validation(format!("cannot encode setting {key}: {error}"))
})?;
self.store.set_raw(key, encoded).await
}
pub async fn customised_keys(&self) -> Result<Vec<String>> {
self.store.keys().await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::StorageSettingsStore;
use origin_domain::testing::FakeClock;
use origin_storage::MemoryStorage;
use time::macros::datetime;
const REFRESH_MINUTES: Setting<u32> = Setting::new("sync.refresh_minutes", || 5);
fn settings() -> Settings {
let clock = Arc::new(FakeClock::new(datetime!(2026-08-23 10:00 UTC)));
let store = StorageSettingsStore::new(Arc::new(MemoryStorage::new()), clock);
Settings::new(Arc::new(store))
}
#[tokio::test]
async fn an_unset_setting_returns_its_default() {
assert_eq!(settings().get(&REFRESH_MINUTES).await.unwrap(), 5);
}
#[tokio::test]
async fn a_stored_value_wins_over_the_default() {
let settings = settings();
settings.set(&REFRESH_MINUTES, &15).await.unwrap();
assert_eq!(settings.get(&REFRESH_MINUTES).await.unwrap(), 15);
assert_eq!(
settings.customised_keys().await.unwrap(),
vec!["sync.refresh_minutes"]
);
}
#[tokio::test]
async fn resetting_restores_the_default() {
let settings = settings();
settings.set(&REFRESH_MINUTES, &15).await.unwrap();
settings.reset(&REFRESH_MINUTES).await.unwrap();
assert_eq!(settings.get(&REFRESH_MINUTES).await.unwrap(), 5);
assert!(settings.customised_keys().await.unwrap().is_empty());
}
#[tokio::test]
async fn an_undecodable_stored_value_falls_back_to_the_default() {
let settings = settings();
settings
.set(
&Setting::<String>::new("sync.refresh_minutes", String::new),
&"often".to_string(),
)
.await
.unwrap();
assert_eq!(settings.get(&REFRESH_MINUTES).await.unwrap(), 5);
}
}