1use xso::{AsXml, FromXml};
8
9use crate::data_forms::DataForm;
10use crate::iq::{IqGetPayload, IqResultPayload};
11use crate::ns;
12use crate::rsm::{SetQuery, SetResult};
13use jid::Jid;
14
15#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
20#[xml(namespace = ns::DISCO_INFO, name = "query")]
21pub struct DiscoInfoQuery {
22 #[xml(attribute(default))]
24 pub node: Option<String>,
25}
26
27impl IqGetPayload for DiscoInfoQuery {}
28
29#[derive(FromXml, AsXml, Debug, Clone, PartialEq, Eq, Hash)]
31#[xml(namespace = ns::DISCO_INFO, name = "feature")]
32pub struct Feature {
33 #[xml(attribute)]
35 pub var: String,
36}
37
38impl Feature {
39 pub fn new<S: Into<String>>(var: S) -> Feature {
41 Feature { var: var.into() }
42 }
43}
44
45#[derive(FromXml, AsXml, Debug, Clone, PartialEq, Eq, Hash)]
47#[xml(namespace = ns::DISCO_INFO, name = "identity")]
48pub struct Identity {
49 #[xml(attribute)]
52 pub category: String,
53
54 #[xml(attribute = "type")]
57 pub type_: String,
58
59 #[xml(attribute(default, name = "xml:lang"))]
61 pub lang: Option<String>,
62
63 #[xml(attribute(default))]
65 pub name: Option<String>,
66}
67
68impl Identity {
69 pub fn new<C, T, L, N>(category: C, type_: T, lang: L, name: N) -> Identity
71 where
72 C: Into<String>,
73 T: Into<String>,
74 L: Into<String>,
75 N: Into<String>,
76 {
77 Identity {
78 category: category.into(),
79 type_: type_.into(),
80 lang: Some(lang.into()),
81 name: Some(name.into()),
82 }
83 }
84
85 pub fn new_anonymous<C, T, L, N>(category: C, type_: T) -> Identity
87 where
88 C: Into<String>,
89 T: Into<String>,
90 {
91 Identity {
92 category: category.into(),
93 type_: type_.into(),
94 lang: None,
95 name: None,
96 }
97 }
98}
99
100#[derive(FromXml, AsXml, Debug, Clone)]
105#[xml(namespace = ns::DISCO_INFO, name = "query")]
106pub struct DiscoInfoResult {
107 #[xml(attribute(default))]
109 pub node: Option<String>,
110
111 #[xml(child(n = ..))]
113 pub identities: Vec<Identity>,
114
115 #[xml(child(n = ..))]
117 pub features: Vec<Feature>,
118
119 #[xml(child(n = ..))]
121 pub extensions: Vec<DataForm>,
122}
123
124impl IqResultPayload for DiscoInfoResult {}
125
126#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
131#[xml(namespace = ns::DISCO_ITEMS, name = "query")]
132pub struct DiscoItemsQuery {
133 #[xml(attribute(default))]
135 pub node: Option<String>,
136
137 #[xml(child(default))]
139 pub rsm: Option<SetQuery>,
140}
141
142impl IqGetPayload for DiscoItemsQuery {}
143
144#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
146#[xml(namespace = ns::DISCO_ITEMS, name = "item")]
147pub struct Item {
148 #[xml(attribute)]
150 pub jid: Jid,
151
152 #[xml(attribute(default))]
154 pub node: Option<String>,
155
156 #[xml(attribute(default))]
158 pub name: Option<String>,
159}
160
161#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
167#[xml(namespace = ns::DISCO_ITEMS, name = "query")]
168pub struct DiscoItemsResult {
169 #[xml(attribute(default))]
171 pub node: Option<String>,
172
173 #[xml(child(n = ..))]
175 pub items: Vec<Item>,
176
177 #[xml(child(default))]
179 pub rsm: Option<SetResult>,
180}
181
182impl IqResultPayload for DiscoItemsResult {}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use jid::BareJid;
188 use minidom::Element;
189 use xso::error::{Error, FromElementError};
190
191 #[cfg(target_pointer_width = "32")]
192 #[test]
193 fn test_size() {
194 assert_size!(Identity, 48);
195 assert_size!(Feature, 12);
196 assert_size!(DiscoInfoQuery, 12);
197 assert_size!(DiscoInfoResult, 48);
198
199 assert_size!(Item, 40);
200 assert_size!(DiscoItemsQuery, 52);
201 assert_size!(DiscoItemsResult, 64);
202 }
203
204 #[cfg(target_pointer_width = "64")]
205 #[test]
206 fn test_size() {
207 assert_size!(Identity, 96);
208 assert_size!(Feature, 24);
209 assert_size!(DiscoInfoQuery, 24);
210 assert_size!(DiscoInfoResult, 96);
211
212 assert_size!(Item, 80);
213 assert_size!(DiscoItemsQuery, 104);
214 assert_size!(DiscoItemsResult, 128);
215 }
216
217 #[test]
218 fn test_simple() {
219 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();
220 let query = DiscoInfoResult::try_from(elem).unwrap();
221 assert!(query.node.is_none());
222 assert_eq!(query.identities.len(), 1);
223 assert_eq!(query.features.len(), 1);
224 assert!(query.extensions.is_empty());
225 }
226
227 #[test]
228 fn test_identity_after_feature() {
229 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();
230 let query = DiscoInfoResult::try_from(elem).unwrap();
231 assert_eq!(query.identities.len(), 1);
232 assert_eq!(query.features.len(), 1);
233 assert!(query.extensions.is_empty());
234 }
235
236 #[test]
237 fn test_feature_after_dataform() {
238 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();
239 let query = DiscoInfoResult::try_from(elem).unwrap();
240 assert_eq!(query.identities.len(), 1);
241 assert_eq!(query.features.len(), 1);
242 assert_eq!(query.extensions.len(), 1);
243 }
244
245 #[test]
246 fn test_extension() {
247 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();
248 let elem1 = elem.clone();
249 let query = DiscoInfoResult::try_from(elem).unwrap();
250 assert!(query.node.is_none());
251 assert_eq!(query.identities.len(), 1);
252 assert_eq!(query.features.len(), 1);
253 assert_eq!(query.extensions.len(), 1);
254 assert_eq!(query.extensions[0].form_type, Some(String::from("example")));
255
256 let elem2 = query.into();
257 assert_eq!(elem1, elem2);
258 }
259
260 #[test]
261 #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")]
262 fn test_invalid() {
263 let elem: Element =
264 "<query xmlns='http://jabber.org/protocol/disco#info'><coucou/></query>"
265 .parse()
266 .unwrap();
267 let error = DiscoInfoResult::try_from(elem).unwrap_err();
268 let message = match error {
269 FromElementError::Invalid(Error::Other(string)) => string,
270 _ => panic!(),
271 };
272 assert_eq!(message, "Unknown child in DiscoInfoResult element.");
273 }
274
275 #[test]
276 fn test_invalid_identity() {
277 let elem: Element =
278 "<query xmlns='http://jabber.org/protocol/disco#info'><identity/></query>"
279 .parse()
280 .unwrap();
281 let error = DiscoInfoResult::try_from(elem).unwrap_err();
282 let message = match error {
283 FromElementError::Invalid(Error::Other(string)) => string,
284 _ => panic!(),
285 };
286 assert_eq!(
287 message,
288 "Required attribute field 'category' on Identity element missing."
289 );
290
291 let elem: Element =
292 "<query xmlns='http://jabber.org/protocol/disco#info'><identity type='coucou'/></query>"
293 .parse()
294 .unwrap();
295 let error = DiscoInfoResult::try_from(elem).unwrap_err();
296 let message = match error {
297 FromElementError::Invalid(Error::Other(string)) => string,
298 _ => panic!(),
299 };
300 assert_eq!(
301 message,
302 "Required attribute field 'category' on Identity element missing."
303 );
304
305 let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='coucou'/></query>".parse().unwrap();
306 let error = DiscoInfoResult::try_from(elem).unwrap_err();
307 let message = match error {
308 FromElementError::Invalid(Error::Other(string)) => string,
309 _ => panic!(),
310 };
311 assert_eq!(
312 message,
313 "Required attribute field 'type_' on Identity element missing."
314 );
315 }
316
317 #[test]
318 fn test_invalid_feature() {
319 let elem: Element =
320 "<query xmlns='http://jabber.org/protocol/disco#info'><feature/></query>"
321 .parse()
322 .unwrap();
323 let error = DiscoInfoResult::try_from(elem).unwrap_err();
324 let message = match error {
325 FromElementError::Invalid(Error::Other(string)) => string,
326 _ => panic!(),
327 };
328 assert_eq!(
329 message,
330 "Required attribute field 'var' on Feature element missing."
331 );
332 }
333
334 #[test]
338 #[ignore]
339 fn test_invalid_result() {
340 let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'/>"
341 .parse()
342 .unwrap();
343 let error = DiscoInfoResult::try_from(elem).unwrap_err();
344 let message = match error {
345 FromElementError::Invalid(Error::Other(string)) => string,
346 _ => panic!(),
347 };
348 assert_eq!(
349 message,
350 "There must be at least one identity in disco#info."
351 );
352
353 let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/></query>".parse().unwrap();
354 let error = DiscoInfoResult::try_from(elem).unwrap_err();
355 let message = match error {
356 FromElementError::Invalid(Error::Other(string)) => string,
357 _ => panic!(),
358 };
359 assert_eq!(message, "There must be at least one feature in disco#info.");
360 }
361
362 #[test]
363 fn test_simple_items() {
364 let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'/>"
365 .parse()
366 .unwrap();
367 let query = DiscoItemsQuery::try_from(elem).unwrap();
368 assert!(query.node.is_none());
369
370 let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items' node='coucou'/>"
371 .parse()
372 .unwrap();
373 let query = DiscoItemsQuery::try_from(elem).unwrap();
374 assert_eq!(query.node, Some(String::from("coucou")));
375 }
376
377 #[test]
378 fn test_simple_items_result() {
379 let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'/>"
380 .parse()
381 .unwrap();
382 let query = DiscoItemsResult::try_from(elem).unwrap();
383 assert!(query.node.is_none());
384 assert!(query.items.is_empty());
385
386 let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items' node='coucou'/>"
387 .parse()
388 .unwrap();
389 let query = DiscoItemsResult::try_from(elem).unwrap();
390 assert_eq!(query.node, Some(String::from("coucou")));
391 assert!(query.items.is_empty());
392 }
393
394 #[test]
395 fn test_answers_items_result() {
396 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();
397 let query = DiscoItemsResult::try_from(elem).unwrap();
398 let elem2 = Element::from(query);
399 let query = DiscoItemsResult::try_from(elem2).unwrap();
400 assert_eq!(query.items.len(), 2);
401 assert_eq!(query.items[0].jid, BareJid::new("component").unwrap());
402 assert_eq!(query.items[0].node, None);
403 assert_eq!(query.items[0].name, None);
404 assert_eq!(query.items[1].jid, BareJid::new("component2").unwrap());
405 assert_eq!(query.items[1].node, Some(String::from("test")));
406 assert_eq!(query.items[1].name, Some(String::from("A component")));
407 }
408
409 #[test]
415 fn test_missing_disco_info_feature_workaround() {
416 let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/muc#user'/></query>".parse().unwrap();
417 let query = DiscoInfoResult::try_from(elem).unwrap();
418 assert_eq!(query.identities.len(), 1);
419 assert_eq!(query.features.len(), 1);
420 assert!(query.extensions.is_empty());
421 }
422}