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 std::convert::TryFrom;
8
9use minidom::Element;
10
11use error::Error;
12use ns;
13
14use data_forms::{DataForm, DataFormType};
15
16#[derive(Debug, Clone, PartialEq)]
17pub struct Feature {
18 pub var: String,
19}
20
21#[derive(Debug, Clone)]
22pub struct Identity {
23 pub category: String, // TODO: use an enum here.
24 pub type_: String, // TODO: use an enum here.
25 pub xml_lang: String,
26 pub name: Option<String>,
27}
28
29#[derive(Debug, Clone)]
30pub struct Disco {
31 pub node: Option<String>,
32 pub identities: Vec<Identity>,
33 pub features: Vec<Feature>,
34 pub extensions: Vec<DataForm>,
35}
36
37impl<'a> TryFrom<&'a Element> for Disco {
38 type Error = Error;
39
40 fn try_from(elem: &'a Element) -> Result<Disco, Error> {
41 if !elem.is("query", ns::DISCO_INFO) {
42 return Err(Error::ParseError("This is not a disco#info element."));
43 }
44
45 let mut identities: Vec<Identity> = vec!();
46 let mut features: Vec<Feature> = vec!();
47 let mut extensions: Vec<DataForm> = vec!();
48
49 let node = elem.attr("node")
50 .and_then(|node| node.parse().ok());
51
52 for child in elem.children() {
53 if child.is("feature", ns::DISCO_INFO) {
54 let feature = child.attr("var")
55 .ok_or(Error::ParseError("Feature must have a 'var' attribute."))?;
56 features.push(Feature {
57 var: feature.to_owned(),
58 });
59 } else if child.is("identity", ns::DISCO_INFO) {
60 let category = child.attr("category")
61 .ok_or(Error::ParseError("Identity must have a 'category' attribute."))?;
62 if category == "" {
63 return Err(Error::ParseError("Identity must have a non-empty 'category' attribute."))
64 }
65
66 let type_ = child.attr("type")
67 .ok_or(Error::ParseError("Identity must have a 'type' attribute."))?;
68 if type_ == "" {
69 return Err(Error::ParseError("Identity must have a non-empty 'type' attribute."))
70 }
71
72 let xml_lang = child.attr("xml:lang").unwrap_or("");
73 let name = child.attr("name")
74 .and_then(|name| name.parse().ok());
75 identities.push(Identity {
76 category: category.to_owned(),
77 type_: type_.to_owned(),
78 xml_lang: xml_lang.to_owned(),
79 name: name,
80 });
81 } else if child.is("x", ns::DATA_FORMS) {
82 let data_form = DataForm::try_from(child)?;
83 match data_form.type_ {
84 DataFormType::Result_ => (),
85 _ => return Err(Error::ParseError("Data form must have a 'result' type in disco#info.")),
86 }
87 match data_form.form_type {
88 Some(_) => extensions.push(data_form),
89 None => return Err(Error::ParseError("Data form found without a FORM_TYPE.")),
90 }
91 } else {
92 return Err(Error::ParseError("Unknown element in disco#info."));
93 }
94 }
95
96 /*
97 // TODO: encode these restrictions only for result disco#info, not get ones.
98 if identities.is_empty() {
99 return Err(Error::ParseError("There must be at least one identity in disco#info."));
100 }
101 if features.is_empty() {
102 return Err(Error::ParseError("There must be at least one feature in disco#info."));
103 }
104 if !features.contains(&Feature { var: ns::DISCO_INFO.to_owned() }) {
105 return Err(Error::ParseError("disco#info feature not present in disco#info."));
106 }
107 */
108
109 Ok(Disco {
110 node: node,
111 identities: identities,
112 features: features,
113 extensions: extensions
114 })
115 }
116}
117
118impl<'a> Into<Element> for &'a Disco {
119 fn into(self) -> Element {
120 let mut root = Element::builder("query")
121 .ns(ns::DISCO_INFO)
122 .attr("node", self.node.clone())
123 .build();
124 for identity in &self.identities {
125 let identity_element = Element::builder("identity")
126 .ns(ns::DISCO_INFO)
127 .attr("category", identity.category.clone())
128 .attr("type", identity.type_.clone())
129 .attr("xml:lang", identity.xml_lang.clone())
130 .attr("name", identity.name.clone())
131 .build();
132 root.append_child(identity_element);
133 }
134 for feature in &self.features {
135 let feature_element = Element::builder("feature")
136 .ns(ns::DISCO_INFO)
137 .attr("var", feature.var.clone())
138 .build();
139 root.append_child(feature_element);
140 }
141 for _ in &self.extensions {
142 panic!("Not yet implemented!");
143 }
144 root
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn test_simple() {
154 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();
155 let query = Disco::try_from(&elem).unwrap();
156 assert!(query.node.is_none());
157 assert_eq!(query.identities.len(), 1);
158 assert_eq!(query.features.len(), 1);
159 assert!(query.extensions.is_empty());
160 }
161
162 #[test]
163 fn test_invalid() {
164 let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><coucou/></query>".parse().unwrap();
165 let error = Disco::try_from(&elem).unwrap_err();
166 let message = match error {
167 Error::ParseError(string) => string,
168 _ => panic!(),
169 };
170 assert_eq!(message, "Unknown element in disco#info.");
171 }
172
173 #[test]
174 fn test_invalid_identity() {
175 let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity/></query>".parse().unwrap();
176 let error = Disco::try_from(&elem).unwrap_err();
177 let message = match error {
178 Error::ParseError(string) => string,
179 _ => panic!(),
180 };
181 assert_eq!(message, "Identity must have a 'category' attribute.");
182
183 let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category=''/></query>".parse().unwrap();
184 let error = Disco::try_from(&elem).unwrap_err();
185 let message = match error {
186 Error::ParseError(string) => string,
187 _ => panic!(),
188 };
189 assert_eq!(message, "Identity must have a non-empty 'category' attribute.");
190
191 let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='coucou'/></query>".parse().unwrap();
192 let error = Disco::try_from(&elem).unwrap_err();
193 let message = match error {
194 Error::ParseError(string) => string,
195 _ => panic!(),
196 };
197 assert_eq!(message, "Identity must have a 'type' attribute.");
198
199 let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='coucou' type=''/></query>".parse().unwrap();
200 let error = Disco::try_from(&elem).unwrap_err();
201 let message = match error {
202 Error::ParseError(string) => string,
203 _ => panic!(),
204 };
205 assert_eq!(message, "Identity must have a non-empty 'type' attribute.");
206 }
207
208 #[test]
209 fn test_invalid_feature() {
210 let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><feature/></query>".parse().unwrap();
211 let error = Disco::try_from(&elem).unwrap_err();
212 let message = match error {
213 Error::ParseError(string) => string,
214 _ => panic!(),
215 };
216 assert_eq!(message, "Feature must have a 'var' attribute.");
217 }
218
219 #[test]
220 #[ignore]
221 fn test_invalid_result() {
222 let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'/>".parse().unwrap();
223 let error = Disco::try_from(&elem).unwrap_err();
224 let message = match error {
225 Error::ParseError(string) => string,
226 _ => panic!(),
227 };
228 assert_eq!(message, "There must be at least one identity in disco#info.");
229
230 let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/></query>".parse().unwrap();
231 let error = Disco::try_from(&elem).unwrap_err();
232 let message = match error {
233 Error::ParseError(string) => string,
234 _ => panic!(),
235 };
236 assert_eq!(message, "There must be at least one feature in disco#info.");
237
238 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();
239 let error = Disco::try_from(&elem).unwrap_err();
240 let message = match error {
241 Error::ParseError(string) => string,
242 _ => panic!(),
243 };
244 assert_eq!(message, "disco#info feature not present in disco#info.");
245 }
246}