Add an IBR parser.

Emmanuel Gil Peyrot created

Change summary

src/ibr.rs | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/lib.rs |   3 
src/ns.rs  |   3 
3 files changed, 207 insertions(+)

Detailed changes

src/ibr.rs πŸ”—

@@ -0,0 +1,201 @@
+// 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::collections::HashMap;
+use std::convert::TryFrom;
+
+use minidom::{Element, IntoElements, ElementEmitter};
+
+use error::Error;
+
+use data_forms::DataForm;
+
+use ns;
+
+#[derive(Debug, Clone)]
+pub struct Query {
+    pub fields: HashMap<String, String>,
+    pub registered: bool,
+    pub remove: bool,
+    pub form: Option<DataForm>,
+    // Not yet implemented.
+    //pub oob: Option<Oob>,
+}
+
+impl TryFrom<Element> for Query {
+    type Error = Error;
+
+    fn try_from(elem: Element) -> Result<Query, Error> {
+        if !elem.is("query", ns::REGISTER) {
+            return Err(Error::ParseError("This is not an ibr element."));
+        }
+        let mut query = Query {
+            registered: false,
+            fields: HashMap::new(),
+            remove: false,
+            form: None,
+        };
+        for child in elem.children() {
+            let namespace = child.ns().unwrap();
+            if namespace == ns::REGISTER {
+                let name = child.name();
+                let fields = vec!["address", "city", "date", "email", "first", "instructions",
+                                  "key", "last", "misc", "name", "nick", "password", "phone",
+                                  "state", "text", "url", "username", "zip"];
+                if fields.binary_search(&name).is_ok() {
+                    query.fields.insert(name.to_owned(), child.text());
+                } else if name == "registered" {
+                    query.registered = true;
+                } else if name == "remove" {
+                    query.remove = true;
+                } else {
+                    return Err(Error::ParseError("Wrong field in ibr element."));
+                }
+            } else if child.is("x", ns::DATA_FORMS) {
+                query.form = Some(DataForm::try_from(child.clone())?);
+            } else {
+                return Err(Error::ParseError("Unknown child in ibr element."));
+            }
+        }
+        Ok(query)
+    }
+}
+
+impl Into<Element> for Query {
+    fn into(self) -> Element {
+        Element::builder("query")
+                .ns(ns::REGISTER)
+                .append(if self.registered { Some(Element::builder("registered").ns(ns::REGISTER)) } else { None })
+                .append(self.fields.iter().map(|(name, value)| {
+                     Element::builder(name.clone()).ns(ns::REGISTER).append(value.clone())
+                 }).collect::<Vec<_>>())
+                .append(if self.remove { Some(Element::builder("remove").ns(ns::REGISTER)) } else { None })
+                .append(self.form)
+                .build()
+    }
+}
+
+impl IntoElements for Query {
+    fn into_elements(self, emitter: &mut ElementEmitter) {
+        emitter.append_child(self.into());
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_simple() {
+        let elem: Element = "<query xmlns='jabber:iq:register'/>".parse().unwrap();
+        Query::try_from(elem).unwrap();
+    }
+
+    #[test]
+    fn test_ex2() {
+        let elem: Element = r#"
+<query xmlns='jabber:iq:register'>
+  <instructions>
+    Choose a username and password for use with this service.
+    Please also provide your email address.
+  </instructions>
+  <username/>
+  <password/>
+  <email/>
+</query>
+"#.parse().unwrap();
+        let query = Query::try_from(elem).unwrap();
+        assert_eq!(query.registered, false);
+        assert_eq!(query.fields["instructions"], "\n    Choose a username and password for use with this service.\n    Please also provide your email address.\n  ");
+        assert_eq!(query.fields["username"], "");
+        assert_eq!(query.fields["password"], "");
+        assert_eq!(query.fields["email"], "");
+        assert_eq!(query.fields.contains_key("name"), false);
+
+        // FIXME: HashMap doesn’t keep the order right.
+        //let elem2 = query.into();
+        //assert_eq!(elem, elem2);
+    }
+
+    #[test]
+    fn test_ex9() {
+        let elem: Element = r#"
+<query xmlns='jabber:iq:register'>
+  <instructions>
+    Use the enclosed form to register. If your Jabber client does not
+    support Data Forms, visit http://www.shakespeare.lit/contests.php
+  </instructions>
+  <x xmlns='jabber:x:data' type='form'>
+    <title>Contest Registration</title>
+    <instructions>
+      Please provide the following information
+      to sign up for our special contests!
+    </instructions>
+    <field type='hidden' var='FORM_TYPE'>
+      <value>jabber:iq:register</value>
+    </field>
+    <field label='Given Name' var='first'>
+      <required/>
+    </field>
+    <field label='Family Name' var='last'>
+      <required/>
+    </field>
+    <field label='Email Address' var='email'>
+      <required/>
+    </field>
+    <field type='list-single' label='Gender' var='x-gender'>
+      <option label='Male'><value>M</value></option>
+      <option label='Female'><value>F</value></option>
+    </field>
+  </x>
+</query>
+"#.parse().unwrap();
+        let elem1 = elem.clone();
+        let query = Query::try_from(elem).unwrap();
+        assert_eq!(query.registered, false);
+        assert!(!query.fields["instructions"].is_empty());
+        let form = query.form.clone().unwrap();
+        assert!(!form.instructions.unwrap().is_empty());
+        assert!(form.fields.binary_search_by(|field| field.var.cmp(&String::from("first"))).is_ok());
+        assert!(form.fields.binary_search_by(|field| field.var.cmp(&String::from("x-gender"))).is_ok());
+        assert!(form.fields.binary_search_by(|field| field.var.cmp(&String::from("coucou"))).is_err());
+        let elem2 = query.into();
+        assert_eq!(elem1, elem2);
+    }
+
+    #[test]
+    fn test_ex10() {
+        let elem: Element = r#"
+<query xmlns='jabber:iq:register'>
+  <x xmlns='jabber:x:data' type='submit'>
+    <field type='hidden' var='FORM_TYPE'>
+      <value>jabber:iq:register</value>
+    </field>
+    <field label='Given Name' var='first'>
+      <value>Juliet</value>
+    </field>
+    <field label='Family Name' var='last'>
+      <value>Capulet</value>
+    </field>
+    <field label='Email Address' var='email'>
+      <value>juliet@capulet.com</value>
+    </field>
+    <field type='list-single' label='Gender' var='x-gender'>
+      <value>F</value>
+    </field>
+  </x>
+</query>
+"#.parse().unwrap();
+        let elem1 = elem.clone();
+        let query = Query::try_from(elem).unwrap();
+        assert_eq!(query.registered, false);
+        for _ in &query.fields {
+            panic!();
+        }
+        let elem2 = query.into();
+        assert_eq!(elem1, elem2);
+    }
+}

src/lib.rs πŸ”—

@@ -144,6 +144,9 @@ pub mod rsm;
 /// XEP-0060: Publish-Subscribe
 pub mod pubsub;
 
+/// XEP-0077: In-Band Registration
+pub mod ibr;
+
 /// XEP-0085: Chat State Notifications
 pub mod chatstates;
 

src/ns.rs πŸ”—

@@ -39,6 +39,9 @@ pub const PUBSUB_EVENT: &str = "http://jabber.org/protocol/pubsub#event";
 /// XEP-0060: Publish-Subscribe
 pub const PUBSUB_OWNER: &str = "http://jabber.org/protocol/pubsub#owner";
 
+/// XEP-0077: In-Band Registration
+pub const REGISTER: &str = "jabber:iq:register";
+
 /// XEP-0085: Chat State Notifications
 pub const CHATSTATES: &str = "http://jabber.org/protocol/chatstates";