use std::fmt;
use std::time::Duration;
use secrecy::{ExposeSecret, SecretString};
use serde::Deserialize;
use serde_with::serde_as;
use sqlx::postgres::{PgConnectOptions, PgPoolOptions, PgSslMode};
#[serde_as]
#[derive(Clone, Deserialize)]
pub struct DatabaseSettings {
pub username: String,
pub password: SecretString,
pub host: String,
pub port: u16,
pub database_name: String,
pub require_ssl: bool,
#[serde(default)]
pub min_connections: Option<u32>,
#[serde(default)]
pub max_connections: Option<u32>,
#[serde(default, alias = "max_lifetime_secs")]
#[serde_as(as = "Option<serde_with::DurationSeconds<u64>>")]
pub max_lifetime: Option<Duration>,
#[serde(default, alias = "acquire_timeout_secs")]
#[serde_as(as = "Option<serde_with::DurationSeconds<u64>>")]
pub acquire_timeout: Option<Duration>,
#[serde(default, alias = "idle_timeout_secs")]
#[serde_as(as = "Option<serde_with::DurationSeconds<u64>>")]
pub idle_timeout: Option<Duration>,
}
impl DatabaseSettings {
pub fn connection_string(&self) -> SecretString {
let connection = format!(
"postgres://{db_user}:{db_password}@{host}:{port}/{db_name}",
db_user = self.username,
db_password = self.password.expose_secret(),
host = self.host,
port = self.port,
db_name = self.database_name,
);
SecretString::from(connection)
}
pub fn pg_pool_options(&self) -> PgPoolOptions {
let mut options = PgPoolOptions::new()
.max_lifetime(self.max_lifetime)
.idle_timeout(self.idle_timeout);
if let Some(acquire) = self.acquire_timeout {
options = options.acquire_timeout(acquire);
}
if let Some(min) = self.min_connections {
options = options.min_connections(min);
}
if let Some(max) = self.max_connections {
options = options.max_connections(max);
}
options
}
pub fn pg_connect_options_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 pg_connect_options_with_db(&self) -> PgConnectOptions {
self.pg_connect_options_without_db().database(&self.database_name)
}
}
impl fmt::Debug for DatabaseSettings {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DatabaseSettings")
.field("username", &self.username)
.field("password", &self.password)
.field("host", &self.host)
.field("port", &self.port)
.field("database_name", &self.database_name)
.field("require_ssl", &self.require_ssl)
.field("min_connections", &self.min_connections)
.field("max_connections", &self.max_connections)
.field("max_lifetime", &self.max_lifetime)
.field("acquire_timeout", &self.acquire_timeout)
.field("idle_timeout", &self.idle_timeout)
.finish()
}
}
impl PartialEq for DatabaseSettings {
fn eq(&self, other: &Self) -> bool {
self.require_ssl == other.require_ssl
&& self.port == other.port
&& self.host == other.host
&& self.username == other.username
&& self.database_name == other.database_name
&& self.password.expose_secret() == other.password.expose_secret()
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches2::{assert_let, assert_matches};
use pretty_assertions::assert_eq;
use trim_margin::MarginTrimmable;
#[test]
fn test_database_equality() {
let base = DatabaseSettings {
username: "Billy".to_string(),
password: SecretString::from("my-secret"),
port: 1234,
host: "localhost".to_string(),
database_name: "db_name".to_string(),
require_ssl: true,
min_connections: None,
max_connections: None,
max_lifetime: None,
acquire_timeout: None,
idle_timeout: None,
};
let same = base.clone();
assert_eq!(base, same);
let mut diff_user = base.clone();
diff_user.username = "NotBilly".to_string();
assert!(base != diff_user);
let mut diff_pass = base.clone();
diff_pass.password = SecretString::from("other-secret");
assert!(base != diff_pass);
let mut diff_port = base.clone();
diff_port.port = 4321;
assert!(base != diff_port);
let mut diff_host = base.clone();
diff_host.host = "otherhost".to_string();
assert!(base != diff_host);
let mut diff_db = base.clone();
diff_db.database_name = "other_db".to_string();
assert!(base != diff_db);
let mut diff_ssl = base.clone();
diff_ssl.require_ssl = false;
assert!(base != diff_ssl);
}
#[test]
fn test_database_connection_string() {
let settings = DatabaseSettings {
username: "Billy".to_string(),
password: SecretString::from("my-secret"),
port: 1234,
host: "localhost".to_string(),
database_name: "db_name".to_string(),
require_ssl: true,
min_connections: None,
max_connections: None,
max_lifetime: None,
acquire_timeout: None,
idle_timeout: None,
};
let conn_str = settings.connection_string();
assert_eq!(
conn_str.expose_secret(),
"postgres://Billy:my-secret@localhost:1234/db_name"
);
}
#[test]
fn test_pg_pool_options() {
let mut settings = DatabaseSettings {
username: "Billy".to_string(),
password: SecretString::from("my-secret"),
port: 1234,
host: "localhost".to_string(),
database_name: "db_name".to_string(),
require_ssl: true,
min_connections: Some(2),
max_connections: Some(20),
max_lifetime: Some(Duration::from_secs(3600)),
acquire_timeout: Some(Duration::from_secs(10)),
idle_timeout: Some(Duration::from_secs(300)),
};
let _options = settings.pg_pool_options();
settings.min_connections = None;
settings.max_connections = None;
settings.max_lifetime = None;
settings.acquire_timeout = None;
settings.idle_timeout = None;
let _options_none = settings.pg_pool_options();
}
#[test]
fn test_pg_connect_options() {
let settings = DatabaseSettings {
username: "Billy".to_string(),
password: SecretString::from("my-secret"),
port: 1234,
host: "localhost".to_string(),
database_name: "db_name".to_string(),
require_ssl: true,
min_connections: None,
max_connections: None,
max_lifetime: None,
acquire_timeout: None,
idle_timeout: None,
};
let _opts_no_db = settings.pg_connect_options_without_db();
let _opts_with_db = settings.pg_connect_options_with_db();
}
#[test]
fn test_database_deser() {
let yaml = r##"
|username: Billy
|password: my-secret
|port: 1234
|host: localhost
|database_name: db_name
|require_ssl: true
|max_connections: 10
|acquire_timeout_secs: 5
|idle_timeout_secs: 180
|"##
.trim_margin()
.unwrap();
assert_let!(Ok(from_yaml) = serde_yaml::from_str::<DatabaseSettings>(yaml.as_str()));
assert_eq!(from_yaml.username, "Billy".to_string(),);
assert_eq!(from_yaml.port, 1234,);
assert_eq!(from_yaml.host, "localhost".to_string(),);
assert_eq!(from_yaml.database_name, "db_name".to_string(),);
assert_eq!(from_yaml.require_ssl, true,);
assert_matches!(from_yaml.min_connections, None);
assert_matches!(from_yaml.max_connections, Some(actual_max));
assert_eq!(actual_max, 10);
assert_matches!(from_yaml.max_lifetime, None);
assert_let!(Some(actual_aquire_timeout) = from_yaml.acquire_timeout);
assert_eq!(actual_aquire_timeout, Duration::from_secs(5));
assert_let!(Some(actual_idle_timeout) = from_yaml.idle_timeout);
assert_eq!(actual_idle_timeout, Duration::from_secs(180));
}
}