diff --git a/parsers/src/jingle.rs b/parsers/src/jingle.rs index 36bd2f618057ef1091a513cb10cccb5e345dd5a9..8ce033c0569f2a64ecec908077d34a8e1be9b561 100644 --- a/parsers/src/jingle.rs +++ b/parsers/src/jingle.rs @@ -17,8 +17,7 @@ use jid::Jid; use minidom::Element; use std::collections::BTreeMap; use std::fmt; -use std::str::FromStr; -use xso::error::{Error, FromElementError}; +use xso::error::Error; generate_attribute!( /// The action attribute. @@ -376,134 +375,106 @@ impl Content { } /// Lists the possible reasons to be included in a Jingle iq. -#[derive(Debug, Clone, PartialEq)] +#[derive(FromXml, AsXml, Debug, Clone, PartialEq)] +#[xml(namespace = ns::JINGLE)] pub enum Reason { /// The party prefers to use an existing session with the peer rather than /// initiate a new session; the Jingle session ID of the alternative /// session SHOULD be provided as the XML character data of the \ /// child. - AlternativeSession, //(String), + #[xml(name = "alternative-session")] + AlternativeSession { + /// Session ID of the alternative session. + #[xml(extract(namespace = ns::JINGLE, name = "sid", default, fields(text(type_ = String))))] + sid: Option, + }, /// The party is busy and cannot accept a session. + #[xml(name = "busy")] Busy, /// The initiator wishes to formally cancel the session initiation request. + #[xml(name = "cancel")] Cancel, /// The action is related to connectivity problems. + #[xml(name = "connectivity-error")] ConnectivityError, /// The party wishes to formally decline the session. + #[xml(name = "decline")] Decline, /// The session length has exceeded a pre-defined time limit (e.g., a /// meeting hosted at a conference service). + #[xml(name = "expired")] Expired, /// The party has been unable to initialize processing related to the /// application type. + #[xml(name = "failed-application")] FailedApplication, /// The party has been unable to establish connectivity for the transport /// method. + #[xml(name = "failed-transport")] FailedTransport, /// The action is related to a non-specific application error. + #[xml(name = "general-error")] GeneralError, /// The entity is going offline or is no longer available. + #[xml(name = "gone")] Gone, /// The party supports the offered application type but does not support /// the offered or negotiated parameters. + #[xml(name = "incompatible-parameters")] IncompatibleParameters, /// The action is related to media processing problems. + #[xml(name = "media-error")] MediaError, /// The action is related to a violation of local security policies. + #[xml(name = "security-error")] SecurityError, /// The action is generated during the normal course of state management /// and does not reflect any error. + #[xml(name = "success")] Success, /// A request has not been answered so the sender is timing out the /// request. + #[xml(name = "timeout")] Timeout, /// The party supports none of the offered application types. + #[xml(name = "unsupported-applications")] UnsupportedApplications, /// The party supports none of the offered transport methods. + #[xml(name = "unsupported-transports")] UnsupportedTransports, } -impl FromStr for Reason { - type Err = Error; - - fn from_str(s: &str) -> Result { - Ok(match s { - "alternative-session" => Reason::AlternativeSession, - "busy" => Reason::Busy, - "cancel" => Reason::Cancel, - "connectivity-error" => Reason::ConnectivityError, - "decline" => Reason::Decline, - "expired" => Reason::Expired, - "failed-application" => Reason::FailedApplication, - "failed-transport" => Reason::FailedTransport, - "general-error" => Reason::GeneralError, - "gone" => Reason::Gone, - "incompatible-parameters" => Reason::IncompatibleParameters, - "media-error" => Reason::MediaError, - "security-error" => Reason::SecurityError, - "success" => Reason::Success, - "timeout" => Reason::Timeout, - "unsupported-applications" => Reason::UnsupportedApplications, - "unsupported-transports" => Reason::UnsupportedTransports, - - _ => return Err(Error::Other("Unknown reason.")), - }) - } -} - -impl From for Element { - fn from(reason: Reason) -> Element { - Element::builder( - match reason { - Reason::AlternativeSession => "alternative-session", - Reason::Busy => "busy", - Reason::Cancel => "cancel", - Reason::ConnectivityError => "connectivity-error", - Reason::Decline => "decline", - Reason::Expired => "expired", - Reason::FailedApplication => "failed-application", - Reason::FailedTransport => "failed-transport", - Reason::GeneralError => "general-error", - Reason::Gone => "gone", - Reason::IncompatibleParameters => "incompatible-parameters", - Reason::MediaError => "media-error", - Reason::SecurityError => "security-error", - Reason::Success => "success", - Reason::Timeout => "timeout", - Reason::UnsupportedApplications => "unsupported-applications", - Reason::UnsupportedTransports => "unsupported-transports", - }, - ns::JINGLE, - ) - .build() - } -} - type Lang = String; /// Informs the recipient of something. -#[derive(Debug, Clone, PartialEq)] +#[derive(FromXml, AsXml, Debug, Clone, PartialEq)] +#[xml(namespace = ns::JINGLE, name = "reason")] pub struct ReasonElement { /// The list of possible reasons to be included in a Jingle iq. + #[xml(child)] pub reason: Reason, /// A human-readable description of this reason. + #[xml(extract(n = .., namespace = ns::JINGLE, name = "text", fields( + attribute(type_ = String, name = "xml:lang", default), + text(type_ = String), + )))] pub texts: BTreeMap, } @@ -519,75 +490,6 @@ impl fmt::Display for ReasonElement { } } -impl TryFrom for ReasonElement { - type Error = FromElementError; - - fn try_from(elem: Element) -> Result { - check_self!(elem, "reason", JINGLE); - check_no_attributes!(elem, "reason"); - let mut reason = None; - let mut texts = BTreeMap::new(); - for child in elem.children() { - if child.is("text", ns::JINGLE) { - check_no_children!(child, "text"); - check_no_unknown_attributes!(child, "text", ["xml:lang"]); - let lang = get_attr!(elem, "xml:lang", Default); - if texts.insert(lang, child.text()).is_some() { - return Err( - Error::Other("Text element present twice for the same xml:lang.").into(), - ); - } - } else if child.has_ns(ns::JINGLE) { - if reason.is_some() { - return Err(Error::Other("Reason must not have more than one reason.").into()); - } - check_no_children!(child, "reason"); - check_no_attributes!(child, "reason"); - reason = Some(child.name().parse()?); - } else { - return Err(Error::Other("Reason contains a foreign element.").into()); - } - } - let reason = reason.ok_or(Error::Other("Reason doesn’t contain a valid reason."))?; - Ok(ReasonElement { reason, texts }) - } -} - -impl From for Element { - fn from(reason: ReasonElement) -> Element { - Element::builder("reason", ns::JINGLE) - .append(Element::from(reason.reason)) - .append_all(reason.texts.into_iter().map(|(lang, text)| { - Element::builder("text", ns::JINGLE) - .attr("xml:lang", lang) - .append(text) - })) - .build() - } -} - -impl ::xso::FromXml for ReasonElement { - type Builder = ::xso::minidom_compat::FromEventsViaElement; - - fn from_events( - qname: ::xso::exports::rxml::QName, - attrs: ::xso::exports::rxml::AttrMap, - ) -> Result { - if qname.0 != crate::ns::JINGLE || qname.1 != "reason" { - return Err(::xso::error::FromEventsError::Mismatch { name: qname, attrs }); - } - Self::Builder::new(qname, attrs) - } -} - -impl ::xso::AsXml for ReasonElement { - type ItemIter<'x> = ::xso::minidom_compat::AsItemsViaElement<'x>; - - fn as_xml_iter(&self) -> Result, ::xso::error::Error> { - ::xso::minidom_compat::AsItemsViaElement::new(self.clone()) - } -} - generate_id!( /// Unique identifier for a session between two JIDs. SessionId @@ -681,6 +583,7 @@ impl Jingle { #[cfg(test)] mod tests { use super::*; + use xso::error::FromElementError; #[cfg(target_pointer_width = "32")] #[test] @@ -691,10 +594,10 @@ mod tests { assert_size!(Disposition, 1); assert_size!(ContentId, 12); assert_size!(Content, 156); - assert_size!(Reason, 1); - assert_size!(ReasonElement, 16); + assert_size!(Reason, 12); + assert_size!(ReasonElement, 24); assert_size!(SessionId, 12); - assert_size!(Jingle, 104); + assert_size!(Jingle, 112); } #[cfg(target_pointer_width = "64")] @@ -706,10 +609,10 @@ mod tests { assert_size!(Disposition, 1); assert_size!(ContentId, 24); assert_size!(Content, 312); - assert_size!(Reason, 1); - assert_size!(ReasonElement, 32); + assert_size!(Reason, 24); + assert_size!(ReasonElement, 48); assert_size!(SessionId, 24); - assert_size!(Jingle, 208); + assert_size!(Jingle, 224); } #[test] @@ -859,7 +762,10 @@ mod tests { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; - assert_eq!(message, "Reason doesn’t contain a valid reason."); + assert_eq!( + message, + "Missing child field 'reason' in ReasonElement element." + ); let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); @@ -867,7 +773,7 @@ mod tests { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; - assert_eq!(message, "Unknown reason."); + assert_eq!(message, "Unknown child in ReasonElement element."); let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); @@ -875,7 +781,7 @@ mod tests { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; - assert_eq!(message, "Reason contains a foreign element."); + assert_eq!(message, "Unknown child in ReasonElement element."); let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); @@ -888,6 +794,9 @@ mod tests { "Jingle element must not have more than one child in field 'reason'." ); + // TODO: Reenable this test once xso is able to validate that no more than one text is + // there for every xml:lang. + /* let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { @@ -895,6 +804,7 @@ mod tests { _ => panic!(), }; assert_eq!(message, "Text element present twice for the same xml:lang."); + */ } #[test]