parsers: port time over to derive macros

Jonas Schäfer created

Change summary

parsers/ChangeLog   |   3 +
parsers/src/date.rs |  58 +++++++++++++++++++
parsers/src/time.rs | 133 ++++++++++++++++++++++++----------------------
3 files changed, 129 insertions(+), 65 deletions(-)

Detailed changes

parsers/ChangeLog 🔗

@@ -64,6 +64,9 @@ XXXX-YY-ZZ RELEASER <admin@example.com>
       - Add a lang property in Presence, to avoid parser errors.
       - pubsub::Owner is now a wrapper for the pubsub::owner::Paylolad enum,
         and all its direct children have been merged into this enum (!532)
+      - time::TimeResult has been ported to use xso. Use From/Into to convert
+        it to/from chrono::DateTime values. The numbered member `0` does not
+        exist anymore (!551).
     * New parsers/serialisers:
       - Stream Features (RFC 6120) (!400)
       - Spam Reporting (XEP-0377) (!506)

parsers/src/date.rs 🔗

@@ -7,11 +7,65 @@
 use alloc::borrow::Cow;
 use core::str::FromStr;
 
-use xso::{error::Error, AsXmlText, FromXmlText};
+use xso::{error::Error, AsXmlText, FromXmlText, TextCodec};
 
-use chrono::{DateTime as ChronoDateTime, FixedOffset};
+use chrono::{DateTime as ChronoDateTime, FixedOffset, Utc};
 use minidom::{IntoAttributeValue, Node};
 
+/// Text codec for
+/// [XEP-0082](https://xmpp.org/extensions/xep-0082.html)-compliant formatting
+/// of dates and times.
+pub struct Xep0082;
+
+impl TextCodec<ChronoDateTime<FixedOffset>> for Xep0082 {
+    fn decode(&self, s: String) -> Result<ChronoDateTime<FixedOffset>, Error> {
+        Ok(ChronoDateTime::parse_from_rfc3339(&s).map_err(Error::text_parse_error)?)
+    }
+
+    fn encode<'x>(
+        &self,
+        value: &'x ChronoDateTime<FixedOffset>,
+    ) -> Result<Option<Cow<'x, str>>, Error> {
+        if value.offset().utc_minus_local() == 0 {
+            Ok(Some(Cow::Owned(
+                value.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
+            )))
+        } else {
+            Ok(Some(Cow::Owned(value.to_rfc3339())))
+        }
+    }
+}
+
+impl TextCodec<ChronoDateTime<Utc>> for Xep0082 {
+    fn decode(&self, s: String) -> Result<ChronoDateTime<Utc>, Error> {
+        Ok(ChronoDateTime::<FixedOffset>::parse_from_rfc3339(&s)
+            .map_err(Error::text_parse_error)?
+            .into())
+    }
+
+    fn encode<'x>(&self, value: &'x ChronoDateTime<Utc>) -> Result<Option<Cow<'x, str>>, Error> {
+        Ok(Some(Cow::Owned(
+            value.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
+        )))
+    }
+}
+
+impl<T> TextCodec<Option<T>> for Xep0082
+where
+    Xep0082: TextCodec<T>,
+{
+    fn decode(&self, s: String) -> Result<Option<T>, Error> {
+        Ok(Some(self.decode(s)?))
+    }
+
+    fn encode<'x>(&self, value: &'x Option<T>) -> Result<Option<Cow<'x, str>>, Error> {
+        value
+            .as_ref()
+            .and_then(|x| self.encode(x).transpose())
+            .transpose()
+    }
+}
+
 /// 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.

parsers/src/time.rs 🔗

@@ -4,15 +4,40 @@
 // 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 alloc::borrow::Cow;
+
 use xso::{AsXml, FromXml};
 
-use crate::date::DateTime;
+use crate::date::Xep0082;
 use crate::iq::{IqGetPayload, IqResultPayload};
 use crate::ns;
-use chrono::FixedOffset;
+use chrono::{DateTime, FixedOffset, Utc};
 use core::str::FromStr;
-use minidom::Element;
-use xso::error::{Error, FromElementError};
+use xso::{error::Error, text::TextCodec};
+
+struct ColonSeparatedOffset;
+
+impl TextCodec<FixedOffset> for ColonSeparatedOffset {
+    fn decode(&self, s: String) -> Result<FixedOffset, Error> {
+        Ok(FixedOffset::from_str(&s).map_err(Error::text_parse_error)?)
+    }
+
+    fn encode<'x>(&self, value: &'x FixedOffset) -> Result<Option<Cow<'x, str>>, Error> {
+        let offset = value.local_minus_utc();
+        let nminutes = offset / 60;
+        let nseconds = offset % 60;
+        let nhours = nminutes / 60;
+        let nminutes = nminutes % 60;
+        if nseconds == 0 {
+            Ok(Some(Cow::Owned(format!("{:+03}:{:02}", nhours, nminutes))))
+        } else {
+            Ok(Some(Cow::Owned(format!(
+                "{:+03}:{:02}:{:02}",
+                nhours, nminutes, nseconds
+            ))))
+        }
+    }
+}
 
 /// An entity time query.
 #[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
