taskserver_protocol 0.1.1

crate abstracting over the taskserver (taskd) protocol for taskwarrior
Documentation
use serde::de;
use std::fmt;

/// all times are defined to be in UTC
pub type DateTime = chrono::DateTime<chrono::offset::Utc>;

/// serialization function as used by serde
pub fn serialize<S>(dt: &DateTime, serializer: S) -> Result<S::Ok, S::Error>
where
    S: serde::Serializer,
{
    use chrono::{Datelike, Timelike};
    let s = format!(
        "{:04}{:02}{:02}T{:02}{:02}{:02}Z",
        dt.year(),
        dt.month(),
        dt.day(),
        dt.hour(),
        dt.minute(),
        dt.second()
    );
    serializer.serialize_str(&s)
}

struct DateTimeVisitor;

impl<'a> de::Visitor<'a> for DateTimeVisitor {
    type Value = DateTime;

    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        formatter.write_str("An ISO 8601 date in the format YYYYMMDDTHHMMSSZ")
    }

    fn visit_str<E>(self, raw: &str) -> Result<DateTime, E>
    where
        E: de::Error,
    {
        if !raw.is_ascii() {
            return Err(E::custom(format!(
                "`{}` contains non ascii characters",
                raw
            )));
        }
        if raw.len() != 16 {
            return Err(E::custom(format!("`{}` has the wrong length", raw)));
        }
        let (year, rest) = raw.split_at(4);
        let (month, rest) = rest.split_at(2);
        let (day, rest) = rest.split_at(2);
        if !rest.starts_with('T') {
            return Err(E::custom(format!("`{}` has the wrong format", raw)));
        }
        let rest = &rest[1..];
        let (hour, rest) = rest.split_at(2);
        let (minute, rest) = rest.split_at(2);
        let (second, rest) = rest.split_at(2);
        if rest != "Z" {
            return Err(E::custom(format!("`{}` has the wrong format", raw)));
        }
        match (
            year.parse(),
            month.parse(),
            day.parse(),
            hour.parse(),
            minute.parse(),
            second.parse(),
        ) {
            (Ok(year), Ok(month), Ok(day), Ok(hour), Ok(minute), Ok(second)) => {
                let d = chrono::NaiveDate::from_ymd(year, month, day).and_hms(hour, minute, second);
                let d = DateTime::from_utc(d, chrono::offset::Utc);
                Ok(d)
            }
            _ => Err(E::custom(format!("`{}` has the wrong format", raw))),
        }
    }
}

/// deserialization function as used by serde
pub fn deserialize<'a, D>(d: D) -> Result<DateTime, D::Error>
where
    D: serde::Deserializer<'a>,
{
    d.deserialize_str(DateTimeVisitor)
}

#[cfg(test)]
mod test {
    use serde::Deserialize;
    #[derive(Deserialize, Debug)]
    struct Dummy {
        #[serde(with = "super")]
        a: DateTime,
    }
    use super::*;
    #[test]
    fn non_ascii_fails() {
        let r = serde_json::from_str::<Dummy>("{\"a\": \"Überraschung\"}");
        println!("{:?}", r);
        assert!(r.is_err());
    }

    #[test]
    fn wrong_length_fails() {
        let r = serde_json::from_str::<Dummy>("{\"a\": \"334\"}");
        assert!(r.is_err())
    }

    #[test]
    fn wrong_format_fails() {
        let r = serde_json::from_str::<Dummy>("{\"a\": \"1111111111111111\"}");
        assert!(r.is_err())
    }

    #[test]
    fn correct_format_succeeds() {
        let r = serde_json::from_str::<Dummy>("{\"a\": \"20120110T231200Z\"}");
        assert!(r.is_ok());
        let d = r.unwrap().a;
        assert_eq!(d.timestamp(), 1_326_237_120);
    }
}