disco.rs

  1// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
  2//
  3// This Source Code Form is subject to the terms of the Mozilla Public
  4// License, v. 2.0. If a copy of the MPL was not distributed with this
  5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6
  7use std::convert::TryFrom;
  8
  9use minidom::Element;
 10
 11use error::Error;
 12use ns;
 13
 14use data_forms::{DataForm, DataFormType};
 15
 16#[derive(Debug, Clone, PartialEq)]
 17pub struct Feature {
 18    pub var: String,
 19}
 20
 21#[derive(Debug, Clone)]
 22pub struct Identity {
 23    pub category: String, // TODO: use an enum here.
 24    pub type_: String, // TODO: use an enum here.
 25    pub xml_lang: String,
 26    pub name: Option<String>,
 27}
 28
 29#[derive(Debug, Clone)]
 30pub struct Disco {
 31    pub node: Option<String>,
 32    pub identities: Vec<Identity>,
 33    pub features: Vec<Feature>,
 34    pub extensions: Vec<DataForm>,
 35}
 36
 37impl<'a> TryFrom<&'a Element> for Disco {
 38    type Error = Error;
 39
 40    fn try_from(elem: &'a Element) -> Result<Disco, Error> {
 41        if !elem.is("query", ns::DISCO_INFO) {
 42            return Err(Error::ParseError("This is not a disco#info element."));
 43        }
 44
 45        let mut identities: Vec<Identity> = vec!();
 46        let mut features: Vec<Feature> = vec!();
 47        let mut extensions: Vec<DataForm> = vec!();
 48
 49        let node = elem.attr("node")
 50                       .and_then(|node| node.parse().ok());
 51
 52        for child in elem.children() {
 53            if child.is("feature", ns::DISCO_INFO) {
 54                let feature = child.attr("var")
 55                                   .ok_or(Error::ParseError("Feature must have a 'var' attribute."))?;
 56                features.push(Feature {
 57                    var: feature.to_owned(),
 58                });
 59            } else if child.is("identity", ns::DISCO_INFO) {
 60                let category = child.attr("category")
 61                                    .ok_or(Error::ParseError("Identity must have a 'category' attribute."))?;
 62                if category == "" {
 63                    return Err(Error::ParseError("Identity must have a non-empty 'category' attribute."))
 64                }
 65
 66                let type_ = child.attr("type")
 67                                 .ok_or(Error::ParseError("Identity must have a 'type' attribute."))?;
 68                if type_ == "" {
 69                    return Err(Error::ParseError("Identity must have a non-empty 'type' attribute."))
 70                }
 71
 72                let xml_lang = child.attr("xml:lang").unwrap_or("");
 73                let name = child.attr("name")
 74                                .and_then(|name| name.parse().ok());
 75                identities.push(Identity {
 76                    category: category.to_owned(),
 77                    type_: type_.to_owned(),
 78                    xml_lang: xml_lang.to_owned(),
 79                    name: name,
 80                });
 81            } else if child.is("x", ns::DATA_FORMS) {
 82                let data_form = DataForm::try_from(child)?;
 83                match data_form.type_ {
 84                    DataFormType::Result_ => (),
 85                    _ => return Err(Error::ParseError("Data form must have a 'result' type in disco#info.")),
 86                }
 87                match data_form.form_type {
 88                    Some(_) => extensions.push(data_form),
 89                    None => return Err(Error::ParseError("Data form found without a FORM_TYPE.")),
 90                }
 91            } else {
 92                return Err(Error::ParseError("Unknown element in disco#info."));
 93            }
 94        }
 95
 96        /*
 97        // TODO: encode these restrictions only for result disco#info, not get ones.
 98        if identities.is_empty() {
 99            return Err(Error::ParseError("There must be at least one identity in disco#info."));
100        }
101        if features.is_empty() {
102            return Err(Error::ParseError("There must be at least one feature in disco#info."));
103        }
104        if !features.contains(&Feature { var: ns::DISCO_INFO.to_owned() }) {
105            return Err(Error::ParseError("disco#info feature not present in disco#info."));
106        }
107        */
108
109        Ok(Disco {
110            node: node,
111            identities: identities,
112            features: features,
113            extensions: extensions
114        })
115    }
116}
117
118impl<'a> Into<Element> for &'a Disco {
119    fn into(self) -> Element {
120        let mut root = Element::builder("query")
121                               .ns(ns::DISCO_INFO)
122                               .attr("node", self.node.clone())
123                               .build();
124        for identity in &self.identities {
125            let identity_element = Element::builder("identity")
126                                           .ns(ns::DISCO_INFO)
127                                           .attr("category", identity.category.clone())
128                                           .attr("type", identity.type_.clone())
129                                           .attr("xml:lang", identity.xml_lang.clone())
130                                           .attr("name", identity.name.clone())
131                                           .build();
132            root.append_child(identity_element);
133        }
134        for feature in &self.features {
135            let feature_element = Element::builder("feature")
136                                          .ns(ns::DISCO_INFO)
137                                          .attr("var", feature.var.clone())
138                                          .build();
139            root.append_child(feature_element);
140        }
141        for _ in &self.extensions {
142            panic!("Not yet implemented!");
143        }
144        root
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_simple() {
154        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#info'/></query>".parse().unwrap();
155        let query = Disco::try_from(&elem).unwrap();
156        assert!(query.node.is_none());
157        assert_eq!(query.identities.len(), 1);
158        assert_eq!(query.features.len(), 1);
159        assert!(query.extensions.is_empty());
160    }
161
162    #[test]
163    fn test_invalid() {
164        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><coucou/></query>".parse().unwrap();
165        let error = Disco::try_from(&elem).unwrap_err();
166        let message = match error {
167            Error::ParseError(string) => string,
168            _ => panic!(),
169        };
170        assert_eq!(message, "Unknown element in disco#info.");
171    }
172
173    #[test]
174    fn test_invalid_identity() {
175        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity/></query>".parse().unwrap();
176        let error = Disco::try_from(&elem).unwrap_err();
177        let message = match error {
178            Error::ParseError(string) => string,
179            _ => panic!(),
180        };
181        assert_eq!(message, "Identity must have a 'category' attribute.");
182
183        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category=''/></query>".parse().unwrap();
184        let error = Disco::try_from(&elem).unwrap_err();
185        let message = match error {
186            Error::ParseError(string) => string,
187            _ => panic!(),
188        };
189        assert_eq!(message, "Identity must have a non-empty 'category' attribute.");
190
191        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='coucou'/></query>".parse().unwrap();
192        let error = Disco::try_from(&elem).unwrap_err();
193        let message = match error {
194            Error::ParseError(string) => string,
195            _ => panic!(),
196        };
197        assert_eq!(message, "Identity must have a 'type' attribute.");
198
199        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='coucou' type=''/></query>".parse().unwrap();
200        let error = Disco::try_from(&elem).unwrap_err();
201        let message = match error {
202            Error::ParseError(string) => string,
203            _ => panic!(),
204        };
205        assert_eq!(message, "Identity must have a non-empty 'type' attribute.");
206    }
207
208    #[test]
209    fn test_invalid_feature() {
210        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><feature/></query>".parse().unwrap();
211        let error = Disco::try_from(&elem).unwrap_err();
212        let message = match error {
213            Error::ParseError(string) => string,
214            _ => panic!(),
215        };
216        assert_eq!(message, "Feature must have a 'var' attribute.");
217    }
218
219    #[test]
220    #[ignore]
221    fn test_invalid_result() {
222        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'/>".parse().unwrap();
223        let error = Disco::try_from(&elem).unwrap_err();
224        let message = match error {
225            Error::ParseError(string) => string,
226            _ => panic!(),
227        };
228        assert_eq!(message, "There must be at least one identity in disco#info.");
229
230        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/></query>".parse().unwrap();
231        let error = Disco::try_from(&elem).unwrap_err();
232        let message = match error {
233            Error::ParseError(string) => string,
234            _ => panic!(),
235        };
236        assert_eq!(message, "There must be at least one feature in disco#info.");
237
238        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#items'/></query>".parse().unwrap();
239        let error = Disco::try_from(&elem).unwrap_err();
240        let message = match error {
241            Error::ParseError(string) => string,
242            _ => panic!(),
243        };
244        assert_eq!(message, "disco#info feature not present in disco#info.");
245    }
246}