#![deny(unsafe_code)]
#[cfg(feature = "serde")]
mod de;
pub(crate) mod parser;
#[cfg(feature = "serde")]
mod ser;
#[cfg(test)]
mod tests;
use parser::{
authority::{userinfo::UserSpec, Authority},
ConnectionUri,
};
use std::{fmt::Display, path::PathBuf, str::FromStr};
use tracing::{debug, trace};
pub use parser::authority::host::Host;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Parameter {
pub keyword: String,
pub value: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ConnectionString {
pub user: Option<String>,
pub password: Option<String>,
pub hostspecs: Vec<HostSpec>,
pub database: Option<String>,
pub parameters: Vec<Parameter>,
pub fragment: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HostSpec {
pub host: Host,
pub port: Option<u16>,
}
impl Display for ConnectionString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "postgresql://",)?;
if let Some(user) = &self.user {
write!(f, "{user}",)?;
if let Some(password) = &self.password {
write!(f, ":{password}",)?;
}
write!(f, "@")?;
}
for (
n,
HostSpec {
host,
port,
},
) in self.hostspecs.iter().enumerate()
{
if let Host::Path(_) = host {
continue;
}
write!(f, "{host}")?;
if let Some(p) = port {
write!(f, ":{p}")?;
}
if n + 1 < self.hostspecs.len() {
write!(f, ",")?;
}
}
if let Some(database) = &self.database {
write!(f, "/{database}")?;
}
for (
n,
Parameter {
keyword,
value,
},
) in self.parameters.iter().enumerate()
{
if n == 0 {
write!(f, "?")?;
}
write!(f, "{keyword}={value}")?;
if n + 1 < self.parameters.len() {
write!(f, "&")?;
}
}
for HostSpec {
host,
..
} in &self.hostspecs
{
match host {
Host::Path(path) => {
write!(
f,
"{}",
if self.parameters.is_empty() {
"?"
} else {
"&"
}
)?;
write!(f, "host={}", path.to_str().unwrap_or("invalid"))?;
},
_ => continue,
}
}
if let Some(frag) = &self.fragment {
write!(f, "#{frag}")?;
}
Ok(())
}
}
impl TryFrom<ConnectionUri> for ConnectionString {
type Error = anyhow::Error;
fn try_from(mut uri: ConnectionUri) -> Result<Self, Self::Error> {
let mut addtl_hosts = vec![];
if let Some(params) = &mut uri.parameters {
if let Some(pos) = params.iter().position(|p| p.keyword == "host") {
let param = params.remove(pos);
addtl_hosts.push(param);
}
}
let mut out = ConnectionString {
database: uri.database,
parameters: uri.parameters.unwrap_or(vec![]),
fragment: uri.fragment,
..ConnectionString::default()
};
trace!(?out, "populated unchanging pieces");
if let Some(Authority {
userspec,
hostspec,
}) = uri.authority
{
trace!(?userspec, ?hostspec, "found authority");
if let Some(UserSpec {
user,
password,
}) = userspec
{
trace!(?user, ?password, "found userspec");
out.user = Some(user);
out.password = password;
}
for spec in hostspec {
trace!(?spec, "adding hostspec");
if let Some(host) = spec.host {
out.hostspecs.push(HostSpec {
host,
port: spec.port,
});
}
}
}
for Parameter {
value,
..
} in addtl_hosts
{
let host = if value.starts_with('/') {
Host::Path(PathBuf::from(value))
} else {
value.parse()?
};
out.hostspecs.push(HostSpec {
host,
port: None,
});
}
Ok(out)
}
}
impl FromStr for ConnectionString {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parsed = parser::consuming_connection_string(s)?;
debug!(?parsed);
parsed.try_into()
}
}
pub fn from_multi_str(
i: &str,
sep: &str,
) -> anyhow::Result<Vec<ConnectionString>> {
let parsed = parser::multi_connection_string(i, sep)?;
eprintln!("{parsed:?}");
let mut out: Vec<ConnectionString> = vec![];
for c in parsed {
out.push(TryFrom::try_from(c)?);
}
Ok(out)
}