use crate::domain::SubscriberEmail; use crate::email_client::EmailClient; use secrecy::{ExposeSecret, Secret}; use serde_aux::field_attributes::deserialize_number_from_string; use sqlx::postgres::{PgConnectOptions, PgSslMode}; use std::convert::{TryFrom, TryInto}; #[derive(serde::Deserialize, Clone)] pub struct Settings { pub database: DatabaseSettings, pub application: ApplicationSettings, pub email_client: EmailClientSettings, pub redis_uri: Secret, } #[derive(serde::Deserialize, Clone)] pub struct ApplicationSettings { #[serde(deserialize_with = "deserialize_number_from_string")] pub port: u16, pub host: String, pub base_url: String, pub hmac_secret: Secret, } #[derive(serde::Deserialize, Clone)] pub struct DatabaseSettings { pub username: String, pub password: Secret, #[serde(deserialize_with = "deserialize_number_from_string")] pub port: u16, pub host: String, pub database_name: String, pub require_ssl: bool, } impl DatabaseSettings { pub fn without_db(&self) -> PgConnectOptions { let ssl_mode = if self.require_ssl { PgSslMode::Require } else { PgSslMode::Prefer }; PgConnectOptions::new() .host(&self.host) .username(&self.username) .password(self.password.expose_secret()) .port(self.port) .ssl_mode(ssl_mode) } pub fn with_db(&self) -> PgConnectOptions { self.without_db().database(&self.database_name) } } #[derive(serde::Deserialize, Clone)] pub struct EmailClientSettings { pub base_url: String, pub sender_email: String, pub authorization_token: Secret, #[serde(deserialize_with = "deserialize_number_from_string")] pub timeout_milliseconds: u64, } impl EmailClientSettings { pub fn client(self) -> EmailClient { let sender_email = self.sender().expect("Invalid sender email address."); let timeout = self.timeout(); EmailClient::new( self.base_url, sender_email, self.authorization_token, timeout, ) } pub fn sender(&self) -> Result { SubscriberEmail::parse(self.sender_email.clone()) } pub fn timeout(&self) -> std::time::Duration { std::time::Duration::from_millis(self.timeout_milliseconds) } } pub fn get_configuration() -> Result { let base_path = std::env::current_dir().expect("Failed to determine the current directory"); let configuration_directory = base_path.join("configuration"); // Detect the running environment. // Default to `local` if unspecified. let environment: Environment = std::env::var("APP_ENVIRONMENT") .unwrap_or_else(|_| "local".into()) .try_into() .expect("Failed to parse APP_ENVIRONMENT."); let environment_filename = format!("{}.yaml", environment.as_str()); let settings = config::Config::builder() .add_source(config::File::from( configuration_directory.join("base.yaml"), )) .add_source(config::File::from( configuration_directory.join(environment_filename), )) // Add in settings from environment variables (with a prefix of APP and '__' as separator) // E.g. `APP_APPLICATION__PORT=5001 would set `Settings.application.port` .add_source( config::Environment::with_prefix("APP") .prefix_separator("_") .separator("__"), ) .build()?; settings.try_deserialize::() } /// The possible runtime environment for our application. pub enum Environment { Local, Production, } impl Environment { pub fn as_str(&self) -> &'static str { match self { Environment::Local => "local", Environment::Production => "production", } } } impl TryFrom for Environment { type Error = String; fn try_from(s: String) -> Result { match s.to_lowercase().as_str() { "local" => Ok(Self::Local), "production" => Ok(Self::Production), other => Err(format!( "{} is not a supported environment. Use either `local` or `production`.", other )), } } }