1use std::fmt;
12use std::fmt::{Debug, Display, Formatter};
13use std::str::FromStr;
14use std::time::Duration;
15
16pub use go_duration::{parse_duration, ParseDurationError};
17use serde::de::{self, Deserializer, Visitor};
18use serde::{Deserialize, Serialize, Serializer};
19
20#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
49pub struct DurationString(Duration);
50
51impl Debug for DurationString {
52 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
53 Debug::fmt(&self.0, f)
54 }
55}
56
57impl DurationString {
58 pub const fn new(d: Duration) -> Self {
60 Self(d)
61 }
62
63 pub const fn as_duration(&self) -> Duration {
65 self.0
66 }
67}
68
69impl From<Duration> for DurationString {
70 fn from(d: Duration) -> Self {
71 Self(d)
72 }
73}
74
75impl From<DurationString> for Duration {
76 fn from(d: DurationString) -> Self {
77 d.0
78 }
79}
80
81impl std::ops::Deref for DurationString {
82 type Target = Duration;
83
84 fn deref(&self) -> &Duration {
85 &self.0
86 }
87}
88
89impl FromStr for DurationString {
90 type Err = ParseDurationError;
91
92 fn from_str(s: &str) -> Result<Self, Self::Err> {
93 parse_string(s).map(Self)
94 }
95}
96
97impl Display for DurationString {
98 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
99 write!(f, "{}s{}ns", self.0.as_secs(), self.0.subsec_nanos())
100 }
101}
102
103impl Serialize for DurationString {
104 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
105 serializer.collect_str(self)
106 }
107}
108
109impl<'de> Deserialize<'de> for DurationString {
110 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
111 deserializer.deserialize_any(DurationStringVisitor)
112 }
113}
114
115struct DurationStringVisitor;
116
117impl<'de> Visitor<'de> for DurationStringVisitor {
118 type Value = DurationString;
119
120 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 f.write_str("a duration string (e.g. \"30s\", \"1h30m\") or a non-negative number of nanoseconds")
122 }
123
124 fn visit_i64<E: de::Error>(self, n: i64) -> Result<DurationString, E> {
125 if n < 0 {
126 return Err(E::custom(ParseDurationError::Negative));
127 }
128 Ok(DurationString(Duration::from_nanos(n as u64)))
129 }
130
131 fn visit_i128<E: de::Error>(self, n: i128) -> Result<DurationString, E> {
132 if n < 0 {
133 return Err(E::custom(ParseDurationError::Negative));
134 }
135 if n > MAX_NANOS_U64 as i128 {
136 return Err(E::custom(ParseDurationError::Overflow));
137 }
138 Ok(DurationString(Duration::from_nanos(n as u64)))
139 }
140
141 fn visit_u64<E: de::Error>(self, n: u64) -> Result<DurationString, E> {
142 if n > MAX_NANOS_U64 {
143 return Err(E::custom(ParseDurationError::Overflow));
144 }
145 Ok(DurationString(Duration::from_nanos(n)))
146 }
147
148 fn visit_u128<E: de::Error>(self, n: u128) -> Result<DurationString, E> {
149 if n > MAX_NANOS_U64 as u128 {
150 return Err(E::custom(ParseDurationError::Overflow));
151 }
152 Ok(DurationString(Duration::from_nanos(n as u64)))
153 }
154
155 fn visit_f64<E: de::Error>(self, f: f64) -> Result<DurationString, E> {
156 if !f.is_finite() {
157 return Err(E::custom("duration nanoseconds must be finite"));
158 }
159 if f < 0.0 {
160 return Err(E::custom(ParseDurationError::Negative));
161 }
162 if f > MAX_NANOS_U64 as f64 {
163 return Err(E::custom(ParseDurationError::Overflow));
164 }
165 Ok(DurationString(Duration::from_nanos(f as u64)))
166 }
167
168 fn visit_str<E: de::Error>(self, s: &str) -> Result<DurationString, E> {
169 parse_string(s).map(DurationString).map_err(E::custom)
170 }
171
172 fn visit_string<E: de::Error>(self, s: String) -> Result<DurationString, E> {
173 self.visit_str(&s)
174 }
175}
176
177const MAX_NANOS_U64: u64 = i64::MAX as u64;
180
181fn parse_string(s: &str) -> Result<Duration, ParseDurationError> {
184 let trimmed = s.trim();
185 match parse_duration(trimmed) {
186 Ok(d) => Ok(d),
187 Err(err) => match trimmed.parse::<i128>() {
188 Ok(n) if n < 0 => Err(ParseDurationError::Negative),
189 Ok(n) => {
190 if n > MAX_NANOS_U64 as i128 {
191 return Err(ParseDurationError::Overflow);
192 }
193 Ok(Duration::from_nanos(n as u64))
194 }
195 Err(_) => Err(err),
196 },
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use anyhow::Context as _;
203 use serde_json::json;
204
205 use super::*;
206
207 const NS: Duration = Duration::from_nanos(1);
208 const _US: Duration = Duration::from_micros(1);
209 const MS: Duration = Duration::from_millis(1);
210 const S: Duration = Duration::from_secs(1);
211 const M: Duration = Duration::from_secs(60);
212 const H: Duration = Duration::from_secs(3600);
213
214 #[test]
215 fn deserialize_integer_succeeds() {
216 let json = r#"{ "value": 15 }"#;
217 let deserialized: SerdeTest = serde_json::from_str(json).unwrap();
218 assert_eq!(deserialized.value.as_duration(), 15 * NS);
219 }
220
221 #[test]
223 fn deserialize_float_succeeds() {
224 let json = r#"{ "value": 1.5 }"#;
225 let deserialized: SerdeTest = serde_json::from_str(json).unwrap();
226 assert_eq!(deserialized.value.as_duration(), 1 * NS);
227 }
228
229 #[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Deserialize, Serialize)]
230 struct SerdeTest {
231 value: DurationString,
232 }
233
234 impl From<Duration> for SerdeTest {
235 fn from(value: Duration) -> Self {
236 Self { value: value.into() }
237 }
238 }
239
240 fn test_json(input_value: &str) -> String {
241 json!({"value": input_value}).to_string()
242 }
243
244 fn test_yaml(input_value: &str) -> String {
245 format!("value: {input_value}")
246 }
247
248 fn run_success_case(input: &str, expected: Duration, serialized: &str) -> anyhow::Result<()> {
249 let expected_struct: SerdeTest = expected.into();
250 let json = test_json(input);
251 let yaml = test_yaml(input);
252 let msg = format!("failure for duration test case '{input}'");
253 let parsed_duration = DurationString::from_str(input).context(msg.clone())?;
254 anyhow::ensure!(
255 expected == parsed_duration.as_duration(),
256 "{msg}, expected: {expected:?}, got {:?}",
257 parsed_duration.as_duration()
258 );
259 let deserialized_from_json: SerdeTest = serde_json::from_str(&json).context(msg.clone())?;
260 anyhow::ensure!(
261 expected_struct == deserialized_from_json,
262 "{msg}, expected: {expected_struct:?}, got {deserialized_from_json:?}"
263 );
264 let roundtrip_json = serde_json::from_str(&serde_json::to_string(&expected_struct)?)?;
265 anyhow::ensure!(
266 expected_struct == roundtrip_json,
267 "{msg}, expected json roundrip to produce {expected_struct:?}, but got {roundtrip_json:?}"
268 );
269 let deserialized_from_yaml: SerdeTest = serde_yaml::from_str(&yaml).context(msg.clone())?;
270 anyhow::ensure!(
271 expected_struct == deserialized_from_yaml,
272 "{msg}, expected: {expected_struct:?}, got {deserialized_from_yaml:?}"
273 );
274 let roundtrip_yaml = serde_yaml::from_str(&serde_yaml::to_string(&expected_struct)?)?;
275 anyhow::ensure!(
276 expected_struct == roundtrip_yaml,
277 "{msg}, expected json roundrip to produce {expected_struct:?}, but got {roundtrip_yaml:?}"
278 );
279 let actual_serialized = parsed_duration.to_string();
280 anyhow::ensure!(
281 serialized == actual_serialized,
282 "Expected the input '{input}' to be serialized as '{serialized}' but got '{actual_serialized}'"
283 );
284 Ok(())
285 }
286
287 #[test]
288 fn parse_duration_supports_go_style_units() {
289 assert_eq!(parse_duration("10s").unwrap(), Duration::from_secs(10));
290 assert_eq!(parse_duration("1m0s").unwrap(), Duration::from_secs(60));
291 assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
292 assert_eq!(
293 parse_duration("1h2m3.5s").unwrap(),
294 Duration::from_secs(3723) + Duration::from_millis(500)
295 );
296 assert_eq!(parse_duration("250us").unwrap(), Duration::from_micros(250));
297 assert_eq!(parse_duration("250µs").unwrap(), Duration::from_micros(250));
298 assert_eq!(parse_duration("250μs").unwrap(), Duration::from_micros(250));
299 }
300
301 #[test]
302 fn parse_duration_rejects_config_only_values() {
303 assert!(parse_duration("").is_err());
304 assert!(parse_duration("abc").is_err());
305 assert!(parse_duration("10").is_err());
306 assert!(parse_duration(" 10s").is_err());
307 assert!(parse_duration("1xs").is_err());
308 }
309
310 #[test]
311 fn duration_string_success_cases() {
312 let cases: &[(&str, Duration, &str)] = &[
313 ("0", Duration::ZERO, "0s0ns"),
314 ("-0", Duration::ZERO, "0s0ns"),
315 ("+0", Duration::ZERO, "0s0ns"),
316 ("+5h", Duration::from_hours(5), "18000s0ns"),
317 (".5s", Duration::from_millis(500), "0s500000000ns"),
318 ("5.s", Duration::from_secs(5), "5s0ns"),
319 ("0.000000001s", Duration::from_nanos(1), "0s1ns"),
320 ("1.5h", Duration::from_mins(90), "5400s0ns"),
321 (
322 "2h45m30.5s",
323 (2 * H) + (45 * M) + (30 * S) + (500 * MS),
324 "9930s500000000ns",
325 ),
326 ("12µs", Duration::from_micros(12), "0s12000ns"),
327 ("12μs", Duration::from_micros(12), "0s12000ns"),
328 ("0s", Duration::ZERO, "0s0ns"),
329 ("1h1m1s1ms1us1ns", H + M + S + MS + (1000 * NS) + NS, "3661s1001001ns"),
330 ("24h", Duration::from_hours(24), "86400s0ns"),
331 (
332 "9223372036854775807ns",
333 Duration::from_nanos(9223372036854775807),
334 "9223372036s854775807ns",
335 ),
336 (
337 "9223372036854775.807us",
338 Duration::from_secs(9223372036) + (854775807 * NS),
339 "9223372036s854775807ns",
340 ),
341 (
342 "2562047h47m16.854775807s",
343 Duration::from_secs(9223372036) + (854775807 * NS),
344 "9223372036s854775807ns",
345 ),
346 ("0.1ns", Duration::ZERO, "0s0ns"),
347 ("05s", Duration::from_secs(5), "5s0ns"),
348 ("1ns1s", S + NS, "1s1ns"),
349 ("100h100m100s", (100 * H) + (100 * M) + (100 * S), "366100s0ns"),
350 ("5m32s", (5 * M) + (32 * S), "332s0ns"),
351 ("1m0s", M, "60s0ns"),
352 ("5m0s", 5 * M, "300s0ns"),
353 ("6m0s", 6 * M, "360s0ns"),
354 ("10m0s", 10 * M, "600s0ns"),
355 ("15m0s", 15 * M, "900s0ns"),
356 ("30m0s", 30 * M, "1800s0ns"),
357 ("40m0s", 40 * M, "2400s0ns"),
358 ("50m0s", 50 * M, "3000s0ns"),
359 ("87600h0m0s", 87600 * H, "315360000s0ns"),
360 ("5", 5 * NS, "0s5ns"),
361 (" 5s", 5 * S, "5s0ns"),
362 ("5s", 5 * S, "5s0ns"),
363 ];
364
365 for (input, expected, serialized) in cases {
366 run_success_case(input, *expected, serialized).unwrap();
367 }
368 }
369
370 fn run_failure_case(input: &str, expected_msg: &str) -> anyhow::Result<()> {
371 let result = DurationString::from_str(input);
372 match result {
373 Ok(value) => {
374 anyhow::bail!("Expected an error when parsing '{input}', but instead received the value '{value:?}'")
375 }
376 Err(e) => {
377 anyhow::ensure!(
378 e.to_string().contains(expected_msg),
379 "Expected the error message when parsing '{input}' to contain {expected_msg:?}, but the message is {e}"
380 );
381 }
382 }
383
384 Ok(())
385 }
386
387 #[test]
388 fn duration_string_failure_cases() {
389 let cases: &[(&str, &str)] = &[
390 ("5m32sFOO", "unknown unit 'sFOO'"),
391 ("", "empty duration"),
392 (" ", "empty duration"),
393 ("+", "empty duration"),
394 ("-", "empty duration"),
395 (".", "expected digits"),
396 ("s", "expected digits"),
397 (".s", "expected digits"),
398 ("--5s", "expected digits"),
399 ("5.5.5s", "missing unit"),
400 ("1e3s", "unknown unit 'e'"),
401 ("5ns5", "missing unit"),
402 ("9223372036854775808ns", "exceeds"),
403 ("-1s", "negative"),
404 ("-0.5h", "negative"),
405 ("1d", "unknown unit 'd'"),
406 ("1w", "unknown unit 'w'"),
407 ("1S", "unknown unit 'S'"),
408 ("12 µs", "missing unit"),
409 ("5 s", "missing unit"),
410 ("5. s", "missing unit"),
411 ];
412
413 for (input, expected_msg) in cases {
414 run_failure_case(input, expected_msg).unwrap();
415 }
416 }
417}