use crate::domain::SubscriberEmail; use anyhow::Context; use secrecy::{ExposeSecret, SecretString}; use serde::Deserialize; use serde_aux::field_attributes::deserialize_number_from_string; use sqlx::postgres::{PgConnectOptions, PgSslMode}; use tower_sessions_redis_store::{ RedisStore, fred::prelude::{ClientLike, Pool}, }; pub fn get_configuration() -> Result { let base_path = std::env::current_dir().expect("Failed to determine the current directory"); let config_dir = base_path.join("configuration"); 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(config_dir.join("base.yaml"))) .add_source(config::File::from(config_dir.join(environment_filename))) .add_source( config::Environment::with_prefix("APP") .prefix_separator("_") .separator("__"), ) .build()?; settings.try_deserialize::() } pub enum Environment { Local, Production, } impl Environment { pub fn as_str(&self) -> &str { match self { Environment::Local => "local", Environment::Production => "production", } } } impl TryFrom for Environment { type Error = String; fn try_from(value: String) -> Result { match value.to_lowercase().as_str() { "local" => Ok(Environment::Local), "production" => Ok(Environment::Production), other => Err(format!( "{} is not a supported environment. Use either `local` or `production`.", other )), } } } #[derive(Clone, Deserialize)] pub struct Settings { pub application: ApplicationSettings, pub database: DatabaseSettings, pub email_client: EmailClientSettings, pub kv_store: RedisSettings, } #[derive(Clone, Deserialize)] pub struct ApplicationSettings { #[serde(deserialize_with = "deserialize_number_from_string")] pub port: u16, pub host: String, pub base_url: String, } #[derive(Clone, Deserialize)] pub struct EmailClientSettings { pub base_url: String, sender_email: String, pub authorization_token: SecretString, pub timeout_milliseconds: u64, } impl EmailClientSettings { pub fn sender(&self) -> Result { SubscriberEmail::parse(self.sender_email.clone()) } pub fn new( base_url: String, sender_email: String, authorization_token: String, timeout_milliseconds: u64, ) -> Self { let authorization_token = SecretString::from(authorization_token); Self { base_url, sender_email, authorization_token, timeout_milliseconds, } } } #[derive(Clone, Deserialize)] pub struct RedisSettings { pub host: String, pub port: u16, } impl RedisSettings { pub fn connection_string(&self) -> String { format!("redis://{}:{}", self.host, self.port) } pub async fn session_store(&self) -> Result, anyhow::Error> { let pool = Pool::new( tower_sessions_redis_store::fred::prelude::Config::from_url(&self.connection_string()) .context("Failed to parse Redis URL string.")?, None, None, None, 6, ) .unwrap(); pool.connect(); pool.wait_for_connect() .await .context("Failed to connect to the Redis server.")?; Ok(RedisStore::new(pool)) } } #[derive(Clone, Deserialize)] pub struct DatabaseSettings { pub username: String, pub password: SecretString, #[serde(deserialize_with = "deserialize_number_from_string")] pub port: u16, pub host: String, pub database_name: String, pub require_ssl: bool, pub timeout_milliseconds: u64, } impl DatabaseSettings { pub fn with_db(&self) -> PgConnectOptions { self.without_db().database(&self.database_name) } 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) } }