qorzen_oxide/
utils_general.rs

1// src/utils.rs
2
3//! Utility functions and helpers for Qorzen Core
4//!
5//! This module provides common utility functions that are used throughout
6//! the Qorzen Core system and can be useful for plugins and applications.
7
8use 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// Platform-specific imports
18#[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    // Platform-specific sleep function
149    #[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                    // Would log warning here if logging was available
192                    #[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                    // Calculate next delay with exponential backoff
212                    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                    // Add jitter if enabled
218                    if config.jitter {
219                        let jitter_range = delay.as_millis() as f64 * 0.1; // 10% jitter
220                        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        // Web doesn't support timeouts easily, just execute the future
364        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        // Simplified implementation for web compatibility
373        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        // Simplified: just take the first future for web compatibility
386        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}