Skip to main content

aperture_cli/
duration.rs

1//! Duration parsing utilities for CLI arguments.
2//!
3//! Supports human-readable duration formats like "500ms", "1s", "30s", "1m".
4
5use crate::error::Error;
6use std::time::Duration;
7
8/// Parses a human-readable duration string into a `Duration`.
9///
10/// Supported formats:
11/// - Milliseconds: "100ms", "500ms"
12/// - Seconds: "1s", "30s", "120s"
13/// - Minutes: "1m", "5m"
14/// - Plain number (treated as milliseconds): "500"
15///
16/// # Errors
17///
18/// Returns an error if the format is invalid or the value is out of range.
19///
20/// # Examples
21///
22/// ```
23/// use aperture_cli::duration::parse_duration;
24/// use std::time::Duration;
25///
26/// assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
27/// assert_eq!(parse_duration("1s").unwrap(), Duration::from_secs(1));
28/// assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
29/// assert_eq!(parse_duration("1m").unwrap(), Duration::from_secs(60));
30/// assert_eq!(parse_duration("500").unwrap(), Duration::from_millis(500));
31/// ```
32pub fn parse_duration(s: &str) -> Result<Duration, Error> {
33    let s = s.trim();
34
35    if s.is_empty() {
36        return Err(Error::invalid_config(
37            "Duration cannot be empty".to_string(),
38        ));
39    }
40
41    // Try parsing with suffixes
42    if let Some(ms_str) = s.strip_suffix("ms") {
43        let ms: u64 = ms_str
44            .trim()
45            .parse()
46            .map_err(|_| Error::invalid_config(format!("Invalid milliseconds value: {ms_str}")))?;
47        return Ok(Duration::from_millis(ms));
48    }
49
50    if let Some(m_str) = s.strip_suffix('m') {
51        // Make sure it's not "ms" (already handled above)
52        let minutes: u64 = m_str
53            .trim()
54            .parse()
55            .map_err(|_| Error::invalid_config(format!("Invalid minutes value: {m_str}")))?;
56        return Ok(Duration::from_secs(minutes * 60));
57    }
58
59    if let Some(s_str) = s.strip_suffix('s') {
60        let secs: u64 = s_str
61            .trim()
62            .parse()
63            .map_err(|_| Error::invalid_config(format!("Invalid seconds value: {s_str}")))?;
64        return Ok(Duration::from_secs(secs));
65    }
66
67    // Plain number - treat as milliseconds
68    let ms: u64 = s.parse().map_err(|_| {
69        Error::invalid_config(format!(
70            "Invalid duration format: {s}. Use format like '500ms', '1s', '30s', or '1m'"
71        ))
72    })?;
73    Ok(Duration::from_millis(ms))
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn test_parse_duration_milliseconds() {
82        assert_eq!(parse_duration("100ms").unwrap(), Duration::from_millis(100));
83        assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
84        assert_eq!(
85            parse_duration("1000ms").unwrap(),
86            Duration::from_millis(1000)
87        );
88    }
89
90    #[test]
91    fn test_parse_duration_seconds() {
92        assert_eq!(parse_duration("1s").unwrap(), Duration::from_secs(1));
93        assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
94        assert_eq!(parse_duration("120s").unwrap(), Duration::from_secs(120));
95    }
96
97    #[test]
98    fn test_parse_duration_minutes() {
99        assert_eq!(parse_duration("1m").unwrap(), Duration::from_secs(60));
100        assert_eq!(parse_duration("5m").unwrap(), Duration::from_secs(300));
101    }
102
103    #[test]
104    fn test_parse_duration_plain_number() {
105        assert_eq!(parse_duration("500").unwrap(), Duration::from_millis(500));
106        assert_eq!(parse_duration("1000").unwrap(), Duration::from_millis(1000));
107    }
108
109    #[test]
110    fn test_parse_duration_with_whitespace() {
111        assert_eq!(
112            parse_duration(" 500ms ").unwrap(),
113            Duration::from_millis(500)
114        );
115        assert_eq!(parse_duration("  1s  ").unwrap(), Duration::from_secs(1));
116    }
117
118    #[test]
119    fn test_parse_duration_empty() {
120        assert!(parse_duration("").is_err());
121        assert!(parse_duration("   ").is_err());
122    }
123
124    #[test]
125    fn test_parse_duration_invalid() {
126        assert!(parse_duration("abc").is_err());
127        assert!(parse_duration("1x").is_err());
128        assert!(parse_duration("ms").is_err());
129        assert!(parse_duration("-1s").is_err());
130    }
131}