Add a XEP-0202 implementation.

Emmanuel Gil Peyrot created

Fixes #7.

Change summary

ChangeLog   |   5 ++
src/date.rs |  15 +++++++
src/lib.rs  |   3 +
src/ns.rs   |   3 +
src/time.rs | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 138 insertions(+)

Detailed changes

ChangeLog 🔗

@@ -1,3 +1,8 @@
+Version TODO:
+TODO  Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+    * New parsers/serialisers:
+        - Entity Time (XEP-0202).
+
 Version 0.13.1:
 2019-04-12  Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
     * Bugfixes:

src/date.rs 🔗

@@ -15,6 +15,20 @@ use std::str::FromStr;
 #[derive(Debug, Clone, PartialEq)]
 pub struct DateTime(ChronoDateTime<FixedOffset>);
 
+impl DateTime {
+    pub fn timezone(&self) -> FixedOffset {
+        self.0.timezone()
+    }
+
+    pub fn with_timezone(&self, tz: &FixedOffset) -> DateTime {
+        DateTime(self.0.with_timezone(tz))
+    }
+
+    pub fn format(&self, fmt: &str) -> String {
+        format!("{}", self.0.format(fmt))
+    }
+}
+
 impl FromStr for DateTime {
     type Err = Error;
 
@@ -41,6 +55,7 @@ mod tests {
     use chrono::{Datelike, Timelike};
     use std::error::Error as StdError;
 
+    // DateTime’s size doesn’t depend on the architecture.
     #[test]
     fn test_size() {
         assert_size!(DateTime, 16);

src/lib.rs 🔗

@@ -128,6 +128,9 @@ pub mod sm;
 /// XEP-0199: XMPP Ping
 pub mod ping;
 
+/// XEP-0202: Entity Time
+pub mod time;
+
 /// XEP-0203: Delayed Delivery
 pub mod delay;
 

src/ns.rs 🔗

@@ -112,6 +112,9 @@ pub const SM: &str = "urn:xmpp:sm:3";
 /// XEP-0199: XMPP Ping
 pub const PING: &str = "urn:xmpp:ping";
 
+/// XEP-0202: Entity Time
+pub const TIME: &str = "urn:xmpp:time";
+
 /// XEP-0203: Delayed Delivery
 pub const DELAY: &str = "urn:xmpp:delay";
 

src/time.rs 🔗

@@ -0,0 +1,112 @@
+// Copyright (c) 2019 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 chrono::FixedOffset;
+use crate::date::DateTime;
+use crate::iq::{IqGetPayload, IqResultPayload};
+use crate::ns;
+use crate::util::error::Error;
+use minidom::Element;
+use std::convert::TryFrom;
+use std::str::FromStr;
+
+generate_empty_element!(
+    TimeQuery, "time", TIME
+);
+
+impl IqGetPayload for TimeQuery {}
+
+#[derive(Debug, Clone)]
+pub struct TimeResult(pub DateTime);
+
+impl IqResultPayload for TimeResult {}
+
+impl TryFrom<Element> for TimeResult {
+    type Error = Error;
+
+    fn try_from(elem: Element) -> Result<TimeResult, Error> {
+        check_self!(elem, "time", TIME);
+        check_no_attributes!(elem, "time");
+
+        let mut tzo = None;
+        let mut utc = None;
+
+        for child in elem.children() {
+            if child.is("tzo", ns::TIME) {
+                if tzo.is_some() {
+                    return Err(Error::ParseError("More than one tzo element in time."));
+                }
+                check_no_children!(child, "tzo");
+                check_no_attributes!(child, "tzo");
+                // TODO: Add a FromStr implementation to FixedOffset to avoid this hack.
+                let fake_date = String::from("2019-04-22T11:38:00") + &child.text();
+                let date_time = DateTime::from_str(&fake_date)?;
+                tzo = Some(date_time.timezone());
+            } else if child.is("utc", ns::TIME) {
+                if utc.is_some() {
+                    return Err(Error::ParseError("More than one utc element in time."));
+                }
+                check_no_children!(child, "utc");
+                check_no_attributes!(child, "utc");
+                let date_time = DateTime::from_str(&child.text())?;
+                if date_time.timezone() != FixedOffset::east(0) {
+                    return Err(Error::ParseError("Non-UTC timezone for utc element."));
+                }
+                utc = Some(date_time);
+            } else {
+                return Err(Error::ParseError(
+                    "Unknown child in time element.",
+                ));
+            }
+        }
+
+        let tzo = tzo.ok_or(Error::ParseError("Missing tzo child in time element."))?;
+        let utc = utc.ok_or(Error::ParseError("Missing utc child in time element."))?;
+        let date = utc.with_timezone(&tzo);
+
+        Ok(TimeResult(date))
+    }
+}
+
+impl From<TimeResult> for Element {
+    fn from(time: TimeResult) -> Element {
+        Element::builder("time")
+            .ns(ns::TIME)
+            .append(Element::builder("tzo")
+                    .append(format!("{}", time.0.timezone()))
+                    .build())
+            .append(Element::builder("utc")
+                    .append(time.0.with_timezone(&FixedOffset::east(0)).format("%FT%TZ"))
+                    .build())
+            .build()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    // DateTime’s size doesn’t depend on the architecture.
+    #[test]
+    fn test_size() {
+        assert_size!(TimeQuery, 0);
+        assert_size!(TimeResult, 16);
+    }
+
+    #[test]
+    fn parse_response() {
+        let elem: Element =
+            "<time xmlns='urn:xmpp:time'><tzo>-06:00</tzo><utc>2006-12-19T17:58:35Z</utc></time>"
+                .parse()
+                .unwrap();
+        let elem1 = elem.clone();
+        let time = TimeResult::try_from(elem).unwrap();
+        assert_eq!(time.0.timezone(), FixedOffset::west(6 * 3600));
+        assert_eq!(time.0, DateTime::from_str("2006-12-19T12:58:35-05:00").unwrap());
+        let elem2 = Element::from(time);
+        assert_eq!(elem1, elem2);
+    }
+}