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