Skip to main content

mabi_core/
utils.rs

1//! Common utilities and helper functions.
2//!
3//! This module provides shared utilities used across the simulator including:
4//! - ID generation
5//! - Time utilities
6//! - String helpers
7//! - Builder pattern macros
8
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
11
12use uuid::Uuid;
13
14// =============================================================================
15// ID Generation
16// =============================================================================
17
18/// Counter for sequential IDs.
19static SEQUENTIAL_COUNTER: AtomicU64 = AtomicU64::new(0);
20
21/// Generate a new UUID v4.
22#[inline]
23pub fn generate_uuid() -> String {
24    Uuid::new_v4().to_string()
25}
26
27/// Generate a short UUID (first 8 characters).
28#[inline]
29pub fn generate_short_uuid() -> String {
30    Uuid::new_v4().to_string()[..8].to_string()
31}
32
33/// Generate a sequential ID with prefix.
34#[inline]
35pub fn generate_sequential_id(prefix: &str) -> String {
36    let seq = SEQUENTIAL_COUNTER.fetch_add(1, Ordering::SeqCst);
37    format!("{}-{:08}", prefix, seq)
38}
39
40/// Generate a timestamp-based ID.
41#[inline]
42pub fn generate_timestamp_id(prefix: &str) -> String {
43    let ts = SystemTime::now()
44        .duration_since(UNIX_EPOCH)
45        .unwrap_or_default()
46        .as_micros();
47    format!("{}-{}", prefix, ts)
48}
49
50// =============================================================================
51// Time Utilities
52// =============================================================================
53
54/// Get current timestamp in milliseconds since UNIX epoch.
55#[inline]
56pub fn current_timestamp_ms() -> u64 {
57    SystemTime::now()
58        .duration_since(UNIX_EPOCH)
59        .unwrap_or_default()
60        .as_millis() as u64
61}
62
63/// Get current timestamp in microseconds since UNIX epoch.
64#[inline]
65pub fn current_timestamp_us() -> u64 {
66    SystemTime::now()
67        .duration_since(UNIX_EPOCH)
68        .unwrap_or_default()
69        .as_micros() as u64
70}
71
72/// Get current timestamp in nanoseconds since UNIX epoch.
73#[inline]
74pub fn current_timestamp_ns() -> u128 {
75    SystemTime::now()
76        .duration_since(UNIX_EPOCH)
77        .unwrap_or_default()
78        .as_nanos()
79}
80
81/// Format duration as human-readable string.
82pub fn format_duration(duration: Duration) -> String {
83    let secs = duration.as_secs();
84    let millis = duration.subsec_millis();
85
86    if secs >= 3600 {
87        let hours = secs / 3600;
88        let mins = (secs % 3600) / 60;
89        format!("{}h {}m", hours, mins)
90    } else if secs >= 60 {
91        let mins = secs / 60;
92        let secs = secs % 60;
93        format!("{}m {}s", mins, secs)
94    } else if secs > 0 {
95        format!("{}.{:03}s", secs, millis)
96    } else if millis > 0 {
97        format!("{}ms", millis)
98    } else {
99        format!("{}µs", duration.as_micros())
100    }
101}
102
103/// Simple stopwatch for measuring elapsed time.
104#[derive(Debug, Clone)]
105pub struct Stopwatch {
106    start: Instant,
107    laps: Vec<(String, Duration)>,
108}
109
110impl Stopwatch {
111    /// Start a new stopwatch.
112    pub fn start() -> Self {
113        Self {
114            start: Instant::now(),
115            laps: Vec::new(),
116        }
117    }
118
119    /// Get elapsed time since start.
120    pub fn elapsed(&self) -> Duration {
121        self.start.elapsed()
122    }
123
124    /// Record a lap.
125    pub fn lap(&mut self, name: impl Into<String>) {
126        self.laps.push((name.into(), self.elapsed()));
127    }
128
129    /// Get all laps.
130    pub fn laps(&self) -> &[(String, Duration)] {
131        &self.laps
132    }
133
134    /// Reset the stopwatch.
135    pub fn reset(&mut self) {
136        self.start = Instant::now();
137        self.laps.clear();
138    }
139
140    /// Get elapsed time in milliseconds.
141    pub fn elapsed_ms(&self) -> u64 {
142        self.elapsed().as_millis() as u64
143    }
144
145    /// Get elapsed time in microseconds.
146    pub fn elapsed_us(&self) -> u64 {
147        self.elapsed().as_micros() as u64
148    }
149}
150
151impl Default for Stopwatch {
152    fn default() -> Self {
153        Self::start()
154    }
155}
156
157// =============================================================================
158// String Utilities
159// =============================================================================
160
161/// Truncate a string to a maximum length.
162pub fn truncate_string(s: &str, max_len: usize) -> String {
163    if s.len() <= max_len {
164        s.to_string()
165    } else {
166        format!("{}...", &s[..max_len.saturating_sub(3)])
167    }
168}
169
170/// Convert bytes to human-readable format.
171pub fn format_bytes(bytes: u64) -> String {
172    const KB: u64 = 1024;
173    const MB: u64 = KB * 1024;
174    const GB: u64 = MB * 1024;
175    const TB: u64 = GB * 1024;
176
177    if bytes >= TB {
178        format!("{:.2} TB", bytes as f64 / TB as f64)
179    } else if bytes >= GB {
180        format!("{:.2} GB", bytes as f64 / GB as f64)
181    } else if bytes >= MB {
182        format!("{:.2} MB", bytes as f64 / MB as f64)
183    } else if bytes >= KB {
184        format!("{:.2} KB", bytes as f64 / KB as f64)
185    } else {
186        format!("{} B", bytes)
187    }
188}
189
190/// Sanitize a string for use as an identifier.
191pub fn sanitize_identifier(s: &str) -> String {
192    s.chars()
193        .map(|c| {
194            if c.is_alphanumeric() || c == '_' || c == '-' {
195                c
196            } else {
197                '_'
198            }
199        })
200        .collect()
201}
202
203// =============================================================================
204// Builder Pattern Macros
205// =============================================================================
206
207/// Implement a builder pattern setter method.
208///
209/// # Example
210///
211/// ```rust,ignore
212/// struct Config {
213///     name: String,
214///     port: u16,
215/// }
216///
217/// impl Config {
218///     mabi_core::builder_setter!(name, String);
219///     mabi_core::builder_setter!(port, u16);
220/// }
221/// ```
222#[macro_export]
223macro_rules! builder_setter {
224    ($field:ident, $type:ty) => {
225        pub fn $field(mut self, value: $type) -> Self {
226            self.$field = value;
227            self
228        }
229    };
230    ($field:ident, $type:ty, $doc:expr) => {
231        #[doc = $doc]
232        pub fn $field(mut self, value: $type) -> Self {
233            self.$field = value;
234            self
235        }
236    };
237}
238
239/// Implement a builder pattern setter method that takes impl Into<T>.
240///
241/// # Example
242///
243/// ```rust,ignore
244/// impl Config {
245///     mabi_core::builder_setter_into!(name, String);
246/// }
247///
248/// let config = Config::default().name("test");
249/// ```
250#[macro_export]
251macro_rules! builder_setter_into {
252    ($field:ident, $type:ty) => {
253        pub fn $field(mut self, value: impl Into<$type>) -> Self {
254            self.$field = value.into();
255            self
256        }
257    };
258    ($field:ident, $type:ty, $doc:expr) => {
259        #[doc = $doc]
260        pub fn $field(mut self, value: impl Into<$type>) -> Self {
261            self.$field = value.into();
262            self
263        }
264    };
265}
266
267/// Implement a builder pattern setter method for Option<T>.
268///
269/// # Example
270///
271/// ```rust,ignore
272/// impl Config {
273///     mabi_core::builder_setter_option!(description, String);
274/// }
275/// ```
276#[macro_export]
277macro_rules! builder_setter_option {
278    ($field:ident, $type:ty) => {
279        pub fn $field(mut self, value: impl Into<$type>) -> Self {
280            self.$field = Some(value.into());
281            self
282        }
283    };
284    ($field:ident, $type:ty, $doc:expr) => {
285        #[doc = $doc]
286        pub fn $field(mut self, value: impl Into<$type>) -> Self {
287            self.$field = Some(value.into());
288            self
289        }
290    };
291}
292
293/// Implement a builder pattern with_* prefix setter method.
294///
295/// # Example
296///
297/// ```rust,ignore
298/// impl Config {
299///     mabi_core::builder_with!(name, String);  // Creates with_name()
300/// }
301/// ```
302#[macro_export]
303macro_rules! builder_with {
304    ($field:ident, $type:ty) => {
305        paste::paste! {
306            pub fn [<with_ $field>](mut self, value: impl Into<$type>) -> Self {
307                self.$field = value.into();
308                self
309            }
310        }
311    };
312    ($field:ident, $type:ty, $doc:expr) => {
313        paste::paste! {
314            #[doc = $doc]
315            pub fn [<with_ $field>](mut self, value: impl Into<$type>) -> Self {
316                self.$field = value.into();
317                self
318            }
319        }
320    };
321}
322
323// =============================================================================
324// Retry Utilities
325// =============================================================================
326
327/// Retry configuration.
328#[derive(Debug, Clone)]
329pub struct RetryConfig {
330    /// Maximum number of attempts.
331    pub max_attempts: u32,
332    /// Initial delay between retries.
333    pub initial_delay: Duration,
334    /// Maximum delay between retries.
335    pub max_delay: Duration,
336    /// Exponential backoff multiplier.
337    pub multiplier: f64,
338}
339
340impl Default for RetryConfig {
341    fn default() -> Self {
342        Self {
343            max_attempts: 3,
344            initial_delay: Duration::from_millis(100),
345            max_delay: Duration::from_secs(10),
346            multiplier: 2.0,
347        }
348    }
349}
350
351impl RetryConfig {
352    /// Create a new retry config.
353    pub fn new(max_attempts: u32) -> Self {
354        Self {
355            max_attempts,
356            ..Default::default()
357        }
358    }
359
360    /// Set initial delay.
361    pub fn with_initial_delay(mut self, delay: Duration) -> Self {
362        self.initial_delay = delay;
363        self
364    }
365
366    /// Set max delay.
367    pub fn with_max_delay(mut self, delay: Duration) -> Self {
368        self.max_delay = delay;
369        self
370    }
371
372    /// Set multiplier.
373    pub fn with_multiplier(mut self, multiplier: f64) -> Self {
374        self.multiplier = multiplier;
375        self
376    }
377
378    /// Calculate delay for attempt number.
379    pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
380        if attempt == 0 {
381            return Duration::ZERO;
382        }
383
384        let delay_ms = self.initial_delay.as_millis() as f64
385            * self.multiplier.powi(attempt.saturating_sub(1) as i32);
386        let delay = Duration::from_millis(delay_ms.min(self.max_delay.as_millis() as f64) as u64);
387
388        delay.min(self.max_delay)
389    }
390}
391
392/// Retry an async operation with exponential backoff.
393///
394/// # Example
395///
396/// ```rust,ignore
397/// use mabi_core::utils::{retry_async, RetryConfig};
398///
399/// let result = retry_async(
400///     RetryConfig::new(3),
401///     || async {
402///         // Your fallible async operation
403///         Ok::<_, std::io::Error>(42)
404///     },
405/// ).await;
406/// ```
407pub async fn retry_async<F, Fut, T, E>(config: RetryConfig, mut f: F) -> Result<T, E>
408where
409    F: FnMut() -> Fut,
410    Fut: std::future::Future<Output = Result<T, E>>,
411{
412    let mut last_error = None;
413
414    for attempt in 0..config.max_attempts {
415        match f().await {
416            Ok(value) => return Ok(value),
417            Err(e) => {
418                last_error = Some(e);
419
420                if attempt + 1 < config.max_attempts {
421                    let delay = config.delay_for_attempt(attempt + 1);
422                    tokio::time::sleep(delay).await;
423                }
424            }
425        }
426    }
427
428    Err(last_error.expect("Should have at least one error"))
429}
430
431// =============================================================================
432// Rate Limiting
433// =============================================================================
434
435/// Simple rate limiter using token bucket algorithm.
436#[derive(Debug)]
437pub struct RateLimiter {
438    /// Maximum tokens in the bucket.
439    capacity: u64,
440    /// Current tokens in the bucket.
441    tokens: AtomicU64,
442    /// Tokens added per second.
443    refill_rate: f64,
444    /// Last refill time.
445    last_refill: parking_lot::Mutex<Instant>,
446}
447
448impl RateLimiter {
449    /// Create a new rate limiter.
450    pub fn new(capacity: u64, refill_rate: f64) -> Self {
451        Self {
452            capacity,
453            tokens: AtomicU64::new(capacity),
454            refill_rate,
455            last_refill: parking_lot::Mutex::new(Instant::now()),
456        }
457    }
458
459    /// Try to acquire a token. Returns true if successful.
460    pub fn try_acquire(&self) -> bool {
461        self.refill();
462
463        loop {
464            let current = self.tokens.load(Ordering::Acquire);
465            if current == 0 {
466                return false;
467            }
468
469            if self
470                .tokens
471                .compare_exchange_weak(current, current - 1, Ordering::AcqRel, Ordering::Acquire)
472                .is_ok()
473            {
474                return true;
475            }
476        }
477    }
478
479    /// Try to acquire multiple tokens. Returns true if successful.
480    pub fn try_acquire_n(&self, n: u64) -> bool {
481        self.refill();
482
483        loop {
484            let current = self.tokens.load(Ordering::Acquire);
485            if current < n {
486                return false;
487            }
488
489            if self
490                .tokens
491                .compare_exchange_weak(current, current - n, Ordering::AcqRel, Ordering::Acquire)
492                .is_ok()
493            {
494                return true;
495            }
496        }
497    }
498
499    /// Get current token count.
500    pub fn available(&self) -> u64 {
501        self.refill();
502        self.tokens.load(Ordering::Acquire)
503    }
504
505    fn refill(&self) {
506        let mut last_refill = self.last_refill.lock();
507        let now = Instant::now();
508        let elapsed = now.duration_since(*last_refill).as_secs_f64();
509
510        if elapsed > 0.0 {
511            let new_tokens = (elapsed * self.refill_rate) as u64;
512            if new_tokens > 0 {
513                let current = self.tokens.load(Ordering::Acquire);
514                let new_value = (current + new_tokens).min(self.capacity);
515                self.tokens.store(new_value, Ordering::Release);
516                *last_refill = now;
517            }
518        }
519    }
520}
521
522#[cfg(test)]
523mod tests {
524    use super::*;
525
526    #[test]
527    fn test_generate_uuid() {
528        let uuid1 = generate_uuid();
529        let uuid2 = generate_uuid();
530        assert_ne!(uuid1, uuid2);
531        assert_eq!(uuid1.len(), 36);
532    }
533
534    #[test]
535    fn test_generate_sequential_id() {
536        let id1 = generate_sequential_id("device");
537        let id2 = generate_sequential_id("device");
538        assert!(id1.starts_with("device-"));
539        assert!(id2.starts_with("device-"));
540        assert_ne!(id1, id2);
541    }
542
543    #[test]
544    fn test_format_duration() {
545        assert_eq!(format_duration(Duration::from_secs(3661)), "1h 1m");
546        assert_eq!(format_duration(Duration::from_secs(61)), "1m 1s");
547        assert_eq!(format_duration(Duration::from_millis(1500)), "1.500s");
548        assert_eq!(format_duration(Duration::from_millis(500)), "500ms");
549        assert_eq!(format_duration(Duration::from_micros(500)), "500µs");
550    }
551
552    #[test]
553    fn test_format_bytes() {
554        assert_eq!(format_bytes(500), "500 B");
555        assert_eq!(format_bytes(1024), "1.00 KB");
556        assert_eq!(format_bytes(1536), "1.50 KB");
557        assert_eq!(format_bytes(1048576), "1.00 MB");
558        assert_eq!(format_bytes(1073741824), "1.00 GB");
559    }
560
561    #[test]
562    fn test_truncate_string() {
563        assert_eq!(truncate_string("hello", 10), "hello");
564        assert_eq!(truncate_string("hello world", 8), "hello...");
565    }
566
567    #[test]
568    fn test_sanitize_identifier() {
569        assert_eq!(sanitize_identifier("hello world!"), "hello_world_");
570        assert_eq!(sanitize_identifier("device-001"), "device-001");
571    }
572
573    #[test]
574    fn test_stopwatch() {
575        let mut sw = Stopwatch::start();
576        std::thread::sleep(Duration::from_millis(10));
577        sw.lap("step1");
578        assert!(sw.elapsed().as_millis() >= 10);
579        assert_eq!(sw.laps().len(), 1);
580    }
581
582    #[test]
583    fn test_retry_config() {
584        let config = RetryConfig::new(3);
585        assert_eq!(config.delay_for_attempt(0), Duration::ZERO);
586        assert_eq!(config.delay_for_attempt(1), Duration::from_millis(100));
587        assert_eq!(config.delay_for_attempt(2), Duration::from_millis(200));
588    }
589
590    #[test]
591    fn test_rate_limiter() {
592        let limiter = RateLimiter::new(10, 100.0);
593        assert!(limiter.try_acquire());
594        assert_eq!(limiter.available(), 9);
595
596        // Exhaust tokens
597        for _ in 0..9 {
598            assert!(limiter.try_acquire());
599        }
600        assert!(!limiter.try_acquire());
601    }
602}