diff --git a/parsers/ChangeLog b/parsers/ChangeLog index 00086acff45806ba7094a73184c30b22b04d0243..5b2bf09170280639944fa5386f8431dc70e46313 100644 --- a/parsers/ChangeLog +++ b/parsers/ChangeLog @@ -64,6 +64,9 @@ XXXX-YY-ZZ RELEASER - 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) diff --git a/parsers/src/date.rs b/parsers/src/date.rs index 4445adfaa28b9544e0bc0a014fc33dae6976305f..8a488ad67d303e29db75f7d10758202fa0e48978 100644 --- a/parsers/src/date.rs +++ b/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> for Xep0082 { + fn decode(&self, s: String) -> Result, Error> { + Ok(ChronoDateTime::parse_from_rfc3339(&s).map_err(Error::text_parse_error)?) + } + + fn encode<'x>( + &self, + value: &'x ChronoDateTime, + ) -> Result>, 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> for Xep0082 { + fn decode(&self, s: String) -> Result, Error> { + Ok(ChronoDateTime::::parse_from_rfc3339(&s) + .map_err(Error::text_parse_error)? + .into()) + } + + fn encode<'x>(&self, value: &'x ChronoDateTime) -> Result>, Error> { + Ok(Some(Cow::Owned( + value.format("%Y-%m-%dT%H:%M:%SZ").to_string(), + ))) + } +} + +impl TextCodec> for Xep0082 +where + Xep0082: TextCodec, +{ + fn decode(&self, s: String) -> Result, Error> { + Ok(Some(self.decode(s)?)) + } + + fn encode<'x>(&self, value: &'x Option) -> Result>, 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. diff --git a/parsers/src/time.rs b/parsers/src/time.rs index 31c0b1ca99750ccb7fcff107c663005013629330..73a359805c88ffdbf119fcc831589e2d4d9d6226 100644 --- a/parsers/src/time.rs +++ b/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 for ColonSeparatedOffset { + fn decode(&self, s: String) -> Result { + Ok(FixedOffset::from_str(&s).map_err(Error::text_parse_error)?) + } + + fn encode<'x>(&self, value: &'x FixedOffset) -> Result>, 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, +} impl IqResultPayload for TimeResult {} -impl TryFrom for TimeResult { - type Error = FromElementError; - - fn try_from(elem: Element) -> Result { - 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 for DateTime { + 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 for DateTime { + fn from(time: TimeResult) -> Self { + time.utc.into() + } +} - Ok(TimeResult(date)) +impl From> for TimeResult { + fn from(dt: DateTime) -> Self { + let tz_offset = *dt.offset(); + let utc = dt.with_timezone(&Utc); + TimeResult { + tz_offset, + utc: utc.into(), + } } } -impl From 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> for TimeResult { + fn from(dt: DateTime) -> Self { + TimeResult { + tz_offset: FixedOffset::east_opt(0).unwrap(), + utc: dt.into(), + } } } @@ -93,6 +97,8 @@ impl From 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::::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::::from_str("2006-12-19T12:58:35-05:00").unwrap() ); let elem2 = Element::from(time); assert_eq!(elem1, elem2);