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:
Emmanuel Gil Peyrot 2024-12-15 21:38:09 +01:00 committed by xmpp ftw
commit 893a2f8e47

View file

@ -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 senders resource priority, if negative it wont receive messages
/// that havent 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 lets 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]