jingle: Simplify parsing and serialisation.

Emmanuel Gil Peyrot created

Change summary

src/jingle.rs | 298 +++++++++++++++++++++++++++-------------------------
1 file changed, 155 insertions(+), 143 deletions(-)

Detailed changes

src/jingle.rs 🔗

@@ -7,7 +7,8 @@
 use std::convert::TryFrom;
 use std::str::FromStr;
 
-use minidom::Element;
+use minidom::{Element, IntoElements, IntoAttributeValue, ElementEmitter};
+use jid::Jid;
 
 use error::Error;
 use ns;
@@ -57,9 +58,9 @@ impl FromStr for Action {
     }
 }
 
-impl From<Action> for String {
-    fn from(action: Action) -> String {
-        String::from(match action {
+impl IntoAttributeValue for Action {
+    fn into_attribute_value(self) -> Option<String> {
+        Some(String::from(match self {
             Action::ContentAccept => "content-accept",
             Action::ContentAdd => "content-add",
             Action::ContentModify => "content-modify",
@@ -75,13 +76,10 @@ impl From<Action> for String {
             Action::TransportInfo => "transport-info",
             Action::TransportReject => "transport-reject",
             Action::TransportReplace => "transport-replace",
-        })
+        }))
     }
 }
 
-// TODO: use a real JID type.
-type Jid = String;
-
 #[derive(Debug, Clone, PartialEq)]
 pub enum Creator {
     Initiator,
@@ -118,6 +116,12 @@ pub enum Senders {
     Responder,
 }
 
+impl Default for Senders {
+    fn default() -> Senders {
+        Senders::Both
+    }
+}
+
 impl FromStr for Senders {
     type Err = Error;
 
@@ -155,6 +159,66 @@ pub struct Content {
     pub security: Option<Element>,
 }
 
+impl TryFrom<Element> for Content {
+    type Error = Error;
+
+    fn try_from(elem: Element) -> Result<Content, Error> {
+        if !elem.is("content", ns::JINGLE) {
+            return Err(Error::ParseError("This is not a content element."));
+        }
+
+        let mut content = Content {
+            creator: get_attr!(elem, "creator", required),
+            disposition: get_attr!(elem, "disposition", optional).unwrap_or(String::from("session")),
+            name: get_attr!(elem, "name", required),
+            senders: get_attr!(elem, "senders", default),
+            description: None,
+            transport: None,
+            security: None,
+        };
+        for child in elem.children() {
+            if child.name() == "description" {
+                if content.description.is_some() {
+                    return Err(Error::ParseError("Content must not have more than one description."));
+                }
+                content.description = Some(child.clone());
+            } else if child.name() == "transport" {
+                if content.transport.is_some() {
+                    return Err(Error::ParseError("Content must not have more than one transport."));
+                }
+                content.transport = Some(child.clone());
+            } else if child.name() == "security" {
+                if content.security.is_some() {
+                    return Err(Error::ParseError("Content must not have more than one security."));
+                }
+                content.security = Some(child.clone());
+            }
+        }
+        Ok(content)
+    }
+}
+
+impl Into<Element> for Content {
+    fn into(self) -> Element {
+        Element::builder("content")
+                .ns(ns::JINGLE)
+                .attr("creator", String::from(self.creator))
+                .attr("disposition", self.disposition)
+                .attr("name", self.name)
+                .attr("senders", String::from(self.senders))
+                .append(self.description)
+                .append(self.transport)
+                .append(self.security)
+                .build()
+    }
+}
+
+impl IntoElements for Content {
+    fn into_elements(self, emitter: &mut ElementEmitter) {
+        emitter.append_child(self.into());
+    }
+}
+
 #[derive(Debug, Clone, PartialEq)]
 pub enum Reason {
     AlternativeSession, //(String),
@@ -234,6 +298,58 @@ pub struct ReasonElement {
     pub text: Option<String>,
 }
 
+impl TryFrom<Element> for ReasonElement {
+    type Error = Error;
+
+    fn try_from(elem: Element) -> Result<ReasonElement, Error> {
+        if !elem.is("reason", ns::JINGLE) {
+            return Err(Error::ParseError("This is not a reason element."));
+        }
+        let mut reason = None;
+        let mut text = None;
+        for child in elem.children() {
+            if child.ns() != Some(ns::JINGLE) {
+                return Err(Error::ParseError("Reason contains a foreign element."));
+            }
+            match child.name() {
+                "text" => {
+                    if text.is_some() {
+                        return Err(Error::ParseError("Reason must not have more than one text."));
+                    }
+                    text = Some(child.text());
+                },
+                name => {
+                    if reason.is_some() {
+                        return Err(Error::ParseError("Reason must not have more than one reason."));
+                    }
+                    reason = Some(name.parse()?);
+                },
+            }
+        }
+        let reason = reason.ok_or(Error::ParseError("Reason doesn’t contain a valid reason."))?;
+        Ok(ReasonElement {
+            reason: reason,
+            text: text,
+        })
+    }
+}
+
+impl Into<Element> for ReasonElement {
+    fn into(self) -> Element {
+        let reason: Element = self.reason.into();
+        Element::builder("reason")
+                .append(reason)
+                .append(self.text)
+                .build()
+    }
+}
+
+impl IntoElements for ReasonElement {
+    fn into_elements(self, emitter: &mut ElementEmitter) {
+        emitter.append_child(self.into());
+    }
+}
+
 #[derive(Debug, Clone)]
 pub struct Jingle {
     pub action: Action,
@@ -253,150 +369,46 @@ impl TryFrom<Element> for Jingle {
             return Err(Error::ParseError("This is not a Jingle element."));
         }
 
-        let mut contents: Vec<Content> = vec!();
-
-        let action = root.attr("action")
-                         .ok_or(Error::ParseError("Jingle must have an 'action' attribute."))?
-                         .parse()?;
-        let initiator = root.attr("initiator")
-                            .and_then(|initiator| initiator.parse().ok());
-        let responder = root.attr("responder")
-                            .and_then(|responder| responder.parse().ok());
-        let sid = root.attr("sid")
-                      .ok_or(Error::ParseError("Jingle must have a 'sid' attribute."))?;
-        let mut reason_element = None;
-        let mut other = vec!();
-
-        for child in root.children() {
+        let mut jingle = Jingle {
+            action: get_attr!(root, "action", required),
+            initiator: get_attr!(root, "initiator", optional),
+            responder: get_attr!(root, "responder", optional),
+            sid: get_attr!(root, "sid", required),
+            contents: vec!(),
+            reason: None,
+            other: vec!(),
+        };
+
+        for child in root.children().cloned() {
             if child.is("content", ns::JINGLE) {
-                let creator = child.attr("creator")
-                                   .ok_or(Error::ParseError("Content must have a 'creator' attribute."))?
-                                   .parse()?;
-                let disposition = child.attr("disposition")
-                                       .unwrap_or("session");
-                let name = child.attr("name")
-                                .ok_or(Error::ParseError("Content must have a 'name' attribute."))?;
-                let senders = child.attr("senders")
-                                   .unwrap_or("both")
-                                   .parse()?;
-                let mut description = None;
-                let mut transport = None;
-                let mut security = None;
-                for stuff in child.children() {
-                    if stuff.name() == "description" {
-                        if description.is_some() {
-                            return Err(Error::ParseError("Content must not have more than one description."));
-                        }
-                        description = Some(stuff.clone());
-                    } else if stuff.name() == "transport" {
-                        if transport.is_some() {
-                            return Err(Error::ParseError("Content must not have more than one transport."));
-                        }
-                        transport = Some(stuff.clone());
-                    } else if stuff.name() == "security" {
-                        if security.is_some() {
-                            return Err(Error::ParseError("Content must not have more than one security."));
-                        }
-                        security = Some(stuff.clone());
-                    }
-                }
-                contents.push(Content {
-                    creator: creator,
-                    disposition: disposition.to_owned(),
-                    name: name.to_owned(),
-                    senders: senders,
-                    description: description,
-                    transport: transport,
-                    security: security,
-                });
+                let content = Content::try_from(child.clone())?;
+                jingle.contents.push(content);
             } else if child.is("reason", ns::JINGLE) {
-                if reason_element.is_some() {
+                if jingle.reason.is_some() {
                     return Err(Error::ParseError("Jingle must not have more than one reason."));
                 }
-                let mut reason = None;
-                let mut text = None;
-                for stuff in child.children() {
-                    if stuff.ns() != Some(ns::JINGLE) {
-                        return Err(Error::ParseError("Reason contains a foreign element."));
-                    }
-                    let name = stuff.name();
-                    if name == "text" {
-                        if text.is_some() {
-                            return Err(Error::ParseError("Reason must not have more than one text."));
-                        }
-                        text = Some(stuff.text());
-                    } else {
-                        reason = Some(name.parse()?);
-                    }
-                }
-                if reason.is_none() {
-                    return Err(Error::ParseError("Reason doesn’t contain a valid reason."));
-                }
-                reason_element = Some(ReasonElement {
-                    reason: reason.unwrap(),
-                    text: text,
-                });
+                let reason = ReasonElement::try_from(child.clone())?;
+                jingle.reason = Some(reason);
             } else {
-                other.push(child.clone());
+                jingle.other.push(child.clone());
             }
         }
 
-        Ok(Jingle {
-            action: action,
-            initiator: initiator,
-            responder: responder,
-            sid: sid.to_owned(),
-            contents: contents,
-            reason: reason_element,
-            other: other,
-        })
-    }
-}
-
-impl Into<Element> for Content {
-    fn into(self) -> Element {
-        let mut root = Element::builder("content")
-                               .ns(ns::JINGLE)
-                               .attr("creator", String::from(self.creator.clone()))
-                               .attr("disposition", self.disposition.clone())
-                               .attr("name", self.name.clone())
-                               .attr("senders", String::from(self.senders.clone()))
-                               .build();
-        if let Some(description) = self.description.clone() {
-            root.append_child(description);
-        }
-        if let Some(transport) = self.transport.clone() {
-            root.append_child(transport);
-        }
-        if let Some(security) = self.security.clone() {
-            root.append_child(security);
-        }
-        root
+        Ok(jingle)
     }
 }
 
 impl Into<Element> for Jingle {
     fn into(self) -> Element {
-        let mut root = Element::builder("jingle")
-                               .ns(ns::JINGLE)
-                               .attr("action", String::from(self.action.clone()))
-                               .attr("initiator", self.initiator.clone())
-                               .attr("responder", self.responder.clone())
-                               .attr("sid", self.sid.clone())
-                               .build();
-        for content in self.contents {
-            let content_elem = content.into();
-            root.append_child(content_elem);
-        }
-        if let Some(reason) = self.reason {
-            let reason2: Element = reason.reason.into();
-            let reason_elem = Element::builder("reason")
-                                      .append(reason2)
-                                      .append(reason.text)
-                                      .build();
-            root.append_child(reason_elem);
-        }
-        root
+        Element::builder("jingle")
+                .ns(ns::JINGLE)
+                .attr("action", self.action)
+                .attr("initiator", match self.initiator { Some(initiator) => Some(String::from(initiator)), None => None })
+                .attr("responder", match self.responder { Some(responder) => Some(String::from(responder)), None => None })
+                .attr("sid", self.sid)
+                .append(self.contents)
+                .append(self.reason)
+                .build()
     }
 }
 
@@ -420,7 +432,7 @@ mod tests {
             Error::ParseError(string) => string,
             _ => panic!(),
         };
-        assert_eq!(message, "Jingle must have an 'action' attribute.");
+        assert_eq!(message, "Required attribute 'action' missing.");
 
         let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-info'/>".parse().unwrap();
         let error = Jingle::try_from(elem).unwrap_err();
@@ -428,7 +440,7 @@ mod tests {
             Error::ParseError(string) => string,
             _ => panic!(),
         };
-        assert_eq!(message, "Jingle must have a 'sid' attribute.");
+        assert_eq!(message, "Required attribute 'sid' missing.");
 
         let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='coucou' sid='coucou'/>".parse().unwrap();
         let error = Jingle::try_from(elem).unwrap_err();
@@ -465,7 +477,7 @@ mod tests {
             Error::ParseError(string) => string,
             _ => panic!(),
         };
-        assert_eq!(message, "Content must have a 'creator' attribute.");
+        assert_eq!(message, "Required attribute 'creator' missing.");
 
         let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator'/></jingle>".parse().unwrap();
         let error = Jingle::try_from(elem).unwrap_err();
@@ -473,7 +485,7 @@ mod tests {
             Error::ParseError(string) => string,
             _ => panic!(),
         };
-        assert_eq!(message, "Content must have a 'name' attribute.");
+        assert_eq!(message, "Required attribute 'name' missing.");
 
         let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='coucou' name='coucou'/></jingle>".parse().unwrap();
         let error = Jingle::try_from(elem).unwrap_err();