generated from phundrak/rust-poem-openapi-template
247 lines
7.2 KiB
Rust
247 lines
7.2 KiB
Rust
|
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());
|
||
|
}
|
||
|
}
|