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 alloc::borrow::Cow;
  8
  9use xso::{AsXml, FromXml};
 10
 11use crate::date::Xep0082;
 12use crate::iq::{IqGetPayload, IqResultPayload};
 13use crate::ns;
 14use chrono::{DateTime, FixedOffset, Utc};
 15use core::str::FromStr;
 16use xso::{error::Error, text::TextCodec};
 17
 18struct ColonSeparatedOffset;
 19
 20impl TextCodec<FixedOffset> for ColonSeparatedOffset {
 21    fn decode(&self, s: String) -> Result<FixedOffset, Error> {
 22        FixedOffset::from_str(&s).map_err(Error::text_parse_error)
 23    }
 24
 25    fn encode<'x>(&self, value: &'x FixedOffset) -> Result<Option<Cow<'x, str>>, Error> {
 26        let offset = value.local_minus_utc();
 27        let nminutes = offset / 60;
 28        let nseconds = offset % 60;
 29        let nhours = nminutes / 60;
 30        let nminutes = nminutes % 60;
 31        if nseconds == 0 {
 32            Ok(Some(Cow::Owned(format!("{:+03}:{:02}", nhours, nminutes))))
 33        } else {
 34            Ok(Some(Cow::Owned(format!(
 35                "{:+03}:{:02}:{:02}",
 36                nhours, nminutes, nseconds
 37            ))))
 38        }
 39    }
 40}
 41
 42/// An entity time query.
 43#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
 44#[xml(namespace = ns::TIME, name = "time")]
 45pub struct TimeQuery;
 46
 47impl IqGetPayload for TimeQuery {}
 48
 49/// An entity time result, containing an unique DateTime.
 50#[derive(Debug, Clone, Copy, FromXml, AsXml)]
 51#[xml(namespace = ns::TIME, name = "time")]
 52pub struct TimeResult {
 53    /// The UTC offset
 54    #[xml(extract(name = "tzo", fields(text(codec = ColonSeparatedOffset))))]
 55    pub tz_offset: FixedOffset,
 56
 57    /// The UTC timestamp
 58    #[xml(extract(name = "utc", fields(text(codec = Xep0082))))]
 59    pub utc: DateTime<Utc>,
 60}
 61
 62impl IqResultPayload for TimeResult {}
 63
 64impl From<TimeResult> for DateTime<FixedOffset> {
 65    fn from(time: TimeResult) -> Self {
 66        time.utc.with_timezone(&time.tz_offset)
 67    }
 68}
 69
 70impl From<TimeResult> for DateTime<Utc> {
 71    fn from(time: TimeResult) -> Self {
 72        time.utc
 73    }
 74}
 75
 76impl From<DateTime<FixedOffset>> for TimeResult {
 77    fn from(dt: DateTime<FixedOffset>) -> Self {
 78        let tz_offset = *dt.offset();
 79        let utc = dt.with_timezone(&Utc);
 80        TimeResult { tz_offset, utc }
 81    }
 82}
 83
 84impl From<DateTime<Utc>> for TimeResult {
 85    fn from(dt: DateTime<Utc>) -> Self {
 86        TimeResult {
 87            tz_offset: FixedOffset::east_opt(0).unwrap(),
 88            utc: dt,
 89        }
 90    }
 91}
 92
 93#[cfg(test)]
 94mod tests {
 95    use super::*;
 96
 97    use minidom::Element;
 98
 99    // DateTime’s size doesn’t depend on the architecture.
100    #[test]
101    fn test_size() {
102        assert_size!(TimeQuery, 0);
103        assert_size!(TimeResult, 16);
104    }
105
106    #[test]
107    fn parse_response() {
108        let elem: Element =
109            "<time xmlns='urn:xmpp:time'><tzo>-06:00</tzo><utc>2006-12-19T17:58:35Z</utc></time>"
110                .parse()
111                .unwrap();
112        let elem1 = elem.clone();
113        let time = TimeResult::try_from(elem).unwrap();
114        let dt = DateTime::<FixedOffset>::from(time);
115        assert_eq!(dt.timezone(), FixedOffset::west_opt(6 * 3600).unwrap());
116        assert_eq!(
117            dt,
118            DateTime::<FixedOffset>::from_str("2006-12-19T12:58:35-05:00").unwrap()
119        );
120        let elem2 = Element::from(time);
121        assert_eq!(elem1, elem2);
122    }
123}