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