1use std::fmt::{Display, Formatter};
2use std::path::{Path, PathBuf};
3use std::time::Duration;
4
5use kellnr_settings::Settings;
6use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, utf8_percent_encode};
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
9pub enum ConString {
10 Postgres(PgConString),
11 Sqlite(SqliteConString),
12}
13
14impl Display for ConString {
15 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
16 let con_string = match self {
17 ConString::Postgres(p) => p.to_string(),
18 ConString::Sqlite(s) => s.to_string(),
19 };
20 write!(f, "{con_string}")
21 }
22}
23
24impl ConString {
25 pub fn admin_pwd(&self) -> String {
26 match self {
27 ConString::Postgres(p) => p.admin.pwd.clone(),
28 ConString::Sqlite(s) => s.admin_pwd.clone(),
29 }
30 }
31
32 pub fn admin_token(&self) -> Option<String> {
33 match self {
34 ConString::Postgres(p) => p.admin.token.clone(),
35 ConString::Sqlite(s) => s.admin_token.clone(),
36 }
37 }
38
39 pub fn session_age(&self) -> Duration {
41 match self {
42 ConString::Postgres(p) => p.session_age,
43 ConString::Sqlite(s) => s.session_age,
44 }
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Hash)]
49pub struct AdminUser {
50 pub pwd: String,
51 pub token: Option<String>,
52}
53
54impl AdminUser {
55 pub fn new(pwd: String, token: Option<String>) -> Self {
56 Self { pwd, token }
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Hash)]
61pub enum PgConStringInner {
62 Structured {
64 addr: String,
65 port: u16,
66 db: String,
67 user: String,
68 pwd: String,
69 },
70 Raw(String),
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Hash)]
75pub struct PgConString {
76 inner: PgConStringInner,
77 admin: AdminUser,
78 pub session_age: Duration,
79}
80
81impl PgConString {
82 pub fn new(
83 addr: &str,
84 port: u16,
85 db: &str,
86 user: &str,
87 pwd: &str,
88 admin: AdminUser,
89 session_age: Duration,
90 ) -> Self {
91 Self {
92 inner: PgConStringInner::Structured {
93 addr: addr.to_owned(),
94 port,
95 db: db.to_owned(),
96 user: user.to_owned(),
97 pwd: pwd.to_owned(),
98 },
99 admin,
100 session_age,
101 }
102 }
103}
104
105impl From<&Settings> for PgConString {
106 fn from(s: &Settings) -> Self {
107 let inner = if let Some(string) = s.postgresql.connection_string.clone() {
108 PgConStringInner::Raw(string)
109 } else {
110 PgConStringInner::Structured {
111 addr: s.postgresql.address.clone(),
112 port: s.postgresql.port,
113 db: s.postgresql.db.clone(),
114 user: s.postgresql.user.clone(),
115 pwd: s.postgresql.pwd.clone(),
116 }
117 };
118
119 Self {
120 inner,
121 admin: AdminUser {
122 pwd: s.setup.admin_pwd.clone(),
123 token: s.setup.admin_token.clone(),
124 },
125 session_age: Duration::from_secs(s.registry.session_age_seconds),
126 }
127 }
128}
129
130const URL_COMPONENT: &AsciiSet = &NON_ALPHANUMERIC
133 .remove(b'-')
134 .remove(b'.')
135 .remove(b'_')
136 .remove(b'~');
137
138impl Display for PgConString {
139 fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
140 match &self.inner {
141 PgConStringInner::Raw(s) => write!(f, "{s}"),
142 PgConStringInner::Structured {
143 user,
144 pwd,
145 addr,
146 port,
147 db,
148 } => {
149 write!(
150 f,
151 "postgres://{}:{}@{}:{}/{}",
152 utf8_percent_encode(user, URL_COMPONENT),
153 utf8_percent_encode(pwd, URL_COMPONENT),
154 addr,
155 port,
156 utf8_percent_encode(db, URL_COMPONENT)
157 )
158 }
159 }
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Hash)]
164pub struct SqliteConString {
165 pub path: PathBuf,
166 pub admin_pwd: String,
167 pub admin_token: Option<String>,
168 pub session_age: Duration,
169}
170
171impl SqliteConString {
172 pub fn new(
173 path: &Path,
174 admin_pwd: &str,
175 admin_token: Option<String>,
176 session_age: Duration,
177 ) -> Self {
178 Self {
179 path: path.to_owned(),
180 admin_pwd: admin_pwd.to_owned(),
181 admin_token,
182 session_age,
183 }
184 }
185}
186
187impl From<&Settings> for SqliteConString {
188 fn from(settings: &Settings) -> Self {
189 Self {
190 path: settings.sqlite_path(),
191 admin_pwd: settings.setup.admin_pwd.clone(),
192 admin_token: settings.setup.admin_token.clone(),
193 session_age: Duration::from_secs(settings.registry.session_age_seconds),
194 }
195 }
196}
197
198impl Display for SqliteConString {
199 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
200 if self.path.to_str() == Some(":memory:") {
201 write!(f, "sqlite::memory:")
202 } else {
203 write!(f, "sqlite://{}?mode=rwc", self.path.display())
204 }
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use std::str::FromStr;
211
212 use percent_encoding::percent_decode_str;
213 use sea_orm::sqlx::postgres::PgConnectOptions;
214 use url::Url;
215
216 use super::*;
217
218 fn pg_con_string(user: &str, pwd: &str, db: &str) -> PgConString {
219 PgConString::new(
220 "localhost",
221 5432,
222 db,
223 user,
224 pwd,
225 AdminUser::new("admin".to_string(), None),
226 Duration::from_mins(1),
227 )
228 }
229
230 #[test]
231 fn pg_con_string_plain_credentials_are_unchanged() {
232 let con = pg_con_string("kellnr", "secret", "kellnr-db");
233
234 assert_eq!(
235 con.to_string(),
236 "postgres://kellnr:secret@localhost:5432/kellnr-db"
237 );
238 }
239
240 #[test]
241 fn pg_con_string_encodes_special_characters() {
242 let con = pg_con_string("us@er", "a/b@c:d%e#f?g", "my db");
243
244 assert_eq!(
245 con.to_string(),
246 "postgres://us%40er:a%2Fb%40c%3Ad%25e%23f%3Fg@localhost:5432/my%20db"
247 );
248 }
249
250 #[test]
251 fn pg_con_string_round_trips_through_sqlx() {
252 let user = "us@er:name";
253 let pwd = "cDlJH15F/Xj@:%2F#?&=+ รครถ";
254 let db = "kellnr/db";
255 let con = pg_con_string(user, pwd, db);
256
257 let opts = PgConnectOptions::from_str(&con.to_string()).unwrap();
258 let url = Url::parse(&con.to_string()).unwrap();
260 let decoded_pwd = percent_decode_str(url.password().unwrap())
261 .decode_utf8()
262 .unwrap();
263
264 assert_eq!(opts.get_username(), user);
265 assert_eq!(decoded_pwd, pwd);
266 assert_eq!(opts.get_host(), "localhost");
267 assert_eq!(opts.get_port(), 5432);
268 assert_eq!(opts.get_database(), Some(db));
269 }
270}