use std::str::FromStr;
use std::time::Duration;
use serde::{Deserialize, Deserializer, de::Error as _};
use crate::error::{Error, Result};
const UNITS: [(char, u64); 3] = [('s', 1), ('m', 60), ('h', 3_600)];
pub fn parse_duration(text: &str) -> Result<Duration> {
let text = text.trim();
let (digits, per_unit) = UNITS
.iter()
.find_map(|(suffix, seconds)| text.strip_suffix(*suffix).map(|left| (left, *seconds)))
.unwrap_or((text, 1));
digits
.parse::<u64>()
.ok()
.and_then(|count| count.checked_mul(per_unit))
.map(Duration::from_secs)
.ok_or_else(|| {
Error::Format(format!(
"`{text}` is not a duration such as `45s`, `30m` or `2h`"
))
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
#[serde(try_from = "String")]
pub enum Debounce {
Once,
Window(Duration),
}
impl FromStr for Debounce {
type Err = Error;
fn from_str(text: &str) -> Result<Self> {
match text.trim() {
"once" => Ok(Debounce::Once),
window => parse_duration(window).map(Debounce::Window),
}
}
}
impl TryFrom<String> for Debounce {
type Error = Error;
fn try_from(text: String) -> Result<Self> {
text.parse()
}
}
pub(crate) fn de_duration<'de, D: Deserializer<'de>>(
de: D,
) -> std::result::Result<Duration, D::Error> {
String::deserialize(de).and_then(|text| parse_duration(&text).map_err(D::Error::custom))
}
pub(crate) fn de_optional_duration<'de, D: Deserializer<'de>>(
de: D,
) -> std::result::Result<Option<Duration>, D::Error> {
Option::<String>::deserialize(de)?
.map(|text| parse_duration(&text).map_err(D::Error::custom))
.transpose()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_suffix_scales_the_count_and_a_bare_number_is_seconds() {
assert_eq!(
parse_duration("45s").expect("parses"),
Duration::from_secs(45)
);
assert_eq!(
parse_duration("30m").expect("parses"),
Duration::from_secs(1_800)
);
assert_eq!(
parse_duration("2h").expect("parses"),
Duration::from_secs(7_200)
);
assert_eq!(
parse_duration("90").expect("parses"),
Duration::from_secs(90)
);
assert_eq!(
parse_duration(" 5m ").expect("parses"),
Duration::from_secs(300)
);
assert_eq!(parse_duration("0s").expect("parses"), Duration::ZERO);
}
#[test]
fn rejects_anything_that_is_not_a_duration() {
for text in [
"",
" ",
"s",
"m",
"-5s",
"5 m",
"5 minutes",
"1.5h",
"5d",
"30\u{e9}",
] {
assert!(
matches!(parse_duration(text), Err(Error::Format(_))),
"{text:?} should be rejected"
);
}
}
#[test]
fn a_count_that_would_overflow_seconds_is_rejected_rather_than_wrapped() {
assert!(parse_duration(&format!("{}h", u64::MAX)).is_err());
}
#[test]
fn a_debounce_is_a_window_or_the_re_arm_cycle() {
assert_eq!("once".parse::<Debounce>().expect("parses"), Debounce::Once);
assert_eq!(
"30m".parse::<Debounce>().expect("parses"),
Debounce::Window(Duration::from_secs(1_800))
);
assert!("never".parse::<Debounce>().is_err());
}
}