use std::{fmt::Display, time::Duration}; use secrecy::{ExposeSecret, Secret}; use serde::Deserialize; use serde_aux::field_attributes::deserialize_number_from_string; use sqlx::{ postgres::{PgConnectOptions, PgSslMode}, ConnectOptions, }; use crate::domain::SubscriberEmail; #[derive(Debug, Deserialize)] pub struct Settings { pub database: DatabaseSettings, pub application: ApplicationSettings, pub email_client: EmailClientSettings, } #[derive(Debug, Deserialize)] 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, } #[derive(Debug, Deserialize)] pub struct ApplicationSettings { #[serde(deserialize_with = "deserialize_number_from_string")] pub port: u16, pub host: String, } #[derive(Debug)] pub enum Environment { Local, Production, } #[derive(Debug, Deserialize)] pub struct EmailClientSettings { pub base_url: String, pub sender_email: String, pub auth_token: Secret, pub timeout_milliseconds: u64, } impl EmailClientSettings { pub fn sender(&self) -> Result { self.sender_email.clone().try_into() } pub fn timeout(&self) -> Duration { Duration::from_millis(self.timeout_milliseconds) } } pub fn get_config() -> Result { let base_path = std::env::current_dir().expect("Failed to determine current directory"); let config_directory = base_path.join("config"); let env: Environment = std::env::var("APP_ENVIRONMENT") .unwrap_or_else(|_| "local".into()) .try_into() .expect("Failed to parse APP_ENVIRONMENT"); let env_filename = format!("{}.toml", &env); let settings = config::Config::builder() .add_source(config::File::from(config_directory.join("base.toml"))) .add_source(config::File::from(config_directory.join(env_filename))) .add_source( config::Environment::with_prefix("APP") .prefix_separator("_") .separator("__"), ) .build()?; settings.try_deserialize::() } 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) .log_statements(tracing_log::log::LevelFilter::Trace) } } impl Display for Environment { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Environment::Local => write!(f, "local"), Environment::Production => write!(f, "production"), } } } impl TryFrom for Environment { type Error = String; fn try_from(value: String) -> Result { match value.to_lowercase().as_str() { "local" => Ok(Self::Local), "production" => Ok(Self::Production), other => Err(format!( "{} is not supported environment. \ Use either `local` or `production`.", other )), } } }