Skip to main content

shared/models/user/
settings.rs

1use serde::de::DeserializeOwned;
2use std::{
3    collections::BTreeMap,
4    ops::{Deref, DerefMut},
5};
6
7pub type UserSettingsMap = BTreeMap<compact_str::CompactString, serde_json::Value>;
8
9#[inline]
10fn cache_key(user_uuid: uuid::Uuid) -> String {
11    format!("user::{user_uuid}::settings")
12}
13
14pub fn validate_settings_keys(
15    settings: &UserSettingsMap,
16    _context: &(),
17) -> Result<(), garde::Error> {
18    for key in settings.keys() {
19        if key.is_empty() || key.len() > 512 {
20            return Err(garde::Error::new(format!(
21                "key '{}' must be between 1 and 512 characters",
22                key
23            )));
24        }
25    }
26
27    Ok(())
28}
29
30async fn fetch_settings(
31    database: &crate::database::Database,
32    user_uuid: uuid::Uuid,
33) -> Result<UserSettingsMap, anyhow::Error> {
34    database
35        .cache
36        .cached(&cache_key(user_uuid), 60, || async {
37            let row = sqlx::query_scalar(
38                r#"
39                SELECT user_settings.settings
40                FROM user_settings
41                WHERE user_settings.user_uuid = $1
42                "#,
43            )
44            .bind(user_uuid)
45            .fetch_optional(database.read())
46            .await?;
47
48            Ok::<_, anyhow::Error>(match row {
49                Some(settings) => serde_json::from_value(settings)?,
50                None => UserSettingsMap::new(),
51            })
52        })
53        .await
54}
55
56impl super::User {
57    /// Returns the settings of this user for reading.
58    ///
59    /// Cached for 60 seconds, invalidated by [`UserSettingsMut::save`].
60    pub async fn get_settings(
61        &self,
62        database: &crate::database::Database,
63    ) -> Result<UserSettings, anyhow::Error> {
64        Ok(UserSettings {
65            settings: fetch_settings(database, self.uuid).await?,
66        })
67    }
68
69    /// Returns the settings of this user for modification, holding a lock that serializes
70    /// concurrent writers until the returned guard is dropped. Changes are only persisted
71    /// by [`UserSettingsMut::save`].
72    pub async fn get_settings_mut(
73        &self,
74        database: &crate::database::Database,
75    ) -> Result<UserSettingsMut, anyhow::Error> {
76        let lock = database
77            .cache
78            .lock(format!("users::{}::settings", self.uuid), Some(30), Some(5))
79            .await?;
80
81        Ok(UserSettingsMut {
82            user_uuid: self.uuid,
83            settings: fetch_settings(database, self.uuid).await?,
84            _lock: lock,
85        })
86    }
87}
88
89pub struct UserSettings {
90    settings: UserSettingsMap,
91}
92
93impl UserSettings {
94    /// Deserializes the setting stored at `key` into `T`. Returns `None` when the key is
95    /// not set or its value does not match `T`.
96    pub fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
97        self.settings
98            .get(key)
99            .and_then(|value| serde_json::from_value(value.clone()).ok())
100    }
101}
102
103impl Deref for UserSettings {
104    type Target = UserSettingsMap;
105
106    fn deref(&self) -> &Self::Target {
107        &self.settings
108    }
109}
110
111impl serde::Serialize for UserSettings {
112    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
113        self.settings.serialize(serializer)
114    }
115}
116
117pub struct UserSettingsMut {
118    user_uuid: uuid::Uuid,
119    settings: UserSettingsMap,
120    _lock: crate::cache::CacheLock,
121}
122
123impl UserSettingsMut {
124    pub async fn save(
125        self,
126        database: &crate::database::Database,
127    ) -> Result<(), crate::database::DatabaseError> {
128        sqlx::query(
129            r#"
130            INSERT INTO user_settings (user_uuid, settings)
131            VALUES ($1, $2)
132            ON CONFLICT (user_uuid) DO UPDATE SET settings = EXCLUDED.settings
133            "#,
134        )
135        .bind(self.user_uuid)
136        .bind(serde_json::to_value(&self.settings)?)
137        .execute(database.write())
138        .await?;
139
140        database
141            .cache
142            .invalidate(&cache_key(self.user_uuid))
143            .await?;
144
145        Ok(())
146    }
147}
148
149impl Deref for UserSettingsMut {
150    type Target = UserSettingsMap;
151
152    fn deref(&self) -> &Self::Target {
153        &self.settings
154    }
155}
156
157impl DerefMut for UserSettingsMut {
158    fn deref_mut(&mut self) -> &mut Self::Target {
159        &mut self.settings
160    }
161}