cratefield_import_supabase/
secret.rs1use std::fmt;
8use zeroize::Zeroizing;
9
10#[derive(Clone)]
13pub struct Secret(Zeroizing<String>);
14
15impl Secret {
16 #[must_use]
18 pub fn new(value: impl Into<String>) -> Self {
19 Self(Zeroizing::new(value.into()))
20 }
21
22 #[must_use]
24 pub fn expose(&self) -> &str {
25 &self.0
26 }
27}
28
29impl fmt::Debug for Secret {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 f.write_str("[redacted]")
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct SourceName {
38 pub host: String,
40 pub port: u16,
42 pub database: String,
44}
45
46pub fn source_name(url: &Secret) -> Result<SourceName, String> {
53 let parsed = url::Url::parse(url.expose()).map_err(|_| {
54 "the database URL is not a valid URL (postgres://user:password@host:5432/postgres)"
55 .to_owned()
56 })?;
57 if !matches!(parsed.scheme(), "postgres" | "postgresql") {
58 return Err(format!(
59 "the database URL must start with postgres:// or postgresql://, not {}://",
60 parsed.scheme()
61 ));
62 }
63 let host = parsed
64 .host_str()
65 .filter(|host| !host.is_empty())
66 .ok_or_else(|| "the database URL names no host".to_owned())?
67 .to_owned();
68 let database = parsed.path().trim_start_matches('/');
69 Ok(SourceName {
70 host,
71 port: parsed.port().unwrap_or(5432),
72 database: if database.is_empty() {
73 "postgres".to_owned()
74 } else {
75 database.to_owned()
76 },
77 })
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn debug_never_prints_the_value() {
86 let secret = Secret::new("postgres://u:hunter2@db.example.test/postgres");
87 assert_eq!(format!("{secret:?}"), "[redacted]");
88 }
89
90 #[test]
91 fn the_source_name_keeps_host_port_and_database_only() {
92 let name = source_name(&Secret::new(
93 "postgresql://inspect.ref:hunter2@db.abc.supabase.co:6543/postgres?sslmode=require",
94 ))
95 .expect("parses");
96 assert_eq!(
97 name,
98 SourceName {
99 host: "db.abc.supabase.co".to_owned(),
100 port: 6543,
101 database: "postgres".to_owned(),
102 }
103 );
104 }
105
106 #[test]
107 fn a_bad_url_is_refused_without_quoting_it() {
108 let error = source_name(&Secret::new("hunter2 is not a url")).unwrap_err();
109 assert!(!error.contains("hunter2"), "{error}");
110 let error = source_name(&Secret::new("mysql://u:hunter2@h/db")).unwrap_err();
111 assert!(!error.contains("hunter2"), "{error}");
112 }
113}