date.rs

  1// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
  2//
  3// This Source Code Form is subject to the terms of the Mozilla Public
  4// License, v. 2.0. If a copy of the MPL was not distributed with this
  5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6
  7use chrono::{DateTime as ChronoDateTime, FixedOffset};
  8use minidom::{IntoAttributeValue, Node};
  9use std::str::FromStr;
 10
 11/// Implements the DateTime profile of XEP-0082, which represents a
 12/// non-recurring moment in time, with an accuracy of seconds or fraction of
 13/// seconds, and includes a timezone.
 14#[derive(Debug, Clone, PartialEq)]
 15pub struct DateTime(pub ChronoDateTime<FixedOffset>);
 16
 17impl DateTime {
 18    /// Retrieves the associated timezone.
 19    pub fn timezone(&self) -> FixedOffset {
 20        self.0.timezone()
 21    }
 22
 23    /// Returns a new `DateTime` with a different timezone.
 24    pub fn with_timezone(&self, tz: FixedOffset) -> DateTime {
 25        DateTime(self.0.with_timezone(&tz))
 26    }
 27
 28    /// Formats this `DateTime` with the specified format string.
 29    pub fn format(&self, fmt: &str) -> String {
 30        format!("{}", self.0.format(fmt))
 31    }
 32}
 33
 34impl FromStr for DateTime {
 35    type Err = chrono::ParseError;
 36
 37    fn from_str(s: &str) -> Result<DateTime, Self::Err> {
 38        Ok(DateTime(ChronoDateTime::parse_from_rfc3339(s)?))
 39    }
 40}
 41
 42impl IntoAttributeValue for DateTime {
 43    fn into_attribute_value(self) -> Option<String> {
 44        Some(self.0.to_rfc3339())
 45    }
 46}
 47
 48impl From<DateTime> for Node {
 49    fn from(date: DateTime) -> Node {
 50        Node::Text(date.0.to_rfc3339())
 51    }
 52}
 53
 54#[cfg(test)]
 55mod tests {
 56    use super::*;
 57    use chrono::{Datelike, Timelike};
 58
 59    // DateTime’s size doesn’t depend on the architecture.
 60    #[test]
 61    fn test_size() {
 62        assert_size!(DateTime, 16);
 63    }
 64
 65    #[test]
 66    fn test_simple() {
 67        let date: DateTime = "2002-09-10T23:08:25Z".parse().unwrap();
 68        assert_eq!(date.0.year(), 2002);
 69        assert_eq!(date.0.month(), 9);
 70        assert_eq!(date.0.day(), 10);
 71        assert_eq!(date.0.hour(), 23);
 72        assert_eq!(date.0.minute(), 08);
 73        assert_eq!(date.0.second(), 25);
 74        assert_eq!(date.0.nanosecond(), 0);
 75        assert_eq!(date.0.timezone(), FixedOffset::east_opt(0).unwrap());
 76    }
 77
 78    #[test]
 79    fn test_invalid_date() {
 80        // There is no thirteenth month.
 81        let error = DateTime::from_str("2017-13-01T12:23:34Z").unwrap_err();
 82        assert_eq!(error.to_string(), "input is out of range");
 83
 84        // Timezone ≥24:00 aren’t allowed.
 85        let error = DateTime::from_str("2017-05-27T12:11:02+25:00").unwrap_err();
 86        assert_eq!(error.to_string(), "input is out of range");
 87
 88        // Timezone without the : separator aren’t allowed.
 89        let error = DateTime::from_str("2017-05-27T12:11:02+0100").unwrap_err();
 90        assert_eq!(error.to_string(), "input contains invalid characters");
 91
 92        // No seconds, error message could be improved.
 93        let error = DateTime::from_str("2017-05-27T12:11+01:00").unwrap_err();
 94        assert_eq!(error.to_string(), "input contains invalid characters");
 95
 96        // TODO: maybe we’ll want to support this one, as per XEP-0082 §4.
 97        let error = DateTime::from_str("20170527T12:11:02+01:00").unwrap_err();
 98        assert_eq!(error.to_string(), "input contains invalid characters");
 99
100        // No timezone.
101        let error = DateTime::from_str("2017-05-27T12:11:02").unwrap_err();
102        assert_eq!(error.to_string(), "premature end of input");
103    }
104
105    #[test]
106    fn test_serialise() {
107        let date =
108            DateTime(ChronoDateTime::parse_from_rfc3339("2017-05-21T20:19:55+01:00").unwrap());
109        let attr = date.into_attribute_value();
110        assert_eq!(attr, Some(String::from("2017-05-21T20:19:55+01:00")));
111    }
112}