edge-schema 0.1.0

Shared schema types for Wasmer Edge.
Documentation
mod reltime;

use std::time::Duration;

pub use self::reltime::parse_timestamp_or_relative_time;

/// Duration that can be parsed and serde de/serialized from human-readable values.
///
/// Format:
/// ( [NUMBER][UNIT] [WHITESPACE]* )+
///
/// Unit:
/// * Seconds: s|sec|secs|seconds
/// * Minutes: m|min|mins|minutes
/// * Hours: h|hour|hours
/// * Days: d|day|days
///
/// Examples:
///
/// * `30s`
/// * `1m30s`
/// * `1 day 2 hours 30 seconds`
/// * ...
///
/// Similar to the `humantime` crate.
/// A custom implementation is used to avoid additional dependencies.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PrettyDuration(pub std::time::Duration);

impl PrettyDuration {
    pub const ZERO: PrettyDuration = PrettyDuration(Duration::ZERO);
    pub const MAX: PrettyDuration = PrettyDuration(Duration::MAX);

    pub fn new(duration: std::time::Duration) -> Self {
        Self(duration)
    }

    pub fn as_duration(&self) -> std::time::Duration {
        self.0
    }

    pub fn from_secs(secs: u64) -> Self {
        Self(std::time::Duration::from_secs(secs))
    }

    pub fn from_mins(mins: u64) -> Self {
        Self(std::time::Duration::from_secs(mins * 60))
    }

    pub fn from_hours(hours: u64) -> Self {
        Self(std::time::Duration::from_secs(hours * 60 * 60))
    }
}

impl std::ops::Deref for PrettyDuration {
    type Target = std::time::Duration;

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

impl From<std::time::Duration> for PrettyDuration {
    fn from(duration: std::time::Duration) -> Self {
        Self::new(duration)
    }
}

impl From<PrettyDuration> for std::time::Duration {
    fn from(duration: PrettyDuration) -> Self {
        duration.0
    }
}

impl std::fmt::Display for PrettyDuration {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        const DAY: u64 = 60 * 60 * 24;
        const HOUR: u64 = 60 * 60;

        let secs = self.0.as_secs();

        let days = secs / DAY;
        if days > 0 {
            write!(f, "{}d", days)?;
        }
        let secs = secs % DAY;

        let hours = secs / HOUR;
        if hours > 0 {
            write!(f, "{}h", hours)?;
        }
        let secs = secs % HOUR;

        let mins = secs / 60;
        if mins > 0 {
            write!(f, "{}m", mins)?;
        }
        let secs = secs % 60;
        if secs > 0 {
            write!(f, "{}s", secs)?;
        }

        Ok(())
    }
}

#[derive(Debug)]
pub struct PrettyDurationParseError {
    value: String,
    message: String,
}

impl std::fmt::Display for PrettyDurationParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Invalid time spec '{}': {}", self.value, self.message)
    }
}

impl std::error::Error for PrettyDurationParseError {}

impl std::str::FromStr for PrettyDuration {
    type Err = PrettyDurationParseError;

    fn from_str(mut input: &str) -> Result<Self, Self::Err> {
        let mut seconds = 0;

        loop {
            input = input.strip_prefix(' ').unwrap_or(input);
            if input.is_empty() {
                break;
            }

            let nums = input.chars().take_while(|x| x.is_ascii_digit()).count();
            if nums < 1 {
                return Err(PrettyDurationParseError {
                    message: "must start with a number".to_string(),
                    value: input.to_string(),
                });
            }

            let number = &input[..nums]
                .parse::<u64>()
                .map_err(|e| PrettyDurationParseError {
                    message: format!("invalid number: {}", e),
                    value: input.to_string(),
                })?;

            input = &input[nums..];
            let chars = input
                .chars()
                .take_while(|x| x.is_ascii_alphabetic())
                .count();
            let unit = &input[..chars];
            input = &input[chars..];

            let scale = match unit {
                "s" | "sec" | "secs" | "seconds" => 1,
                "m" | "min" | "mins" | "minutes" => 60,
                "h" | "hour" | "hours" => 60 * 60,
                "d" | "day" | "days" => 60 * 60 * 24,
                _ => {
                    return Err(PrettyDurationParseError {
                        message: "unknown unit".to_string(),
                        value: input.to_string(),
                    });
                }
            };

            seconds += number * scale;
        }

        let dur = std::time::Duration::from_secs(seconds);

        Ok(Self(dur))
    }
}

impl serde::Serialize for PrettyDuration {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        self.to_string().serialize(serializer)
    }
}

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

impl schemars::JsonSchema for PrettyDuration {
    fn schema_name() -> String {
        "StringWebcPackageIdent".to_string()
    }

    fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
        schemars::schema::Schema::Object(schemars::schema::SchemaObject {
            instance_type: Some(schemars::schema::InstanceType::String.into()),
            ..Default::default()
        })
    }
}

#[cfg(test)]
mod tests {
    use std::time::Duration;

    use super::*;

    #[test]
    fn test_pretty_duration_constructors() {
        assert_eq!(
            PrettyDuration::from_secs(1).as_duration(),
            Duration::from_secs(1)
        );
        assert_eq!(
            PrettyDuration::from_mins(1).as_duration(),
            Duration::from_secs(60)
        );
        assert_eq!(
            PrettyDuration::from_hours(1).as_duration(),
            Duration::from_secs(60 * 60)
        );
    }

    #[test]
    fn test_pretty_duration_parse() {
        let cases = &[
            ("1s", Duration::from_secs(1), "1s"),
            ("10s", Duration::from_secs(10), "10s"),
            ("59s", Duration::from_secs(59), "59s"),
            ("60s", Duration::from_secs(60), "1m"),
            ("1m", Duration::from_secs(60), "1m"),
            ("11m", Duration::from_secs(60) * 11, "11m"),
            ("60m", Duration::from_secs(60) * 60, "1h"),
            ("1h", Duration::from_secs(60) * 60, "1h"),
            ("11h", Duration::from_secs(60) * 60 * 11, "11h"),
            ("1h1m", Duration::from_secs(61) * 60, "1h1m"),
            ("1h1m1s", Duration::from_secs(61 * 60 + 1), "1h1m1s"),
        ];

        for (index, (input, duration, output)) in cases.iter().enumerate() {
            eprintln!("test case {index}: {input} => {duration:?} => {output}");
            let p = input.parse::<PrettyDuration>().unwrap();
            assert_eq!(p, PrettyDuration::new(*duration));
            assert_eq!(p.to_string(), output.to_string());
        }
    }

    #[test]
    fn test_pretty_duration_serde() {
        let dur = PrettyDuration::from_secs(1);
        let json = serde_json::to_string(&dur).unwrap();
        assert_eq!(json, "\"1s\"");

        let dur2: PrettyDuration = serde_json::from_str(&json).unwrap();
        assert_eq!(dur, dur2);
    }
}