Lines
47.47 %
Functions
26.23 %
Branches
100 %
use crate::db::get_connection;
use derive_more::From;
use rust_i18n::t;
use sqlx::Error::RowNotFound;
use std::env::var;
use std::fmt;
use std::ops::Deref;
use std::sync::LazyLock;
use thiserror::Error;
/// Represents an error that can occur while accessing the configuration.
///
/// # Variants
/// - `NoConfig(String)`: Returned when a specific configuration field is not found.
/// - `DB`: Indicates a database access error.
/// - `Sqlx`: An error propagated from the `SQLx` crate.
/// # Example
/// ```rust
/// use thiserror::Error;
/// #[derive(Debug, Error)]
/// pub enum ConfigError {
/// #[error("No such config field: {0}")]
/// NoConfig(String),
/// #[error("Can't access db")]
/// DB,
/// #[error("Sqlx")]
/// Sqlx(#[from] sqlx::Error),
/// }
/// ```
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("No such config field: {0}")]
NoConfig(String),
#[error("Can't access db")]
DB,
#[error("Sqlx")]
Sqlx(#[from] sqlx::Error),
}
/// Represents a configuration value stored in the system.
/// - `String`: Stores the configuration as a string.
/// - `Blob`: Stores the configuration as a binary blob.
/// use server::config::ConfigOption;
/// let config_str = ConfigOption::String("example".to_string());
/// let config_blob = ConfigOption::Blob(vec![1, 2, 3, 4]);
/// You can also convert `String` and `Vec<u8>` directly into `ConfigOption`
/// using the `From` trait:
/// let config: ConfigOption = "example".to_string().into();
/// let blob: ConfigOption = vec![1, 2, 3, 4].into();
#[derive(Debug, From, PartialEq, Clone)]
pub enum ConfigOption {
String(#[from] String),
Blob(#[from] Vec<u8>),
impl fmt::Display for ConfigOption {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigOption::String(s) => write!(f, "{s}"),
ConfigOption::Blob(b) => write!(f, "ConfigOption<Blob>: \"{}\" bytes", b.len()),
/// Convert `ConfigOption` into a `&str` reference when the variant is a `String`.
/// # Panics
/// This will panic if the `ConfigOption` is of type `Blob`, as it cannot be
/// converted to a `&str`.
/// ```rust,should_panic
/// let config = ConfigOption::Blob(vec![1, 2, 3]);
/// let str_ref: &str = config.as_ref(); // Will panic
impl AsRef<str> for ConfigOption {
fn as_ref(&self) -> &str {
ConfigOption::String(s) => s.as_str(),
ConfigOption::Blob(_) => panic!("ConfigOption::Blob cannot be converted to &str"),
/// Dereference `ConfigOption::String` to `&str`.
/// This allows you to use `ConfigOption` directly in contexts where a `&str`
/// is expected, without having to manually call `.as_ref()`.
/// This will panic if the `ConfigOption` is of type `Blob`.
/// let str_ref: &str = &*config; // Will panic
impl Deref for ConfigOption {
type Target = str;
fn deref(&self) -> &Self::Target {
ConfigOption::Blob(_) => panic!("ConfigOption::Blob cannot be dereferenced as &str"),
/// Allows for the conversion from a `&str` to `ConfigOption`.
/// This is useful for passing string literals or string slices directly as `ConfigOption`.
/// The conversion will wrap the `&str` in the `ConfigOption::String` variant.
/// let config_option: ConfigOption = "example text".into();
/// assert_eq!(config_option, ConfigOption::String("example text".to_string()));
impl From<&str> for ConfigOption {
fn from(value: &str) -> Self {
ConfigOption::String(value.to_string())
/// Convert `ConfigOption` into a `&[u8]` reference when the variant is a `Blob`.
/// let byte_ref: &[u8] = config.as_ref(); // Works
impl AsRef<[u8]> for ConfigOption {
fn as_ref(&self) -> &[u8] {
ConfigOption::String(s) => s.as_bytes(),
ConfigOption::Blob(b) => b.as_slice(),
/// Fetches the `contents` of a field from the configdata.
/// This function attempts to retrieve the configuration stored in the
/// `configdata` table. If the field is not found, it returns a
/// `ConfigError::NoConfig`.
/// # Arguments
/// - `field`: The name of the field to retrieve.
/// # Returns
/// - `Ok(Some(ConfigOption))` if the field exists.
/// - `Ok(None)` if the field doesn't exist.
/// - `Err(ConfigError)` if there is an error during retrieval.
/// ```rust,ignore
/// let config = configdata("example_field").await?;
/// if let Some(option) = config {
/// println!("Config value: {}", option);
/// # Errors
/// Returns a `ConfigError::DB` if there is an issue with the database connection.
async fn configdata(field: &str) -> Result<Option<ConfigOption>, ConfigError> {
let mut conn = get_connection().await.map_err(|err| {
log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
ConfigError::DB
})?;
let value = sqlx::query_file_scalar!("../server/sql/select/config/data.sql", field)
.fetch_one(&mut *conn)
.await
.map_err(|err| {
if let RowNotFound = err {
ConfigError::NoConfig(String::from(field))
} else {
Ok(Some(ConfigOption::Blob(value)))
/// Retrieves the configuration for a given field.
/// This function first checks the `config` table for the requested field. If
/// it is not found, it will check the `configdata` table as a fallback using
/// `configdata`.
/// let config_value = config("language").await?;
/// if let Some(option) = config_value {
/// Returns `ConfigError::DB` if there is an issue with the database connection.
pub async fn config(field: &str) -> Result<Option<ConfigOption>, ConfigError> {
let value = sqlx::query_file_scalar!("../server/sql/select/config/string.sql", field)
.await;
match value {
Ok(l) => Ok(Some(ConfigOption::String(l))),
Err(RowNotFound) => configdata(field).await,
Err(err) => {
Err(ConfigError::DB)
/// Stores a `ConfigOption` in the database.
/// This function inserts or updates a configuration option into the
/// appropriate table (either `config` or `configdata` depending on the variant).
/// - `field`: The name of the field to store.
/// - `contents`: The `ConfigOption` to store, which can be either a string or a binary blob.
/// set_config("language", ConfigOption::String("en".to_string())).await?;
pub async fn set_config(field: &str, contents: ConfigOption) -> Result<(), ConfigError> {
let id = uuid::Uuid::new_v4();
match contents {
ConfigOption::String(s) => {
sqlx::query_file!("../server/sql/set/config/string.sql", &id, field, s)
ConfigOption::Blob(b) => {
sqlx::query_file!("../server/sql/set/config/blob.sql", &id, field, b)
.execute(&mut *conn)
Ok(())
static LOCALE_NAME: LazyLock<String> = LazyLock::new(|| var("LANG").unwrap_or(String::from("en")));
pub async fn load_config() -> Result<(), ConfigError> {
if let Ok(Some(locale)) = config("locale").await {
rust_i18n::set_locale(&locale);
log::info!(
"{}",
t!(
"Loaded the %{loc} locale from config, setting as main",
loc = &locale
)
);
rust_i18n::set_locale(&LOCALE_NAME);
#[cfg(test)]
mod config_tests {
use super::*;
use crate::db::DB_POOL;
use sqlx::PgPool;
use supp_macro::local_db_sqlx_test;
use tokio::sync::OnceCell;
/// Context for keeping environment intact
static CONTEXT: OnceCell<()> = OnceCell::const_new();
async fn setup() {
CONTEXT
.get_or_init(|| async {
#[cfg(feature = "testlog")]
let _ = env_logger::builder()
.is_test(true)
.filter_level(log::LevelFilter::Trace)
.try_init();
})
#[local_db_sqlx_test]
async fn test_config_string(pool: PgPool) {
let opt = ConfigOption::String("testval".to_string());
set_config("testfield", opt.clone()).await.unwrap();
let val = config("testfield").await.unwrap().unwrap();
assert_eq!(val, opt);
async fn test_config_configdata(pool: PgPool) {
let opt = ConfigOption::Blob(vec![0, 1, 2, 3]);
set_config("testblob", opt.clone()).await.unwrap();
let val = config("testblob").await.unwrap().unwrap();