//! Contains structs and helpers related to server configuration. use serde_aux::field_attributes::deserialize_number_from_string; use sqlx::postgres::{PgConnectOptions, PgSslMode}; use std::convert::{TryFrom, TryInto}; /// Top-level configuration struct. #[derive(serde::Deserialize, Clone)] pub struct Settings { pub database: DatabaseSettings, pub application: ApplicationSettings, // pub email_client: EmailClientSettings, } /// Contains settings relevant at the application level. #[derive(serde::Deserialize, Clone)] pub struct ApplicationSettings { #[serde(deserialize_with = "deserialize_number_from_string")] pub port: u16, pub host: String, pub base_url: String, } /// Contains settings for the database connection. #[derive(serde::Deserialize, Clone)] pub struct DatabaseSettings { pub username: String, pub password: String, #[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) .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: String, // pub timeout_milliseconds: u64, // } // impl EmailClientSettings { // // 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) // } // } /// Load settings from the configuration directory and environment variables. pub fn get_configuration() -> Result { let mut settings = config::Config::default(); let base_path = std::env::current_dir().expect("Failed to determine the current directory"); let configuration_directory = base_path.join("configuration"); // Read the "default" configuration file settings.merge(config::File::from(configuration_directory.join("base")).required(true))?; // 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."); // Layer on the environment-specific values. settings.merge( config::File::from(configuration_directory.join(environment.as_str())).required(true), )?; // 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` settings.merge(config::Environment::with_prefix("app").separator("__"))?; settings.try_into() } /// 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 )), } } }