Add a stanza error parser and serialiser.

Emmanuel Gil Peyrot created

Change summary

src/lib.rs          |   2 
src/ns.rs           |   2 
src/stanza_error.rs | 275 +++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 279 insertions(+)

Detailed changes

src/lib.rs 🔗

@@ -32,6 +32,8 @@ pub mod message;
 pub mod presence;
 /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
 pub mod iq;
+/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
+pub mod stanza_error;
 
 /// RFC 6121: Extensible Messaging and Presence Protocol (XMPP): Instant Messaging and Presence
 pub mod body;

src/ns.rs 🔗

@@ -6,6 +6,8 @@
 
 /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
 pub const JABBER_CLIENT: &'static str = "jabber:client";
+/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
+pub const XMPP_STANZAS: &'static str = "urn:ietf:params:xml:ns:xmpp-stanzas";
 
 /// XEP-0004: Data Forms
 pub const DATA_FORMS: &'static str = "jabber:x:data";

src/stanza_error.rs 🔗

@@ -0,0 +1,275 @@
+// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// 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 std::str::FromStr;
+use std::collections::BTreeMap;
+
+use minidom::Element;
+
+use error::Error;
+use jid::Jid;
+use ns;
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum ErrorType {
+    Auth,
+    Cancel,
+    Continue,
+    Modify,
+    Wait,
+}
+
+impl FromStr for ErrorType {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<ErrorType, Error> {
+        Ok(match s {
+            "auth" => ErrorType::Auth,
+            "cancel" => ErrorType::Cancel,
+            "continue" => ErrorType::Continue,
+            "modify" => ErrorType::Modify,
+            "wait" => ErrorType::Wait,
+
+            _ => return Err(Error::ParseError("Unknown error type.")),
+        })
+    }
+}
+
+impl From<ErrorType> for String {
+    fn from(type_: ErrorType) -> String {
+        String::from(match type_ {
+            ErrorType::Auth => "auth",
+            ErrorType::Cancel => "cancel",
+            ErrorType::Continue => "continue",
+            ErrorType::Modify => "modify",
+            ErrorType::Wait => "wait",
+        })
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum DefinedCondition {
+    BadRequest,
+    Conflict,
+    FeatureNotImplemented,
+    Forbidden,
+    Gone,
+    InternalServerError,
+    ItemNotFound,
+    JidMalformed,
+    NotAcceptable,
+    NotAllowed,
+    NotAuthorized,
+    PolicyViolation,
+    RecipientUnavailable,
+    Redirect,
+    RegistrationRequired,
+    RemoteServerNotFound,
+    RemoteServerTimeout,
+    ResourceConstraint,
+    ServiceUnavailable,
+    SubscriptionRequired,
+    UndefinedCondition,
+    UnexpectedRequest,
+}
+
+impl FromStr for DefinedCondition {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<DefinedCondition, Error> {
+        Ok(match s {
+            "bad-request" => DefinedCondition::BadRequest,
+            "conflict" => DefinedCondition::Conflict,
+            "feature-not-implemented" => DefinedCondition::FeatureNotImplemented,
+            "forbidden" => DefinedCondition::Forbidden,
+            "gone" => DefinedCondition::Gone,
+            "internal-server-error" => DefinedCondition::InternalServerError,
+            "item-not-found" => DefinedCondition::ItemNotFound,
+            "jid-malformed" => DefinedCondition::JidMalformed,
+            "not-acceptable" => DefinedCondition::NotAcceptable,
+            "not-allowed" => DefinedCondition::NotAllowed,
+            "not-authorized" => DefinedCondition::NotAuthorized,
+            "policy-violation" => DefinedCondition::PolicyViolation,
+            "recipient-unavailable" => DefinedCondition::RecipientUnavailable,
+            "redirect" => DefinedCondition::Redirect,
+            "registration-required" => DefinedCondition::RegistrationRequired,
+            "remote-server-not-found" => DefinedCondition::RemoteServerNotFound,
+            "remote-server-timeout" => DefinedCondition::RemoteServerTimeout,
+            "resource-constraint" => DefinedCondition::ResourceConstraint,
+            "service-unavailable" => DefinedCondition::ServiceUnavailable,
+            "subscription-required" => DefinedCondition::SubscriptionRequired,
+            "undefined-condition" => DefinedCondition::UndefinedCondition,
+            "unexpected-request" => DefinedCondition::UnexpectedRequest,
+
+            _ => return Err(Error::ParseError("Unknown defined-condition.")),
+        })
+    }
+}
+
+impl From<DefinedCondition> for String {
+    fn from(defined_condition: DefinedCondition) -> String {
+        String::from(match defined_condition {
+            DefinedCondition::BadRequest => "bad-request",
+            DefinedCondition::Conflict => "conflict",
+            DefinedCondition::FeatureNotImplemented => "feature-not-implemented",
+            DefinedCondition::Forbidden => "forbidden",
+            DefinedCondition::Gone => "gone",
+            DefinedCondition::InternalServerError => "internal-server-error",
+            DefinedCondition::ItemNotFound => "item-not-found",
+            DefinedCondition::JidMalformed => "jid-malformed",
+            DefinedCondition::NotAcceptable => "not-acceptable",
+            DefinedCondition::NotAllowed => "not-allowed",
+            DefinedCondition::NotAuthorized => "not-authorized",
+            DefinedCondition::PolicyViolation => "policy-violation",
+            DefinedCondition::RecipientUnavailable => "recipient-unavailable",
+            DefinedCondition::Redirect => "redirect",
+            DefinedCondition::RegistrationRequired => "registration-required",
+            DefinedCondition::RemoteServerNotFound => "remote-server-not-found",
+            DefinedCondition::RemoteServerTimeout => "remote-server-timeout",
+            DefinedCondition::ResourceConstraint => "resource-constraint",
+            DefinedCondition::ServiceUnavailable => "service-unavailable",
+            DefinedCondition::SubscriptionRequired => "subscription-required",
+            DefinedCondition::UndefinedCondition => "undefined-condition",
+            DefinedCondition::UnexpectedRequest => "unexpected-request",
+        })
+    }
+}
+
+pub type Lang = String;
+
+#[derive(Debug, Clone)]
+pub struct StanzaError {
+    pub type_: ErrorType,
+    pub by: Option<Jid>,
+    pub defined_condition: DefinedCondition,
+    pub texts: BTreeMap<Lang, String>,
+    pub other: Option<Element>,
+}
+
+pub fn parse_stanza_error(root: &Element) -> Result<StanzaError, Error> {
+    if !root.is("error", ns::JABBER_CLIENT) {
+        return Err(Error::ParseError("This is not an error element."));
+    }
+
+    let type_ = root.attr("type")
+                    .ok_or(Error::ParseError("Error must have a 'type' attribute."))?
+                    .parse()?;
+    let by = root.attr("by")
+                 .and_then(|by| by.parse().ok());
+    let mut defined_condition = None;
+    let mut texts = BTreeMap::new();
+    let mut other = None;
+
+    for child in root.children() {
+        if child.is("text", ns::XMPP_STANZAS) {
+            for _ in child.children() {
+                return Err(Error::ParseError("Unknown element in error text."));
+            }
+            let lang = child.attr("xml:lang").unwrap_or("").to_owned();
+            if let Some(_) = texts.insert(lang, child.text()) {
+                return Err(Error::ParseError("Text element present twice for the same xml:lang."));
+            }
+        } else if child.ns() == Some(ns::XMPP_STANZAS) {
+            if defined_condition.is_some() {
+                return Err(Error::ParseError("Error must not have more than one defined-condition."));
+            }
+            for _ in child.children() {
+                return Err(Error::ParseError("Unknown element in defined-condition."));
+            }
+            let condition = DefinedCondition::from_str(child.name())?;
+            defined_condition = Some(condition);
+        } else {
+            if other.is_some() {
+                return Err(Error::ParseError("Error must not have more than one other element."));
+            }
+            other = Some(child.clone());
+        }
+    }
+
+    if defined_condition.is_none() {
+        return Err(Error::ParseError("Error must have a defined-condition."));
+    }
+    let defined_condition = defined_condition.unwrap();
+
+    Ok(StanzaError {
+        type_: type_,
+        by: by,
+        defined_condition: defined_condition,
+        texts: texts,
+        other: other,
+    })
+}
+
+pub fn serialise(error: &StanzaError) -> Element {
+    let mut root = Element::builder("error")
+                           .ns(ns::JABBER_CLIENT)
+                           .attr("type", String::from(error.type_.clone()))
+                           .attr("by", match error.by {
+                                Some(ref by) => Some(String::from(by.clone())),
+                                None => None,
+                            })
+                           .append(Element::builder(error.defined_condition.clone())
+                                           .ns(ns::XMPP_STANZAS)
+                                           .build())
+                           .build();
+    for (lang, text) in error.texts.clone() {
+        let elem = Element::builder("text")
+                           .ns(ns::XMPP_STANZAS)
+                           .attr("xml:lang", lang)
+                           .append(text)
+                           .build();
+        root.append_child(elem);
+    }
+    if let Some(ref other) = error.other {
+        root.append_child(other.clone());
+    }
+    root
+}
+
+#[cfg(test)]
+mod tests {
+    use minidom::Element;
+    use error::Error;
+    use stanza_error;
+
+    #[test]
+    fn test_simple() {
+        let elem: Element = "<error xmlns='jabber:client' type='cancel'><undefined-condition xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error>".parse().unwrap();
+        let error = stanza_error::parse_stanza_error(&elem).unwrap();
+        assert_eq!(error.type_, stanza_error::ErrorType::Cancel);
+        assert_eq!(error.defined_condition, stanza_error::DefinedCondition::UndefinedCondition);
+    }
+
+    #[test]
+    fn test_invalid_type() {
+        let elem: Element = "<error xmlns='jabber:client'/>".parse().unwrap();
+        let error = stanza_error::parse_stanza_error(&elem).unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Error must have a 'type' attribute.");
+
+        let elem: Element = "<error xmlns='jabber:client' type='coucou'/>".parse().unwrap();
+        let error = stanza_error::parse_stanza_error(&elem).unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Unknown error type.");
+    }
+
+    #[test]
+    fn test_invalid_condition() {
+        let elem: Element = "<error xmlns='jabber:client' type='cancel'/>".parse().unwrap();
+        let error = stanza_error::parse_stanza_error(&elem).unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Error must have a defined-condition.");
+    }
+}