1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use url::Url;
9pub fn parse_bool(value: &str) -> Result<bool, String> {
26 let normalized = value.trim().to_lowercase();
27
28 match normalized.as_str() {
29 "true" | "1" | "yes" | "on" | "ok" | "y" => Ok(true),
30 "false" | "0" | "no" | "off" | "n" => Ok(false),
31 _ => Err(format!("Invalid boolean value: {}", value)),
32 }
33}
34pub fn parse_list(value: &str) -> Vec<String> {
48 value
49 .split(',')
50 .map(|s| s.trim().to_string())
51 .filter(|s| !s.is_empty())
52 .collect()
53}
54pub fn parse_dict(value: &str) -> HashMap<String, String> {
67 let mut map = HashMap::new();
68
69 for pair in value.split(',') {
70 if let Some((key, val)) = pair.split_once('=') {
71 let key = key.trim().to_string();
72 let val = val.trim().to_string();
73
74 if !key.is_empty() && !val.is_empty() {
76 map.insert(key, val);
77 }
78 }
79 }
80
81 map
82}
83
84#[derive(Clone, Serialize, Deserialize, PartialEq)]
86pub struct DatabaseUrl {
87 pub engine: String,
89
90 pub name: String,
92
93 pub user: Option<String>,
95
96 pub password: Option<String>,
98
99 pub host: Option<String>,
101
102 pub port: Option<u16>,
104
105 pub options: HashMap<String, String>,
107
108 pub url: String,
110}
111
112impl std::fmt::Debug for DatabaseUrl {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 f.debug_struct("DatabaseUrl")
115 .field("engine", &self.engine)
116 .field("name", &self.name)
117 .field("user", &self.user)
118 .field(
119 "password",
120 if self.password.is_some() {
121 &"Some([REDACTED])"
122 } else {
123 &"None"
124 },
125 )
126 .field("host", &self.host)
127 .field("port", &self.port)
128 .field("options", &self.options)
129 .field("url", &"[REDACTED]")
130 .finish()
131 }
132}
133
134pub fn parse_database_url(url_str: &str) -> Result<DatabaseUrl, String> {
157 if url_str.starts_with("sqlite:") {
159 return parse_sqlite_url(url_str);
160 }
161
162 let url = Url::parse(url_str).map_err(|e| format!("Invalid URL: {}", e))?;
163
164 let scheme = url.scheme();
165 let engine = match scheme {
166 "postgresql" | "postgres" => "reinhardt.db.backends.postgresql",
167 "mysql" | "mariadb" => "reinhardt.db.backends.mysql",
168 "sqlite" => "reinhardt.db.backends.sqlite3",
169 other => return Err(format!("Unsupported database scheme: {}", other)),
170 };
171
172 let name = url.path().trim_start_matches('/').to_string();
173
174 if name.is_empty() && scheme != "sqlite" {
175 return Err("Database name is required".to_string());
176 }
177
178 let user = if url.username().is_empty() {
179 None
180 } else {
181 Some(url.username().to_string())
182 };
183
184 let password = url.password().map(|p| p.to_string());
185 let host = url.host_str().map(|h| h.to_string());
186 let port = url.port();
187
188 let mut options = HashMap::new();
190 for (key, value) in url.query_pairs() {
191 options.insert(key.to_string(), value.to_string());
192 }
193
194 Ok(DatabaseUrl {
195 engine: engine.to_string(),
196 name,
197 user,
198 password,
199 host,
200 port,
201 options,
202 url: url_str.to_string(),
203 })
204}
205
206fn parse_sqlite_url(url_str: &str) -> Result<DatabaseUrl, String> {
208 let name = if url_str == "sqlite::memory:" || url_str == "sqlite://:memory:" {
209 ":memory:".to_string()
210 } else if url_str.starts_with("sqlite:///") {
211 url_str.trim_start_matches("sqlite:///").to_string()
212 } else if url_str.starts_with("sqlite://") {
213 url_str.trim_start_matches("sqlite://").to_string()
214 } else if url_str.starts_with("sqlite:") {
215 url_str.trim_start_matches("sqlite:").to_string()
217 } else {
218 return Err("Invalid SQLite URL format".to_string());
219 };
220
221 Ok(DatabaseUrl {
222 engine: "reinhardt.db.backends.sqlite3".to_string(),
223 name,
224 user: None,
225 password: None,
226 host: None,
227 port: None,
228 options: HashMap::new(),
229 url: url_str.to_string(),
230 })
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct CacheUrl {
236 pub backend: String,
238 pub location: Option<String>,
240 pub options: HashMap<String, String>,
242}
243pub fn parse_cache_url(url_str: &str) -> Result<CacheUrl, String> {
263 if url_str == "locmem://" || url_str.starts_with("locmem://") {
264 return Ok(CacheUrl {
265 backend: "reinhardt.cache.backends.locmem.LocMemCache".to_string(),
266 location: None,
267 options: HashMap::new(),
268 });
269 }
270
271 let url = Url::parse(url_str).map_err(|e| format!("Invalid cache URL: {}", e))?;
272
273 let (backend, location) = match url.scheme() {
274 "redis" => (
275 "reinhardt.cache.backends.redis.RedisCache",
276 Some(url_str.to_string()),
277 ),
278 "memcached" => (
279 "reinhardt.cache.backends.memcached.PyMemcacheCache",
280 Some(format!(
281 "{}:{}",
282 url.host_str().unwrap_or("localhost"),
283 url.port().unwrap_or(11211)
284 )),
285 ),
286 other => return Err(format!("Unsupported cache scheme: {}", other)),
287 };
288
289 let mut options = HashMap::new();
290 for (key, value) in url.query_pairs() {
291 options.insert(key.to_string(), value.to_string());
292 }
293
294 Ok(CacheUrl {
295 backend: backend.to_string(),
296 location,
297 options,
298 })
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn test_parse_bool() {
307 assert!(parse_bool("true").unwrap());
308 assert!(parse_bool("True").unwrap());
309 assert!(parse_bool("1").unwrap());
310 assert!(parse_bool("yes").unwrap());
311 assert!(parse_bool("on").unwrap());
312
313 assert!(!parse_bool("false").unwrap());
314 assert!(!parse_bool("False").unwrap());
315 assert!(!parse_bool("0").unwrap());
316 assert!(!parse_bool("no").unwrap());
317 assert!(!parse_bool("off").unwrap());
318
319 assert!(parse_bool("invalid").is_err());
320 }
321
322 #[test]
323 fn test_parse_list() {
324 assert_eq!(parse_list("a,b,c"), vec!["a", "b", "c"]);
325 assert_eq!(parse_list("a, b, c"), vec!["a", "b", "c"]);
326 assert_eq!(parse_list(""), Vec::<String>::new());
327 assert_eq!(parse_list("single"), vec!["single"]);
328 }
329
330 #[test]
331 fn test_parse_dict() {
332 let dict = parse_dict("key1=value1,key2=value2");
333 assert_eq!(dict.get("key1").unwrap(), "value1");
334 assert_eq!(dict.get("key2").unwrap(), "value2");
335 }
336
337 #[test]
338 fn test_parse_dict_skips_empty_key_or_value() {
339 let dict = parse_dict("=value");
341 assert!(dict.is_empty());
342
343 let dict = parse_dict("key=");
345 assert!(dict.is_empty());
346
347 let dict = parse_dict("=");
349 assert!(dict.is_empty());
350
351 let dict = parse_dict("=value,key=,valid=entry,=");
353 assert_eq!(dict.len(), 1);
354 assert_eq!(dict.get("valid").unwrap(), "entry");
355 }
356
357 #[test]
358 fn test_parse_sqlite_memory() {
359 let db = parse_database_url("sqlite::memory:").unwrap();
360 assert_eq!(db.engine, "reinhardt.db.backends.sqlite3");
361 assert_eq!(db.name, ":memory:");
362 assert!(db.user.is_none());
363 }
364
365 #[test]
366 fn test_parse_sqlite_file() {
367 let db = parse_database_url("sqlite:///path/to/db.sqlite3").unwrap();
368 assert_eq!(db.engine, "reinhardt.db.backends.sqlite3");
369 assert_eq!(db.name, "path/to/db.sqlite3");
370 }
371
372 #[test]
373 fn test_parse_postgresql() {
374 let db = parse_database_url("postgresql://user:pass@localhost:5432/mydb").unwrap();
375 assert_eq!(db.engine, "reinhardt.db.backends.postgresql");
376 assert_eq!(db.name, "mydb");
377 assert_eq!(db.user.unwrap(), "user");
378 assert_eq!(db.password.unwrap(), "pass");
379 assert_eq!(db.host.unwrap(), "localhost");
380 assert_eq!(db.port.unwrap(), 5432);
381 }
382
383 #[test]
384 fn test_parse_mysql() {
385 let db = parse_database_url("mysql://root:secret@127.0.0.1:3306/testdb").unwrap();
386 assert_eq!(db.engine, "reinhardt.db.backends.mysql");
387 assert_eq!(db.name, "testdb");
388 assert_eq!(db.user.unwrap(), "root");
389 assert_eq!(db.password.unwrap(), "secret");
390 assert_eq!(db.host.unwrap(), "127.0.0.1");
391 assert_eq!(db.port.unwrap(), 3306);
392 }
393
394 #[test]
395 fn test_parse_mariadb() {
396 let db = parse_database_url("mariadb://root:secret@127.0.0.1:3306/testdb").unwrap();
397 assert_eq!(db.engine, "reinhardt.db.backends.mysql");
398 assert_eq!(db.name, "testdb");
399 assert_eq!(db.user.unwrap(), "root");
400 assert_eq!(db.password.unwrap(), "secret");
401 assert_eq!(db.host.unwrap(), "127.0.0.1");
402 assert_eq!(db.port.unwrap(), 3306);
403 }
404
405 #[test]
406 fn test_parse_cache_locmem() {
407 let cache = parse_cache_url("locmem://").unwrap();
408 assert_eq!(cache.backend, "reinhardt.cache.backends.locmem.LocMemCache");
409 assert!(cache.location.is_none());
410 }
411
412 #[test]
413 fn test_parse_cache_redis() {
414 let cache = parse_cache_url("redis://localhost:6379/0").unwrap();
415 assert_eq!(cache.backend, "reinhardt.cache.backends.redis.RedisCache");
416 assert_eq!(cache.location.unwrap(), "redis://localhost:6379/0");
417 }
418}