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 alloc::borrow::Cow;
  8use core::str::FromStr;
  9
 10use xso::{error::Error, AsXmlText, FromXmlText, TextCodec};
 11
 12use chrono::{DateTime as ChronoDateTime, FixedOffset, Utc};
 13use minidom::{IntoAttributeValue, Node};
 14
 15/// Text codec for
 16/// [XEP-0082](https://xmpp.org/extensions/xep-0082.html)-compliant formatting
 17/// of dates and times.
 18pub struct Xep0082;
 19
 20impl TextCodec<ChronoDateTime<FixedOffset>> for Xep0082 {
 21    fn decode(&self, s: String) -> Result<ChronoDateTime<FixedOffset>, Error> {
 22        ChronoDateTime::parse_from_rfc3339(&s).map_err(Error::text_parse_error)
 23    }
 24
 25    fn encode<'x>(
 26        &self,
 27        value: &'x ChronoDateTime<FixedOffset>,
 28    ) -> Result<Option<Cow<'x, str>>, Error> {
 29        if value.offset().utc_minus_local() == 0 {
 30            Ok(Some(Cow::Owned(
 31                value.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
 32            )))
 33        } else {
 34            Ok(Some(Cow::Owned(value.to_rfc3339())))
 35        }
 36    }
 37}
 38
 39impl TextCodec<ChronoDateTime<Utc>> for Xep0082 {
 40    fn decode(&self, s: String) -> Result<ChronoDateTime<Utc>, Error> {
 41        Ok(ChronoDateTime::<FixedOffset>::parse_from_rfc3339(&s)
 42            .map_err(Error::text_parse_error)?
 43            .into())
 44    }
 45
 46    fn encode<'x>(&self, value: &'x ChronoDateTime<Utc>) -> Result<Option<Cow<'x, str>>, Error> {
 47        Ok(Some(Cow::Owned(
 48            value.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
 49        )))
 50    }
 51}
 52
 53impl<T> TextCodec<Option<T>> for Xep0082
 54where
 55    Xep0082: TextCodec<T>,
 56{
 57    fn decode(&self, s: String) -> Result<Option<T>, Error> {
 58        Ok(Some(self.decode(s)?))
 59    }
 60
 61    fn encode<'x>(&self, value: &'x Option<T>) -> Result<Option<Cow<'x, str>>, Error> {
 62        value
 63            .as_ref()
 64            .and_then(|x| self.encode(x).transpose())
 65            .transpose()
 66    }
 67}
 68
 69/// Implements the DateTime profile of XEP-0082, which represents a
 70/// non-recurring moment in time, with an accuracy of seconds or fraction of
 71/// seconds, and includes a timezone.
 72#[derive(Debug, Clone, PartialEq)]
 73pub struct DateTime(pub ChronoDateTime<FixedOffset>);
 74
 75impl DateTime {
 76    /// Retrieves the associated timezone.
 77    pub fn timezone(&self) -> FixedOffset {
 78        self.0.timezone()
 79    }
 80
 81    /// Returns a new `DateTime` with a different timezone.
 82    pub fn with_timezone(&self, tz: FixedOffset) -> DateTime {
 83        DateTime(self.0.with_timezone(&tz))
 84    }
 85
 86    /// Formats this `DateTime` with the specified format string.
 87    pub fn format(&self, fmt: &str) -> String {
 88        format!("{}", self.0.format(fmt))
 89    }
 90}
 91
 92impl FromStr for DateTime {
 93    type Err = chrono::ParseError;
 94
 95    fn from_str(s: &str) -> Result<DateTime, Self::Err> {
 96        Ok(DateTime(ChronoDateTime::parse_from_rfc3339(s)?))
 97    }
 98}
 99
100impl FromXmlText for DateTime {
101    fn from_xml_text(s: String) -> Result<Self, Error> {
102        s.parse().map_err(Error::text_parse_error)
103    }
104}
105
106impl AsXmlText for DateTime {
107    fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
108        Ok(Cow::Owned(self.0.to_rfc3339()))
109    }
110}
111
112impl IntoAttributeValue for DateTime {
113    fn into_attribute_value(self) -> Option<String> {
114        Some(self.0.to_rfc3339())
115    }
116}
117
118impl From<DateTime> for Node {
119    fn from(date: DateTime) -> Node {
120        Node::Text(date.0.to_rfc3339())
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use chrono::{Datelike, Timelike};
128
129    // DateTime’s size doesn’t depend on the architecture.
130    #[test]
131    fn test_size() {
132        assert_size!(DateTime, 16);
133    }
134
135    #[test]
136    fn test_simple() {
137        let date: DateTime = "2002-09-10T23:08:25Z".parse().unwrap();
138        assert_eq!(date.0.year(), 2002);
139        assert_eq!(date.0.month(), 9);
140        assert_eq!(date.0.day(), 10);
141        assert_eq!(date.0.hour(), 23);
142        assert_eq!(date.0.minute(), 08);
143        assert_eq!(date.0.second(), 25);
144        assert_eq!(date.0.nanosecond(), 0);
145        assert_eq!(date.0.timezone(), FixedOffset::east_opt(0).unwrap());
146    }
147
148    #[test]
149    fn test_invalid_date() {
150        // There is no thirteenth month.
151        let error = DateTime::from_str("2017-13-01T12:23:34Z").unwrap_err();
152        assert_eq!(error.to_string(), "input is out of range");
153
154        // Timezone ≥24:00 aren’t allowed.
155        let error = DateTime::from_str("2017-05-27T12:11:02+25:00").unwrap_err();
156        assert_eq!(error.to_string(), "input is out of range");
157
158        // Timezone without the : separator aren’t allowed.
159        let error = DateTime::from_str("2017-05-27T12:11:02+0100").unwrap_err();
160        assert_eq!(error.to_string(), "input contains invalid characters");
161
162        // No seconds, error message could be improved.
163        let error = DateTime::from_str("2017-05-27T12:11+01:00").unwrap_err();
164        assert_eq!(error.to_string(), "input contains invalid characters");
165
166        // TODO: maybe we’ll want to support this one, as per XEP-0082 §4.
167        let error = DateTime::from_str("20170527T12:11:02+01:00").unwrap_err();
168        assert_eq!(error.to_string(), "input contains invalid characters");
169
170        // No timezone.
171        let error = DateTime::from_str("2017-05-27T12:11:02").unwrap_err();
172        assert_eq!(error.to_string(), "premature end of input");
173    }
174
175    #[test]
176    fn test_serialise() {
177        let date =
178            DateTime(ChronoDateTime::parse_from_rfc3339("2017-05-21T20:19:55+01:00").unwrap());
179        let attr = date.into_attribute_value();
180        assert_eq!(attr, Some(String::from("2017-05-21T20:19:55+01:00")));
181    }
182}