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