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