use std::fmt::{Display, Formatter};
use std::path::{Path, PathBuf};
use std::time::Duration;
use kellnr_settings::Settings;
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, utf8_percent_encode};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ConString {
Postgres(PgConString),
Sqlite(SqliteConString),
}
impl Display for ConString {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let con_string = match self {
ConString::Postgres(p) => p.to_string(),
ConString::Sqlite(s) => s.to_string(),
};
write!(f, "{con_string}")
}
}
impl ConString {
pub fn admin_pwd(&self) -> String {
match self {
ConString::Postgres(p) => p.admin.pwd.clone(),
ConString::Sqlite(s) => s.admin_pwd.clone(),
}
}
pub fn admin_token(&self) -> Option<String> {
match self {
ConString::Postgres(p) => p.admin.token.clone(),
ConString::Sqlite(s) => s.admin_token.clone(),
}
}
pub fn session_age(&self) -> Duration {
match self {
ConString::Postgres(p) => p.session_age,
ConString::Sqlite(s) => s.session_age,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AdminUser {
pub pwd: String,
pub token: Option<String>,
}
impl AdminUser {
pub fn new(pwd: String, token: Option<String>) -> Self {
Self { pwd, token }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum PgConStringInner {
Structured {
addr: String,
port: u16,
db: String,
user: String,
pwd: String,
},
Raw(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PgConString {
inner: PgConStringInner,
admin: AdminUser,
pub session_age: Duration,
}
impl PgConString {
pub fn new(
addr: &str,
port: u16,
db: &str,
user: &str,
pwd: &str,
admin: AdminUser,
session_age: Duration,
) -> Self {
Self {
inner: PgConStringInner::Structured {
addr: addr.to_owned(),
port,
db: db.to_owned(),
user: user.to_owned(),
pwd: pwd.to_owned(),
},
admin,
session_age,
}
}
}
impl From<&Settings> for PgConString {
fn from(s: &Settings) -> Self {
let inner = if let Some(string) = s.postgresql.connection_string.clone() {
PgConStringInner::Raw(string)
} else {
PgConStringInner::Structured {
addr: s.postgresql.address.clone(),
port: s.postgresql.port,
db: s.postgresql.db.clone(),
user: s.postgresql.user.clone(),
pwd: s.postgresql.pwd.clone(),
}
};
Self {
inner,
admin: AdminUser {
pwd: s.setup.admin_pwd.clone(),
token: s.setup.admin_token.clone(),
},
session_age: Duration::from_secs(s.registry.session_age_seconds),
}
}
}
const URL_COMPONENT: &AsciiSet = &NON_ALPHANUMERIC
.remove(b'-')
.remove(b'.')
.remove(b'_')
.remove(b'~');
impl Display for PgConString {
fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
match &self.inner {
PgConStringInner::Raw(s) => write!(f, "{s}"),
PgConStringInner::Structured {
user,
pwd,
addr,
port,
db,
} => {
write!(
f,
"postgres://{}:{}@{}:{}/{}",
utf8_percent_encode(user, URL_COMPONENT),
utf8_percent_encode(pwd, URL_COMPONENT),
addr,
port,
utf8_percent_encode(db, URL_COMPONENT)
)
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SqliteConString {
pub path: PathBuf,
pub admin_pwd: String,
pub admin_token: Option<String>,
pub session_age: Duration,
}
impl SqliteConString {
pub fn new(
path: &Path,
admin_pwd: &str,
admin_token: Option<String>,
session_age: Duration,
) -> Self {
Self {
path: path.to_owned(),
admin_pwd: admin_pwd.to_owned(),
admin_token,
session_age,
}
}
}
impl From<&Settings> for SqliteConString {
fn from(settings: &Settings) -> Self {
Self {
path: settings.sqlite_path(),
admin_pwd: settings.setup.admin_pwd.clone(),
admin_token: settings.setup.admin_token.clone(),
session_age: Duration::from_secs(settings.registry.session_age_seconds),
}
}
}
impl Display for SqliteConString {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.path.to_str() == Some(":memory:") {
write!(f, "sqlite::memory:")
} else {
write!(f, "sqlite://{}?mode=rwc", self.path.display())
}
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use percent_encoding::percent_decode_str;
use sea_orm::sqlx::postgres::PgConnectOptions;
use url::Url;
use super::*;
fn pg_con_string(user: &str, pwd: &str, db: &str) -> PgConString {
PgConString::new(
"localhost",
5432,
db,
user,
pwd,
AdminUser::new("admin".to_string(), None),
Duration::from_mins(1),
)
}
#[test]
fn pg_con_string_plain_credentials_are_unchanged() {
let con = pg_con_string("kellnr", "secret", "kellnr-db");
assert_eq!(
con.to_string(),
"postgres://kellnr:secret@localhost:5432/kellnr-db"
);
}
#[test]
fn pg_con_string_encodes_special_characters() {
let con = pg_con_string("us@er", "a/b@c:d%e#f?g", "my db");
assert_eq!(
con.to_string(),
"postgres://us%40er:a%2Fb%40c%3Ad%25e%23f%3Fg@localhost:5432/my%20db"
);
}
#[test]
fn pg_con_string_round_trips_through_sqlx() {
let user = "us@er:name";
let pwd = "cDlJH15F/Xj@:%2F#?&=+ รครถ";
let db = "kellnr/db";
let con = pg_con_string(user, pwd, db);
let opts = PgConnectOptions::from_str(&con.to_string()).unwrap();
let url = Url::parse(&con.to_string()).unwrap();
let decoded_pwd = percent_decode_str(url.password().unwrap())
.decode_utf8()
.unwrap();
assert_eq!(opts.get_username(), user);
assert_eq!(decoded_pwd, pwd);
assert_eq!(opts.get_host(), "localhost");
assert_eq!(opts.get_port(), 5432);
assert_eq!(opts.get_database(), Some(db));
}
}