Split out DateTime parsing into its own module (implement XEP-0082).

Emmanuel Gil Peyrot created

Change summary

src/date.rs         | 116 +++++++++++++++++++++++++++++++++++++++++++++++
src/delay.rs        |  22 ++------
src/idle.rs         |  36 ++------------
src/jingle_ft.rs    |   8 +-
src/lib.rs          |   3 +
src/pubsub/event.rs |   6 +-
6 files changed, 139 insertions(+), 52 deletions(-)

Detailed changes

src/date.rs πŸ”—

@@ -0,0 +1,116 @@
+// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+use std::str::FromStr;
+
+use minidom::{IntoAttributeValue, IntoElements, ElementEmitter};
+use chrono::{DateTime as ChronoDateTime, FixedOffset};
+
+use error::Error;
+
+/// Implements the DateTime profile of XEP-0082, which represents a
+/// non-recurring moment in time, with an accuracy of seconds or fraction of
+/// seconds, and includes a timezone.
+#[derive(Debug, Clone, PartialEq)]
+pub struct DateTime(ChronoDateTime<FixedOffset>);
+
+impl FromStr for DateTime {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<DateTime, Error> {
+        Ok(DateTime(ChronoDateTime::parse_from_rfc3339(s)?))
+    }
+}
+
+impl IntoAttributeValue for DateTime {
+    fn into_attribute_value(self) -> Option<String> {
+        Some(self.0.to_rfc3339())
+    }
+}
+
+impl IntoElements for DateTime {
+    fn into_elements(self, emitter: &mut ElementEmitter) {
+        emitter.append_text_node(self.0.to_rfc3339())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use chrono::{Datelike, Timelike};
+    use std::error::Error as StdError;
+
+    #[test]
+    fn test_simple() {
+        let date: DateTime = "2002-09-10T23:08:25Z".parse().unwrap();
+        assert_eq!(date.0.year(), 2002);
+        assert_eq!(date.0.month(), 9);
+        assert_eq!(date.0.day(), 10);
+        assert_eq!(date.0.hour(), 23);
+        assert_eq!(date.0.minute(), 08);
+        assert_eq!(date.0.second(), 25);
+        assert_eq!(date.0.nanosecond(), 0);
+        assert_eq!(date.0.timezone(), FixedOffset::east(0));
+    }
+
+    #[test]
+    fn test_invalid_date() {
+        // There is no thirteenth month.
+        let error = DateTime::from_str("2017-13-01T12:23:34Z").unwrap_err();
+        let message = match error {
+            Error::ChronoParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message.description(), "input is out of range");
+
+        // Timezone β‰₯24:00 aren’t allowed.
+        let error = DateTime::from_str("2017-05-27T12:11:02+25:00").unwrap_err();
+        let message = match error {
+            Error::ChronoParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message.description(), "input is out of range");
+
+        // Timezone without the : separator aren’t allowed.
+        let error = DateTime::from_str("2017-05-27T12:11:02+0100").unwrap_err();
+        let message = match error {
+            Error::ChronoParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message.description(), "input contains invalid characters");
+
+        // No seconds, error message could be improved.
+        let error = DateTime::from_str("2017-05-27T12:11+01:00").unwrap_err();
+        let message = match error {
+            Error::ChronoParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message.description(), "input contains invalid characters");
+
+        // TODO: maybe we’ll want to support this one, as per XEP-0082 Β§4.
+        let error = DateTime::from_str("20170527T12:11:02+01:00").unwrap_err();
+        let message = match error {
+            Error::ChronoParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message.description(), "input contains invalid characters");
+
+        // No timezone.
+        let error = DateTime::from_str("2017-05-27T12:11:02").unwrap_err();
+        let message = match error {
+            Error::ChronoParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message.description(), "premature end of input");
+    }
+
+    #[test]
+    fn test_serialise() {
+        let date = DateTime(ChronoDateTime::parse_from_rfc3339("2017-05-21T20:19:55+01:00").unwrap());
+        let attr = date.into_attribute_value();
+        assert_eq!(attr, Some(String::from("2017-05-21T20:19:55+01:00")));
+    }
+}

src/delay.rs πŸ”—

@@ -7,7 +7,7 @@
 use try_from::TryFrom;
 
 use minidom::Element;
-use chrono::{DateTime, FixedOffset};
+use date::DateTime;
 
 use error::Error;
 use jid::Jid;
@@ -17,7 +17,7 @@ use ns;
 #[derive(Debug, Clone)]
 pub struct Delay {
     pub from: Option<Jid>,
-    pub stamp: DateTime<FixedOffset>,
+    pub stamp: DateTime,
     pub data: Option<String>,
 }
 
@@ -32,7 +32,7 @@ impl TryFrom<Element> for Delay {
             return Err(Error::ParseError("Unknown child in delay element."));
         }
         let from = get_attr!(elem, "from", optional);
-        let stamp = get_attr!(elem, "stamp", required, stamp, DateTime::parse_from_rfc3339(stamp)?);
+        let stamp = get_attr!(elem, "stamp", required);
         let data = match elem.text().as_ref() {
             "" => None,
             text => Some(text.to_owned()),
@@ -50,7 +50,7 @@ impl From<Delay> for Element {
         Element::builder("delay")
                 .ns(ns::DELAY)
                 .attr("from", delay.from)
-                .attr("stamp", delay.stamp.to_rfc3339())
+                .attr("stamp", delay.stamp)
                 .append(delay.data)
                 .build()
     }
@@ -60,21 +60,13 @@ impl From<Delay> for Element {
 mod tests {
     use super::*;
     use std::str::FromStr;
-    use chrono::{Datelike, Timelike};
 
     #[test]
     fn test_simple() {
         let elem: Element = "<delay xmlns='urn:xmpp:delay' from='capulet.com' stamp='2002-09-10T23:08:25Z'/>".parse().unwrap();
         let delay = Delay::try_from(elem).unwrap();
         assert_eq!(delay.from, Some(Jid::from_str("capulet.com").unwrap()));
-        assert_eq!(delay.stamp.year(), 2002);
-        assert_eq!(delay.stamp.month(), 9);
-        assert_eq!(delay.stamp.day(), 10);
-        assert_eq!(delay.stamp.hour(), 23);
-        assert_eq!(delay.stamp.minute(), 08);
-        assert_eq!(delay.stamp.second(), 25);
-        assert_eq!(delay.stamp.nanosecond(), 0);
-        assert_eq!(delay.stamp.timezone(), FixedOffset::east(0));
+        assert_eq!(delay.stamp, DateTime::from_str("2002-09-10T23:08:25Z").unwrap());
         assert_eq!(delay.data, None);
     }
 
@@ -105,7 +97,7 @@ mod tests {
         let elem: Element = "<delay xmlns='urn:xmpp:delay' stamp='2002-09-10T23:08:25+00:00'/>".parse().unwrap();
         let delay = Delay {
             from: None,
-            stamp: DateTime::parse_from_rfc3339("2002-09-10T23:08:25Z").unwrap(),
+            stamp: DateTime::from_str("2002-09-10T23:08:25Z").unwrap(),
             data: None,
         };
         let elem2 = delay.into();
@@ -117,7 +109,7 @@ mod tests {
         let elem: Element = "<delay xmlns='urn:xmpp:delay' from='juliet@example.org' stamp='2002-09-10T23:08:25+00:00'>Reason</delay>".parse().unwrap();
         let delay = Delay {
             from: Some(Jid::from_str("juliet@example.org").unwrap()),
-            stamp: DateTime::parse_from_rfc3339("2002-09-10T23:08:25Z").unwrap(),
+            stamp: DateTime::from_str("2002-09-10T23:08:25Z").unwrap(),
             data: Some(String::from("Reason")),
         };
         let elem2 = delay.into();

src/idle.rs πŸ”—

@@ -7,44 +7,20 @@
 use try_from::TryFrom;
 
 use minidom::Element;
-use chrono::{DateTime, FixedOffset};
+use date::DateTime;
 
 use error::Error;
 
 use ns;
 
-#[derive(Debug, Clone)]
-pub struct Idle {
-    pub since: DateTime<FixedOffset>,
-}
-
-impl TryFrom<Element> for Idle {
-    type Err = Error;
-
-    fn try_from(elem: Element) -> Result<Idle, Error> {
-        if !elem.is("idle", ns::IDLE) {
-            return Err(Error::ParseError("This is not an idle element."));
-        }
-        for _ in elem.children() {
-            return Err(Error::ParseError("Unknown child in idle element."));
-        }
-        let since = get_attr!(elem, "since", required, since, DateTime::parse_from_rfc3339(since)?);
-        Ok(Idle { since: since })
-    }
-}
-
-impl From<Idle> for Element {
-    fn from(idle: Idle) -> Element {
-        Element::builder("idle")
-                .ns(ns::IDLE)
-                .attr("since", idle.since.to_rfc3339())
-                .build()
-    }
-}
+generate_element_with_only_attributes!(Idle, "idle", ns::IDLE, [
+    since: DateTime = "since" => required,
+]);
 
 #[cfg(test)]
 mod tests {
     use super::*;
+    use std::str::FromStr;
     use std::error::Error as StdError;
 
     #[test]
@@ -135,7 +111,7 @@ mod tests {
     #[test]
     fn test_serialise() {
         let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='2017-05-21T20:19:55+01:00'/>".parse().unwrap();
-        let idle = Idle { since: DateTime::parse_from_rfc3339("2017-05-21T20:19:55+01:00").unwrap() };
+        let idle = Idle { since: DateTime::from_str("2017-05-21T20:19:55+01:00").unwrap() };
         let elem2 = idle.into();
         assert_eq!(elem, elem2);
     }

src/jingle_ft.rs πŸ”—

@@ -11,9 +11,9 @@ use std::str::FromStr;
 
 use hashes::Hash;
 use jingle::{Creator, ContentId};
+use date::DateTime;
 
 use minidom::{Element, IntoAttributeValue};
-use chrono::{DateTime, FixedOffset};
 
 use error::Error;
 use ns;
@@ -60,7 +60,7 @@ generate_id!(Desc);
 
 #[derive(Debug, Clone)]
 pub struct File {
-    pub date: Option<DateTime<FixedOffset>>,
+    pub date: Option<DateTime>,
     pub media_type: Option<String>,
     pub name: Option<String>,
     pub descs: BTreeMap<Lang, Desc>,
@@ -137,7 +137,7 @@ impl From<File> for Element {
         if let Some(date) = file.date {
             root.append_child(Element::builder("date")
                                       .ns(ns::JINGLE_FT)
-                                      .append(date.to_rfc3339())
+                                      .append(date)
                                       .build());
         }
         if let Some(media_type) = file.media_type {
@@ -285,7 +285,7 @@ mod tests {
         assert_eq!(desc.file.media_type, Some(String::from("text/plain")));
         assert_eq!(desc.file.name, Some(String::from("test.txt")));
         assert_eq!(desc.file.descs, BTreeMap::new());
-        assert_eq!(desc.file.date, Some(DateTime::parse_from_rfc3339("2015-07-26T21:46:00+01:00").unwrap()));
+        assert_eq!(desc.file.date, DateTime::from_str("2015-07-26T21:46:00+01:00").ok());
         assert_eq!(desc.file.size, Some(6144u64));
         assert_eq!(desc.file.range, None);
         assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1);

src/lib.rs πŸ”—

@@ -325,6 +325,9 @@ pub mod pubsub;
 /// XEP-0077: In-Band Registration
 pub mod ibr;
 
+/// XEP-0082: XMPP Date and Time Profiles
+pub mod date;
+
 /// XEP-0085: Chat State Notifications
 pub mod chatstates;
 

src/pubsub/event.rs πŸ”—

@@ -9,7 +9,7 @@ use std::str::FromStr;
 
 use minidom::{Element, IntoAttributeValue};
 use jid::Jid;
-use chrono::{DateTime, FixedOffset};
+use date::DateTime;
 
 use error::Error;
 
@@ -98,7 +98,7 @@ pub enum PubSubEvent {
     },
     Subscription {
         node: NodeName,
-        expiry: Option<DateTime<FixedOffset>>,
+        expiry: Option<DateTime>,
         jid: Option<Jid>,
         subid: Option<SubscriptionId>,
         subscription: Option<Subscription>,
@@ -256,7 +256,7 @@ impl From<PubSubEvent> for Element {
                 Element::builder("subscription")
                         .ns(ns::PUBSUB_EVENT)
                         .attr("node", node)
-                        .attr("expiry", expiry.map(|expiry| expiry.to_rfc3339()))
+                        .attr("expiry", expiry)
                         .attr("jid", jid)
                         .attr("subid", subid)
                         .attr("subscription", subscription)