1use std::future::Future;
9use std::pin::Pin;
10use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
11
12use serde::{Deserialize, Serialize};
13use uuid::Uuid;
14
15use crate::error::{Error, ErrorKind, Result};
16
17#[cfg(not(target_arch = "wasm32"))]
19use tokio::time::{sleep, timeout};
20
21#[cfg(target_arch = "wasm32")]
22use wasm_bindgen_futures::JsFuture;
23
24pub mod timing {
25 use super::*;
26
27 #[derive(Debug, Clone)]
28 pub struct Stopwatch {
29 start_time: Instant,
30 lap_times: Vec<Instant>,
31 }
32
33 impl Stopwatch {
34 pub fn start() -> Self {
35 Self {
36 start_time: Instant::now(),
37 lap_times: Vec::new(),
38 }
39 }
40
41 pub fn lap(&mut self) -> Duration {
42 let now = Instant::now();
43 self.lap_times.push(now);
44 now.duration_since(self.start_time)
45 }
46
47 pub fn elapsed(&self) -> Duration {
48 Instant::now().duration_since(self.start_time)
49 }
50
51 pub fn stop(self) -> Duration {
52 Instant::now().duration_since(self.start_time)
53 }
54
55 pub fn lap_times(&self) -> Vec<Duration> {
56 self.lap_times
57 .iter()
58 .map(|&time| time.duration_since(self.start_time))
59 .collect()
60 }
61
62 pub fn reset(&mut self) {
63 self.start_time = Instant::now();
64 self.lap_times.clear();
65 }
66 }
67
68 pub async fn measure_async<F, T>(future: F) -> (T, Duration)
69 where
70 F: Future<Output = T>,
71 {
72 let start = Instant::now();
73 let result = future.await;
74 let duration = start.elapsed();
75 (result, duration)
76 }
77
78 pub fn measure_sync<F, T>(func: F) -> (T, Duration)
79 where
80 F: FnOnce() -> T,
81 {
82 let start = Instant::now();
83 let result = func();
84 let duration = start.elapsed();
85 (result, duration)
86 }
87
88 pub fn unix_timestamp() -> u64 {
89 SystemTime::now()
90 .duration_since(UNIX_EPOCH)
91 .unwrap_or_default()
92 .as_secs()
93 }
94
95 pub fn unix_timestamp_ms() -> u64 {
96 SystemTime::now()
97 .duration_since(UNIX_EPOCH)
98 .unwrap_or_default()
99 .as_millis() as u64
100 }
101
102 pub fn duration_to_human(duration: Duration) -> String {
103 let total_seconds = duration.as_secs();
104 let days = total_seconds / 86400;
105 let hours = (total_seconds % 86400) / 3600;
106 let minutes = (total_seconds % 3600) / 60;
107 let seconds = total_seconds % 60;
108 let millis = duration.subsec_millis();
109
110 if days > 0 {
111 format!("{}d {}h {}m {}s", days, hours, minutes, seconds)
112 } else if hours > 0 {
113 format!("{}h {}m {}s", hours, minutes, seconds)
114 } else if minutes > 0 {
115 format!("{}m {}s", minutes, seconds)
116 } else if seconds > 0 {
117 format!("{}.{:03}s", seconds, millis)
118 } else {
119 format!("{}ms", millis)
120 }
121 }
122}
123
124pub mod retry {
125 use super::*;
126
127 #[derive(Debug, Clone, Serialize, Deserialize)]
128 pub struct RetryConfig {
129 pub max_attempts: u32,
130 pub initial_delay: Duration,
131 pub max_delay: Duration,
132 pub backoff_multiplier: f64,
133 pub jitter: bool,
134 }
135
136 impl Default for RetryConfig {
137 fn default() -> Self {
138 Self {
139 max_attempts: 3,
140 initial_delay: Duration::from_millis(100),
141 max_delay: Duration::from_secs(30),
142 backoff_multiplier: 2.0,
143 jitter: true,
144 }
145 }
146 }
147
148 #[cfg(not(target_arch = "wasm32"))]
150 async fn platform_sleep(duration: Duration) {
151 sleep(duration).await;
152 }
153
154 #[cfg(target_arch = "wasm32")]
155 async fn platform_sleep(duration: Duration) {
156 let promise = js_sys::Promise::new(&mut |resolve, _reject| {
157 let timeout_id = web_sys::window()
158 .unwrap()
159 .set_timeout_with_callback_and_timeout_and_arguments_0(
160 &resolve,
161 duration.as_millis() as i32,
162 )
163 .unwrap();
164 let _ = timeout_id;
165 });
166 let _ = JsFuture::from(promise).await;
167 }
168
169 pub async fn retry_async<F, Fut, T, E>(
170 mut func: F,
171 config: RetryConfig,
172 ) -> std::result::Result<T, E>
173 where
174 F: FnMut() -> Fut,
175 Fut: Future<Output = std::result::Result<T, E>>,
176 E: std::fmt::Display,
177 {
178 let mut attempt = 0;
179 let mut delay = config.initial_delay;
180
181 loop {
182 attempt += 1;
183
184 match func().await {
185 Ok(result) => return Ok(result),
186 Err(error) => {
187 if attempt >= config.max_attempts {
188 return Err(error);
189 }
190
191 #[cfg(not(target_arch = "wasm32"))]
193 tracing::warn!(
194 "Attempt {} failed, retrying in {:?}: {}",
195 attempt,
196 delay,
197 error
198 );
199
200 #[cfg(target_arch = "wasm32")]
201 web_sys::console::warn_1(
202 &format!(
203 "Attempt {} failed, retrying in {:?}: {}",
204 attempt, delay, error
205 )
206 .into(),
207 );
208
209 platform_sleep(delay).await;
210
211 delay = Duration::from_millis(
213 ((delay.as_millis() as f64) * config.backoff_multiplier) as u64,
214 );
215 delay = delay.min(config.max_delay);
216
217 if config.jitter {
219 let jitter_range = delay.as_millis() as f64 * 0.1; let jitter = (rand::random::<f64>() - 0.5) * 2.0 * jitter_range;
221 let jittered_ms = (delay.as_millis() as f64 + jitter).max(0.0) as u64;
222 delay = Duration::from_millis(jittered_ms);
223 }
224 }
225 }
226 }
227 }
228}
229
230pub mod collections {
231 use std::collections::HashMap;
232 use std::hash::Hash;
233
234 pub fn group_by<T, K, F>(items: Vec<T>, key_fn: F) -> HashMap<K, Vec<T>>
235 where
236 K: Hash + Eq,
237 F: Fn(&T) -> K,
238 {
239 let mut groups = HashMap::new();
240 for item in items {
241 let key = key_fn(&item);
242 groups.entry(key).or_insert_with(Vec::new).push(item);
243 }
244 groups
245 }
246
247 pub fn partition<T, F>(items: Vec<T>, predicate: F) -> (Vec<T>, Vec<T>)
248 where
249 F: Fn(&T) -> bool,
250 {
251 let mut true_items = Vec::new();
252 let mut false_items = Vec::new();
253
254 for item in items {
255 if predicate(&item) {
256 true_items.push(item);
257 } else {
258 false_items.push(item);
259 }
260 }
261
262 (true_items, false_items)
263 }
264
265 pub fn find_duplicates<T>(items: &[T]) -> Vec<T>
266 where
267 T: Hash + Eq + Clone,
268 {
269 let mut seen = std::collections::HashSet::new();
270 let mut duplicates = std::collections::HashSet::new();
271
272 for item in items {
273 if !seen.insert(item) {
274 duplicates.insert(item.clone());
275 }
276 }
277
278 duplicates.into_iter().collect()
279 }
280}
281
282pub mod strings {
283 pub fn truncate(s: &str, max_len: usize) -> String {
284 if s.len() <= max_len {
285 s.to_string()
286 } else {
287 format!("{}...", &s[..max_len.saturating_sub(3)])
288 }
289 }
290
291 pub fn to_snake_case(s: &str) -> String {
292 let mut result = String::new();
293 let mut prev_char_was_uppercase = false;
294
295 for (i, ch) in s.chars().enumerate() {
296 if ch.is_uppercase() {
297 if i > 0 && !prev_char_was_uppercase {
298 result.push('_');
299 }
300 result.push(ch.to_lowercase().next().unwrap_or(ch));
301 prev_char_was_uppercase = true;
302 } else {
303 result.push(ch);
304 prev_char_was_uppercase = false;
305 }
306 }
307
308 result
309 }
310
311 pub fn to_kebab_case(s: &str) -> String {
312 to_snake_case(s).replace('_', "-")
313 }
314
315 pub fn to_pascal_case(s: &str) -> String {
316 s.split(&['_', '-', ' '][..])
317 .map(|word| {
318 let mut chars = word.chars();
319 match chars.next() {
320 None => String::new(),
321 Some(first) => {
322 first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase()
323 }
324 }
325 })
326 .collect()
327 }
328
329 pub fn random_string(length: usize) -> String {
330 use rand::Rng;
331 const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
332 abcdefghijklmnopqrstuvwxyz\
333 0123456789";
334
335 let mut rng = rand::thread_rng();
336 (0..length)
337 .map(|_| {
338 let idx = rng.gen_range(0..CHARSET.len());
339 CHARSET[idx] as char
340 })
341 .collect()
342 }
343}
344
345pub mod async_utils {
346 use super::*;
347
348 #[cfg(not(target_arch = "wasm32"))]
349 pub async fn with_timeout<F, T>(future: F, timeout_duration: Duration) -> Result<T>
350 where
351 F: Future<Output = T>,
352 {
353 timeout(timeout_duration, future)
354 .await
355 .map_err(|_| Error::timeout("Operation timed out"))
356 }
357
358 #[cfg(target_arch = "wasm32")]
359 pub async fn with_timeout<F, T>(future: F, _timeout_duration: Duration) -> Result<T>
360 where
361 F: Future<Output = T>,
362 {
363 Ok(future.await)
365 }
366
367 pub async fn execute_with_concurrency_limit<F, T>(futures: Vec<F>, _limit: usize) -> Vec<T>
368 where
369 F: Future<Output = T> + Send + 'static,
370 T: Send + 'static,
371 {
372 let mut results = Vec::new();
374 for future in futures {
375 results.push(future.await);
376 }
377 results
378 }
379
380 pub async fn race<T>(futures: Vec<Pin<Box<dyn Future<Output = T> + Send>>>) -> Option<T> {
381 if futures.is_empty() {
382 return None;
383 }
384
385 let mut futures = futures;
387 if let Some(future) = futures.pop() {
388 Some(future.await)
389 } else {
390 None
391 }
392 }
393}
394
395pub mod validation {
396 use super::*;
397 use std::net::IpAddr;
398 use std::str::FromStr;
399
400 pub fn is_valid_email(email: &str) -> bool {
401 email.contains('@') && email.contains('.') && email.len() > 5
402 }
403
404 pub fn is_valid_url(url: &str) -> bool {
405 url.starts_with("http://") || url.starts_with("https://")
406 }
407
408 pub fn is_valid_ip(ip: &str) -> bool {
409 IpAddr::from_str(ip).is_ok()
410 }
411
412 pub fn is_valid_uuid(uuid: &str) -> bool {
413 Uuid::from_str(uuid).is_ok()
414 }
415
416 pub fn is_valid_port(port: u16) -> bool {
417 port > 0
418 }
419
420 pub fn is_safe_path(path: &str) -> bool {
421 !path.contains("..") && !path.starts_with('/') && !path.contains('\0')
422 }
423
424 pub fn validate_password_strength(password: &str, min_length: usize) -> Vec<String> {
425 let mut errors = Vec::new();
426
427 if password.len() < min_length {
428 errors.push(format!(
429 "Password must be at least {} characters",
430 min_length
431 ));
432 }
433
434 if !password.chars().any(|c| c.is_uppercase()) {
435 errors.push("Password must contain at least one uppercase letter".to_string());
436 }
437
438 if !password.chars().any(|c| c.is_lowercase()) {
439 errors.push("Password must contain at least one lowercase letter".to_string());
440 }
441
442 if !password.chars().any(|c| c.is_ascii_digit()) {
443 errors.push("Password must contain at least one digit".to_string());
444 }
445
446 if !password
447 .chars()
448 .any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c))
449 {
450 errors.push("Password must contain at least one special character".to_string());
451 }
452
453 errors
454 }
455}
456
457#[cfg(not(target_arch = "wasm32"))]
458pub mod compression {
459 use super::*;
460
461 pub fn compress_gzip(data: &[u8]) -> Result<Vec<u8>> {
462 use flate2::write::GzEncoder;
463 use flate2::Compression;
464 use std::io::Write;
465
466 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
467 encoder
468 .write_all(data)
469 .map_err(|e| Error::new(ErrorKind::Io, format!("Failed to compress data: {}", e)))?;
470
471 encoder.finish().map_err(|e| {
472 Error::new(
473 ErrorKind::Io,
474 format!("Failed to finish compression: {}", e),
475 )
476 })
477 }
478
479 pub fn decompress_gzip(data: &[u8]) -> Result<Vec<u8>> {
480 use flate2::read::GzDecoder;
481 use std::io::Read;
482
483 let mut decoder = GzDecoder::new(data);
484 let mut decompressed = Vec::new();
485 decoder
486 .read_to_end(&mut decompressed)
487 .map_err(|e| Error::new(ErrorKind::Io, format!("Failed to decompress data: {}", e)))?;
488
489 Ok(decompressed)
490 }
491}
492
493#[cfg(target_arch = "wasm32")]
494pub mod compression {
495 use super::*;
496
497 pub fn compress_gzip(_data: &[u8]) -> Result<Vec<u8>> {
498 Err(Error::new(
499 ErrorKind::Io,
500 "Compression not available on web",
501 ))
502 }
503
504 pub fn decompress_gzip(_data: &[u8]) -> Result<Vec<u8>> {
505 Err(Error::new(
506 ErrorKind::Io,
507 "Decompression not available on web",
508 ))
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515
516 #[test]
517 fn test_stopwatch() {
518 let mut stopwatch = timing::Stopwatch::start();
519 std::thread::sleep(Duration::from_millis(10));
520 let lap1 = stopwatch.lap();
521 std::thread::sleep(Duration::from_millis(10));
522 let total = stopwatch.stop();
523
524 assert!(lap1.as_millis() >= 10);
525 assert!(total.as_millis() >= 20);
526 }
527
528 #[test]
529 fn test_duration_to_human() {
530 assert_eq!(
531 timing::duration_to_human(Duration::from_millis(500)),
532 "500ms"
533 );
534 assert_eq!(timing::duration_to_human(Duration::from_secs(1)), "1.000s");
535 assert_eq!(timing::duration_to_human(Duration::from_secs(61)), "1m 1s");
536 assert_eq!(
537 timing::duration_to_human(Duration::from_secs(3661)),
538 "1h 1m 1s"
539 );
540 }
541
542 #[test]
543 fn test_string_utilities() {
544 assert_eq!(strings::to_snake_case("HelloWorld"), "hello_world");
545 assert_eq!(strings::to_kebab_case("HelloWorld"), "hello-world");
546 assert_eq!(strings::to_pascal_case("hello_world"), "HelloWorld");
547 assert_eq!(strings::truncate("Hello, World!", 10), "Hello, ...");
548 }
549
550 #[test]
551 fn test_validation() {
552 assert!(validation::is_valid_email("test@example.com"));
553 assert!(!validation::is_valid_email("invalid-email"));
554 assert!(validation::is_valid_url("https://example.com"));
555 assert!(!validation::is_valid_url("not-a-url"));
556 assert!(validation::is_valid_ip("192.168.1.1"));
557 assert!(!validation::is_valid_ip("999.999.999.999"));
558 }
559
560 #[test]
561 fn test_collections() {
562 let items = vec!["apple", "banana", "apricot", "berry"];
563 let groups = collections::group_by(items, |item| item.chars().next().unwrap());
564
565 assert_eq!(groups.get(&'a').unwrap().len(), 2);
566 assert_eq!(groups.get(&'b').unwrap().len(), 2);
567
568 let numbers = vec![1, 2, 3, 4, 5, 6];
569 let (evens, odds) = collections::partition(numbers, |&n| n % 2 == 0);
570 assert_eq!(evens, vec![2, 4, 6]);
571 assert_eq!(odds, vec![1, 3, 5]);
572 }
573
574 #[tokio::test]
575 #[cfg(not(target_arch = "wasm32"))]
576 async fn test_retry() {
577 let mut attempts = 0;
578 let result = retry::retry_async(
579 || {
580 attempts += 1;
581 async move {
582 if attempts < 3 {
583 Err("Failed")
584 } else {
585 Ok("Success")
586 }
587 }
588 },
589 retry::RetryConfig {
590 max_attempts: 5,
591 initial_delay: Duration::from_millis(1),
592 ..Default::default()
593 },
594 )
595 .await;
596
597 assert_eq!(result.unwrap(), "Success");
598 assert_eq!(attempts, 3);
599 }
600}