@@ -22,70 +47,49 @@ pub struct TimeQuery;
 impl IqGetPayload for TimeQuery {}
 
 /// An entity time result, containing an unique DateTime.
-#[derive(Debug, Clone)]
-pub struct TimeResult(pub DateTime);
+#[derive(Debug, Clone, Copy, FromXml, AsXml)]
+#[xml(namespace = ns::TIME, name = "time")]
+pub struct TimeResult {
+    /// The UTC offset
+    #[xml(extract(name = "tzo", fields(text(codec = ColonSeparatedOffset))))]
+    pub tz_offset: FixedOffset,
+
+    /// The UTC timestamp
+    #[xml(extract(name = "utc", fields(text(codec = Xep0082))))]
+    pub utc: DateTime<Utc>,
+}
 
 impl IqResultPayload for TimeResult {}
 
-impl TryFrom<Element> for TimeResult {
-    type Error = FromElementError;
-
-    fn try_from(elem: Element) -> Result<TimeResult, FromElementError> {
-        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::Other("More than one tzo element in time.").into());
-                }
-                check_no_children!(child, "tzo");
-                check_no_attributes!(child, "tzo");
-                // TODO: Add a FromStr implementation to FixedOffset to avoid this hack.
-                let fake_date = format!("{}{}", "2019-04-22T11:38:00", child.text());
-                let date_time = DateTime::from_str(&fake_date).map_err(Error::text_parse_error)?;
-                tzo = Some(date_time.timezone());
-            } else if child.is("utc", ns::TIME) {
-                if utc.is_some() {
-                    return Err(Error::Other("More than one utc element in time.").into());
-                }
-                check_no_children!(child, "utc");
-                check_no_attributes!(child, "utc");
-                let date_time =
-                    DateTime::from_str(&child.text()).map_err(Error::text_parse_error)?;
-                match FixedOffset::east_opt(0) {
-                    Some(tz) if date_time.timezone() == tz => (),
-                    _ => return Err(Error::Other("Non-UTC timezone for utc element.").into()),
-                }
-                utc = Some(date_time);
-            } else {
-                return Err(Error::Other("Unknown child in time element.").into());
-            }
-        }
+impl From<TimeResult> for DateTime<FixedOffset> {
+    fn from(time: TimeResult) -> Self {
+        time.utc.with_timezone(&time.tz_offset)
+    }
+}
 
-        let tzo = tzo.ok_or(Error::Other("Missing tzo child in time element."))?;
-        let utc = utc.ok_or(Error::Other("Missing utc child in time element."))?;
-        let date = utc.with_timezone(tzo);
+impl From<TimeResult> for DateTime<Utc> {
+    fn from(time: TimeResult) -> Self {
+        time.utc.into()
+    }
+}
 
-        Ok(TimeResult(date))
+impl From<DateTime<FixedOffset>> for TimeResult {
+    fn from(dt: DateTime<FixedOffset>) -> Self {
+        let tz_offset = *dt.offset();
+        let utc = dt.with_timezone(&Utc);
+        TimeResult {
+            tz_offset,
+            utc: utc.into(),
+        }
     }
 }
 
-impl From<TimeResult> for Element {
-    fn from(time: TimeResult) -> Element {
-        Element::builder("time", ns::TIME)
-            .append(Element::builder("tzo", ns::TIME).append(format!("{}", time.0.timezone())))
-            .append(
-                Element::builder("utc", ns::TIME).append(
-                    time.0
-                        .with_timezone(FixedOffset::east_opt(0).unwrap())
-                        .format("%FT%TZ"),
-                ),
-            )
-            .build()
+impl From<DateTime<Utc>> for TimeResult {
+    fn from(dt: DateTime<Utc>) -> Self {
+        TimeResult {
+            tz_offset: FixedOffset::east_opt(0).unwrap(),
+            utc: dt.into(),
+        }
     }
 }
 
@@ -93,6 +97,8 @@ impl From<TimeResult> for Element {
 mod tests {
     use super::*;
 
+    use minidom::Element;
+
     // DateTime’s size doesn’t depend on the architecture.
     #[test]
     fn test_size() {
@@ -108,10 +114,11 @@ mod tests {
                 .unwrap();
         let elem1 = elem.clone();
         let time = TimeResult::try_from(elem).unwrap();
-        assert_eq!(time.0.timezone(), FixedOffset::west_opt(6 * 3600).unwrap());
+        let dt = DateTime::<FixedOffset>::from(time);
+        assert_eq!(dt.timezone(), FixedOffset::west_opt(6 * 3600).unwrap());
         assert_eq!(
-            time.0,
-            DateTime::from_str("2006-12-19T12:58:35-05:00").unwrap()
+            dt,
+            DateTime::<FixedOffset>::from_str("2006-12-19T12:58:35-05:00").unwrap()
         );
         let elem2 = Element::from(time);
         assert_eq!(elem1, elem2);