1
use crate::db::get_connection;
2
use derive_more::From;
3
use rust_i18n::t;
4
use sqlx::Error::RowNotFound;
5
use std::env::var;
6
use std::fmt;
7
use std::ops::Deref;
8
use std::sync::LazyLock;
9
use thiserror::Error;
10

            
11
/// Represents an error that can occur while accessing the configuration.
12
///
13
/// # Variants
14
///
15
/// - `NoConfig(String)`: Returned when a specific configuration field is not found.
16
/// - `DB`: Indicates a database access error.
17
/// - `Sqlx`: An error propagated from the `SQLx` crate.
18
///
19
/// # Example
20
///
21
/// ```rust
22
/// use thiserror::Error;
23
///
24
/// #[derive(Debug, Error)]
25
/// pub enum ConfigError {
26
///     #[error("No such config field: {0}")]
27
///     NoConfig(String),
28
///     #[error("Can't access db")]
29
///     DB,
30
///     #[error("Sqlx")]
31
///     Sqlx(#[from] sqlx::Error),
32
/// }
33
/// ```
34
#[derive(Debug, Error)]
35
pub enum ConfigError {
36
    #[error("No such config field: {0}")]
37
    NoConfig(String),
38
    #[error("Can't access db")]
39
    DB,
40
    #[error("Sqlx")]
41
    Sqlx(#[from] sqlx::Error),
42
}
43

            
44
/// Represents a configuration value stored in the system.
45
///
46
/// # Variants
47
///
48
/// - `String`: Stores the configuration as a string.
49
/// - `Blob`: Stores the configuration as a binary blob.
50
///
51
/// # Example
52
///
53
/// ```rust
54
/// use server::config::ConfigOption;
55
/// let config_str = ConfigOption::String("example".to_string());
56
/// let config_blob = ConfigOption::Blob(vec![1, 2, 3, 4]);
57
/// ```
58
///
59
/// You can also convert `String` and `Vec<u8>` directly into `ConfigOption`
60
/// using the `From` trait:
61
///
62
/// ```rust
63
/// use server::config::ConfigOption;
64
/// let config: ConfigOption = "example".to_string().into();
65
/// let blob: ConfigOption = vec![1, 2, 3, 4].into();
66
/// ```
67
#[derive(Debug, From, PartialEq, Clone)]
68
pub enum ConfigOption {
69
    String(#[from] String),
70
    Blob(#[from] Vec<u8>),
71
}
72

            
73
impl fmt::Display for ConfigOption {
74
2
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75
2
        match self {
76
2
            ConfigOption::String(s) => write!(f, "{s}"),
77
            ConfigOption::Blob(b) => write!(f, "ConfigOption<Blob>: \"{}\" bytes", b.len()),
78
        }
79
2
    }
80
}
81

            
82
/// Convert `ConfigOption` into a `&str` reference when the variant is a `String`.
83
///
84
/// # Panics
85
///
86
/// This will panic if the `ConfigOption` is of type `Blob`, as it cannot be
87
/// converted to a `&str`.
88
///
89
/// # Example
90
///
91
/// ```rust,should_panic
92
/// use server::config::ConfigOption;
93
/// let config = ConfigOption::Blob(vec![1, 2, 3]);
94
/// let str_ref: &str = config.as_ref(); // Will panic
95
/// ```
96
impl AsRef<str> for ConfigOption {
97
    fn as_ref(&self) -> &str {
98
        match self {
99
            ConfigOption::String(s) => s.as_str(),
100
            ConfigOption::Blob(_) => panic!("ConfigOption::Blob cannot be converted to &str"),
101
        }
102
    }
103
}
104

            
105
/// Dereference `ConfigOption::String` to `&str`.
106
///
107
/// This allows you to use `ConfigOption` directly in contexts where a `&str`
108
/// is expected, without having to manually call `.as_ref()`.
109
///
110
/// # Panics
111
///
112
/// This will panic if the `ConfigOption` is of type `Blob`.
113
///
114
/// # Example
115
///
116
/// ```rust,should_panic
117
/// use server::config::ConfigOption;
118
/// let config = ConfigOption::Blob(vec![1, 2, 3]);
119
/// let str_ref: &str = &*config; // Will panic
120
/// ```
121
impl Deref for ConfigOption {
122
    type Target = str;
123

            
124
    fn deref(&self) -> &Self::Target {
125
        match self {
126
            ConfigOption::String(s) => s.as_str(),
127
            ConfigOption::Blob(_) => panic!("ConfigOption::Blob cannot be dereferenced as &str"),
128
        }
129
    }
130
}
131

            
132
/// Allows for the conversion from a `&str` to `ConfigOption`.
133
///
134
/// This is useful for passing string literals or string slices directly as `ConfigOption`.
135
/// The conversion will wrap the `&str` in the `ConfigOption::String` variant.
136
///
137
/// # Example
138
///
139
/// ```rust
140
/// use server::config::ConfigOption;
141
/// let config_option: ConfigOption = "example text".into();
142
/// assert_eq!(config_option, ConfigOption::String("example text".to_string()));
143
/// ```
144
impl From<&str> for ConfigOption {
145
    fn from(value: &str) -> Self {
146
        ConfigOption::String(value.to_string())
147
    }
148
}
149

            
150
/// Convert `ConfigOption` into a `&[u8]` reference when the variant is a `Blob`.
151
///
152
/// # Example
153
///
154
/// ```rust
155
/// use server::config::ConfigOption;
156
/// let config = ConfigOption::Blob(vec![1, 2, 3]);
157
/// let byte_ref: &[u8] = config.as_ref(); // Works
158
/// ```
159
impl AsRef<[u8]> for ConfigOption {
160
    fn as_ref(&self) -> &[u8] {
161
        match self {
162
            ConfigOption::String(s) => s.as_bytes(),
163
            ConfigOption::Blob(b) => b.as_slice(),
164
        }
165
    }
166
}
167

            
168
/// Fetches the `contents` of a field from the configdata.
169
///
170
/// This function attempts to retrieve the configuration stored in the
171
/// `configdata` table.  If the field is not found, it returns a
172
/// `ConfigError::NoConfig`.
173
///
174
/// # Arguments
175
///
176
/// - `field`: The name of the field to retrieve.
177
///
178
/// # Returns
179
///
180
/// - `Ok(Some(ConfigOption))` if the field exists.
181
/// - `Ok(None)` if the field doesn't exist.
182
/// - `Err(ConfigError)` if there is an error during retrieval.
183
///
184
/// # Example
185
///
186
/// ```rust,ignore
187
/// let config = configdata("example_field").await?;
188
/// if let Some(option) = config {
189
///     println!("Config value: {}", option);
190
/// }
191
/// ```
192
///
193
/// # Errors
194
///
195
/// Returns a `ConfigError::DB` if there is an issue with the database connection.
196
11
async fn configdata(field: &str) -> Result<Option<ConfigOption>, ConfigError> {
197
9
    let mut conn = get_connection().await.map_err(|err| {
198
        log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
199
        ConfigError::DB
200
    })?;
201
9
    let value = sqlx::query_file_scalar!("../server/sql/select/config/data.sql", field)
202
9
        .fetch_one(&mut *conn)
203
9
        .await
204
9
        .map_err(|err| {
205
7
            if let RowNotFound = err {
206
7
                ConfigError::NoConfig(String::from(field))
207
            } else {
208
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
209
                ConfigError::DB
210
            }
211
7
        })?;
212

            
213
2
    Ok(Some(ConfigOption::Blob(value)))
214
9
}
215

            
216
/// Retrieves the configuration for a given field.
217
///
218
/// This function first checks the `config` table for the requested field. If
219
/// it is not found, it will check the `configdata` table as a fallback using
220
/// `configdata`.
221
///
222
/// # Arguments
223
///
224
/// - `field`: The name of the field to retrieve.
225
///
226
/// # Returns
227
///
228
/// - `Ok(Some(ConfigOption))` if the field exists.
229
/// - `Ok(None)` if the field doesn't exist.
230
/// - `Err(ConfigError)` if there is an error during retrieval.
231
///
232
/// # Example
233
///
234
/// ```rust,ignore
235
/// let config_value = config("language").await?;
236
/// if let Some(option) = config_value {
237
///     println!("Config value: {}", option);
238
/// }
239
/// ```
240
///
241
/// # Errors
242
///
243
/// Returns `ConfigError::DB` if there is an issue with the database connection.
244
17
pub async fn config(field: &str) -> Result<Option<ConfigOption>, ConfigError> {
245
13
    let mut conn = get_connection().await.map_err(|err| {
246
        log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
247
        ConfigError::DB
248
    })?;
249

            
250
13
    let value = sqlx::query_file_scalar!("../server/sql/select/config/string.sql", field)
251
13
        .fetch_one(&mut *conn)
252
13
        .await;
253
9
    match value {
254
4
        Ok(l) => Ok(Some(ConfigOption::String(l))),
255
9
        Err(RowNotFound) => configdata(field).await,
256
        Err(err) => {
257
            log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
258
            Err(ConfigError::DB)
259
        }
260
    }
261
13
}
262

            
263
/// Stores a `ConfigOption` in the database.
264
///
265
/// This function inserts or updates a configuration option into the
266
/// appropriate table (either `config` or `configdata` depending on the variant).
267
///
268
/// # Arguments
269
///
270
/// - `field`: The name of the field to store.
271
/// - `contents`: The `ConfigOption` to store, which can be either a string or a binary blob.
272
///
273
/// # Example
274
///
275
/// ```rust,ignore
276
/// set_config("language", ConfigOption::String("en".to_string())).await?;
277
/// ```
278
///
279
/// # Errors
280
///
281
/// Returns `ConfigError::DB` if there is an issue with the database connection.
282
9
pub async fn set_config(field: &str, contents: ConfigOption) -> Result<(), ConfigError> {
283
6
    let mut conn = get_connection().await.map_err(|err| {
284
        log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
285
        ConfigError::DB
286
    })?;
287
6
    let id = uuid::Uuid::new_v4();
288
6
    match contents {
289
4
        ConfigOption::String(s) => {
290
4
            sqlx::query_file!("../server/sql/set/config/string.sql", &id, field, s)
291
        }
292
2
        ConfigOption::Blob(b) => {
293
2
            sqlx::query_file!("../server/sql/set/config/blob.sql", &id, field, b)
294
        }
295
    }
296
6
    .execute(&mut *conn)
297
6
    .await
298
6
    .map_err(|err| {
299
        if let RowNotFound = err {
300
            ConfigError::NoConfig(String::from(field))
301
        } else {
302
            log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
303
            ConfigError::DB
304
        }
305
    })?;
306

            
307
6
    Ok(())
308
6
}
309

            
310
static LOCALE_NAME: LazyLock<String> = LazyLock::new(|| var("LANG").unwrap_or(String::from("en")));
311

            
312
pub async fn load_config() -> Result<(), ConfigError> {
313
    if let Ok(Some(locale)) = config("locale").await {
314
        rust_i18n::set_locale(&locale);
315
        log::info!(
316
            "{}",
317
            t!(
318
                "Loaded the %{loc} locale from config, setting as main",
319
                loc = &locale
320
            )
321
        );
322
    } else {
323
        rust_i18n::set_locale(&LOCALE_NAME);
324
    }
325
    Ok(())
326
}
327

            
328
#[cfg(test)]
329
mod config_tests {
330
    use super::*;
331
    use crate::db::DB_POOL;
332
    use sqlx::PgPool;
333
    use supp_macro::local_db_sqlx_test;
334
    use tokio::sync::OnceCell;
335

            
336
    /// Context for keeping environment intact
337
    static CONTEXT: OnceCell<()> = OnceCell::const_new();
338

            
339
6
    async fn setup() {
340
4
        CONTEXT
341
4
            .get_or_init(|| async {
342
                #[cfg(feature = "testlog")]
343
2
                let _ = env_logger::builder()
344
2
                    .is_test(true)
345
2
                    .filter_level(log::LevelFilter::Trace)
346
2
                    .try_init();
347
4
            })
348
4
            .await;
349
4
    }
350

            
351
    #[local_db_sqlx_test]
352
    async fn test_config_string(pool: PgPool) {
353
        let opt = ConfigOption::String("testval".to_string());
354
        set_config("testfield", opt.clone()).await.unwrap();
355
        let val = config("testfield").await.unwrap().unwrap();
356
        assert_eq!(val, opt);
357
    }
358

            
359
    #[local_db_sqlx_test]
360
    async fn test_config_configdata(pool: PgPool) {
361
        let opt = ConfigOption::Blob(vec![0, 1, 2, 3]);
362
        set_config("testblob", opt.clone()).await.unwrap();
363
        let val = config("testblob").await.unwrap().unwrap();
364
        assert_eq!(val, opt);
365
    }
366
}