server/config.rs
1use crate::db::get_connection;
2use derive_more::From;
3use rust_i18n::t;
4use sqlx::Error::RowNotFound;
5use std::env::var;
6use std::fmt;
7use std::ops::Deref;
8use std::sync::LazyLock;
9use 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)]
35pub 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)]
68pub enum ConfigOption {
69 String(#[from] String),
70 Blob(#[from] Vec<u8>),
71}
72
73impl fmt::Display for ConfigOption {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 match self {
76 ConfigOption::String(s) => write!(f, "{s}"),
77 ConfigOption::Blob(b) => write!(f, "ConfigOption<Blob>: \"{}\" bytes", b.len()),
78 }
79 }
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/// ```
96impl 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/// ```
121impl 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/// ```
144impl 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/// ```
159impl 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.
196async fn configdata(field: &str) -> Result<Option<ConfigOption>, ConfigError> {
197 let mut conn = get_connection().await.map_err(|err| {
198 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
199 ConfigError::DB
200 })?;
201 let value = sqlx::query_file_scalar!("../server/sql/select/config/data.sql", field)
202 .fetch_one(&mut *conn)
203 .await
204 .map_err(|err| {
205 if let RowNotFound = err {
206 ConfigError::NoConfig(String::from(field))
207 } else {
208 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
209 ConfigError::DB
210 }
211 })?;
212
213 Ok(Some(ConfigOption::Blob(value)))
214}
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.
244pub async fn config(field: &str) -> Result<Option<ConfigOption>, ConfigError> {
245 let mut conn = get_connection().await.map_err(|err| {
246 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
247 ConfigError::DB
248 })?;
249
250 let value = sqlx::query_file_scalar!("../server/sql/select/config/string.sql", field)
251 .fetch_one(&mut *conn)
252 .await;
253 match value {
254 Ok(l) => Ok(Some(ConfigOption::String(l))),
255 Err(RowNotFound) => configdata(field).await,
256 Err(err) => {
257 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
258 Err(ConfigError::DB)
259 }
260 }
261}
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.
282pub async fn set_config(field: &str, contents: ConfigOption) -> Result<(), ConfigError> {
283 let mut conn = get_connection().await.map_err(|err| {
284 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
285 ConfigError::DB
286 })?;
287 let id = uuid::Uuid::new_v4();
288 match contents {
289 ConfigOption::String(s) => {
290 sqlx::query_file!("../server/sql/set/config/string.sql", &id, field, s)
291 }
292 ConfigOption::Blob(b) => {
293 sqlx::query_file!("../server/sql/set/config/blob.sql", &id, field, b)
294 }
295 }
296 .execute(&mut *conn)
297 .await
298 .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 Ok(())
308}
309
310static LOCALE_NAME: LazyLock<String> = LazyLock::new(|| var("LANG").unwrap_or(String::from("en")));
311
312pub 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)]
329mod 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 async fn setup() {
340 CONTEXT
341 .get_or_init(|| async {
342 #[cfg(feature = "testlog")]
343 let _ = env_logger::builder()
344 .is_test(true)
345 .filter_level(log::LevelFilter::Trace)
346 .try_init();
347 })
348 .await;
349 }
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}