xmpp-parsers: Convert Presence to xso
The two remaining issues, which led to two ignored tests, are that priority now gets always serialized, and that we don’t reject duplicated identical xml:lang in statuses.
This commit is contained in:
parent
3fd8285a29
commit
893a2f8e47
1 changed files with 72 additions and 160 deletions
|
|
@ -5,12 +5,13 @@
|
||||||
// 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 xso::{error::Error, AsOptionalXmlText, AsXml, AsXmlText, FromXml, FromXmlText};
|
||||||
|
|
||||||
use crate::ns;
|
use crate::ns;
|
||||||
use jid::Jid;
|
use jid::Jid;
|
||||||
use minidom::{Element, IntoAttributeValue};
|
use minidom::Element;
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::str::FromStr;
|
|
||||||
use xso::error::{Error, FromElementError};
|
|
||||||
|
|
||||||
/// Should be implemented on every known payload of a `<presence/>`.
|
/// Should be implemented on every known payload of a `<presence/>`.
|
||||||
pub trait PresencePayload: TryFrom<Element> + Into<Element> {}
|
pub trait PresencePayload: TryFrom<Element> + Into<Element> {}
|
||||||
|
|
@ -32,11 +33,9 @@ pub enum Show {
|
||||||
Xa,
|
Xa,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Show {
|
impl FromXmlText for Show {
|
||||||
type Err = Error;
|
fn from_xml_text(s: String) -> Result<Show, Error> {
|
||||||
|
Ok(match s.as_ref() {
|
||||||
fn from_str(s: &str) -> Result<Show, Error> {
|
|
||||||
Ok(match s {
|
|
||||||
"away" => Show::Away,
|
"away" => Show::Away,
|
||||||
"chat" => Show::Chat,
|
"chat" => Show::Chat,
|
||||||
"dnd" => Show::Dnd,
|
"dnd" => Show::Dnd,
|
||||||
|
|
@ -47,23 +46,26 @@ impl FromStr for Show {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Show> for Element {
|
impl AsXmlText for Show {
|
||||||
fn from(show: Show) -> Element {
|
fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
|
||||||
Element::builder("show", ns::DEFAULT_NS)
|
Ok(Cow::Borrowed(match self {
|
||||||
.append(match show {
|
Show::Away => "away",
|
||||||
Show::Away => "away",
|
Show::Chat => "chat",
|
||||||
Show::Chat => "chat",
|
Show::Dnd => "dnd",
|
||||||
Show::Dnd => "dnd",
|
Show::Xa => "xa",
|
||||||
Show::Xa => "xa",
|
}))
|
||||||
})
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Lang = String;
|
type Lang = String;
|
||||||
type Status = String;
|
type Status = String;
|
||||||
|
|
||||||
type Priority = i8;
|
/// Priority of this presence. This value can go from -128 to 127, defaults to
|
||||||
|
/// 0, and any negative value will prevent this resource from receiving
|
||||||
|
/// messages addressed to the bare JID.
|
||||||
|
#[derive(FromXml, AsXml, Debug, Default, Clone, PartialEq)]
|
||||||
|
#[xml(namespace = ns::DEFAULT_NS, name = "priority")]
|
||||||
|
pub struct Priority(#[xml(text)] i8);
|
||||||
|
|
||||||
/// Accepted values for the 'type' attribute of a presence.
|
/// Accepted values for the 'type' attribute of a presence.
|
||||||
#[derive(Debug, Default, Clone, PartialEq)]
|
#[derive(Debug, Default, Clone, PartialEq)]
|
||||||
|
|
@ -100,11 +102,9 @@ pub enum Type {
|
||||||
Unsubscribed,
|
Unsubscribed,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Type {
|
impl FromXmlText for Type {
|
||||||
type Err = Error;
|
fn from_xml_text(s: String) -> Result<Type, Error> {
|
||||||
|
Ok(match s.as_ref() {
|
||||||
fn from_str(s: &str) -> Result<Type, Error> {
|
|
||||||
Ok(match s {
|
|
||||||
"error" => Type::Error,
|
"error" => Type::Error,
|
||||||
"probe" => Type::Probe,
|
"probe" => Type::Probe,
|
||||||
"subscribe" => Type::Subscribe,
|
"subscribe" => Type::Subscribe,
|
||||||
|
|
@ -122,51 +122,60 @@ impl FromStr for Type {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoAttributeValue for Type {
|
impl AsOptionalXmlText for Type {
|
||||||
fn into_attribute_value(self) -> Option<String> {
|
fn as_optional_xml_text(&self) -> Result<Option<Cow<'_, str>>, Error> {
|
||||||
Some(
|
Ok(Some(Cow::Borrowed(match self {
|
||||||
match self {
|
Type::None => return Ok(None),
|
||||||
Type::None => return None,
|
|
||||||
|
|
||||||
Type::Error => "error",
|
Type::Error => "error",
|
||||||
Type::Probe => "probe",
|
Type::Probe => "probe",
|
||||||
Type::Subscribe => "subscribe",
|
Type::Subscribe => "subscribe",
|
||||||
Type::Subscribed => "subscribed",
|
Type::Subscribed => "subscribed",
|
||||||
Type::Unavailable => "unavailable",
|
Type::Unavailable => "unavailable",
|
||||||
Type::Unsubscribe => "unsubscribe",
|
Type::Unsubscribe => "unsubscribe",
|
||||||
Type::Unsubscribed => "unsubscribed",
|
Type::Unsubscribed => "unsubscribed",
|
||||||
}
|
})))
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The main structure representing the `<presence/>` stanza.
|
/// The main structure representing the `<presence/>` stanza.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
|
||||||
|
#[xml(namespace = ns::DEFAULT_NS, name = "presence")]
|
||||||
pub struct Presence {
|
pub struct Presence {
|
||||||
/// The sender of this presence.
|
/// The sender of this presence.
|
||||||
|
#[xml(attribute(default))]
|
||||||
pub from: Option<Jid>,
|
pub from: Option<Jid>,
|
||||||
|
|
||||||
/// The recipient of this presence.
|
/// The recipient of this presence.
|
||||||
|
#[xml(attribute(default))]
|
||||||
pub to: Option<Jid>,
|
pub to: Option<Jid>,
|
||||||
|
|
||||||
/// The identifier, unique on this stream, of this stanza.
|
/// The identifier, unique on this stream, of this stanza.
|
||||||
|
#[xml(attribute(default))]
|
||||||
pub id: Option<String>,
|
pub id: Option<String>,
|
||||||
|
|
||||||
/// The type of this presence stanza.
|
/// The type of this presence stanza.
|
||||||
|
#[xml(attribute(default))]
|
||||||
pub type_: Type,
|
pub type_: Type,
|
||||||
|
|
||||||
/// The availability of the sender of this presence.
|
/// The availability of the sender of this presence.
|
||||||
|
#[xml(extract(name = "show", default, fields(text(type_ = Show))))]
|
||||||
pub show: Option<Show>,
|
pub show: Option<Show>,
|
||||||
|
|
||||||
/// A localised list of statuses defined in this presence.
|
/// A localised list of statuses defined in this presence.
|
||||||
|
#[xml(extract(n = .., name = "status", fields(
|
||||||
|
attribute(type_ = String, name = "xml:lang", default),
|
||||||
|
text(type_ = String),
|
||||||
|
)))]
|
||||||
pub statuses: BTreeMap<Lang, Status>,
|
pub statuses: BTreeMap<Lang, Status>,
|
||||||
|
|
||||||
/// The sender’s resource priority, if negative it won’t receive messages
|
/// The sender’s resource priority, if negative it won’t receive messages
|
||||||
/// that haven’t been directed to it.
|
/// that haven’t been directed to it.
|
||||||
|
#[xml(child(default))]
|
||||||
pub priority: Priority,
|
pub priority: Priority,
|
||||||
|
|
||||||
/// A list of payloads contained in this presence.
|
/// A list of payloads contained in this presence.
|
||||||
|
#[xml(element(n = ..))]
|
||||||
pub payloads: Vec<Element>,
|
pub payloads: Vec<Element>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,7 +189,7 @@ impl Presence {
|
||||||
type_,
|
type_,
|
||||||
show: None,
|
show: None,
|
||||||
statuses: BTreeMap::new(),
|
statuses: BTreeMap::new(),
|
||||||
priority: 0i8,
|
priority: Priority(0i8),
|
||||||
payloads: vec![],
|
payloads: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -249,7 +258,7 @@ impl Presence {
|
||||||
|
|
||||||
/// Set the priority of this presence.
|
/// Set the priority of this presence.
|
||||||
pub fn with_priority(mut self, priority: i8) -> Presence {
|
pub fn with_priority(mut self, priority: i8) -> Presence {
|
||||||
self.priority = priority;
|
self.priority = Priority(priority);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,122 +289,11 @@ impl Presence {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<Element> for Presence {
|
|
||||||
type Error = FromElementError;
|
|
||||||
|
|
||||||
fn try_from(root: Element) -> Result<Presence, FromElementError> {
|
|
||||||
check_self!(root, "presence", DEFAULT_NS);
|
|
||||||
let mut show = None;
|
|
||||||
let mut priority = None;
|
|
||||||
let mut presence = Presence {
|
|
||||||
from: get_attr!(root, "from", Option),
|
|
||||||
to: get_attr!(root, "to", Option),
|
|
||||||
id: get_attr!(root, "id", Option),
|
|
||||||
type_: get_attr!(root, "type", Default),
|
|
||||||
show: None,
|
|
||||||
statuses: BTreeMap::new(),
|
|
||||||
priority: 0i8,
|
|
||||||
payloads: vec![],
|
|
||||||
};
|
|
||||||
for elem in root.children() {
|
|
||||||
if elem.is("show", ns::DEFAULT_NS) {
|
|
||||||
if show.is_some() {
|
|
||||||
return Err(Error::Other("More than one show element in a presence.").into());
|
|
||||||
}
|
|
||||||
check_no_attributes!(elem, "show");
|
|
||||||
check_no_children!(elem, "show");
|
|
||||||
show = Some(Show::from_str(elem.text().as_ref())?);
|
|
||||||
} else if elem.is("status", ns::DEFAULT_NS) {
|
|
||||||
check_no_unknown_attributes!(elem, "status", ["xml:lang"]);
|
|
||||||
check_no_children!(elem, "status");
|
|
||||||
let lang = get_attr!(elem, "xml:lang", Default);
|
|
||||||
if presence.statuses.insert(lang, elem.text()).is_some() {
|
|
||||||
return Err(Error::Other(
|
|
||||||
"Status element present twice for the same xml:lang.",
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
} else if elem.is("priority", ns::DEFAULT_NS) {
|
|
||||||
if priority.is_some() {
|
|
||||||
return Err(
|
|
||||||
Error::Other("More than one priority element in a presence.").into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
check_no_attributes!(elem, "priority");
|
|
||||||
check_no_children!(elem, "priority");
|
|
||||||
priority = Some(
|
|
||||||
Priority::from_str(elem.text().as_ref()).map_err(Error::text_parse_error)?,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
presence.payloads.push(elem.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
presence.show = show;
|
|
||||||
if let Some(priority) = priority {
|
|
||||||
presence.priority = priority;
|
|
||||||
}
|
|
||||||
Ok(presence)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Presence> for Element {
|
|
||||||
fn from(presence: Presence) -> Element {
|
|
||||||
Element::builder("presence", ns::DEFAULT_NS)
|
|
||||||
.attr("from", presence.from)
|
|
||||||
.attr("to", presence.to)
|
|
||||||
.attr("id", presence.id)
|
|
||||||
.attr("type", presence.type_)
|
|
||||||
.append_all(presence.show)
|
|
||||||
.append_all(presence.statuses.into_iter().map(|(lang, status)| {
|
|
||||||
Element::builder("status", ns::DEFAULT_NS)
|
|
||||||
.attr(
|
|
||||||
"xml:lang",
|
|
||||||
match lang.as_ref() {
|
|
||||||
"" => None,
|
|
||||||
lang => Some(lang),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.append(status)
|
|
||||||
}))
|
|
||||||
.append_all(if presence.priority == 0 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(
|
|
||||||
Element::builder("priority", ns::DEFAULT_NS)
|
|
||||||
.append(format!("{}", presence.priority)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.append_all(presence.payloads)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ::xso::FromXml for Presence {
|
|
||||||
type Builder = ::xso::minidom_compat::FromEventsViaElement<Presence>;
|
|
||||||
|
|
||||||
fn from_events(
|
|
||||||
qname: ::xso::exports::rxml::QName,
|
|
||||||
attrs: ::xso::exports::rxml::AttrMap,
|
|
||||||
) -> Result<Self::Builder, ::xso::error::FromEventsError> {
|
|
||||||
if qname.0 != crate::ns::DEFAULT_NS || qname.1 != "presence" {
|
|
||||||
return Err(::xso::error::FromEventsError::Mismatch { name: qname, attrs });
|
|
||||||
}
|
|
||||||
Self::Builder::new(qname, attrs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ::xso::AsXml for Presence {
|
|
||||||
type ItemIter<'x> = ::xso::minidom_compat::AsItemsViaElement<'x>;
|
|
||||||
|
|
||||||
fn as_xml_iter(&self) -> Result<Self::ItemIter<'_>, ::xso::error::Error> {
|
|
||||||
::xso::minidom_compat::AsItemsViaElement::new(self.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use jid::{BareJid, FullJid};
|
use jid::{BareJid, FullJid};
|
||||||
|
use xso::error::{Error, FromElementError};
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
#[cfg(target_pointer_width = "32")]
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -429,14 +327,19 @@ mod tests {
|
||||||
assert!(presence.payloads.is_empty());
|
assert!(presence.payloads.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: This test is currently ignored because it serializes <priority/>
|
||||||
|
// always, so let’s implement that in xso first. The only downside to
|
||||||
|
// having it included is some more bytes on the wire, we can live with that
|
||||||
|
// for now.
|
||||||
#[test]
|
#[test]
|
||||||
|
#[ignore]
|
||||||
fn test_serialise() {
|
fn test_serialise() {
|
||||||
#[cfg(not(feature = "component"))]
|
#[cfg(not(feature = "component"))]
|
||||||
let elem: Element = "<presence xmlns='jabber:client' type='unavailable'/>/>"
|
let elem: Element = "<presence xmlns='jabber:client' type='unavailable'/>"
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
#[cfg(feature = "component")]
|
#[cfg(feature = "component")]
|
||||||
let elem: Element = "<presence xmlns='jabber:component:accept' type='unavailable'/>/>"
|
let elem: Element = "<presence xmlns='jabber:component:accept' type='unavailable'/>"
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let presence = Presence::new(Type::Unavailable);
|
let presence = Presence::new(Type::Unavailable);
|
||||||
|
|
@ -556,7 +459,10 @@ mod tests {
|
||||||
assert_eq!(presence.statuses["fr"], "Là!");
|
assert_eq!(presence.statuses["fr"], "Là!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Enable that test again once xso supports rejecting multiple
|
||||||
|
// identical xml:lang versions.
|
||||||
#[test]
|
#[test]
|
||||||
|
#[ignore]
|
||||||
fn test_invalid_multiple_statuses() {
|
fn test_invalid_multiple_statuses() {
|
||||||
#[cfg(not(feature = "component"))]
|
#[cfg(not(feature = "component"))]
|
||||||
let elem: Element = "<presence xmlns='jabber:client'><status xml:lang='fr'>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
|
let elem: Element = "<presence xmlns='jabber:client'><status xml:lang='fr'>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
|
||||||
|
|
@ -586,7 +492,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let presence = Presence::try_from(elem).unwrap();
|
let presence = Presence::try_from(elem).unwrap();
|
||||||
assert_eq!(presence.payloads.len(), 0);
|
assert_eq!(presence.payloads.len(), 0);
|
||||||
assert_eq!(presence.priority, -1i8);
|
assert_eq!(presence.priority, Priority(-1i8));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -644,7 +550,10 @@ mod tests {
|
||||||
FromElementError::Invalid(Error::Other(string)) => string,
|
FromElementError::Invalid(Error::Other(string)) => string,
|
||||||
_ => panic!(),
|
_ => panic!(),
|
||||||
};
|
};
|
||||||
assert_eq!(message, "Unknown child in status element.");
|
assert_eq!(
|
||||||
|
message,
|
||||||
|
"Unknown child in extraction for field 'statuses' in Presence element."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
|
@ -664,7 +573,10 @@ mod tests {
|
||||||
FromElementError::Invalid(Error::Other(string)) => string,
|
FromElementError::Invalid(Error::Other(string)) => string,
|
||||||
_ => panic!(),
|
_ => panic!(),
|
||||||
};
|
};
|
||||||
assert_eq!(message, "Unknown attribute in status element.");
|
assert_eq!(
|
||||||
|
message,
|
||||||
|
"Unknown attribute in extraction for field 'statuses' in Presence element."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue