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| if c.is_alphanumeric() || c == '_' || c == '-' { c } else { '_' })
194        .collect()
195}
196
197// =============================================================================
198// Builder Pattern Macros
199// =============================================================================
200
201/// Implement a builder pattern setter method.
202///
203/// # Example
204///
205/// ```rust,ignore
206/// struct Config {
207///     name: String,
208///     port: u16,
209/// }
210///
211/// impl Config {
212///     mabi_core::builder_setter!(name, String);
213///     mabi_core::builder_setter!(port, u16);
214/// }
215/// ```
216#[macro_export]
217macro_rules! builder_setter {
218    ($field:ident, $type:ty) => {
219        pub fn $field(mut self, value: $type) -> Self {
220            self.$field = value;
221            self
222        }
223    };
224    ($field:ident, $type:ty, $doc:expr) => {
225        #[doc = $doc]
226        pub fn $field(mut self, value: $type) -> Self {
227            self.$field = value;
228            self
229        }
230    };
231}
232
233/// Implement a builder pattern setter method that takes impl Into<T>.
234///
235/// # Example
236///
237/// ```rust,ignore
238/// impl Config {
239///     mabi_core::builder_setter_into!(name, String);
240/// }
241///
242/// let config = Config::default().name("test");
243/// ```
244#[macro_export]
245macro_rules! builder_setter_into {
246    ($field:ident, $type:ty) => {
247        pub fn $field(mut self, value: impl Into<$type>) -> Self {
248            self.$field = value.into();
249            self
250        }
251    };
252    ($field:ident, $type:ty, $doc:expr) => {
253        #[doc = $doc]
254        pub fn $field(mut self, value: impl Into<$type>) -> Self {
255            self.$field = value.into();
256            self
257        }
258    };
259}
260
261/// Implement a builder pattern setter method for Option<T>.
262///
263/// # Example
264///
265/// ```rust,ignore
266/// impl Config {
267///     mabi_core::builder_setter_option!(description, String);
268/// }
269/// ```
270#[macro_export]
271macro_rules! builder_setter_option {
272    ($field:ident, $type:ty) => {
273        pub fn $field(mut self, value: impl Into<$type>) -> Self {
274            self.$field = Some(value.into());
275            self
276        }
277    };
278    ($field:ident, $type:ty, $doc:expr) => {
279        #[doc = $doc]
280        pub fn $field(mut self, value: impl Into<$type>) -> Self {
281            self.$field = Some(value.into());
282            self
283        }
284    };
285}
286
287/// Implement a builder pattern with_* prefix setter method.
288///
289/// # Example
290///
291/// ```rust,ignore
292/// impl Config {
293///     mabi_core::builder_with!(name, String);  // Creates with_name()
294/// }
295/// ```
296#[macro_export]
297macro_rules! builder_with {
298    ($field:ident, $type:ty) => {
299        paste::paste! {
300            pub fn [<with_ $field>](mut self, value: impl Into<$type>) -> Self {
301                self.$field = value.into();
302                self
303            }
304        }
305    };
306    ($field:ident, $type:ty, $doc:expr) => {
307        paste::paste! {
308            #[doc = $doc]
309            pub fn [<with_ $field>](mut self, value: impl Into<$type>) -> Self {
310                self.$field = value.into();
311                self
312            }
313        }
314    };
315}
316
317// =============================================================================
318// Retry Utilities
319// =============================================================================
320
321/// Retry configuration.
322#[derive(Debug, Clone)]
323pub struct RetryConfig {
324    /// Maximum number of attempts.
325    pub max_attempts: u32,
326    /// Initial delay between retries.
327    pub initial_delay: Duration,
328    /// Maximum delay between retries.
329    pub max_delay: Duration,
330    /// Exponential backoff multiplier.
331    pub multiplier: f64,
332}
333
334impl Default for RetryConfig {
335    fn default() -> Self {
336        Self {
337            max_attempts: 3,
338            initial_delay: Duration::from_millis(100),
339            max_delay: Duration::from_secs(10),
340            multiplier: 2.0,
341        }
342    }
343}
344
345impl RetryConfig {
346    /// Create a new retry config.
347    pub fn new(max_attempts: u32) -> Self {
348        Self {
349            max_attempts,
350            ..Default::default()
351        }
352    }
353
354    /// Set initial delay.
355    pub fn with_initial_delay(mut self, delay: Duration) -> Self {
356        self.initial_delay = delay;
357        self
358    }
359
360    /// Set max delay.
361    pub fn with_max_delay(mut self, delay: Duration) -> Self {
362        self.max_delay = delay;
363        self
364    }
365
366    /// Set multiplier.
367    pub fn with_multiplier(mut self, multiplier: f64) -> Self {
368        self.multiplier = multiplier;
369        self
370    }
371
372    /// Calculate delay for attempt number.
373    pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
374        if attempt == 0 {
375            return Duration::ZERO;
376        }
377
378        let delay_ms = self.initial_delay.as_millis() as f64
379            * self.multiplier.powi(attempt.saturating_sub(1) as i32);
380        let delay = Duration::from_millis(delay_ms.min(self.max_delay.as_millis() as f64) as u64);
381
382        delay.min(self.max_delay)
383    }
384}
385
386/// Retry an async operation with exponential backoff.
387///
388/// # Example
389///
390/// ```rust,ignore
391/// use mabi_core::utils::{retry_async, RetryConfig};
392///
393/// let result = retry_async(
394///     RetryConfig::new(3),
395///     || async {
396///         // Your fallible async operation
397///         Ok::<_, std::io::Error>(42)
398///     },
399/// ).await;
400/// ```
401pub async fn retry_async<F, Fut, T, E>(config: RetryConfig, mut f: F) -> Result<T, E>
402where
403    F: FnMut() -> Fut,
404    Fut: std::future::Future<Output = Result<T, E>>,
405{
406    let mut last_error = None;
407
408    for attempt in 0..config.max_attempts {
409        match f().await {
410            Ok(value) => return Ok(value),
411            Err(e) => {
412                last_error = Some(e);
413
414                if attempt + 1 < config.max_attempts {
415                    let delay = config.delay_for_attempt(attempt + 1);
416                    tokio::time::sleep(delay).await;
417                }
418            }
419        }
420    }
421
422    Err(last_error.expect("Should have at least one error"))
423}
424
425// =============================================================================
426// Rate Limiting
427// =============================================================================
428
429/// Simple rate limiter using token bucket algorithm.
430#[derive(Debug)]
431pub struct RateLimiter {
432    /// Maximum tokens in the bucket.
433    capacity: u64,
434    /// Current tokens in the bucket.
435    tokens: AtomicU64,
436    /// Tokens added per second.
437    refill_rate: f64,
438    /// Last refill time.
439    last_refill: parking_lot::Mutex<Instant>,
440}
441
442impl RateLimiter {
443    /// Create a new rate limiter.
444    pub fn new(capacity: u64, refill_rate: f64) -> Self {
445        Self {
446            capacity,
447            tokens: AtomicU64::new(capacity),
448            refill_rate,
449            last_refill: parking_lot::Mutex::new(Instant::now()),
450        }
451    }
452
453    /// Try to acquire a token. Returns true if successful.
454    pub fn try_acquire(&self) -> bool {
455        self.refill();
456
457        loop {
458            let current = self.tokens.load(Ordering::Acquire);
459            if current == 0 {
460                return false;
461            }
462
463            if self
464                .tokens
465                .compare_exchange_weak(current, current - 1, Ordering::AcqRel, Ordering::Acquire)
466                .is_ok()
467            {
468                return true;
469            }
470        }
471    }
472
473    /// Try to acquire multiple tokens. Returns true if successful.
474    pub fn try_acquire_n(&self, n: u64) -> bool {
475        self.refill();
476
477        loop {
478            let current = self.tokens.load(Ordering::Acquire);
479            if current < n {
480                return false;
481            }
482
483            if self
484                .tokens
485                .compare_exchange_weak(current, current - n, Ordering::AcqRel, Ordering::Acquire)
486                .is_ok()
487            {
488                return true;
489            }
490        }
491    }
492
493    /// Get current token count.
494    pub fn available(&self) -> u64 {
495        self.refill();
496        self.tokens.load(Ordering::Acquire)
497    }
498
499    fn refill(&self) {
500        let mut last_refill = self.last_refill.lock();
501        let now = Instant::now();
502        let elapsed = now.duration_since(*last_refill).as_secs_f64();
503
504        if elapsed > 0.0 {
505            let new_tokens = (elapsed * self.refill_rate) as u64;
506            if new_tokens > 0 {
507                let current = self.tokens.load(Ordering::Acquire);
508                let new_value = (current + new_tokens).min(self.capacity);
509                self.tokens.store(new_value, Ordering::Release);
510                *last_refill = now;
511            }
512        }
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519
520    #[test]
521    fn test_generate_uuid() {
522        let uuid1 = generate_uuid();
523        let uuid2 = generate_uuid();
524        assert_ne!(uuid1, uuid2);
525        assert_eq!(uuid1.len(), 36);
526    }
527
528    #[test]
529    fn test_generate_sequential_id() {
530        let id1 = generate_sequential_id("device");
531        let id2 = generate_sequential_id("device");
532        assert!(id1.starts_with("device-"));
533        assert!(id2.starts_with("device-"));
534        assert_ne!(id1, id2);
535    }
536
537    #[test]
538    fn test_format_duration() {
539        assert_eq!(format_duration(Duration::from_secs(3661)), "1h 1m");
540        assert_eq!(format_duration(Duration::from_secs(61)), "1m 1s");
541        assert_eq!(format_duration(Duration::from_millis(1500)), "1.500s");
542        assert_eq!(format_duration(Duration::from_millis(500)), "500ms");
543        assert_eq!(format_duration(Duration::from_micros(500)), "500µs");
544    }
545
546    #[test]
547    fn test_format_bytes() {
548        assert_eq!(format_bytes(500), "500 B");
549        assert_eq!(format_bytes(1024), "1.00 KB");
550        assert_eq!(format_bytes(1536), "1.50 KB");
551        assert_eq!(format_bytes(1048576), "1.00 MB");
552        assert_eq!(format_bytes(1073741824), "1.00 GB");
553    }
554
555    #[test]
556    fn test_truncate_string() {
557        assert_eq!(truncate_string("hello", 10), "hello");
558        assert_eq!(truncate_string("hello world", 8), "hello...");
559    }
560
561    #[test]
562    fn test_sanitize_identifier() {
563        assert_eq!(sanitize_identifier("hello world!"), "hello_world_");
564        assert_eq!(sanitize_identifier("device-001"), "device-001");
565    }
566
567    #[test]
568    fn test_stopwatch() {
569        let mut sw = Stopwatch::start();
570        std::thread::sleep(Duration::from_millis(10));
571        sw.lap("step1");
572        assert!(sw.elapsed().as_millis() >= 10);
573        assert_eq!(sw.laps().len(), 1);
574    }
575
576    #[test]
577    fn test_retry_config() {
578        let config = RetryConfig::new(3);
579        assert_eq!(config.delay_for_attempt(0), Duration::ZERO);
580        assert_eq!(config.delay_for_attempt(1), Duration::from_millis(100));
581        assert_eq!(config.delay_for_attempt(2), Duration::from_millis(200));
582    }
583
584    #[test]
585    fn test_rate_limiter() {
586        let limiter = RateLimiter::new(10, 100.0);
587        assert!(limiter.try_acquire());
588        assert_eq!(limiter.available(), 9);
589
590        // Exhaust tokens
591        for _ in 0..9 {
592            assert!(limiter.try_acquire());
593        }
594        assert!(!limiter.try_acquire());
595    }
596}