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_size() {
48 assert_size!(DateTime, 16);
49 }
50
51 #[test]
52 fn test_simple() {
53 let date: DateTime = "2002-09-10T23:08:25Z".parse().unwrap();
54 assert_eq!(date.0.year(), 2002);
55 assert_eq!(date.0.month(), 9);
56 assert_eq!(date.0.day(), 10);
57 assert_eq!(date.0.hour(), 23);
58 assert_eq!(date.0.minute(), 08);
59 assert_eq!(date.0.second(), 25);
60 assert_eq!(date.0.nanosecond(), 0);
61 assert_eq!(date.0.timezone(), FixedOffset::east(0));
62 }
63
64 #[test]
65 fn test_invalid_date() {
66 // There is no thirteenth month.
67 let error = DateTime::from_str("2017-13-01T12:23:34Z").unwrap_err();
68 let message = match error {
69 Error::ChronoParseError(string) => string,
70 _ => panic!(),
71 };
72 assert_eq!(message.description(), "input is out of range");
73
74 // Timezone ≥24:00 aren’t allowed.
75 let error = DateTime::from_str("2017-05-27T12:11:02+25:00").unwrap_err();
76 let message = match error {
77 Error::ChronoParseError(string) => string,
78 _ => panic!(),
79 };
80 assert_eq!(message.description(), "input is out of range");
81
82 // Timezone without the : separator aren’t allowed.
83 let error = DateTime::from_str("2017-05-27T12:11:02+0100").unwrap_err();
84 let message = match error {
85 Error::ChronoParseError(string) => string,
86 _ => panic!(),
87 };
88 assert_eq!(message.description(), "input contains invalid characters");
89
90 // No seconds, error message could be improved.
91 let error = DateTime::from_str("2017-05-27T12:11+01:00").unwrap_err();
92 let message = match error {
93 Error::ChronoParseError(string) => string,
94 _ => panic!(),
95 };
96 assert_eq!(message.description(), "input contains invalid characters");
97
98 // TODO: maybe we’ll want to support this one, as per XEP-0082 §4.
99 let error = DateTime::from_str("20170527T12:11:02+01:00").unwrap_err();
100 let message = match error {
101 Error::ChronoParseError(string) => string,
102 _ => panic!(),
103 };
104 assert_eq!(message.description(), "input contains invalid characters");
105
106 // No timezone.
107 let error = DateTime::from_str("2017-05-27T12:11:02").unwrap_err();
108 let message = match error {
109 Error::ChronoParseError(string) => string,
110 _ => panic!(),
111 };
112 assert_eq!(message.description(), "premature end of input");
113 }
114
115 #[test]
116 fn test_serialise() {
117 let date = DateTime(ChronoDateTime::parse_from_rfc3339("2017-05-21T20:19:55+01:00").unwrap());
118 let attr = date.into_attribute_value();
119 assert_eq!(attr, Some(String::from("2017-05-21T20:19:55+01:00")));
120 }
121}