config-types 1.1.0

A library which provides ergononic types for configuration files
Documentation
use regex::Regex;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::ops::Deref;
use std::time::Duration;

#[derive(Debug, Clone, Default)]
pub struct DurationConf(Duration);

impl DurationConf {
    pub fn new(duration: Duration) -> Self {
        Self(duration)
    }
}

impl<'de> Deserialize<'de> for DurationConf {
    fn deserialize<D>(deserializer: D) -> Result<DurationConf, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        let duration = parse_duration(&s).map_err(serde::de::Error::custom)?;
        Ok(DurationConf(duration))
    }
}

impl Serialize for DurationConf {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let duration_str = format_duration(self.0);
        serializer.serialize_str(&duration_str)
    }
}

impl Deref for DurationConf {
    type Target = Duration;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl Into<Duration> for DurationConf {
    fn into(self) -> Duration {
        self.0
    }
}

fn parse_duration(s: &str) -> Result<Duration, String> {
    let re = Regex::new(r"^(\d+)\s*(ns|us|ms|s|m|h)$").map_err(|_| "Invalid regex")?;
    let caps = re.captures(s).ok_or_else(|| format!("Invalid duration: {}", s))?;
    let value: u64 = caps[1].parse().map_err(|_| "Invalid number")?;
    match &caps[2] {
        "ns" => Ok(Duration::from_nanos(value)),
        "us" => Ok(Duration::from_micros(value)),
        "ms" => Ok(Duration::from_millis(value)),
        "s" => Ok(Duration::from_secs(value)),
        "m" => Ok(Duration::from_secs(value * 60)),
        "h" => Ok(Duration::from_secs(value * 3600)),
        _ => Err("Invalid unit".to_string()),
    }
}

fn format_duration(duration: Duration) -> String {
    if duration.as_nanos() % 1_000 != 0 {
        format!("{}ns", duration.as_nanos())
    } else if duration.as_micros() % 1_000 != 0 {
        format!("{}us", duration.as_micros())
    } else if duration.as_millis() % 1_000 != 0 {
        format!("{}ms", duration.as_millis())
    } else if duration.as_secs() % 60 != 0 {
        format!("{}s", duration.as_secs())
    } else if duration.as_secs() % 3600 != 0 {
        format!("{}m", duration.as_secs() / 60)
    } else {
        format!("{}h", duration.as_secs() / 3600)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_duration() {
        assert_eq!(parse_duration("1ns").unwrap(), Duration::from_nanos(1));
        assert_eq!(parse_duration("1us").unwrap(), Duration::from_micros(1));
        assert_eq!(parse_duration("1ms").unwrap(), Duration::from_millis(1));
        assert_eq!(parse_duration("1s").unwrap(), Duration::from_secs(1));
        assert_eq!(parse_duration("1m").unwrap(), Duration::from_secs(60));
        assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
    }

    #[test]
    fn test_format_duration() {
        assert_eq!(format_duration(Duration::from_nanos(1)), "1ns");
        assert_eq!(format_duration(Duration::from_micros(1)), "1us");
        assert_eq!(format_duration(Duration::from_millis(1)), "1ms");
        assert_eq!(format_duration(Duration::from_secs(1)), "1s");
        assert_eq!(format_duration(Duration::from_secs(60)), "1m");
        assert_eq!(format_duration(Duration::from_secs(3600)), "1h");
    }
}