rust-poem-openapi-template/src/settings.rs

247 lines
7.2 KiB
Rust
Raw Normal View History

2024-08-09 07:05:42 +00:00
use sqlx::ConnectOptions;
#[derive(Debug, serde::Deserialize, Clone, Default)]
pub struct Settings {
pub application: ApplicationSettings,
pub database: Database,
pub debug: bool,
pub email: EmailSettings,
pub frontend_url: String,
}
impl Settings {
#[must_use]
pub fn web_address(&self) -> String {
if self.debug {
format!(
"{}:{}",
self.application.base_url.clone(),
self.application.port
)
} else {
self.application.base_url.clone()
}
}
/// Multipurpose function that helps detect the current
/// environment the application is running in using the
/// `APP_ENVIRONMENT` environment variable.
///
/// ```text
/// APP_ENVIRONMENT = development | dev | production | prod
/// ```
///
/// After detection, it loads the appropriate `.yaml` file. It
/// then loads the environment variables that overrides whatever
/// is set in the `.yaml` files. For this to work, the environment
/// variable MUST be in uppercase and start with `APP`, a `_`
/// separator, then the category of settings, followed by a `__`
/// separator, and finally the variable itself. For instance,
/// `APP__APPLICATION_PORT=3001` for `port` to be set as `3001`.
///
/// # Errors
///
/// Function may return an error if it fails to parse its config
/// files.
///
/// # Panics
///
/// Panics if the program fails to detect the directory it is
/// running in. Can also panic if it fails to parse the
/// environment variable `APP_ENVIRONMENT` and it fails to fall
/// back to its default value.
pub fn new() -> Result<Self, config::ConfigError> {
let base_path = std::env::current_dir().expect("Failed to determine the current directory");
let settings_directory = base_path.join("settings");
let environment: Environment = std::env::var("APP_ENVIRONMENT")
.unwrap_or_else(|_| "development".into())
.try_into()
.expect("Failed to parse APP_ENVIRONMENT");
let environment_filename = format!("{environment}.yaml");
let settings = config::Config::builder()
.add_source(config::File::from(settings_directory.join("base.yaml")))
.add_source(config::File::from(
settings_directory.join(environment_filename),
))
.add_source(
config::Environment::with_prefix("APP")
.prefix_separator("_")
.separator("__"),
)
.build()?;
settings.try_deserialize::<Self>()
}
}
#[derive(Debug, serde::Deserialize, Clone, Default)]
pub struct ApplicationSettings {
pub name: String,
pub version: String,
pub port: u16,
pub host: String,
pub base_url: String,
pub protocol: String,
}
#[derive(Debug, serde::Deserialize, Clone, Default)]
pub struct Database {
pub host: String,
pub port: u16,
pub name: String,
pub user: String,
pub password: String,
pub require_ssl: bool,
}
impl Database {
#[must_use]
pub fn get_connect_options(&self) -> sqlx::postgres::PgConnectOptions {
let ssl_mode = if self.require_ssl {
sqlx::postgres::PgSslMode::Require
} else {
sqlx::postgres::PgSslMode::Prefer
};
sqlx::postgres::PgConnectOptions::new()
.host(&self.host)
.username(&self.user)
.password(&self.password)
.port(self.port)
.ssl_mode(ssl_mode)
.database(&self.name)
.log_statements(tracing::log::LevelFilter::Trace)
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum Environment {
Development,
Production,
}
impl Default for Environment {
fn default() -> Self {
Self::Development
}
}
impl std::fmt::Display for Environment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let self_str = match self {
Self::Development => "development",
Self::Production => "production",
};
write!(f, "{self_str}")
}
}
impl TryFrom<String> for Environment {
type Error = String;
fn try_from(value: String) -> Result<Self, Self::Error> {
match value.to_lowercase().as_str() {
"development" | "dev" => Ok(Self::Development),
"production" | "prod" => Ok(Self::Production),
other => Err(format!(
"{other} is not a supported environment. Use either `development` or `production`"
)),
}
}
}
impl TryFrom<&str> for Environment {
type Error = String;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::try_from(value.to_string())
}
}
#[derive(serde::Deserialize, Clone, Debug, Default)]
pub struct EmailSettings {
pub host: String,
pub user: String,
pub password: String,
pub from: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_environment_works() {
let default_environment = Environment::default();
assert_eq!(Environment::Development, default_environment);
}
#[test]
fn display_environment_works() {
let expected_prod = "production".to_string();
let expected_dev = "development".to_string();
let prod = Environment::Production.to_string();
let dev = Environment::Development.to_string();
assert_eq!(expected_prod, prod);
assert_eq!(expected_dev, dev);
}
#[test]
fn try_from_works() {
[
"DEVELOPMENT",
"DEVEloPmENT",
"Development",
"DEV",
"Dev",
"dev",
]
.iter()
.map(|v| (*v).to_string())
.for_each(|v| {
let environment = Environment::try_from(v);
assert!(environment.is_ok());
assert_eq!(Environment::Development, environment.unwrap());
});
[
"PRODUCTION",
"Production",
"PRODuction",
"production",
"PROD",
"Prod",
"prod",
]
.iter()
.map(|v| (*v).to_string())
.for_each(|v| {
let environment = Environment::try_from(v);
assert!(environment.is_ok());
assert_eq!(Environment::Production, environment.unwrap());
});
let environment = Environment::try_from("invalid");
assert!(environment.is_err());
assert_eq!(
"invalid is not a supported environment. Use either `development` or `production`"
.to_string(),
environment.err().unwrap()
);
}
#[test]
fn web_address_works() {
let mut settings = Settings {
debug: false,
application: ApplicationSettings {
base_url: "127.0.0.1".to_string(),
port: 3000,
..Default::default()
},
..Default::default()
};
let expected_no_debug = "127.0.0.1".to_string();
let expected_debug = "127.0.0.1:3000".to_string();
assert_eq!(expected_no_debug, settings.web_address());
settings.debug = true;
assert_eq!(expected_debug, settings.web_address());
}
}