parsers: port time over to derive macros
This commit is contained in:
parent
d203a348cb
commit
7faa6dc15b
3 changed files with 129 additions and 65 deletions
|
|
@ -64,6 +64,9 @@ XXXX-YY-ZZ RELEASER <admin@example.com>
|
|||
- Add a lang property in Presence, to avoid parser errors.
|
||||
- pubsub::Owner is now a wrapper for the pubsub::owner::Paylolad enum,
|
||||
and all its direct children have been merged into this enum (!532)
|
||||
- time::TimeResult has been ported to use xso. Use From/Into to convert
|
||||
it to/from chrono::DateTime values. The numbered member `0` does not
|
||||
exist anymore (!551).
|
||||
* New parsers/serialisers:
|
||||
- Stream Features (RFC 6120) (!400)
|
||||
- Spam Reporting (XEP-0377) (!506)
|
||||
|
|
|
|||
|
|
@ -7,11 +7,65 @@
|
|||
use alloc::borrow::Cow;
|
||||
use core::str::FromStr;
|
||||
|
||||
use xso::{error::Error, AsXmlText, FromXmlText};
|
||||
use xso::{error::Error, AsXmlText, FromXmlText, TextCodec};
|
||||
|
||||
use chrono::{DateTime as ChronoDateTime, FixedOffset};
|
||||
use chrono::{DateTime as ChronoDateTime, FixedOffset, Utc};
|
||||
use minidom::{IntoAttributeValue, Node};
|
||||
|
||||
/// Text codec for
|
||||
/// [XEP-0082](https://xmpp.org/extensions/xep-0082.html)-compliant formatting
|
||||
/// of dates and times.
|
||||
pub struct Xep0082;
|
||||
|
||||
impl TextCodec<ChronoDateTime<FixedOffset>> for Xep0082 {
|
||||
fn decode(&self, s: String) -> Result<ChronoDateTime<FixedOffset>, Error> {
|
||||
Ok(ChronoDateTime::parse_from_rfc3339(&s).map_err(Error::text_parse_error)?)
|
||||
}
|
||||
|
||||
fn encode<'x>(
|
||||
&self,
|
||||
value: &'x ChronoDateTime<FixedOffset>,
|
||||
) -> Result<Option<Cow<'x, str>>, Error> {
|
||||
if value.offset().utc_minus_local() == 0 {
|
||||
Ok(Some(Cow::Owned(
|
||||
value.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
|
||||
)))
|
||||
} else {
|
||||
Ok(Some(Cow::Owned(value.to_rfc3339())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextCodec<ChronoDateTime<Utc>> for Xep0082 {
|
||||
fn decode(&self, s: String) -> Result<ChronoDateTime<Utc>, Error> {
|
||||
Ok(ChronoDateTime::<FixedOffset>::parse_from_rfc3339(&s)
|
||||
.map_err(Error::text_parse_error)?
|
||||
.into())
|
||||
}
|
||||
|
||||
fn encode<'x>(&self, value: &'x ChronoDateTime<Utc>) -> Result<Option<Cow<'x, str>>, Error> {
|
||||
Ok(Some(Cow::Owned(
|
||||
value.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> TextCodec<Option<T>> for Xep0082
|
||||
where
|
||||
Xep0082: TextCodec<T>,
|
||||
{
|
||||
fn decode(&self, s: String) -> Result<Option<T>, Error> {
|
||||
Ok(Some(self.decode(s)?))
|
||||
}
|
||||
|
||||
fn encode<'x>(&self, value: &'x Option<T>) -> Result<Option<Cow<'x, str>>, Error> {
|
||||
value
|
||||
.as_ref()
|
||||
.and_then(|x| self.encode(x).transpose())
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements the DateTime profile of XEP-0082, which represents a
|
||||
/// non-recurring moment in time, with an accuracy of seconds or fraction of
|
||||
/// seconds, and includes a timezone.
|
||||
|
|
|
|||
|
|
@ -4,15 +4,40 @@
|
|||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use alloc::borrow::Cow;
|
||||
|
||||
use xso::{AsXml, FromXml};
|
||||
|
||||
use crate::date::DateTime;
|
||||
use crate::date::Xep0082;
|
||||
use crate::iq::{IqGetPayload, IqResultPayload};
|
||||
use crate::ns;
|
||||
use chrono::FixedOffset;
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
use core::str::FromStr;
|
||||
use minidom::Element;
|
||||
use xso::error::{Error, FromElementError};
|
||||
use xso::{error::Error, text::TextCodec};
|
||||
|
||||
struct ColonSeparatedOffset;
|
||||
|
||||
impl TextCodec<FixedOffset> for ColonSeparatedOffset {
|
||||
fn decode(&self, s: String) -> Result<FixedOffset, Error> {
|
||||
Ok(FixedOffset::from_str(&s).map_err(Error::text_parse_error)?)
|
||||
}
|
||||
|
||||
fn encode<'x>(&self, value: &'x FixedOffset) -> Result<Option<Cow<'x, str>>, Error> {
|
||||
let offset = value.local_minus_utc();
|
||||
let nminutes = offset / 60;
|
||||
let nseconds = offset % 60;
|
||||
let nhours = nminutes / 60;
|
||||
let nminutes = nminutes % 60;
|
||||
if nseconds == 0 {
|
||||
Ok(Some(Cow::Owned(format!("{:+03}:{:02}", nhours, nminutes))))
|
||||
} else {
|
||||
Ok(Some(Cow::Owned(format!(
|
||||
"{:+03}:{:02}:{:02}",
|
||||
nhours, nminutes, nseconds
|
||||
))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An entity time query.
|
||||
#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
|
||||
|
|
@ -22,70 +47,49 @@ pub struct TimeQuery;
|
|||
impl IqGetPayload for TimeQuery {}
|
||||
|
||||
/// An entity time result, containing an unique DateTime.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TimeResult(pub DateTime);
|
||||
#[derive(Debug, Clone, Copy, FromXml, AsXml)]
|
||||
#[xml(namespace = ns::TIME, name = "time")]
|
||||
pub struct TimeResult {
|
||||
/// The UTC offset
|
||||
#[xml(extract(name = "tzo", fields(text(codec = ColonSeparatedOffset))))]
|
||||
pub tz_offset: FixedOffset,
|
||||
|
||||
/// The UTC timestamp
|
||||
#[xml(extract(name = "utc", fields(text(codec = Xep0082))))]
|
||||
pub utc: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl IqResultPayload for TimeResult {}
|
||||
|
||||
impl TryFrom<Element> for TimeResult {
|
||||
type Error = FromElementError;
|
||||
|
||||
fn try_from(elem: Element) -> Result<TimeResult, FromElementError> {
|
||||
check_self!(elem, "time", TIME);
|
||||
check_no_attributes!(elem, "time");
|
||||
|
||||
let mut tzo = None;
|
||||
let mut utc = None;
|
||||
|
||||
for child in elem.children() {
|
||||
if child.is("tzo", ns::TIME) {
|
||||
if tzo.is_some() {
|
||||
return Err(Error::Other("More than one tzo element in time.").into());
|
||||
}
|
||||
check_no_children!(child, "tzo");
|
||||
check_no_attributes!(child, "tzo");
|
||||
// TODO: Add a FromStr implementation to FixedOffset to avoid this hack.
|
||||
let fake_date = format!("{}{}", "2019-04-22T11:38:00", child.text());
|
||||
let date_time = DateTime::from_str(&fake_date).map_err(Error::text_parse_error)?;
|
||||
tzo = Some(date_time.timezone());
|
||||
} else if child.is("utc", ns::TIME) {
|
||||
if utc.is_some() {
|
||||
return Err(Error::Other("More than one utc element in time.").into());
|
||||
}
|
||||
check_no_children!(child, "utc");
|
||||
check_no_attributes!(child, "utc");
|
||||
let date_time =
|
||||
DateTime::from_str(&child.text()).map_err(Error::text_parse_error)?;
|
||||
match FixedOffset::east_opt(0) {
|
||||
Some(tz) if date_time.timezone() == tz => (),
|
||||
_ => return Err(Error::Other("Non-UTC timezone for utc element.").into()),
|
||||
}
|
||||
utc = Some(date_time);
|
||||
} else {
|
||||
return Err(Error::Other("Unknown child in time element.").into());
|
||||
}
|
||||
}
|
||||
|
||||
let tzo = tzo.ok_or(Error::Other("Missing tzo child in time element."))?;
|
||||
let utc = utc.ok_or(Error::Other("Missing utc child in time element."))?;
|
||||
let date = utc.with_timezone(tzo);
|
||||
|
||||
Ok(TimeResult(date))
|
||||
impl From<TimeResult> for DateTime<FixedOffset> {
|
||||
fn from(time: TimeResult) -> Self {
|
||||
time.utc.with_timezone(&time.tz_offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TimeResult> for Element {
|
||||
fn from(time: TimeResult) -> Element {
|
||||
Element::builder("time", ns::TIME)
|
||||
.append(Element::builder("tzo", ns::TIME).append(format!("{}", time.0.timezone())))
|
||||
.append(
|
||||
Element::builder("utc", ns::TIME).append(
|
||||
time.0
|
||||
.with_timezone(FixedOffset::east_opt(0).unwrap())
|
||||
.format("%FT%TZ"),
|
||||
),
|
||||
)
|
||||
.build()
|
||||
impl From<TimeResult> for DateTime<Utc> {
|
||||
fn from(time: TimeResult) -> Self {
|
||||
time.utc.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DateTime<FixedOffset>> for TimeResult {
|
||||
fn from(dt: DateTime<FixedOffset>) -> Self {
|
||||
let tz_offset = *dt.offset();
|
||||
let utc = dt.with_timezone(&Utc);
|
||||
TimeResult {
|
||||
tz_offset,
|
||||
utc: utc.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DateTime<Utc>> for TimeResult {
|
||||
fn from(dt: DateTime<Utc>) -> Self {
|
||||
TimeResult {
|
||||
tz_offset: FixedOffset::east_opt(0).unwrap(),
|
||||
utc: dt.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -93,6 +97,8 @@ impl From<TimeResult> for Element {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use minidom::Element;
|
||||
|
||||
// DateTime’s size doesn’t depend on the architecture.
|
||||
#[test]
|
||||
fn test_size() {
|
||||
|
|
@ -108,10 +114,11 @@ mod tests {
|
|||
.unwrap();
|
||||
let elem1 = elem.clone();
|
||||
let time = TimeResult::try_from(elem).unwrap();
|
||||
assert_eq!(time.0.timezone(), FixedOffset::west_opt(6 * 3600).unwrap());
|
||||
let dt = DateTime::<FixedOffset>::from(time);
|
||||
assert_eq!(dt.timezone(), FixedOffset::west_opt(6 * 3600).unwrap());
|
||||
assert_eq!(
|
||||
time.0,
|
||||
DateTime::from_str("2006-12-19T12:58:35-05:00").unwrap()
|
||||
dt,
|
||||
DateTime::<FixedOffset>::from_str("2006-12-19T12:58:35-05:00").unwrap()
|
||||
);
|
||||
let elem2 = Element::from(time);
|
||||
assert_eq!(elem1, elem2);
|
||||
|
|
|
|||
Loading…
Reference in a new issue