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
  7#![deny(missing_docs)]
  8
  9use try_from::TryFrom;
 10
 11use minidom::Element;
 12use jid::Jid;
 13
 14use error::Error;
 15use ns;
 16
 17use data_forms::{DataForm, DataFormType};
 18
 19generate_element_with_only_attributes!(
 20/// Structure representing a `<query xmlns='http://jabber.org/protocol/disco#info'/>` element.
 21///
 22/// It should only be used in an `<iq type='get'/>`, as it can only represent
 23/// the request, and not a result.
 24DiscoInfoQuery, "query", ns::DISCO_INFO, [
 25    /// Node on which we are doing the discovery.
 26    node: Option<String> = "node" => optional,
 27]);
 28
 29generate_element_with_only_attributes!(
 30/// Structure representing a `<feature xmlns='http://jabber.org/protocol/disco#info'/>` element.
 31#[derive(PartialEq)]
 32Feature, "feature", ns::DISCO_INFO, [
 33    /// Namespace of the feature we want to represent.
 34    var: String = "var" => required,
 35]);
 36
 37/// Structure representing an `<identity xmlns='http://jabber.org/protocol/disco#info'/>` element.
 38#[derive(Debug, Clone)]
 39pub struct Identity {
 40    /// Category of this identity.
 41    pub category: String, // TODO: use an enum here.
 42
 43    /// Type of this identity.
 44    pub type_: String, // TODO: use an enum here.
 45
 46    /// Lang of the name of this identity.
 47    pub lang: Option<String>,
 48
 49    /// Name of this identity.
 50    pub name: Option<String>,
 51}
 52
 53impl TryFrom<Element> for Identity {
 54    type Err = Error;
 55
 56    fn try_from(elem: Element) -> Result<Identity, Error> {
 57        check_self!(elem, "identity", ns::DISCO_INFO, "disco#info identity");
 58        check_no_children!(elem, "disco#info identity");
 59        check_no_unknown_attributes!(elem, "disco#info identity", ["category", "type", "xml:lang", "name"]);
 60
 61        let category = get_attr!(elem, "category", required);
 62        if category == "" {
 63            return Err(Error::ParseError("Identity must have a non-empty 'category' attribute."))
 64        }
 65
 66        let type_ = get_attr!(elem, "type", required);
 67        if type_ == "" {
 68            return Err(Error::ParseError("Identity must have a non-empty 'type' attribute."))
 69        }
 70
 71        Ok(Identity {
 72            category: category,
 73            type_: type_,
 74            lang: get_attr!(elem, "xml:lang", optional),
 75            name: get_attr!(elem, "name", optional),
 76        })
 77    }
 78}
 79
 80impl From<Identity> for Element {
 81    fn from(identity: Identity) -> Element {
 82        Element::builder("identity")
 83                .ns(ns::DISCO_INFO)
 84                .attr("category", identity.category)
 85                .attr("type", identity.type_)
 86                .attr("xml:lang", identity.lang)
 87                .attr("name", identity.name)
 88                .build()
 89    }
 90}
 91
 92/// Structure representing a `<query xmlns='http://jabber.org/protocol/disco#info'/>` element.
 93///
 94/// It should only be used in an `<iq type='result'/>`, as it can only
 95/// represent the result, and not a request.
 96#[derive(Debug, Clone)]
 97pub struct DiscoInfoResult {
 98    /// Node on which we have done this discovery.
 99    pub node: Option<String>,
100
101    /// List of identities exposed by this entity.
102    pub identities: Vec<Identity>,
103
104    /// List of features supported by this entity.
105    pub features: Vec<Feature>,
106
107    /// List of extensions reported by this entity.
108    pub extensions: Vec<DataForm>,
109}
110
111impl TryFrom<Element> for DiscoInfoResult {
112    type Err = Error;
113
114    fn try_from(elem: Element) -> Result<DiscoInfoResult, Error> {
115        check_self!(elem, "query", ns::DISCO_INFO, "disco#info result");
116        check_no_unknown_attributes!(elem, "disco#info result", ["node"]);
117
118        let mut result = DiscoInfoResult {
119            node: get_attr!(elem, "node", optional),
120            identities: vec!(),
121            features: vec!(),
122            extensions: vec!(),
123        };
124        let mut parsing_identities_done = false;
125        let mut parsing_features_done = false;
126
127        for child in elem.children() {
128            if child.is("identity", ns::DISCO_INFO) {
129                if parsing_identities_done {
130                    return Err(Error::ParseError("Identity found after features or data forms in disco#info."));
131                }
132                let identity = Identity::try_from(child.clone())?;
133                result.identities.push(identity);
134            } else if child.is("feature", ns::DISCO_INFO) {
135                parsing_identities_done = true;
136                if parsing_features_done {
137                    return Err(Error::ParseError("Feature found after data forms in disco#info."));
138                }
139                let feature = Feature::try_from(child.clone())?;
140                result.features.push(feature);
141            } else if child.is("x", ns::DATA_FORMS) {
142                parsing_identities_done = true;
143                parsing_features_done = true;
144                let data_form = DataForm::try_from(child.clone())?;
145                if data_form.type_ != DataFormType::Result_ {
146                    return Err(Error::ParseError("Data form must have a 'result' type in disco#info."));
147                }
148                if data_form.form_type.is_none() {
149                    return Err(Error::ParseError("Data form found without a FORM_TYPE."));
150                }
151                result.extensions.push(data_form);
152            } else {
153                return Err(Error::ParseError("Unknown element in disco#info."));
154            }
155        }
156
157        if result.identities.is_empty() {
158            return Err(Error::ParseError("There must be at least one identity in disco#info."));
159        }
160        if result.features.is_empty() {
161            return Err(Error::ParseError("There must be at least one feature in disco#info."));
162        }
163        if !result.features.contains(&Feature { var: ns::DISCO_INFO.to_owned() }) {
164            return Err(Error::ParseError("disco#info feature not present in disco#info."));
165        }
166
167        Ok(result)
168    }
169}
170
171impl From<DiscoInfoResult> for Element {
172    fn from(disco: DiscoInfoResult) -> Element {
173        Element::builder("query")
174                .ns(ns::DISCO_INFO)
175                .attr("node", disco.node)
176                .append(disco.identities)
177                .append(disco.features)
178                .append(disco.extensions)
179                .build()
180    }
181}
182
183generate_element_with_only_attributes!(
184/// Structure representing a `<query xmlns='http://jabber.org/protocol/disco#items'/>` element.
185///
186/// It should only be used in an `<iq type='get'/>`, as it can only represent
187/// the request, and not a result.
188DiscoItemsQuery, "query", ns::DISCO_ITEMS, [
189    /// Node on which we are doing the discovery.
190    node: Option<String> = "node" => optional,
191]);
192
193generate_element_with_only_attributes!(
194/// Structure representing an `<item xmlns='http://jabber.org/protocol/disco#items'/>` element.
195Item, "item", ns::DISCO_ITEMS, [
196    /// JID of the entity pointed by this item.
197    jid: Jid = "jid" => required,
198    /// Node of the entity pointed by this item.
199    node: Option<String> = "node" => optional,
200    /// Name of the entity pointed by this item.
201    name: Option<String> = "name" => optional,
202]);
203
204/// Structure representing a `<query
205/// xmlns='http://jabber.org/protocol/disco#items'/>` element.
206///
207/// It should only be used in an `<iq type='result'/>`, as it can only
208/// represent the result, and not a request.
209#[derive(Debug, Clone)]
210pub struct DiscoItemsResult {
211    /// Node on which we have done this discovery.
212    pub node: Option<String>,
213
214    /// List of items pointed by this entity.
215    pub items: Vec<Item>,
216}
217
218impl TryFrom<Element> for DiscoItemsResult {
219    type Err = Error;
220
221    fn try_from(elem: Element) -> Result<DiscoItemsResult, Error> {
222        check_self!(elem, "query", ns::DISCO_ITEMS, "disco#items query");
223        check_no_unknown_attributes!(elem, "disco#items query", ["node"]);
224
225        let mut items: Vec<Item> = vec!();
226        for child in elem.children() {
227            if child.is("item", ns::DISCO_ITEMS) {
228                items.push(Item::try_from(child.clone())?);
229            } else {
230                return Err(Error::ParseError("Unknown element in disco#items."));
231            }
232        }
233
234        Ok(DiscoItemsResult {
235            node: get_attr!(elem, "node", optional),
236            items: items,
237        })
238    }
239}
240
241impl From<DiscoItemsResult> for Element {
242    fn from(disco: DiscoItemsResult) -> Element {
243        Element::builder("query")
244                .ns(ns::DISCO_ITEMS)
245                .attr("node", disco.node)
246                .append(disco.items)
247                .build()
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use compare_elements::NamespaceAwareCompare;
255    use std::str::FromStr;
256
257    #[test]
258    fn test_simple() {
259        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();
260        let query = DiscoInfoResult::try_from(elem).unwrap();
261        assert!(query.node.is_none());
262        assert_eq!(query.identities.len(), 1);
263        assert_eq!(query.features.len(), 1);
264        assert!(query.extensions.is_empty());
265    }
266
267    #[test]
268    fn test_extension() {
269        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#info'/><x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>example</value></field></x></query>".parse().unwrap();
270        let elem1 = elem.clone();
271        let query = DiscoInfoResult::try_from(elem).unwrap();
272        assert!(query.node.is_none());
273        assert_eq!(query.identities.len(), 1);
274        assert_eq!(query.features.len(), 1);
275        assert_eq!(query.extensions.len(), 1);
276        assert_eq!(query.extensions[0].form_type, Some(String::from("example")));
277
278        let elem2 = query.into();
279        assert!(elem1.compare_to(&elem2));
280    }
281
282    #[test]
283    fn test_invalid() {
284        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><coucou/></query>".parse().unwrap();
285        let error = DiscoInfoResult::try_from(elem).unwrap_err();
286        let message = match error {
287            Error::ParseError(string) => string,
288            _ => panic!(),
289        };
290        assert_eq!(message, "Unknown element in disco#info.");
291    }
292
293    #[test]
294    fn test_invalid_identity() {
295        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity/></query>".parse().unwrap();
296        let error = DiscoInfoResult::try_from(elem).unwrap_err();
297        let message = match error {
298            Error::ParseError(string) => string,
299            _ => panic!(),
300        };
301        assert_eq!(message, "Required attribute 'category' missing.");
302
303        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category=''/></query>".parse().unwrap();
304        let error = DiscoInfoResult::try_from(elem).unwrap_err();
305        let message = match error {
306            Error::ParseError(string) => string,
307            _ => panic!(),
308        };
309        assert_eq!(message, "Identity must have a non-empty 'category' attribute.");
310
311        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='coucou'/></query>".parse().unwrap();
312        let error = DiscoInfoResult::try_from(elem).unwrap_err();
313        let message = match error {
314            Error::ParseError(string) => string,
315            _ => panic!(),
316        };
317        assert_eq!(message, "Required attribute 'type' missing.");
318
319        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='coucou' type=''/></query>".parse().unwrap();
320        let error = DiscoInfoResult::try_from(elem).unwrap_err();
321        let message = match error {
322            Error::ParseError(string) => string,
323            _ => panic!(),
324        };
325        assert_eq!(message, "Identity must have a non-empty 'type' attribute.");
326    }
327
328    #[test]
329    fn test_invalid_feature() {
330        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><feature/></query>".parse().unwrap();
331        let error = DiscoInfoResult::try_from(elem).unwrap_err();
332        let message = match error {
333            Error::ParseError(string) => string,
334            _ => panic!(),
335        };
336        assert_eq!(message, "Required attribute 'var' missing.");
337    }
338
339    #[test]
340    fn test_invalid_result() {
341        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'/>".parse().unwrap();
342        let error = DiscoInfoResult::try_from(elem).unwrap_err();
343        let message = match error {
344            Error::ParseError(string) => string,
345            _ => panic!(),
346        };
347        assert_eq!(message, "There must be at least one identity in disco#info.");
348
349        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/></query>".parse().unwrap();
350        let error = DiscoInfoResult::try_from(elem).unwrap_err();
351        let message = match error {
352            Error::ParseError(string) => string,
353            _ => panic!(),
354        };
355        assert_eq!(message, "There must be at least one feature in disco#info.");
356
357        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();
358        let error = DiscoInfoResult::try_from(elem).unwrap_err();
359        let message = match error {
360            Error::ParseError(string) => string,
361            _ => panic!(),
362        };
363        assert_eq!(message, "disco#info feature not present in disco#info.");
364
365        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><feature var='http://jabber.org/protocol/disco#info'/><identity category='client' type='pc'/></query>".parse().unwrap();
366        let error = DiscoInfoResult::try_from(elem).unwrap_err();
367        let message = match error {
368            Error::ParseError(string) => string,
369            _ => panic!(),
370        };
371        assert_eq!(message, "Identity found after features or data forms in disco#info.");
372
373        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>coucou</value></field></x><feature var='http://jabber.org/protocol/disco#info'/></query>".parse().unwrap();
374        let error = DiscoInfoResult::try_from(elem).unwrap_err();
375        let message = match error {
376            Error::ParseError(string) => string,
377            _ => panic!(),
378        };
379        assert_eq!(message, "Feature found after data forms in disco#info.");
380    }
381
382    #[test]
383    fn test_simple_items() {
384        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'/>".parse().unwrap();
385        let query = DiscoItemsQuery::try_from(elem).unwrap();
386        assert!(query.node.is_none());
387
388        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items' node='coucou'/>".parse().unwrap();
389        let query = DiscoItemsQuery::try_from(elem).unwrap();
390        assert_eq!(query.node, Some(String::from("coucou")));
391    }
392
393    #[test]
394    fn test_simple_items_result() {
395        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'/>".parse().unwrap();
396        let query = DiscoItemsResult::try_from(elem).unwrap();
397        assert!(query.node.is_none());
398        assert!(query.items.is_empty());
399
400        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items' node='coucou'/>".parse().unwrap();
401        let query = DiscoItemsResult::try_from(elem).unwrap();
402        assert_eq!(query.node, Some(String::from("coucou")));
403        assert!(query.items.is_empty());
404    }
405
406    #[test]
407    fn test_answers_items_result() {
408        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'><item jid='component'/><item jid='component2' node='test' name='A component'/></query>".parse().unwrap();
409        let query = DiscoItemsResult::try_from(elem).unwrap();
410        assert_eq!(query.items.len(), 2);
411        assert_eq!(query.items[0].jid, Jid::from_str("component").unwrap());
412        assert_eq!(query.items[0].node, None);
413        assert_eq!(query.items[0].name, None);
414        assert_eq!(query.items[1].jid, Jid::from_str("component2").unwrap());
415        assert_eq!(query.items[1].node, Some(String::from("test")));
416        assert_eq!(query.items[1].name, Some(String::from("A component")));
417    }
418}