1// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7use std::str::FromStr;
8
9use minidom::{IntoAttributeValue, IntoElements, ElementEmitter};
10use chrono::{DateTime as ChronoDateTime, FixedOffset};
11
12use error::Error;
13
14/// Implements the DateTime profile of XEP-0082, which represents a
15/// non-recurring moment in time, with an accuracy of seconds or fraction of
16/// seconds, and includes a timezone.
17#[derive(Debug, Clone, PartialEq)]
18pub struct DateTime(ChronoDateTime<FixedOffset>);
19
20impl FromStr for DateTime {
21 type Err = Error;
22
23 fn from_str(s: &str) -> Result<DateTime, Error> {
24 Ok(DateTime(ChronoDateTime::parse_from_rfc3339(s)?))
25 }
26}
27
28impl IntoAttributeValue for DateTime {
29 fn into_attribute_value(self) -> Option<String> {
30 Some(self.0.to_rfc3339())
31 }
32}
33
34impl IntoElements for DateTime {
35 fn into_elements(self, emitter: &mut ElementEmitter) {
36 emitter.append_text_node(self.0.to_rfc3339())
37 }
38}
39
40#[cfg(test)]
41mod tests {
42 use super::*;
43 use chrono::{Datelike, Timelike};
44 use std::error::Error as StdError;
45
46 #[test]
47 fn test_simple() {
48 let date: DateTime = "2002-09-10T23:08:25Z".parse().unwrap();
49 assert_eq!(date.0.year(), 2002);
50 assert_eq!(date.0.month(), 9);
51 assert_eq!(date.0.day(), 10);
52 assert_eq!(date.0.hour(), 23);
53 assert_eq!(date.0.minute(), 08);
54 assert_eq!(date.0.second(), 25);
55 assert_eq!(date.0.nanosecond(), 0);
56 assert_eq!(date.0.timezone(), FixedOffset::east(0));
57 }
58
59 #[test]
60 fn test_invalid_date() {
61 // There is no thirteenth month.
62 let error = DateTime::from_str("2017-13-01T12:23:34Z").unwrap_err();
63 let message = match error {
64 Error::ChronoParseError(string) => string,
65 _ => panic!(),
66 };
67 assert_eq!(message.description(), "input is out of range");
68
69 // Timezone ≥24:00 aren’t allowed.
70 let error = DateTime::from_str("2017-05-27T12:11:02+25:00").unwrap_err();
71 let message = match error {
72 Error::ChronoParseError(string) => string,
73 _ => panic!(),
74 };
75 assert_eq!(message.description(), "input is out of range");
76
77 // Timezone without the : separator aren’t allowed.
78 let error = DateTime::from_str("2017-05-27T12:11:02+0100").unwrap_err();
79 let message = match error {
80 Error::ChronoParseError(string) => string,
81 _ => panic!(),
82 };
83 assert_eq!(message.description(), "input contains invalid characters");
84
85 // No seconds, error message could be improved.
86 let error = DateTime::from_str("2017-05-27T12:11+01:00").unwrap_err();
87 let message = match error {
88 Error::ChronoParseError(string) => string,
89 _ => panic!(),
90 };
91 assert_eq!(message.description(), "input contains invalid characters");
92
93 // TODO: maybe we’ll want to support this one, as per XEP-0082 §4.
94 let error = DateTime::from_str("20170527T12:11:02+01:00").unwrap_err();
95 let message = match error {
96 Error::ChronoParseError(string) => string,
97 _ => panic!(),
98 };
99 assert_eq!(message.description(), "input contains invalid characters");
100
101 // No timezone.
102 let error = DateTime::from_str("2017-05-27T12:11:02").unwrap_err();
103 let message = match error {
104 Error::ChronoParseError(string) => string,
105 _ => panic!(),
106 };
107 assert_eq!(message.description(), "premature end of input");
108 }
109
110 #[test]
111 fn test_serialise() {
112 let date = DateTime(ChronoDateTime::parse_from_rfc3339("2017-05-21T20:19:55+01:00").unwrap());
113 let attr = date.into_attribute_value();
114 assert_eq!(attr, Some(String::from("2017-05-21T20:19:55+01:00")));
115 }
116}