1use std::time::Duration;
12
13const MAX_VALUE: u128 = 99_999_999;
14
15const UNITS: &[(u128, char)] = &[
18 (1, 'n'),
19 (1_000, 'u'),
20 (1_000_000, 'm'),
21 (1_000_000_000, 'S'),
22 (60 * 1_000_000_000, 'M'),
23 (3600 * 1_000_000_000, 'H'),
24];
25
26pub fn format_grpc_timeout(d: Duration) -> String {
31 let nanos = d.as_nanos();
32 for &(unit_nanos, suffix) in UNITS {
33 let value = nanos.div_ceil(unit_nanos);
34 if value <= MAX_VALUE {
35 return format!("{value}{suffix}");
36 }
37 }
38 format!("{MAX_VALUE}H")
39}
40
41pub fn parse_grpc_timeout(s: &str) -> Option<Duration> {
45 let bytes = s.as_bytes();
46 let (&suffix, digits) = bytes.split_last()?;
47 if digits.is_empty() || !digits.iter().all(u8::is_ascii_digit) {
48 return None;
49 }
50 let value: u64 = std::str::from_utf8(digits).ok()?.parse().ok()?;
51 let unit_nanos: u64 = match suffix {
52 b'n' => 1,
53 b'u' => 1_000,
54 b'm' => 1_000_000,
55 b'S' => 1_000_000_000,
56 b'M' => 60 * 1_000_000_000,
57 b'H' => 3600 * 1_000_000_000,
58 _ => return None,
59 };
60 let total_nanos = (value as u128).checked_mul(unit_nanos as u128)?;
63 let secs = u64::try_from(total_nanos / 1_000_000_000).ok()?;
64 let nanos = (total_nanos % 1_000_000_000) as u32;
65 Some(Duration::new(secs, nanos))
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn format_picks_smallest_fitting_unit() {
74 assert_eq!(format_grpc_timeout(Duration::from_nanos(500)), "500n");
75 assert_eq!(format_grpc_timeout(Duration::from_micros(500)), "500000n");
76 assert_eq!(format_grpc_timeout(Duration::from_millis(500)), "500000u");
78 assert_eq!(format_grpc_timeout(Duration::from_secs(5)), "5000000u");
80 assert_eq!(format_grpc_timeout(Duration::from_secs(300)), "300000m");
83 assert_eq!(format_grpc_timeout(Duration::from_secs(3600)), "3600000m");
85 assert_eq!(
87 format_grpc_timeout(Duration::from_secs(86_400)),
88 "86400000m"
89 );
90 }
91
92 #[test]
93 fn format_rounds_up() {
94 assert_eq!(format_grpc_timeout(Duration::from_nanos(1500)), "1500n");
96 assert_eq!(
101 format_grpc_timeout(Duration::from_nanos(99_999_999_500)),
102 "100000m"
103 );
104 }
105
106 #[test]
107 fn format_zero() {
108 assert_eq!(format_grpc_timeout(Duration::ZERO), "0n");
109 }
110
111 #[test]
112 fn format_saturates_at_max_hours() {
113 let huge = Duration::from_secs(u64::MAX); assert_eq!(format_grpc_timeout(huge), "99999999H");
117 }
118
119 #[test]
120 fn parse_each_unit() {
121 assert_eq!(parse_grpc_timeout("500n"), Some(Duration::from_nanos(500)));
122 assert_eq!(parse_grpc_timeout("500u"), Some(Duration::from_micros(500)));
123 assert_eq!(parse_grpc_timeout("500m"), Some(Duration::from_millis(500)));
124 assert_eq!(parse_grpc_timeout("5S"), Some(Duration::from_secs(5)));
125 assert_eq!(parse_grpc_timeout("2M"), Some(Duration::from_secs(120)));
126 assert_eq!(parse_grpc_timeout("1H"), Some(Duration::from_secs(3600)));
127 }
128
129 #[test]
130 fn parse_extreme_value() {
131 let d = parse_grpc_timeout("99999999H").unwrap();
133 assert_eq!(d.as_secs(), 99_999_999u64 * 3600);
134 }
135
136 #[test]
137 fn parse_rejects_malformed() {
138 assert_eq!(parse_grpc_timeout(""), None);
139 assert_eq!(parse_grpc_timeout("S"), None); assert_eq!(parse_grpc_timeout("5"), None); assert_eq!(parse_grpc_timeout("5s"), None); assert_eq!(parse_grpc_timeout("5x"), None); assert_eq!(parse_grpc_timeout("-1S"), None); assert_eq!(parse_grpc_timeout("1.5S"), None); assert_eq!(parse_grpc_timeout(" 5S"), None); }
147
148 #[test]
149 fn parse_zero_is_valid() {
150 assert_eq!(parse_grpc_timeout("0n"), Some(Duration::ZERO));
151 assert_eq!(parse_grpc_timeout("0S"), Some(Duration::ZERO));
152 }
153
154 #[test]
155 fn roundtrip_typical_durations() {
156 for d in [
157 Duration::from_millis(1),
158 Duration::from_millis(100),
159 Duration::from_secs(1),
160 Duration::from_secs(30),
161 Duration::from_secs(300),
162 ] {
163 let formatted = format_grpc_timeout(d);
164 let parsed = parse_grpc_timeout(&formatted).unwrap();
165 assert_eq!(parsed, d, "roundtrip failed for {d:?} → {formatted:?}");
166 }
167 }
168}