zero-to-production/src/configuration.rs

145 lines
4.3 KiB
Rust
Raw Permalink Normal View History

2021-01-16 23:01:08 +00:00
use crate::domain::SubscriberEmail;
2022-03-13 19:36:44 +00:00
use crate::email_client::EmailClient;
2021-12-27 12:24:24 +00:00
use secrecy::{ExposeSecret, Secret};
use serde_aux::field_attributes::deserialize_number_from_string;
use sqlx::postgres::{PgConnectOptions, PgSslMode};
use std::convert::{TryFrom, TryInto};
2021-02-14 16:27:04 +00:00
#[derive(serde::Deserialize, Clone)]
pub struct Settings {
pub database: DatabaseSettings,
pub application: ApplicationSettings,
2021-01-16 15:11:17 +00:00
pub email_client: EmailClientSettings,
pub redis_uri: Secret<String>,
}
2021-02-14 16:27:04 +00:00
#[derive(serde::Deserialize, Clone)]
pub struct ApplicationSettings {
#[serde(deserialize_with = "deserialize_number_from_string")]
pub port: u16,
pub host: String,
2021-03-11 09:24:57 +00:00
pub base_url: String,
pub hmac_secret: Secret<String>,
}
2021-02-14 16:27:04 +00:00
#[derive(serde::Deserialize, Clone)]
pub struct DatabaseSettings {
pub username: String,
2021-12-27 12:24:24 +00:00
pub password: Secret<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)
2021-12-28 16:51:36 +00:00
.password(self.password.expose_secret())
.port(self.port)
.ssl_mode(ssl_mode)
}
pub fn with_db(&self) -> PgConnectOptions {
2023-09-14 07:43:32 +00:00
self.without_db().database(&self.database_name)
}
}
2021-02-14 16:27:04 +00:00
#[derive(serde::Deserialize, Clone)]
2021-01-16 15:11:17 +00:00
pub struct EmailClientSettings {
pub base_url: String,
2021-01-16 23:01:08 +00:00
pub sender_email: String,
2021-12-27 12:24:24 +00:00
pub authorization_token: Secret<String>,
#[serde(deserialize_with = "deserialize_number_from_string")]
pub timeout_milliseconds: u64,
2021-01-16 23:01:08 +00:00
}
impl EmailClientSettings {
2022-03-13 19:36:44 +00:00
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,
)
}
2021-01-16 23:01:08 +00:00
pub fn sender(&self) -> Result<SubscriberEmail, String> {
SubscriberEmail::parse(self.sender_email.clone())
}
pub fn timeout(&self) -> std::time::Duration {
std::time::Duration::from_millis(self.timeout_milliseconds)
}
2021-01-16 15:11:17 +00:00
}
pub fn get_configuration() -> Result<Settings, config::ConfigError> {
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.");
2022-07-05 16:47:21 +00:00
let environment_filename = format!("{}.yaml", environment.as_str());
let settings = config::Config::builder()
2022-10-01 17:07:52 +00:00
.add_source(config::File::from(
configuration_directory.join("base.yaml"),
))
.add_source(config::File::from(
2023-02-18 22:26:43 +00:00
configuration_directory.join(environment_filename),
2022-10-01 17:07:52 +00:00
))
2022-07-05 16:47:21 +00:00
// 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`
2022-10-01 17:07:52 +00:00
.add_source(
config::Environment::with_prefix("APP")
.prefix_separator("_")
.separator("__"),
)
2022-07-05 16:47:21 +00:00
.build()?;
2022-10-01 17:07:52 +00:00
2022-07-05 16:47:21 +00:00
settings.try_deserialize::<Settings>()
}
/// 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<String> for Environment {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
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
)),
}
}
}