use secrecy::{ExposeSecret, SecretString}; use serde::Deserialize; use serde_aux::field_attributes::deserialize_number_from_string; use sqlx::{ postgres::{PgConnectOptions, PgSslMode}, ConnectOptions, }; use std::{fmt::Display, str::FromStr}; #[derive(Debug, Deserialize, Clone)] pub struct Settings { pub database: DatabaseSettings, pub application: ApplicationSettings, } #[derive(Debug, Deserialize, Clone)] 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, } #[derive(Debug, Deserialize, Clone)] pub struct ApplicationSettings { #[serde(deserialize_with = "deserialize_number_from_string")] pub port: u16, pub host: String, } #[derive(Debug, Clone)] pub enum Environment { Local, Production, } 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 )), } } } impl FromStr for Environment { type Err = String; fn from_str(s: &str) -> Result { s.to_owned().try_into() } }