1use std::time::Duration;
7
8use serde::{Deserialize, Deserializer, Serializer};
9
10pub fn parse_duration(input: &str) -> Result<Duration, humantime::DurationError> {
23 humantime::parse_duration(input)
24}
25
26pub fn deserialize_duration<'de, D>(deserializer: D) -> Result<Duration, D::Error>
28where
29 D: Deserializer<'de>,
30{
31 #[derive(Deserialize)]
32 #[serde(untagged)]
33 enum DurationHelper {
34 String(String),
35 U64(u64),
36 }
37
38 match DurationHelper::deserialize(deserializer)? {
39 DurationHelper::String(s) => parse_duration(&s)
40 .map_err(|e| serde::de::Error::custom(format!("invalid duration: {e}"))),
41 DurationHelper::U64(ms) => Ok(Duration::from_millis(ms)),
42 }
43}
44
45pub fn serialize_duration<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
47where
48 S: Serializer,
49{
50 serializer.serialize_u64(duration.as_millis() as u64)
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56 use proptest::prelude::*;
57
58 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
59 struct DurationHolder {
60 #[serde(
61 deserialize_with = "deserialize_duration",
62 serialize_with = "serialize_duration"
63 )]
64 value: Duration,
65 }
66
67 #[test]
68 fn parse_duration_accepts_human_readable_values() {
69 assert_eq!(
70 parse_duration("250ms").expect("parse"),
71 Duration::from_millis(250)
72 );
73 assert_eq!(parse_duration("2s").expect("parse"), Duration::from_secs(2));
74 }
75
76 #[test]
77 fn deserialize_accepts_number_and_string() {
78 let from_num: DurationHolder = serde_json::from_str(r#"{"value":1500}"#).expect("json");
79 assert_eq!(from_num.value, Duration::from_millis(1500));
80
81 let from_str: DurationHolder = serde_json::from_str(r#"{"value":"1500ms"}"#).expect("json");
82 assert_eq!(from_str.value, Duration::from_millis(1500));
83 }
84
85 #[test]
86 fn serialize_writes_milliseconds() {
87 let value = DurationHolder {
88 value: Duration::from_millis(4321),
89 };
90 let json = serde_json::to_value(&value).expect("json");
91 assert_eq!(json["value"], 4321);
92 }
93
94 #[test]
95 fn deserialize_rejects_invalid_duration_string() {
96 let err = serde_json::from_str::<DurationHolder>(r#"{"value":"not-a-duration"}"#)
97 .expect_err("must fail");
98 assert!(err.to_string().contains("invalid duration"));
99 }
100
101 proptest! {
102 #[test]
103 fn duration_roundtrips_as_milliseconds(ms in 0_u64..10_000_000_000) {
104 let holder = DurationHolder {
105 value: Duration::from_millis(ms),
106 };
107
108 let json = serde_json::to_string(&holder).expect("serialize");
109 let reparsed: DurationHolder = serde_json::from_str(&json).expect("deserialize");
110
111 prop_assert_eq!(reparsed, holder);
112 }
113 }
114
115 #[test]
116 fn serde_json_full_roundtrip() {
117 let holder = DurationHolder {
118 value: Duration::from_secs(3661),
119 };
120 let json = serde_json::to_string(&holder).unwrap();
121 let reparsed: DurationHolder = serde_json::from_str(&json).unwrap();
122 assert_eq!(reparsed, holder);
123 }
124
125 #[test]
126 fn serde_toml_string_deserialization() {
127 let toml_str = r#"value = "2m 30s""#;
128 let holder: DurationHolder = toml::from_str(toml_str).unwrap();
129 assert_eq!(holder.value, Duration::from_secs(150));
130 }
131
132 #[test]
133 fn deserialize_rejects_boolean() {
134 let err =
135 serde_json::from_str::<DurationHolder>(r#"{"value":true}"#).expect_err("must fail");
136 assert!(!err.to_string().is_empty());
137 }
138
139 #[test]
140 fn deserialize_rejects_float() {
141 let err =
142 serde_json::from_str::<DurationHolder>(r#"{"value":1.5}"#).expect_err("must fail");
143 assert!(!err.to_string().is_empty());
144 }
145}
146
147#[cfg(test)]
148mod proptests {
149 use super::*;
150 use proptest::prelude::*;
151
152 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
153 struct DurationHolder {
154 #[serde(
155 deserialize_with = "deserialize_duration",
156 serialize_with = "serialize_duration"
157 )]
158 value: Duration,
159 }
160
161 proptest! {
162 #[test]
164 fn format_is_never_empty(ms in 0u64..10_000_000_000u64) {
165 let d = Duration::from_millis(ms);
166 let formatted = humantime::format_duration(d).to_string();
167 prop_assert!(!formatted.is_empty(), "formatted duration was empty for {ms}ms");
168 }
169
170 #[test]
172 fn format_consistency(ms in 0u64..10_000_000_000u64) {
173 let d = Duration::from_millis(ms);
174 let first = humantime::format_duration(d).to_string();
175 let second = humantime::format_duration(d).to_string();
176 prop_assert_eq!(first, second);
177 }
178
179 #[test]
181 fn parse_format_roundtrip(ms in 0u64..10_000_000u64) {
182 let d = Duration::from_millis(ms);
183 let formatted = humantime::format_duration(d).to_string();
184 let parsed = parse_duration(&formatted).expect("should parse formatted duration");
185 prop_assert_eq!(parsed, d);
186 }
187
188 #[test]
190 fn millisecond_range_contains_ms(ms in 1u64..1000u64) {
191 let d = Duration::from_millis(ms);
192 let formatted = humantime::format_duration(d).to_string();
193 prop_assert!(formatted.contains("ms"), "expected 'ms' in \"{formatted}\"");
194 }
195
196 #[test]
198 fn seconds_range_contains_s(secs in 1u64..60u64) {
199 let d = Duration::from_secs(secs);
200 let formatted = humantime::format_duration(d).to_string();
201 prop_assert!(formatted.contains('s'), "expected 's' in \"{formatted}\"");
202 }
203
204 #[test]
206 fn minutes_range_contains_m(mins in 1u64..60u64) {
207 let d = Duration::from_secs(mins * 60);
208 let formatted = humantime::format_duration(d).to_string();
209 prop_assert!(formatted.contains('m'), "expected 'm' in \"{formatted}\"");
210 }
211
212 #[test]
214 fn hours_range_contains_h(hours in 1u64..24u64) {
215 let d = Duration::from_secs(hours * 3600);
216 let formatted = humantime::format_duration(d).to_string();
217 prop_assert!(formatted.contains('h'), "expected 'h' in \"{formatted}\"");
218 }
219
220 #[test]
222 fn serde_json_u64_roundtrip(ms in 0u64..10_000_000_000u64) {
223 let json = format!(r#"{{"value":{ms}}}"#);
224 let holder: DurationHolder = serde_json::from_str(&json).expect("deserialize");
225 prop_assert_eq!(holder.value, Duration::from_millis(ms));
226 }
227
228 #[test]
230 fn serde_toml_string_roundtrip(ms in 1u64..10_000_000u64) {
231 let d = Duration::from_millis(ms);
232 let formatted = humantime::format_duration(d).to_string();
233 let toml_str = format!("value = \"{formatted}\"");
234 let holder: DurationHolder = toml::from_str(&toml_str).expect("toml deserialize");
235 prop_assert_eq!(holder.value, d);
236 }
237
238 #[test]
240 fn arbitrary_strings_never_panic(s in "\\PC{0,64}") {
241 let _ = parse_duration(&s);
242 }
243
244 #[test]
246 fn combined_duration_format_roundtrip(
247 a_ms in 0u64..1_000_000u64,
248 b_ms in 0u64..1_000_000u64,
249 ) {
250 let combined = Duration::from_millis(a_ms) + Duration::from_millis(b_ms);
251 let formatted = humantime::format_duration(combined).to_string();
252 let parsed = parse_duration(&formatted).expect("should parse formatted combined duration");
253 prop_assert_eq!(parsed, combined);
254 }
255 }
256}
257
258#[cfg(test)]
259mod edge_case_tests {
260 use super::*;
261
262 #[test]
265 fn parse_zero_ms() {
266 assert_eq!(parse_duration("0ms").unwrap(), Duration::ZERO);
267 }
268
269 #[test]
270 fn parse_zero_seconds() {
271 assert_eq!(parse_duration("0s").unwrap(), Duration::ZERO);
272 }
273
274 #[test]
275 fn deserialize_zero_from_integer() {
276 #[derive(serde::Deserialize)]
277 struct H {
278 #[serde(deserialize_with = "deserialize_duration")]
279 v: Duration,
280 }
281 let h: H = serde_json::from_str(r#"{"v":0}"#).unwrap();
282 assert_eq!(h.v, Duration::ZERO);
283 }
284
285 #[test]
286 fn serialize_zero_is_zero() {
287 #[derive(serde::Serialize)]
288 struct H {
289 #[serde(serialize_with = "serialize_duration")]
290 v: Duration,
291 }
292 let json = serde_json::to_value(H { v: Duration::ZERO }).unwrap();
293 assert_eq!(json["v"], 0);
294 }
295
296 #[test]
299 fn parse_large_hours() {
300 let d = parse_duration("9999h").unwrap();
301 assert_eq!(d, Duration::from_secs(9999 * 3600));
302 }
303
304 #[test]
305 fn serialize_large_millis() {
306 #[derive(serde::Serialize)]
307 struct H {
308 #[serde(serialize_with = "serialize_duration")]
309 v: Duration,
310 }
311 let large = Duration::from_secs(365 * 24 * 3600); let json = serde_json::to_value(H { v: large }).unwrap();
313 assert_eq!(json["v"], 365 * 24 * 3600 * 1000_u64);
314 }
315
316 #[test]
319 fn parse_microseconds() {
320 let d = parse_duration("500us").unwrap();
321 assert_eq!(d, Duration::from_micros(500));
322 }
323
324 #[test]
325 fn parse_nanoseconds() {
326 let d = parse_duration("100ns").unwrap();
327 assert_eq!(d, Duration::from_nanos(100));
328 }
329
330 #[test]
331 fn serialize_truncates_sub_millis_to_zero() {
332 #[derive(serde::Serialize)]
334 struct H {
335 #[serde(serialize_with = "serialize_duration")]
336 v: Duration,
337 }
338 let d = Duration::from_micros(999);
339 let json = serde_json::to_value(H { v: d }).unwrap();
340 assert_eq!(json["v"], 0);
341 }
342
343 #[test]
346 fn parse_empty_string_is_error() {
347 assert!(parse_duration("").is_err());
348 }
349
350 #[test]
351 fn parse_whitespace_only_is_error() {
352 assert!(parse_duration(" ").is_err());
353 }
354
355 #[test]
356 fn parse_combined_units() {
357 let d = parse_duration("1h 30m 15s").unwrap();
358 assert_eq!(d, Duration::from_secs(3600 + 30 * 60 + 15));
359 }
360
361 #[test]
362 fn parse_day_unit() {
363 let d = parse_duration("2days").unwrap();
364 assert_eq!(d, Duration::from_secs(2 * 86400));
365 }
366
367 #[test]
370 fn parsed_durations_maintain_ordering() {
371 let a = parse_duration("500ms").unwrap();
372 let b = parse_duration("1s").unwrap();
373 let c = parse_duration("1m").unwrap();
374 let d = parse_duration("1h").unwrap();
375 assert!(a < b);
376 assert!(b < c);
377 assert!(c < d);
378 }
379
380 #[test]
383 fn parsed_durations_support_addition() {
384 let a = parse_duration("30s").unwrap();
385 let b = parse_duration("30s").unwrap();
386 assert_eq!(a + b, Duration::from_secs(60));
387 }
388
389 #[test]
390 fn parsed_durations_support_subtraction() {
391 let a = parse_duration("2m").unwrap();
392 let b = parse_duration("30s").unwrap();
393 assert_eq!(a - b, Duration::from_secs(90));
394 }
395
396 #[test]
397 fn parsed_duration_supports_multiplication() {
398 let a = parse_duration("500ms").unwrap();
399 assert_eq!(a * 4, Duration::from_secs(2));
400 }
401
402 #[test]
405 fn parse_combined_no_spaces() {
406 assert_eq!(
407 parse_duration("1h30m").unwrap(),
408 Duration::from_secs(3600 + 30 * 60)
409 );
410 assert_eq!(
411 parse_duration("2m30s").unwrap(),
412 Duration::from_secs(2 * 60 + 30)
413 );
414 }
415
416 #[test]
419 fn parse_zero_forms_are_equivalent() {
420 let zero = Duration::ZERO;
421 assert_eq!(parse_duration("0s").unwrap(), zero);
422 assert_eq!(parse_duration("0ms").unwrap(), zero);
423 assert_eq!(parse_duration("0ns").unwrap(), zero);
424 assert_eq!(parse_duration("0us").unwrap(), zero);
425 }
426
427 #[test]
430 fn parse_number_without_unit_is_error() {
431 assert!(parse_duration("42").is_err());
432 }
433
434 #[test]
435 fn parse_unknown_unit_is_error() {
436 assert!(parse_duration("5xyz").is_err());
437 }
438
439 #[test]
440 fn parse_negative_is_error() {
441 assert!(parse_duration("-5s").is_err());
442 }
443
444 #[test]
445 fn parse_overflow_is_error() {
446 assert!(parse_duration("99999999999999999999999999999s").is_err());
447 }
448
449 #[test]
452 fn format_then_parse_roundtrip_deterministic() {
453 let cases = [
454 Duration::ZERO,
455 Duration::from_millis(250),
456 Duration::from_secs(42),
457 Duration::from_secs(3661),
458 Duration::from_secs(90061),
459 ];
460 for d in cases {
461 let formatted = humantime::format_duration(d).to_string();
462 let parsed = parse_duration(&formatted).unwrap();
463 assert_eq!(parsed, d, "roundtrip failed for {formatted}");
464 }
465 }
466
467 #[test]
470 fn parse_days_hours_minutes_combined() {
471 let d = parse_duration("1day 2h 30m").unwrap();
472 assert_eq!(d, Duration::from_secs(86400 + 2 * 3600 + 30 * 60));
473 }
474}
475
476#[cfg(test)]
477mod snapshot_tests {
478 use super::*;
479 use insta::assert_debug_snapshot;
480
481 #[test]
482 fn snapshot_parsed_zero() {
483 assert_debug_snapshot!(parse_duration("0s").unwrap());
484 }
485
486 #[test]
487 fn snapshot_parsed_millis() {
488 assert_debug_snapshot!(parse_duration("250ms").unwrap());
489 }
490
491 #[test]
492 fn snapshot_parsed_seconds() {
493 assert_debug_snapshot!(parse_duration("42s").unwrap());
494 }
495
496 #[test]
497 fn snapshot_parsed_minutes() {
498 assert_debug_snapshot!(parse_duration("5m").unwrap());
499 }
500
501 #[test]
502 fn snapshot_parsed_hours() {
503 assert_debug_snapshot!(parse_duration("2h").unwrap());
504 }
505
506 #[test]
507 fn snapshot_parsed_combined() {
508 assert_debug_snapshot!(parse_duration("1h 30m 15s 200ms").unwrap());
509 }
510
511 #[test]
512 fn snapshot_parse_error() {
513 assert_debug_snapshot!(parse_duration("not-valid"));
514 }
515
516 #[test]
517 fn snapshot_deserialized_from_integer() {
518 #[derive(Debug, serde::Deserialize)]
519 #[allow(dead_code)]
520 struct H {
521 #[serde(deserialize_with = "deserialize_duration")]
522 v: Duration,
523 }
524 let h: H = serde_json::from_str(r#"{"v":3661000}"#).unwrap();
525 assert_debug_snapshot!(h);
526 }
527
528 #[test]
529 fn snapshot_deserialized_from_string() {
530 #[derive(Debug, serde::Deserialize)]
531 #[allow(dead_code)]
532 struct H {
533 #[serde(deserialize_with = "deserialize_duration")]
534 v: Duration,
535 }
536 let h: H = serde_json::from_str(r#"{"v":"1h 1m 1s"}"#).unwrap();
537 assert_debug_snapshot!(h);
538 }
539
540 #[test]
541 fn snapshot_formatted_common_durations() {
542 let formatted: Vec<(&str, String)> = vec![
543 ("zero", Duration::ZERO),
544 ("half_second", Duration::from_millis(500)),
545 ("one_minute", Duration::from_secs(60)),
546 ("one_hour_one_min_one_sec", Duration::from_secs(3661)),
547 ("one_day", Duration::from_secs(86400)),
548 ]
549 .into_iter()
550 .map(|(name, d)| (name, humantime::format_duration(d).to_string()))
551 .collect();
552 assert_debug_snapshot!(formatted);
553 }
554
555 #[test]
556 fn snapshot_parse_errors_various() {
557 let results: Vec<(&str, String)> = vec!["", " ", "42", "-5s", "5xyz"]
558 .into_iter()
559 .map(|input| (input, parse_duration(input).unwrap_err().to_string()))
560 .collect();
561 assert_debug_snapshot!(results);
562 }
563}