schema_core/common/
connection_url.rs1use nutype::nutype;
2use std::fmt;
3
4#[nutype(
10 sanitize(trim),
11 validate(regex = r"^(postgresql|postgres)://\S+$"),
12 derive(Clone, AsRef, Deref, Hash, Eq, PartialEq, Serialize, Deserialize)
13)]
14pub struct ConnectionUrl(String);
15
16fn redact_password(url: &str) -> String {
20 let Some(after_scheme) = url.find("://").map(|i| i + 3) else {
21 return url.to_owned();
22 };
23 let Some(at) = url[after_scheme..].find('@').map(|i| after_scheme + i) else {
24 return url.to_owned();
25 };
26 let userinfo = &url[after_scheme..at];
27 match userinfo.find(':') {
28 Some(colon) => format!(
29 "{}{}:***{}",
30 &url[..after_scheme],
31 &userinfo[..colon],
32 &url[at..]
33 ),
34 None => url.to_owned(),
35 }
36}
37
38impl fmt::Display for ConnectionUrl {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 f.write_str(&redact_password(self.as_ref()))
42 }
43}
44
45impl fmt::Debug for ConnectionUrl {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 write!(f, "ConnectionUrl({})", redact_password(self.as_ref()))
49 }
50}
51
52#[derive(Debug, Clone, Default)]
53pub enum Scheme {
54 #[default]
55 Postgresql,
56 Postgres,
57}
58
59impl fmt::Display for Scheme {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 match self {
62 Self::Postgresql => write!(f, "postgresql"),
63 Self::Postgres => write!(f, "postgres"),
64 }
65 }
66}
67
68#[bon::bon]
69impl ConnectionUrl {
70 #[builder]
71 pub fn from_parts(
72 #[builder(default)] scheme: Scheme,
73 #[builder(into)] username: String,
74 #[builder(into)] password: Option<String>,
75 #[builder(into)] host: String,
76 port: Option<u16>,
77 #[builder(into)] database: Option<String>,
78 ) -> Result<Self, ConnectionUrlError> {
79 let mut url = format!("{}://{}", scheme, username);
80
81 if let Some(pwd) = password {
82 url.push(':');
83 url.push_str(&pwd);
84 }
85
86 url.push('@');
87 url.push_str(&host);
88
89 if let Some(p) = port {
90 url.push(':');
91 url.push_str(&p.to_string());
92 }
93
94 if let Some(db) = database {
95 url.push('/');
96 url.push_str(&db);
97 }
98
99 Self::try_new(url)
100 }
101}
102
103#[cfg(test)]
104#[allow(clippy::unwrap_used)]
105mod tests;