From 5cdb2d77ab27b0b496a8506f2d7f19f451b909d1 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Tue, 31 Oct 2017 19:41:45 +0000 Subject: [PATCH] Split out DateTime parsing into its own module (implement XEP-0082). --- 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(-) create mode 100644 src/date.rs diff --git a/src/date.rs b/src/date.rs new file mode 100644 index 0000000000000000000000000000000000000000..a344eb58d97dc4ad297de5fd471811c8594fd2f9 --- /dev/null +++ b/src/date.rs @@ -0,0 +1,116 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// 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); + +impl FromStr for DateTime { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(DateTime(ChronoDateTime::parse_from_rfc3339(s)?)) + } +} + +impl IntoAttributeValue for DateTime { + fn into_attribute_value(self) -> Option { + 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"))); + } +} diff --git a/src/delay.rs b/src/delay.rs index 97c10ad76423132acb7d86782cccb0e4bd02f819..53462792680991e7575a084a596a8e23af2f91dd 100644 --- a/src/delay.rs +++ b/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, - pub stamp: DateTime, + pub stamp: DateTime, pub data: Option, } @@ -32,7 +32,7 @@ impl TryFrom 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 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 for Element { mod tests { use super::*; use std::str::FromStr; - use chrono::{Datelike, Timelike}; #[test] fn test_simple() { let elem: Element = "".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 = "".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 = "Reason".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(); diff --git a/src/idle.rs b/src/idle.rs index 51c203934056755d7a07bc771aea6837ced92d63..e4b09924580777f39746e0d25a743a24279fdfb0 100644 --- a/src/idle.rs +++ b/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, -} - -impl TryFrom for Idle { - type Err = Error; - - fn try_from(elem: Element) -> Result { - 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 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 = "".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); } diff --git a/src/jingle_ft.rs b/src/jingle_ft.rs index 15a21c67efa16c70df390cb3774b4d3d505a59de..fda8dbeffd0819f9150e00bb63c96c5269cc01a8 100644 --- a/src/jingle_ft.rs +++ b/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>, + pub date: Option, pub media_type: Option, pub name: Option, pub descs: BTreeMap, @@ -137,7 +137,7 @@ impl From 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); diff --git a/src/lib.rs b/src/lib.rs index 2a63302cf100c0adc4697ca0cace696d17ebc0b2..68302afe8f6a8ed199aa0cbe7af1a617d8335cc9 100644 --- a/src/lib.rs +++ b/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; diff --git a/src/pubsub/event.rs b/src/pubsub/event.rs index 74b59ae6cdf32baf92705de5a9f5796d5fd3f511..cbeb6ef759b519fd07b082a92f555b3b28838329 100644 --- a/src/pubsub/event.rs +++ b/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>, + expiry: Option, jid: Option, subid: Option, subscription: Option, @@ -256,7 +256,7 @@ impl From 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)