use serde::{Deserialize, Deserializer}; use std::time::Duration as StdDuration; use time::Duration as TimeDuration; fn de_error(msg: impl std::fmt::Display) -> E { E::custom(msg) } fn parse_native_time(s: &str) -> Result { let (seconds, nanos) = s .split_once('.') .ok_or_else(|| format!("expected 'seconds.nanoseconds', got {s:?}"))?; let seconds: i64 = seconds .parse() .map_err(|_| format!("invalid seconds in {s:?}"))?; let nanos: i64 = nanos .parse() .map_err(|_| format!("invalid nanoseconds in {s:?}"))?; Ok(TimeDuration::seconds(seconds) + TimeDuration::nanoseconds(nanos)) } fn parse_human_time(s: &str) -> Result { let s = s.trim(); if s.is_empty() { return Err("empty duration string".to_string()); } if let Ok(secs) = s.parse::() { return Ok(TimeDuration::seconds(secs)); } let mut total = TimeDuration::ZERO; let mut buf = String::new(); let mut any = false; for ch in s.chars() { if ch.is_ascii_digit() || ch == '-' { buf.push(ch); } else { let value: i64 = buf .parse() .map_err(|_| format!("invalid number in duration {s:?}"))?; match ch { 's' => total += TimeDuration::seconds(value), 'm' => total += TimeDuration::minutes(value), 'h' => total += TimeDuration::hours(value), 'd' => total += TimeDuration::days(value), other => return Err(format!("unknown duration unit {other:?} in {s:?}")), } buf.clear(); any = true; } } if !buf.is_empty() { return Err(format!("trailing number without unit in {s:?}")); } if !any { return Err(format!("cannot parse duration {s:?}")); } Ok(total) } pub fn deserialize_time_duration<'de, D>(d: D) -> Result where D: Deserializer<'de>, { #[derive(Deserialize)] #[serde(untagged)] enum Repr { Secs(i64), SecsF64(f64), Text(String), } match Repr::deserialize(d)? { Repr::Secs(secs) => Ok(TimeDuration::seconds(secs)), Repr::SecsF64(secs) => Ok(TimeDuration::seconds_f64(secs)), Repr::Text(t) if t.contains('.') => parse_native_time(&t).map_err(de_error), Repr::Text(t) => parse_human_time(&t).map_err(de_error), } } pub fn deserialize_std_duration<'de, D>(d: D) -> Result where D: Deserializer<'de>, { #[derive(Deserialize)] #[serde(untagged)] enum Repr { Secs(u64), Struct { secs: u64, nanos: u32 }, } match Repr::deserialize(d)? { Repr::Secs(secs) => Ok(StdDuration::from_secs(secs)), Repr::Struct { secs, nanos } => Ok(StdDuration::new(secs, nanos)), } } pub fn deserialize_opt_std_duration<'de, D>(d: D) -> Result, D::Error> where D: Deserializer<'de>, { #[derive(Deserialize)] #[serde(untagged)] enum Repr { Secs(u64), Struct { secs: u64, nanos: u32 }, } match Option::::deserialize(d)? { Some(Repr::Secs(secs)) => Ok(Some(StdDuration::from_secs(secs))), Some(Repr::Struct { secs, nanos }) => Ok(Some(StdDuration::new(secs, nanos))), None => Ok(None), } } #[cfg(test)] mod tests { use super::*; use serde::Deserialize; #[derive(Deserialize)] struct TimeWrap { #[serde(deserialize_with = "deserialize_time_duration")] d: TimeDuration, } #[derive(Deserialize)] struct StdWrap { #[serde(deserialize_with = "deserialize_std_duration")] d: StdDuration, } #[derive(Deserialize)] struct OptStdWrap { #[serde(default, deserialize_with = "deserialize_opt_std_duration")] d: Option, } #[test] fn time_duration_from_bare_seconds() { let w: TimeWrap = serde_json::from_str(r#"{"d":900}"#).unwrap(); assert_eq!(w.d, TimeDuration::seconds(900)); } #[test] fn time_duration_from_human_string() { let w: TimeWrap = serde_json::from_str(r#"{"d":"1h30m"}"#).unwrap(); assert_eq!(w.d, TimeDuration::hours(1) + TimeDuration::minutes(30)); } #[test] fn time_duration_from_native_string() { let w: TimeWrap = serde_json::from_str(r#"{"d":"900.000000000"}"#).unwrap(); assert_eq!(w.d, TimeDuration::seconds(900)); } #[test] fn std_duration_from_bare_seconds() { let w: StdWrap = serde_json::from_str(r#"{"d":30}"#).unwrap(); assert_eq!(w.d, StdDuration::from_secs(30)); } #[test] fn std_duration_from_native_struct() { let w: StdWrap = serde_json::from_str(r#"{"d":{"secs":30,"nanos":0}}"#).unwrap(); assert_eq!(w.d, StdDuration::from_secs(30)); } #[test] fn opt_std_duration_none_when_absent() { let w: OptStdWrap = serde_json::from_str(r#"{}"#).unwrap(); assert_eq!(w.d, None); } #[test] fn opt_std_duration_some_from_seconds() { let w: OptStdWrap = serde_json::from_str(r#"{"d":300}"#).unwrap(); assert_eq!(w.d, Some(StdDuration::from_secs(300))); } }