1use std::path::{Path, PathBuf};
11use std::time::{Duration, SystemTime, UNIX_EPOCH};
12
13use crate::error::Result;
14
15pub mod string {
17 pub fn truncate(s: &str, max_len: usize) -> String {
26 if s.len() <= max_len {
27 s.to_string()
28 } else {
29 format!("{}...", &s[..max_len.saturating_sub(3)])
30 }
31 }
32
33 pub fn is_valid_identifier(s: &str) -> bool {
41 if s.is_empty() {
42 return false;
43 }
44
45 let first = s.chars().next().unwrap();
46 if !first.is_alphabetic() && first != '_' && first != '$' {
47 return false;
48 }
49
50 s.chars()
51 .all(|c| c.is_alphanumeric() || c == '_' || c == '$')
52 }
53
54 pub fn snake_to_camel(s: &str) -> String {
62 let mut result = String::new();
63 let mut capitalize_next = false;
64
65 for c in s.chars() {
66 if c == '_' {
67 capitalize_next = true;
68 } else if capitalize_next {
69 result.push(c.to_ascii_uppercase());
70 capitalize_next = false;
71 } else {
72 result.push(c);
73 }
74 }
75
76 result
77 }
78
79 pub fn escape_json(s: &str) -> String {
87 s.replace('\\', "\\\\")
88 .replace('"', "\\\"")
89 .replace('\n', "\\n")
90 .replace('\r', "\\r")
91 .replace('\t', "\\t")
92 }
93}
94
95pub mod time {
97 use super::*;
98
99 pub fn now_millis() -> u64 {
104 SystemTime::now()
105 .duration_since(UNIX_EPOCH)
106 .unwrap_or(Duration::from_secs(0))
107 .as_millis() as u64
108 }
109
110 pub fn now_secs() -> u64 {
115 SystemTime::now()
116 .duration_since(UNIX_EPOCH)
117 .unwrap_or(Duration::from_secs(0))
118 .as_secs()
119 }
120
121 pub fn format_duration(duration: Duration) -> String {
129 let secs = duration.as_secs();
130 let millis = duration.subsec_millis();
131
132 if secs == 0 {
133 return format!("{}ms", millis);
134 }
135
136 let hours = secs / 3600;
137 let minutes = (secs % 3600) / 60;
138 let seconds = secs % 60;
139
140 let mut parts = Vec::new();
141
142 if hours > 0 {
143 parts.push(format!("{}h", hours));
144 }
145 if minutes > 0 {
146 parts.push(format!("{}m", minutes));
147 }
148 if seconds > 0 || parts.is_empty() {
149 parts.push(format!("{}s", seconds));
150 }
151
152 parts.join(" ")
153 }
154
155 pub fn parse_duration(s: &str) -> Option<Duration> {
163 let s = s.trim();
164 if s.is_empty() {
165 return None;
166 }
167
168 let (num_str, unit) = s.split_at(s.len() - 1);
169 let num: u64 = num_str.parse().ok()?;
170
171 match unit {
172 "s" => Some(Duration::from_secs(num)),
173 "m" => Some(Duration::from_secs(num * 60)),
174 "h" => Some(Duration::from_secs(num * 3600)),
175 _ => None,
176 }
177 }
178}
179
180pub mod fs {
182 use super::*;
183
184 pub fn ensure_dir_exists<P: AsRef<Path>>(path: P) -> Result<()> {
192 let path = path.as_ref();
193 if !path.exists() {
194 std::fs::create_dir_all(path)?;
195 }
196 Ok(())
197 }
198
199 pub fn get_extension<P: AsRef<Path>>(path: P) -> Option<String> {
207 path.as_ref()
208 .extension()
209 .and_then(|e| e.to_str())
210 .map(|s| s.to_string())
211 }
212
213 pub fn is_valid_file<P: AsRef<Path>>(path: P) -> bool {
221 let path = path.as_ref();
222 path.exists() && path.is_file()
223 }
224
225 pub fn expand_home(path: &str) -> PathBuf {
233 if path.starts_with("~/")
234 && let Some(home) = dirs::home_dir() {
235 return home.join(&path[2..]);
236 }
237 PathBuf::from(path)
238 }
239}
240
241pub mod validate {
243 pub fn is_valid_database_name(name: &str) -> bool {
251 if name.is_empty() || name.len() > 64 {
252 return false;
253 }
254
255 let invalid_chars = ['/', '\\', '.', ' ', '"', '$', '*', '<', '>', ':', '|', '?'];
256 !name.chars().any(|c| invalid_chars.contains(&c))
257 }
258
259 pub fn is_valid_collection_name(name: &str) -> bool {
267 if name.is_empty() || name.len() > 120 {
268 return false;
269 }
270
271 if name.starts_with("system.") {
272 return false;
273 }
274
275 let invalid_chars = ['$', '\0'];
276 !name.chars().any(|c| invalid_chars.contains(&c))
277 }
278
279 pub fn is_valid_connection_uri(uri: &str) -> bool {
287 uri.starts_with("mongodb://") || uri.starts_with("mongodb+srv://")
288 }
289
290 pub fn is_valid_field_name(name: &str) -> bool {
298 if name.is_empty() {
299 return false;
300 }
301
302 if name.starts_with('$') && !name.starts_with("$set") && !name.starts_with("$inc") {
304 return false;
305 }
306
307 !name.contains('\0')
309 }
310}
311
312pub mod convert {
314 use mongodb::bson::Bson;
315
316 pub fn bson_to_string(value: &Bson) -> String {
324 match value {
325 Bson::Double(v) => format!("{}", v),
326 Bson::String(v) => v.clone(),
327 Bson::Boolean(v) => format!("{}", v),
328 Bson::Int32(v) => format!("{}", v),
329 Bson::Int64(v) => format!("{}", v),
330 Bson::Null => "null".to_string(),
331 _ => format!("{:?}", value),
332 }
333 }
334
335 pub fn format_bytes(bytes: u64) -> String {
343 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
344 let mut size = bytes as f64;
345 let mut unit_index = 0;
346
347 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
348 size /= 1024.0;
349 unit_index += 1;
350 }
351
352 if unit_index == 0 {
353 format!("{} {}", size as u64, UNITS[unit_index])
354 } else {
355 format!("{:.2} {}", size, UNITS[unit_index])
356 }
357 }
358
359 pub fn parse_bytes(s: &str) -> Option<u64> {
367 let s = s.trim().to_uppercase();
368 let (num_str, unit) = if s.ends_with("TB") {
369 (s.trim_end_matches("TB"), 1024u64.pow(4))
370 } else if s.ends_with("GB") {
371 (s.trim_end_matches("GB"), 1024u64.pow(3))
372 } else if s.ends_with("MB") {
373 (s.trim_end_matches("MB"), 1024u64.pow(2))
374 } else if s.ends_with("KB") {
375 (s.trim_end_matches("KB"), 1024u64)
376 } else if s.ends_with('B') {
377 (s.trim_end_matches('B'), 1)
378 } else {
379 return s.parse().ok();
380 };
381
382 num_str
383 .trim()
384 .parse::<f64>()
385 .ok()
386 .map(|n| (n * unit as f64) as u64)
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[test]
395 fn test_truncate() {
396 assert_eq!(string::truncate("hello", 10), "hello");
397 assert_eq!(string::truncate("hello world", 8), "hello...");
398 }
399
400 #[test]
401 fn test_valid_identifier() {
402 assert!(string::is_valid_identifier("myVar"));
403 assert!(string::is_valid_identifier("_private"));
404 assert!(string::is_valid_identifier("$special"));
405 assert!(!string::is_valid_identifier("123invalid"));
406 assert!(!string::is_valid_identifier(""));
407 }
408
409 #[test]
410 fn test_snake_to_camel() {
411 assert_eq!(string::snake_to_camel("hello_world"), "helloWorld");
412 assert_eq!(string::snake_to_camel("my_var_name"), "myVarName");
413 }
414
415 #[test]
416 fn test_format_duration() {
417 assert_eq!(time::format_duration(Duration::from_secs(0)), "0ms");
418 assert_eq!(time::format_duration(Duration::from_secs(90)), "1m 30s");
419 assert_eq!(time::format_duration(Duration::from_secs(3661)), "1h 1m 1s");
420 }
421
422 #[test]
423 fn test_parse_duration() {
424 assert_eq!(time::parse_duration("30s"), Some(Duration::from_secs(30)));
425 assert_eq!(time::parse_duration("5m"), Some(Duration::from_secs(300)));
426 assert_eq!(time::parse_duration("1h"), Some(Duration::from_secs(3600)));
427 assert_eq!(time::parse_duration("invalid"), None);
428 }
429
430 #[test]
431 fn test_valid_database_name() {
432 assert!(validate::is_valid_database_name("mydb"));
433 assert!(validate::is_valid_database_name("test123"));
434 assert!(!validate::is_valid_database_name("my/db"));
435 assert!(!validate::is_valid_database_name(""));
436 }
437
438 #[test]
439 fn test_valid_collection_name() {
440 assert!(validate::is_valid_collection_name("users"));
441 assert!(validate::is_valid_collection_name("my_collection"));
442 assert!(!validate::is_valid_collection_name("system.users"));
443 assert!(!validate::is_valid_collection_name("invalid$name"));
444 }
445
446 #[test]
447 fn test_valid_connection_uri() {
448 assert!(validate::is_valid_connection_uri(
449 "mongodb://localhost:27017"
450 ));
451 assert!(validate::is_valid_connection_uri(
452 "mongodb+srv://cluster.example.com"
453 ));
454 assert!(!validate::is_valid_connection_uri("http://localhost"));
455 }
456
457 #[test]
458 fn test_format_bytes() {
459 assert_eq!(convert::format_bytes(500), "500 B");
460 assert_eq!(convert::format_bytes(1024), "1.00 KB");
461 assert_eq!(convert::format_bytes(1024 * 1024), "1.00 MB");
462 }
463
464 #[test]
465 fn test_parse_bytes() {
466 assert_eq!(convert::parse_bytes("1024"), Some(1024));
467 assert_eq!(convert::parse_bytes("1KB"), Some(1024));
468 assert_eq!(convert::parse_bytes("1MB"), Some(1024 * 1024));
469 assert_eq!(
470 convert::parse_bytes("1.5GB"),
471 Some((1.5 * 1024.0 * 1024.0 * 1024.0) as u64)
472 );
473 }
474}