time.rs

  1// Copyright (c) 2019 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 xso::{FromXml, IntoXml};
  8
  9use crate::date::DateTime;
 10use crate::iq::{IqGetPayload, IqResultPayload};
 11use crate::ns;
 12use crate::Element;
 13use chrono::FixedOffset;
 14use std::str::FromStr;
 15use xso::error::{Error, FromElementError};
 16
 17/// An entity time query.
 18#[derive(FromXml, IntoXml, PartialEq, Debug, Clone)]
 19#[xml(namespace = ns::TIME, name = "time")]
 20pub struct TimeQuery;
 21
 22impl IqGetPayload for TimeQuery {}
 23
 24/// An entity time result, containing an unique DateTime.
 25#[derive(Debug, Clone)]
 26pub struct TimeResult(pub DateTime);
 27
 28impl IqResultPayload for TimeResult {}
 29
 30impl TryFrom<Element> for TimeResult {
 31    type Error = FromElementError;
 32
 33    fn try_from(elem: Element) -> Result<TimeResult, FromElementError> {
 34        check_self!(elem, "time", TIME);
 35        check_no_attributes!(elem, "time");
 36
 37        let mut tzo = None;
 38        let mut utc = None;
 39
 40        for child in elem.children() {
 41            if child.is("tzo", ns::TIME) {
 42                if tzo.is_some() {
 43                    return Err(Error::Other("More than one tzo element in time.").into());
 44                }
 45                check_no_children!(child, "tzo");
 46                check_no_attributes!(child, "tzo");
 47                // TODO: Add a FromStr implementation to FixedOffset to avoid this hack.
 48                let fake_date = format!("{}{}", "2019-04-22T11:38:00", child.text());
 49                let date_time = DateTime::from_str(&fake_date).map_err(Error::text_parse_error)?;
 50                tzo = Some(date_time.timezone());
 51            } else if child.is("utc", ns::TIME) {
 52                if utc.is_some() {
 53                    return Err(Error::Other("More than one utc element in time.").into());
 54                }
 55                check_no_children!(child, "utc");
 56                check_no_attributes!(child, "utc");
 57                let date_time =
 58                    DateTime::from_str(&child.text()).map_err(Error::text_parse_error)?;
 59                match FixedOffset::east_opt(0) {
 60                    Some(tz) if date_time.timezone() == tz => (),
 61                    _ => return Err(Error::Other("Non-UTC timezone for utc element.").into()),
 62                }
 63                utc = Some(date_time);
 64            } else {
 65                return Err(Error::Other("Unknown child in time element.").into());
 66            }
 67        }
 68
 69        let tzo = tzo.ok_or(Error::Other("Missing tzo child in time element."))?;
 70        let utc = utc.ok_or(Error::Other("Missing utc child in time element."))?;
 71        let date = utc.with_timezone(tzo);
 72
 73        Ok(TimeResult(date))
 74    }
 75}
 76
 77impl From<TimeResult> for Element {
 78    fn from(time: TimeResult) -> Element {
 79        Element::builder("time", ns::TIME)
 80            .append(Element::builder("tzo", ns::TIME).append(format!("{}", time.0.timezone())))
 81            .append(
 82                Element::builder("utc", ns::TIME).append(
 83                    time.0
 84                        .with_timezone(FixedOffset::east_opt(0).unwrap())
 85                        .format("%FT%TZ"),
 86                ),
 87            )
 88            .build()
 89    }
 90}
 91
 92#[cfg(test)]
 93mod tests {
 94    use super::*;
 95
 96    // DateTime’s size doesn’t depend on the architecture.
 97    #[test]
 98    fn test_size() {
 99        assert_size!(TimeQuery, 0);
100        assert_size!(TimeResult, 16);
101    }
102
103    #[test]
104    fn parse_response() {
105        let elem: Element =
106            "<time xmlns='urn:xmpp:time'><tzo>-06:00</tzo><utc>2006-12-19T17:58:35Z</utc></time>"
107                .parse()
108                .unwrap();
109        let elem1 = elem.clone();
110        let time = TimeResult::try_from(elem).unwrap();
111        assert_eq!(time.0.timezone(), FixedOffset::west_opt(6 * 3600).unwrap());
112        assert_eq!(
113            time.0,
114            DateTime::from_str("2006-12-19T12:58:35-05:00").unwrap()
115        );
116        let elem2 = Element::from(time);
117        assert_eq!(elem1, elem2);
118    }
119}