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 { 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::() } } #[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 for Environment { type Error = String; fn try_from(value: String) -> Result { 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::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()); } }