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.
|
- Add a lang property in Presence, to avoid parser errors.
|
||||||
- pubsub::Owner is now a wrapper for the pubsub::owner::Paylolad enum,
|
- pubsub::Owner is now a wrapper for the pubsub::owner::Paylolad enum,
|
||||||
and all its direct children have been merged into this enum (!532)
|
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:
|
* New parsers/serialisers:
|
||||||
- Stream Features (RFC 6120) (!400)
|
- Stream Features (RFC 6120) (!400)
|
||||||
- Spam Reporting (XEP-0377) (!506)
|
- Spam Reporting (XEP-0377) (!506)
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,65 @@
|
||||||
use alloc::borrow::Cow;
|
use alloc::borrow::Cow;
|
||||||
use core::str::FromStr;
|
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};
|
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
|
/// Implements the DateTime profile of XEP-0082, which represents a
|
||||||
/// non-recurring moment in time, with an accuracy of seconds or fraction of
|
/// non-recurring moment in time, with an accuracy of seconds or fraction of
|
||||||
/// seconds, and includes a timezone.
|
/// seconds, and includes a timezone.
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,40 @@
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
// 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/.
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use alloc::borrow::Cow;
|
||||||
|
|
||||||
use xso::{AsXml, FromXml};
|
use xso::{AsXml, FromXml};
|
||||||
|
|
||||||
use crate::date::DateTime;
|
use crate::date::Xep0082;
|
||||||
use crate::iq::{IqGetPayload, IqResultPayload};
|
use crate::iq::{IqGetPayload, IqResultPayload};
|
||||||
use crate::ns;
|
use crate::ns;
|
||||||
use chrono::FixedOffset;
|
use chrono::{DateTime, FixedOffset, Utc};
|
||||||
use core::str::FromStr;
|
use core::str::FromStr;
|
||||||
use minidom::Element;
|
use xso::{error::Error, text::TextCodec};
|
||||||
use xso::error::{Error, FromElementError};
|
|
||||||
|
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.
|
/// An entity time query.
|
||||||
#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
|
#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
|
||||||
|
|
@ -22,70 +47,49 @@ pub struct TimeQuery;
|
||||||
impl IqGetPayload for TimeQuery {}
|
impl IqGetPayload for TimeQuery {}
|
||||||
|
|
||||||
/// An entity time result, containing an unique DateTime.
|
/// An entity time result, containing an unique DateTime.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Copy, FromXml, AsXml)]
|
||||||
pub struct TimeResult(pub DateTime);
|
#[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 IqResultPayload for TimeResult {}
|
||||||
|
|
||||||
impl TryFrom<Element> for TimeResult {
|
impl From<TimeResult> for DateTime<FixedOffset> {
|
||||||
type Error = FromElementError;
|
fn from(time: TimeResult) -> Self {
|
||||||
|
time.utc.with_timezone(&time.tz_offset)
|
||||||
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 Element {
|
impl From<TimeResult> for DateTime<Utc> {
|
||||||
fn from(time: TimeResult) -> Element {
|
fn from(time: TimeResult) -> Self {
|
||||||
Element::builder("time", ns::TIME)
|
time.utc.into()
|
||||||
.append(Element::builder("tzo", ns::TIME).append(format!("{}", time.0.timezone())))
|
}
|
||||||
.append(
|
}
|
||||||
Element::builder("utc", ns::TIME).append(
|
|
||||||
time.0
|
impl From<DateTime<FixedOffset>> for TimeResult {
|
||||||
.with_timezone(FixedOffset::east_opt(0).unwrap())
|
fn from(dt: DateTime<FixedOffset>) -> Self {
|
||||||
.format("%FT%TZ"),
|
let tz_offset = *dt.offset();
|
||||||
),
|
let utc = dt.with_timezone(&Utc);
|
||||||
)
|
TimeResult {
|
||||||
.build()
|
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 {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use minidom::Element;
|
||||||
|
|
||||||
// DateTime’s size doesn’t depend on the architecture.
|
// DateTime’s size doesn’t depend on the architecture.
|
||||||
#[test]
|
#[test]
|
||||||
fn test_size() {
|
fn test_size() {
|
||||||
|
|
@ -108,10 +114,11 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let elem1 = elem.clone();
|
let elem1 = elem.clone();
|
||||||
let time = TimeResult::try_from(elem).unwrap();
|
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!(
|
assert_eq!(
|
||||||
time.0,
|
dt,
|
||||||
DateTime::from_str("2006-12-19T12:58:35-05:00").unwrap()
|
DateTime::<FixedOffset>::from_str("2006-12-19T12:58:35-05:00").unwrap()
|
||||||
);
|
);
|
||||||
let elem2 = Element::from(time);
|
let elem2 = Element::from(time);
|
||||||
assert_eq!(elem1, elem2);
|
assert_eq!(elem1, elem2);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue