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
|
||||
// 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 jid::Jid;
|
||||
use minidom::{Element, IntoAttributeValue};
|
||||
use minidom::Element;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
use xso::error::{Error, FromElementError};
|
||||
|
||||
/// Should be implemented on every known payload of a `<presence/>`.
|
||||
pub trait PresencePayload: TryFrom<Element> + Into<Element> {}
|
||||
|
|
@ -32,11 +33,9 @@ pub enum Show {
|
|||
Xa,
|
||||
}
|
||||
|
||||
impl FromStr for Show {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Show, Error> {
|
||||
Ok(match s {
|
||||
impl FromXmlText for Show {
|
||||
fn from_xml_text(s: String) -> Result<Show, Error> {
|
||||
Ok(match s.as_ref() {
|
||||
"away" => Show::Away,
|
||||
"chat" => Show::Chat,
|
||||
"dnd" => Show::Dnd,
|
||||
|
|
@ -47,23 +46,26 @@ impl FromStr for Show {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Show> for Element {
|
||||
fn from(show: Show) -> Element {
|
||||
Element::builder("show", ns::DEFAULT_NS)
|
||||
.append(match show {
|
||||
Show::Away => "away",
|
||||
Show::Chat => "chat",
|
||||
Show::Dnd => "dnd",
|
||||
Show::Xa => "xa",
|
||||
})
|
||||
.build()
|
||||
impl AsXmlText for Show {
|
||||
fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
|
||||
Ok(Cow::Borrowed(match self {
|
||||
Show::Away => "away",
|
||||
Show::Chat => "chat",
|
||||
Show::Dnd => "dnd",
|
||||
Show::Xa => "xa",
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
type Lang = 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.
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
|
|
@ -100,11 +102,9 @@ pub enum Type {
|
|||
Unsubscribed,
|
||||
}
|
||||
|
||||
impl FromStr for Type {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Type, Error> {
|
||||
Ok(match s {
|
||||
impl FromXmlText for Type {
|
||||
fn from_xml_text(s: String) -> Result<Type, Error> {
|
||||
Ok(match s.as_ref() {
|
||||
"error" => Type::Error,
|
||||
"probe" => Type::Probe,
|
||||
"subscribe" => Type::Subscribe,
|
||||
|
|
@ -122,51 +122,60 @@ impl FromStr for Type {
|
|||
}
|
||||
}
|
||||
|
||||
impl IntoAttributeValue for Type {
|
||||
fn into_attribute_value(self) -> Option<String> {
|
||||
Some(
|
||||
match self {
|
||||
Type::None => return None,
|
||||
impl AsOptionalXmlText for Type {
|
||||
fn as_optional_xml_text(&self) -> Result<Option<Cow<'_, str>>, Error> {
|
||||
Ok(Some(Cow::Borrowed(match self {
|
||||
Type::None => return Ok(None),
|
||||
|
||||
Type::Error => "error",
|
||||
Type::Probe => "probe",
|
||||
Type::Subscribe => "subscribe",
|
||||
Type::Subscribed => "subscribed",
|
||||
Type::Unavailable => "unavailable",
|
||||
Type::Unsubscribe => "unsubscribe",
|
||||
Type::Unsubscribed => "unsubscribed",
|
||||
}
|
||||
.to_owned(),
|
||||
)
|
||||
Type::Error => "error",
|
||||
Type::Probe => "probe",
|
||||
Type::Subscribe => "subscribe",
|
||||
Type::Subscribed => "subscribed",
|
||||
Type::Unavailable => "unavailable",
|
||||
Type::Unsubscribe => "unsubscribe",
|
||||
Type::Unsubscribed => "unsubscribed",
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// The sender of this presence.
|
||||
#[xml(attribute(default))]
|
||||
pub from: Option<Jid>,
|
||||
|
||||
/// The recipient of this presence.
|
||||
#[xml(attribute(default))]
|
||||
pub to: Option<Jid>,
|
||||
|
||||
/// The identifier, unique on this stream, of this stanza.
|
||||
#[xml(attribute(default))]
|
||||
pub id: Option<String>,
|
||||
|
||||
/// The type of this presence stanza.
|
||||
#[xml(attribute(default))]
|
||||
pub type_: Type,
|
||||
|
||||
/// The availability of the sender of this presence.
|
||||
#[xml(extract(name = "show", default, fields(text(type_ = Show))))]
|
||||
pub show: Option<Show>,
|
||||
|
||||
/// 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>,
|
||||
|
||||
/// The sender’s resource priority, if negative it won’t receive messages
|
||||
/// that haven’t been directed to it.
|
||||
#[xml(child(default))]
|
||||
pub priority: Priority,
|
||||
|
||||
/// A list of payloads contained in this presence.
|
||||
#[xml(element(n = ..))]
|
||||
pub payloads: Vec<Element>,
|
||||
}
|
||||
|
||||
|
|
@ -180,7 +189,7 @@ impl Presence {
|
|||
type_,
|
||||
show: None,
|
||||
statuses: BTreeMap::new(),
|
||||
priority: 0i8,
|
||||
priority: Priority(0i8),
|
||||
payloads: vec![],
|
||||
}
|
||||
}
|
||||
|
|
@ -249,7 +258,7 @@ impl Presence {
|
|||
|
||||
/// Set the priority of this presence.
|
||||
pub fn with_priority(mut self, priority: i8) -> Presence {
|
||||
self.priority = priority;
|
||||
self.priority = Priority(priority);
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use jid::{BareJid, FullJid};
|
||||
use xso::error::{Error, FromElementError};
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
|
|
@ -429,14 +327,19 @@ mod tests {
|
|||
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]
|
||||
#[ignore]
|
||||
fn test_serialise() {
|
||||
#[cfg(not(feature = "component"))]
|
||||
let elem: Element = "<presence xmlns='jabber:client' type='unavailable'/>/>"
|
||||
let elem: Element = "<presence xmlns='jabber:client' type='unavailable'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
#[cfg(feature = "component")]
|
||||
let elem: Element = "<presence xmlns='jabber:component:accept' type='unavailable'/>/>"
|
||||
let elem: Element = "<presence xmlns='jabber:component:accept' type='unavailable'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let presence = Presence::new(Type::Unavailable);
|
||||
|
|
@ -556,7 +459,10 @@ mod tests {
|
|||
assert_eq!(presence.statuses["fr"], "Là!");
|
||||
}
|
||||
|
||||
// TODO: Enable that test again once xso supports rejecting multiple
|
||||
// identical xml:lang versions.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_invalid_multiple_statuses() {
|
||||
#[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();
|
||||
|
|
@ -586,7 +492,7 @@ mod tests {
|
|||
.unwrap();
|
||||
let presence = Presence::try_from(elem).unwrap();
|
||||
assert_eq!(presence.payloads.len(), 0);
|
||||
assert_eq!(presence.priority, -1i8);
|
||||
assert_eq!(presence.priority, Priority(-1i8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -644,7 +550,10 @@ mod tests {
|
|||
FromElementError::Invalid(Error::Other(string)) => string,
|
||||
_ => 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"))]
|
||||
|
|
@ -664,7 +573,10 @@ mod tests {
|
|||
FromElementError::Invalid(Error::Other(string)) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown attribute in status element.");
|
||||
assert_eq!(
|
||||
message,
|
||||
"Unknown attribute in extraction for field 'statuses' in Presence element."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue