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