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(
79                format!("users::{}::settings", self.uuid),
80                Some(30),
81                Some(5000),
82            )
83            .await?;
84
85        Ok(UserSettingsMut {
86            user_uuid: self.uuid,
87            settings: fetch_settings(database, self.uuid).await?,
88            _lock: lock,
89        })
90    }
91}
92
93pub struct UserSettings {
94    settings: UserSettingsMap,
95}
96
97impl UserSettings {
98    /// Deserializes the setting stored at `key` into `T`. Returns `None` when the key is
99    /// not set or its value does not match `T`.
100    pub fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
101        self.settings
102            .get(key)
103            .and_then(|value| serde_json::from_value(value.clone()).ok())
104    }
105}
106
107impl Deref for UserSettings {
108    type Target = UserSettingsMap;
109
110    fn deref(&self) -> &Self::Target {
111        &self.settings
112    }
113}
114
115impl serde::Serialize for UserSettings {
116    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
117        self.settings.serialize(serializer)
118    }
119}
120
121pub struct UserSettingsMut {
122    user_uuid: uuid::Uuid,
123    settings: UserSettingsMap,
124    _lock: crate::cache::CacheLock,
125}
126
127impl UserSettingsMut {
128    pub async fn save(
129        self,
130        database: &crate::database::Database,
131    ) -> Result<(), crate::database::DatabaseError> {
132        sqlx::query(
133            r#"
134            INSERT INTO user_settings (user_uuid, settings)
135            VALUES ($1, $2)
136            ON CONFLICT (user_uuid) DO UPDATE SET settings = EXCLUDED.settings
137            "#,
138        )
139        .bind(self.user_uuid)
140        .bind(serde_json::to_value(&self.settings)?)
141        .execute(database.write())
142        .await?;
143
144        database
145            .cache
146            .invalidate(&cache_key(self.user_uuid))
147            .await?;
148
149        Ok(())
150    }
151}
152
153impl Deref for UserSettingsMut {
154    type Target = UserSettingsMap;
155
156    fn deref(&self) -> &Self::Target {
157        &self.settings
158    }
159}
160
161impl DerefMut for UserSettingsMut {
162    fn deref_mut(&mut self) -> &mut Self::Target {
163        &mut self.settings
164    }
165}