gateway_api/duration.rs
1//! GEP-2257-compliant Duration type for Gateway API
2//!
3//! `gateway_api::Duration` is a duration type where parsing and formatting
4//! obey GEP-2257. It is based on `std::time::Duration` and uses
5//! `kube::core::Duration` for the heavy lifting of parsing.
6//!
7//! GEP-2257 defines a duration format for the Gateway API that is based on
8//! Go's `time.ParseDuration`, with additional restrictions: negative
9//! durations, units smaller than millisecond, and floating point are not
10//! allowed, and durations are limited to four components of no more than five
11//! digits each. See https://gateway-api.sigs.k8s.io/geps/gep-2257 for the
12//! complete specification.
13
14use kube::core::Duration as k8sDuration;
15use once_cell::sync::Lazy;
16use regex::Regex;
17use std::fmt;
18use std::str::FromStr;
19use std::time::Duration as stdDuration;
20
21/// GEP-2257-compliant Duration type for Gateway API
22///
23/// `gateway_api::Duration` is a duration type where parsing and formatting
24/// obey GEP-2257. It is based on `std::time::Duration` and uses
25/// `kube::core::Duration` for the heavy lifting of parsing.
26///
27/// See https://gateway-api.sigs.k8s.io/geps/gep-2257 for the complete
28/// specification.
29///
30/// Per GEP-2257, when parsing a `gateway_api::Duration` from a string, the
31/// string must match
32///
33/// `^([0-9]{1,5}(h|m|s|ms)){1,4}$`
34///
35/// and is otherwise parsed the same way that Go's `time.ParseDuration` parses
36/// durations. When formatting a `gateway_api::Duration` as a string,
37/// zero-valued durations must always be formatted as `0s`, and non-zero
38/// durations must be formatted to with only one instance of each applicable
39/// unit, greatest unit first.
40///
41/// The rules above imply that `gateway_api::Duration` cannot represent
42/// negative durations, durations with sub-millisecond precision, or durations
43/// larger than 99999h59m59s999ms. Since there's no meaningful way in Rust to
44/// allow string formatting to fail, these conditions are checked instead when
45/// instantiating `gateway_api::Duration`.
46#[derive(Copy, Clone, PartialEq, Eq)]
47pub struct Duration(stdDuration);
48
49/// Regex pattern defining valid GEP-2257 Duration strings.
50const GEP2257_PATTERN: &str = r"^([0-9]{1,5}(h|m|s|ms)){1,4}$";
51
52/// Maximum duration that can be represented by GEP-2257, in milliseconds.
53const MAX_DURATION_MS: u128 = (((99999 * 3600) + (59 * 60) + 59) * 1_000) + 999;
54
55/// Checks if a duration is valid according to GEP-2257. If it's not, return
56/// an error result explaining why the duration is not valid.
57///
58/// ```rust
59/// use gateway_api::duration::is_valid;
60/// use std::time::Duration as stdDuration;
61///
62/// // sub-millisecond precision is not allowed
63/// let sub_millisecond_duration = stdDuration::from_nanos(600);
64/// # assert!(is_valid(sub_millisecond_duration).is_err());
65///
66/// // but precision at a millisecond is fine
67/// let non_sub_millisecond_duration = stdDuration::from_millis(1);
68/// # assert!(is_valid(non_sub_millisecond_duration).is_ok());
69/// ```
70pub fn is_valid(duration: stdDuration) -> Result<(), String> {
71 // Check nanoseconds to see if we have sub-millisecond precision in
72 // this duration.
73 if duration.subsec_nanos() % 1_000_000 != 0 {
74 return Err("Cannot express sub-millisecond precision in GEP-2257".to_string());
75 }
76
77 // Check the duration to see if it's greater than GEP-2257's maximum.
78 if duration.as_millis() > MAX_DURATION_MS {
79 return Err("Duration exceeds GEP-2257 maximum 99999h59m59s999ms".to_string());
80 }
81
82 Ok(())
83}
84
85/// Converting from `std::time::Duration` to `gateway_api::Duration` is
86/// allowed, but we need to make sure that the incoming duration is valid
87/// according to GEP-2257.
88///
89/// ```rust
90/// use gateway_api::Duration;
91/// use std::convert::TryFrom;
92/// use std::time::Duration as stdDuration;
93///
94/// // A one-hour duration is valid according to GEP-2257.
95/// let std_duration = stdDuration::from_secs(3600);
96/// let duration = Duration::try_from(std_duration);
97/// # assert!(duration.as_ref().is_ok());
98/// # assert_eq!(format!("{}", duration.as_ref().unwrap()), "1h");
99///
100/// // This should output "Duration: 1h".
101/// match duration {
102/// Ok(d) => println!("Duration: {}", d),
103/// Err(e) => eprintln!("Error: {}", e),
104/// }
105///
106/// // A 600-nanosecond duration is not valid according to GEP-2257.
107/// let std_duration = stdDuration::from_nanos(600);
108/// let duration = Duration::try_from(std_duration);
109/// # assert!(duration.is_err());
110///
111/// // This should output "Error: Cannot express sub-millisecond
112/// // precision in GEP-2257".
113/// match duration {
114/// Ok(d) => println!("Duration: {}", d),
115/// Err(e) => eprintln!("Error: {}", e),
116/// }
117/// ```
118impl TryFrom<stdDuration> for Duration {
119 type Error = String;
120
121 fn try_from(duration: stdDuration) -> Result<Self, Self::Error> {
122 // Check validity, and propagate any error if it's not.
123 is_valid(duration)?;
124
125 // It's valid, so we can safely convert it to a gateway_api::Duration.
126 Ok(Duration(duration))
127 }
128}
129
130/// Converting from `k8s::time::Duration` to `gateway_api::Duration` is
131/// allowed, but we need to make sure that the incoming duration is valid
132/// according to GEP-2257.
133///
134/// ```rust
135/// use gateway_api::Duration;
136/// use std::convert::TryFrom;
137/// use std::str::FromStr;
138/// use kube::core::Duration as k8sDuration;
139///
140/// // A one-hour duration is valid according to GEP-2257.
141/// let k8s_duration = k8sDuration::from_str("1h").unwrap();
142/// let duration = Duration::try_from(k8s_duration);
143/// # assert!(duration.as_ref().is_ok());
144/// # assert_eq!(format!("{}", duration.as_ref().unwrap()), "1h");
145///
146/// // This should output "Duration: 1h".
147/// match duration {
148/// Ok(d) => println!("Duration: {}", d),
149/// Err(e) => eprintln!("Error: {}", e),
150/// }
151///
152/// // A 600-nanosecond duration is not valid according to GEP-2257.
153/// let k8s_duration = k8sDuration::from_str("600ns").unwrap();
154/// let duration = Duration::try_from(k8s_duration);
155/// # assert!(duration.as_ref().is_err());
156///
157/// // This should output "Error: Cannot express sub-millisecond
158/// // precision in GEP-2257".
159/// match duration {
160/// Ok(d) => println!("Duration: {}", d),
161/// Err(e) => eprintln!("Error: {}", e),
162/// }
163///
164/// // kube::core::Duration can also express negative durations, which are not
165/// // valid according to GEP-2257.
166/// let k8s_duration = k8sDuration::from_str("-5s").unwrap();
167/// let duration = Duration::try_from(k8s_duration);
168/// # assert!(duration.as_ref().is_err());
169///
170/// // This should output "Error: Cannot express sub-millisecond
171/// // precision in GEP-2257".
172/// match duration {
173/// Ok(d) => println!("Duration: {}", d),
174/// Err(e) => eprintln!("Error: {}", e),
175/// }
176/// ```
177
178impl TryFrom<k8sDuration> for Duration {
179 type Error = String;
180
181 fn try_from(duration: k8sDuration) -> Result<Self, Self::Error> {
182 // We can't rely on kube::core::Duration to check validity for
183 // gateway_api::Duration, so first we need to make sure that our
184 // k8sDuration is not negative...
185 if duration.is_negative() {
186 return Err("Duration cannot be negative".to_string());
187 }
188
189 // Once we know it's not negative, we can safely convert it to a
190 // std::time::Duration (which will always succeed) and then check it
191 // for validity as in TryFrom<stdDuration>.
192 let stddur = stdDuration::from(duration);
193 is_valid(stddur)?;
194 Ok(Duration(stddur))
195 }
196}
197
198impl Duration {
199 /// Create a new `gateway_api::Duration` from seconds and nanoseconds,
200 /// while requiring that the resulting duration is valid according to
201 /// GEP-2257.
202 ///
203 /// ```rust
204 /// use gateway_api::Duration;
205 ///
206 /// let duration = Duration::new(7200, 600_000_000);
207 /// # assert!(duration.as_ref().is_ok());
208 /// # assert_eq!(format!("{}", duration.unwrap()), "2h600ms");
209 /// ```
210 pub fn new(secs: u64, nanos: u32) -> Result<Self, String> {
211 let stddur = stdDuration::new(secs, nanos);
212
213 // Propagate errors if not valid, or unwrap the new Duration if all's
214 // well.
215 is_valid(stddur)?;
216 Ok(Self(stddur))
217 }
218
219 /// Create a new `gateway_api::Duration` from seconds, while requiring
220 /// that the resulting duration is valid according to GEP-2257.
221 ///
222 /// ```rust
223 /// use gateway_api::Duration;
224 /// let duration = Duration::from_secs(3600);
225 /// # assert!(duration.as_ref().is_ok());
226 /// # assert_eq!(format!("{}", duration.unwrap()), "1h");
227 /// ```
228 pub fn from_secs(secs: u64) -> Result<Self, String> {
229 Self::new(secs, 0)
230 }
231
232 /// Create a new `gateway_api::Duration` from microseconds, while
233 /// requiring that the resulting duration is valid according to GEP-2257.
234 ///
235 /// ```rust
236 /// use gateway_api::Duration;
237 /// let duration = Duration::from_micros(1_000_000);
238 /// # assert!(duration.as_ref().is_ok());
239 /// # assert_eq!(format!("{}", duration.unwrap()), "1s");
240 /// ```
241 pub fn from_micros(micros: u64) -> Result<Self, String> {
242 let sec = micros / 1_000_000;
243 let ns = ((micros % 1_000_000) * 1_000) as u32;
244
245 Self::new(sec, ns)
246 }
247
248 /// Create a new `gateway_api::Duration` from milliseconds, while
249 /// requiring that the resulting duration is valid according to GEP-2257.
250 ///
251 /// ```rust
252 /// use gateway_api::Duration;
253 /// let duration = Duration::from_millis(1000);
254 /// # assert!(duration.as_ref().is_ok());
255 /// # assert_eq!(format!("{}", duration.unwrap()), "1s");
256 /// ```
257 pub fn from_millis(millis: u64) -> Result<Self, String> {
258 let sec = millis / 1_000;
259 let ns = ((millis % 1_000) * 1_000_000) as u32;
260
261 Self::new(sec, ns)
262 }
263
264 /// The number of whole seconds in the entire duration.
265 ///
266 /// ```rust
267 /// use gateway_api::Duration;
268 ///
269 /// let duration = Duration::from_secs(3600); // 1h
270 /// # assert!(duration.as_ref().is_ok());
271 /// let seconds = duration.unwrap().as_secs(); // 3600
272 /// # assert_eq!(seconds, 3600);
273 ///
274 /// let duration = Duration::from_millis(1500); // 1s500ms
275 /// # assert!(duration.as_ref().is_ok());
276 /// let seconds = duration.unwrap().as_secs(); // 1
277 /// # assert_eq!(seconds, 1);
278 /// ```
279 pub fn as_secs(&self) -> u64 {
280 self.0.as_secs()
281 }
282
283 /// The number of milliseconds in the whole duration. GEP-2257 doesn't
284 /// support sub-millisecond precision, so this is always exact.
285 ///
286 /// ```rust
287 /// use gateway_api::Duration;
288 ///
289 /// let duration = Duration::from_millis(1500); // 1s500ms
290 /// # assert!(duration.as_ref().is_ok());
291 /// let millis = duration.unwrap().as_millis(); // 1500
292 /// # assert_eq!(millis, 1500);
293 /// ```
294 pub fn as_millis(&self) -> u128 {
295 self.0.as_millis()
296 }
297
298 /// The number of nanoseconds in the whole duration. This is always exact.
299 ///
300 /// ```rust
301 /// use gateway_api::Duration;
302 ///
303 /// let duration = Duration::from_millis(1500); // 1s500ms
304 /// # assert!(duration.as_ref().is_ok());
305 /// let nanos = duration.unwrap().as_nanos(); // 1_500_000_000
306 /// # assert_eq!(nanos, 1_500_000_000);
307 /// ```
308 pub fn as_nanos(&self) -> u128 {
309 self.0.as_nanos()
310 }
311
312 /// The number of nanoseconds in the part of the duration that's not whole
313 /// seconds. Since GEP-2257 doesn't support sub-millisecond precision, this
314 /// will always be 0 or a multiple of 1,000,000.
315 ///
316 /// ```rust
317 /// use gateway_api::Duration;
318 ///
319 /// let duration = Duration::from_millis(1500); // 1s500ms
320 /// # assert!(duration.as_ref().is_ok());
321 /// let subsec_nanos = duration.unwrap().subsec_nanos(); // 500_000_000
322 /// # assert_eq!(subsec_nanos, 500_000_000);
323 /// ```
324 pub fn subsec_nanos(&self) -> u32 {
325 self.0.subsec_nanos()
326 }
327
328 /// Checks whether the duration is zero.
329 ///
330 /// ```rust
331 /// use gateway_api::Duration;
332 ///
333 /// let duration = Duration::from_secs(0);
334 /// # assert!(duration.as_ref().is_ok());
335 /// assert!(duration.unwrap().is_zero());
336 ///
337 /// let duration = Duration::from_secs(1);
338 /// # assert!(duration.as_ref().is_ok());
339 /// assert!(!duration.unwrap().is_zero());
340 /// ```
341 pub fn is_zero(&self) -> bool {
342 self.0.is_zero()
343 }
344}
345
346/// Parsing a `gateway_api::Duration` from a string requires that the input
347/// string obey GEP-2257:
348///
349/// - input strings must match `^([0-9]{1,5}(h|m|s|ms)){1,4}$`
350/// - durations are parsed the same way that Go's `time.ParseDuration` does
351///
352/// If the input string is not valid according to GEP-2257, an error is
353/// returned explaining what went wrong.
354///
355/// ```rust
356/// use gateway_api::Duration;
357/// use std::str::FromStr;
358///
359/// let duration = Duration::from_str("1h");
360/// # assert!(duration.as_ref().is_ok());
361/// # assert_eq!(format!("{}", duration.as_ref().unwrap()), "1h");
362///
363/// // This should output "Parsed duration: 1h".
364/// match duration {
365/// Ok(d) => println!("Parsed duration: {}", d),
366/// Err(e) => eprintln!("Error: {}", e),
367/// }
368///
369/// let duration = Duration::from_str("1h30m500ns");
370/// # assert!(duration.as_ref().is_err());
371///
372/// // This should output "Error: Cannot express sub-millisecond
373/// // precision in GEP-2257".
374/// match duration {
375/// Ok(d) => println!("Parsed duration: {}", d),
376/// Err(e) => eprintln!("Error: {}", e),
377/// }
378/// ```
379impl FromStr for Duration {
380 type Err = String;
381
382 // Parse a GEP-2257-compliant duration string into a
383 // `gateway_api::Duration`.
384 fn from_str(duration_str: &str) -> Result<Self, Self::Err> {
385 // GEP-2257 dictates that string values must match GEP2257_PATTERN and
386 // be parsed the same way that Go's time.ParseDuration parses
387 // durations.
388 //
389 // This Lazy Regex::new should never ever fail, given that the regex
390 // is a compile-time constant. But just in case.....
391 static RE: Lazy<Regex> = Lazy::new(|| {
392 Regex::new(GEP2257_PATTERN).expect(
393 format!(
394 r#"GEP2257 regex "{}" did not compile (this is a bug!)"#,
395 GEP2257_PATTERN
396 )
397 .as_str(),
398 )
399 });
400
401 // If the string doesn't match the regex, it's invalid.
402 if !RE.is_match(duration_str) {
403 return Err("Invalid duration format".to_string());
404 }
405
406 // We use kube::core::Duration to do the heavy lifting of parsing.
407 match k8sDuration::from_str(duration_str) {
408 // If the parse fails, return an error immediately...
409 Err(err) => Err(err.to_string()),
410
411 // ...otherwise, we need to try to turn the k8sDuration into a
412 // gateway_api::Duration (which will check validity).
413 Ok(kd) => Duration::try_from(kd),
414 }
415 }
416}
417
418/// Formatting a `gateway_api::Duration` for display is defined only for valid
419/// durations, and must follow the GEP-2257 rules for formatting:
420///
421/// - zero-valued durations must always be formatted as `0s`
422/// - non-zero durations must be formatted with only one instance of each
423/// applicable unit, greatest unit first.
424///
425/// ```rust
426/// use gateway_api::Duration;
427/// use std::fmt::Display;
428///
429/// // Zero-valued durations are always formatted as "0s".
430/// let duration = Duration::from_secs(0);
431/// # assert!(duration.as_ref().is_ok());
432/// assert_eq!(format!("{}", duration.unwrap()), "0s");
433///
434/// // Non-zero durations are formatted with only one instance of each
435/// // applicable unit, greatest unit first.
436/// let duration = Duration::from_secs(3600);
437/// # assert!(duration.as_ref().is_ok());
438/// assert_eq!(format!("{}", duration.unwrap()), "1h");
439///
440/// let duration = Duration::from_millis(1500);
441/// # assert!(duration.as_ref().is_ok());
442/// assert_eq!(format!("{}", duration.unwrap()), "1s500ms");
443///
444/// let duration = Duration::from_millis(9005500);
445/// # assert!(duration.as_ref().is_ok());
446/// assert_eq!(format!("{}", duration.unwrap()), "2h30m5s500ms");
447/// ```
448impl fmt::Display for Duration {
449 /// Format a `gateway_api::Duration` for display, following GEP-2257 rules.
450 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451 // Short-circuit if the duration is zero, since "0s" is the special
452 // case for a zero-valued duration.
453 if self.is_zero() {
454 return write!(f, "0s");
455 }
456
457 // Unfortunately, we can't rely on kube::core::Duration for
458 // formatting, since it can happily hand back things like "5400s"
459 // instead of "1h30m".
460 //
461 // So we'll do the formatting ourselves. Start by grabbing the
462 // milliseconds part of the Duration (remember, the constructors make
463 // sure that we don't have sub-millisecond precision)...
464 let ms = self.subsec_nanos() / 1_000_000;
465
466 // ...then after that, do the usual div & mod tree to take seconds and
467 // get hours, minutes, and seconds from it.
468 let mut secs = self.as_secs();
469
470 let hours = secs / 3600;
471
472 if hours > 0 {
473 secs -= hours * 3600;
474 write!(f, "{}h", hours)?;
475 }
476
477 let minutes = secs / 60;
478 if minutes > 0 {
479 secs -= minutes * 60;
480 write!(f, "{}m", minutes)?;
481 }
482
483 if secs > 0 {
484 write!(f, "{}s", secs)?;
485 }
486
487 if ms > 0 {
488 write!(f, "{}ms", ms)?;
489 }
490
491 Ok(())
492 }
493}
494
495/// Formatting a `gateway_api::Duration` for debug is the same as formatting
496/// it for display.
497impl fmt::Debug for Duration {
498 /// Format a `gateway_api::Duration` for debug, following GEP-2257 rules.
499 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
500 // Yes, we format GEP-2257 Durations the same in debug and display.
501 fmt::Display::fmt(self, f)
502 }
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508
509 #[test]
510 /// Test that the validation logic in `Duration`'s constructor
511 /// method(s) correctly handles known-good durations. (The tests are
512 /// ordered to match the from_str test cases.)
513 fn test_gep2257_from_valid_duration() {
514 let test_cases = vec![
515 Duration::from_secs(0), // 0s / 0h0m0s / 0m0s
516 Duration::from_secs(3600), // 1h
517 Duration::from_secs(1800), // 30m
518 Duration::from_secs(10), // 10s
519 Duration::from_millis(500), // 500ms
520 Duration::from_secs(9000), // 2h30m / 150m
521 Duration::from_secs(5410), // 1h30m10s / 10s30m1h
522 Duration::new(7200, 600_000_000), // 2h600ms
523 Duration::new(7200 + 1800, 600_000_000), // 2h30m600ms
524 Duration::new(7200 + 1800 + 10, 600_000_000), // 2h30m10s600ms
525 Duration::from_millis(MAX_DURATION_MS as u64), // 99999h59m59s999ms
526 ];
527
528 for (idx, duration) in test_cases.iter().enumerate() {
529 assert!(
530 duration.is_ok(),
531 "{:?}: Duration {:?} should be OK",
532 idx,
533 duration
534 );
535 }
536 }
537
538 #[test]
539 /// Test that the validation logic in `Duration`'s constructor
540 /// method(s) correctly handles known-bad durations.
541 fn test_gep2257_from_invalid_duration() {
542 let test_cases = vec![
543 (
544 Duration::from_micros(100),
545 Err("Cannot express sub-millisecond precision in GEP-2257".to_string()),
546 ),
547 (
548 Duration::from_secs(10000 * 86400),
549 Err("Duration exceeds GEP-2257 maximum 99999h59m59s999ms".to_string()),
550 ),
551 (
552 Duration::from_millis((MAX_DURATION_MS + 1) as u64),
553 Err("Duration exceeds GEP-2257 maximum 99999h59m59s999ms".to_string()),
554 ),
555 ];
556
557 for (idx, (duration, expected)) in test_cases.into_iter().enumerate() {
558 assert_eq!(
559 duration, expected,
560 "{:?}: Duration {:?} should be an error",
561 idx, duration
562 );
563 }
564 }
565
566 #[test]
567 /// Test that the TryFrom implementation for k8sDuration correctly converts
568 /// to gateway_api::Duration and validates the result.
569 fn test_gep2257_from_valid_k8s_duration() {
570 let test_cases = vec![
571 (
572 k8sDuration::from_str("0s").unwrap(),
573 Duration::from_secs(0).unwrap(),
574 ),
575 (
576 k8sDuration::from_str("1h").unwrap(),
577 Duration::from_secs(3600).unwrap(),
578 ),
579 (
580 k8sDuration::from_str("500ms").unwrap(),
581 Duration::from_millis(500).unwrap(),
582 ),
583 (
584 k8sDuration::from_str("2h600ms").unwrap(),
585 Duration::new(7200, 600_000_000).unwrap(),
586 ),
587 ];
588
589 for (idx, (k8s_duration, expected)) in test_cases.into_iter().enumerate() {
590 let duration = Duration::try_from(k8s_duration);
591
592 assert!(
593 duration.as_ref().is_ok_and(|d| *d == expected),
594 "{:?}: Duration {:?} should be {:?}",
595 idx,
596 duration,
597 expected
598 );
599 }
600 }
601
602 #[test]
603 /// Test that the TryFrom implementation for k8sDuration correctly fails
604 /// for kube::core::Durations that aren't valid GEP-2257 durations.
605 fn test_gep2257_from_invalid_k8s_duration() {
606 let test_cases: Vec<(k8sDuration, Result<Duration, String>)> = vec![
607 (
608 k8sDuration::from_str("100us").unwrap(),
609 Err("Cannot express sub-millisecond precision in GEP-2257".to_string()),
610 ),
611 (
612 k8sDuration::from_str("100000h").unwrap(),
613 Err("Duration exceeds GEP-2257 maximum 99999h59m59s999ms".to_string()),
614 ),
615 (
616 k8sDuration::from(stdDuration::from_millis((MAX_DURATION_MS + 1) as u64)),
617 Err("Duration exceeds GEP-2257 maximum 99999h59m59s999ms".to_string()),
618 ),
619 (
620 k8sDuration::from_str("-5s").unwrap(),
621 Err("Duration cannot be negative".to_string()),
622 ),
623 ];
624
625 for (idx, (k8s_duration, expected)) in test_cases.into_iter().enumerate() {
626 assert_eq!(
627 Duration::try_from(k8s_duration),
628 expected,
629 "{:?}: k8sDuration {:?} should be error {:?}",
630 idx,
631 k8s_duration,
632 expected
633 );
634 }
635 }
636
637 #[test]
638 fn test_gep2257_from_str() {
639 // Test vectors are mostly taken directly from GEP-2257, but there are
640 // some extras thrown in and it's not meaningful to test e.g. "0.5m"
641 // in Rust.
642 let test_cases = vec![
643 ("0h", Duration::from_secs(0)),
644 ("0s", Duration::from_secs(0)),
645 ("0h0m0s", Duration::from_secs(0)),
646 ("1h", Duration::from_secs(3600)),
647 ("30m", Duration::from_secs(1800)),
648 ("10s", Duration::from_secs(10)),
649 ("500ms", Duration::from_millis(500)),
650 ("2h30m", Duration::from_secs(9000)),
651 ("150m", Duration::from_secs(9000)),
652 ("7230s", Duration::from_secs(7230)),
653 ("1h30m10s", Duration::from_secs(5410)),
654 ("10s30m1h", Duration::from_secs(5410)),
655 ("100ms200ms300ms", Duration::from_millis(600)),
656 ("100ms200ms300ms", Duration::from_millis(600)),
657 (
658 "99999h59m59s999ms",
659 Duration::from_millis(MAX_DURATION_MS as u64),
660 ),
661 ("1d", Err("Invalid duration format".to_string())),
662 ("1", Err("Invalid duration format".to_string())),
663 ("1m1", Err("Invalid duration format".to_string())),
664 (
665 "1h30m10s20ms50h",
666 Err("Invalid duration format".to_string()),
667 ),
668 ("999999h", Err("Invalid duration format".to_string())),
669 ("1.5h", Err("Invalid duration format".to_string())),
670 ("-15m", Err("Invalid duration format".to_string())),
671 (
672 "99999h59m59s1000ms",
673 Err("Duration exceeds GEP-2257 maximum 99999h59m59s999ms".to_string()),
674 ),
675 ];
676
677 for (idx, (duration_str, expected)) in test_cases.into_iter().enumerate() {
678 assert_eq!(
679 Duration::from_str(duration_str),
680 expected,
681 "{:?}: Duration {:?} should be {:?}",
682 idx,
683 duration_str,
684 expected
685 );
686 }
687 }
688
689 #[test]
690 fn test_gep2257_format() {
691 // Formatting should always succeed for valid durations, and we've
692 // covered invalid durations in the constructor and parse tests.
693 let test_cases = vec![
694 (Duration::from_secs(0), "0s".to_string()),
695 (Duration::from_secs(3600), "1h".to_string()),
696 (Duration::from_secs(1800), "30m".to_string()),
697 (Duration::from_secs(10), "10s".to_string()),
698 (Duration::from_millis(500), "500ms".to_string()),
699 (Duration::from_secs(9000), "2h30m".to_string()),
700 (Duration::from_secs(5410), "1h30m10s".to_string()),
701 (Duration::from_millis(600), "600ms".to_string()),
702 (Duration::new(7200, 600_000_000), "2h600ms".to_string()),
703 (
704 Duration::new(7200 + 1800, 600_000_000),
705 "2h30m600ms".to_string(),
706 ),
707 (
708 Duration::new(7200 + 1800 + 10, 600_000_000),
709 "2h30m10s600ms".to_string(),
710 ),
711 ];
712
713 for (idx, (duration, expected)) in test_cases.into_iter().enumerate() {
714 assert!(
715 duration
716 .as_ref()
717 .is_ok_and(|d| format!("{}", d) == expected),
718 "{:?}: Duration {:?} should be {:?}",
719 idx,
720 duration,
721 expected
722 );
723 }
724 }
725}