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;
8use std::str::FromStr;
9
10use minidom::{Element, IntoAttributeValue};
11use jid::Jid;
12use chrono::{DateTime, FixedOffset};
13
14use error::Error;
15
16use ns;
17
18use data_forms::DataForm;
19
20#[derive(Debug, Clone)]
21pub struct Item {
22 payload: Option<Element>,
23 id: Option<String>,
24 node: Option<String>,
25 publisher: Option<Jid>,
26}
27
28impl From<Item> for Element {
29 fn from(item: Item) -> Element {
30 Element::builder("item")
31 .ns(ns::PUBSUB_EVENT)
32 .attr("id", item.id)
33 .attr("node", item.node)
34 .attr("publisher", item.publisher)
35 .append(item.payload)
36 .build()
37 }
38}
39
40generate_attribute!(Subscription, "subscription", {
41 None => "none",
42 Pending => "pending",
43 Subscribed => "subscribed",
44 Unconfigured => "unconfigured",
45}, Default = None);
46
47#[derive(Debug, Clone)]
48pub enum PubSubEvent {
49 /*
50 Collection {
51 },
52 */
53 Configuration {
54 node: String,
55 form: Option<DataForm>,
56 },
57 Delete {
58 node: String,
59 redirect: Option<String>,
60 },
61 EmptyItems {
62 node: String,
63 },
64 PublishedItems {
65 node: String,
66 items: Vec<Item>,
67 },
68 RetractedItems {
69 node: String,
70 items: Vec<String>,
71 },
72 Purge {
73 node: String,
74 },
75 Subscription {
76 node: String,
77 expiry: Option<DateTime<FixedOffset>>,
78 jid: Option<Jid>,
79 subid: Option<String>,
80 subscription: Option<Subscription>,
81 },
82}
83
84fn parse_items(elem: Element, node: String) -> Result<PubSubEvent, Error> {
85 let mut is_retract = None;
86 let mut items = vec!();
87 let mut retracts = vec!();
88 for child in elem.children() {
89 if child.is("item", ns::PUBSUB_EVENT) {
90 match is_retract {
91 None => is_retract = Some(false),
92 Some(false) => (),
93 Some(true) => return Err(Error::ParseError("Mix of item and retract in items element.")),
94 }
95 let mut payloads = child.children().cloned().collect::<Vec<_>>();
96 let payload = payloads.pop();
97 if !payloads.is_empty() {
98 return Err(Error::ParseError("More than a single payload in item element."));
99 }
100 let item = Item {
101 payload,
102 id: get_attr!(child, "id", optional),
103 node: get_attr!(child, "node", optional),
104 publisher: get_attr!(child, "publisher", optional),
105 };
106 items.push(item);
107 } else if child.is("retract", ns::PUBSUB_EVENT) {
108 match is_retract {
109 None => is_retract = Some(true),
110 Some(true) => (),
111 Some(false) => return Err(Error::ParseError("Mix of item and retract in items element.")),
112 }
113 for _ in child.children() {
114 return Err(Error::ParseError("Unknown child in retract element."));
115 }
116 for (attr, _) in child.attrs() {
117 if attr != "id" {
118 return Err(Error::ParseError("Unknown attribute in retract element."));
119 }
120 }
121 let id = get_attr!(child, "id", required);
122 retracts.push(id);
123 } else {
124 return Err(Error::ParseError("Invalid child in items element."));
125 }
126 }
127 Ok(match is_retract {
128 None => PubSubEvent::EmptyItems { node },
129 Some(false) => PubSubEvent::PublishedItems { node, items },
130 Some(true) => PubSubEvent::RetractedItems { node, items: retracts },
131 })
132}
133
134impl TryFrom<Element> for PubSubEvent {
135 type Err = Error;
136
137 fn try_from(elem: Element) -> Result<PubSubEvent, Error> {
138 if !elem.is("event", ns::PUBSUB_EVENT) {
139 return Err(Error::ParseError("This is not an event element."));
140 }
141 for _ in elem.attrs() {
142 return Err(Error::ParseError("Unknown attribute in event element."));
143 }
144 let mut payload = None;
145 for child in elem.children() {
146 /*
147 for (attr, _) in child.attrs() {
148 if attr != "node" {
149 return Err(Error::ParseError("Unknown attribute in items element."));
150 }
151 }
152 */
153 let node = get_attr!(child, "node", required);
154 if child.is("configuration", ns::PUBSUB_EVENT) {
155 let mut payloads = child.children().cloned().collect::<Vec<_>>();
156 let item = payloads.pop();
157 if !payloads.is_empty() {
158 return Err(Error::ParseError("More than a single payload in configuration element."));
159 }
160 let form = match item {
161 None => None,
162 Some(payload) => Some(DataForm::try_from(payload)?),
163 };
164 payload = Some(PubSubEvent::Configuration { node, form });
165 } else if child.is("delete", ns::PUBSUB_EVENT) {
166 let mut redirect = None;
167 for item in child.children() {
168 if item.is("redirect", ns::PUBSUB_EVENT) {
169 if redirect.is_some() {
170 return Err(Error::ParseError("More than one redirect in delete element."));
171 }
172 let uri = get_attr!(item, "uri", required);
173 redirect = Some(uri);
174 } else {
175 return Err(Error::ParseError("Unknown child in delete element."));
176 }
177 }
178 payload = Some(PubSubEvent::Delete { node, redirect });
179 } else if child.is("items", ns::PUBSUB_EVENT) {
180 payload = Some(parse_items(child.clone(), node)?);
181 } else if child.is("purge", ns::PUBSUB_EVENT) {
182 for _ in child.children() {
183 return Err(Error::ParseError("Unknown child in purge element."));
184 }
185 payload = Some(PubSubEvent::Purge { node });
186 } else if child.is("subscription", ns::PUBSUB_EVENT) {
187 for _ in child.children() {
188 return Err(Error::ParseError("Unknown child in purge element."));
189 }
190 payload = Some(PubSubEvent::Subscription {
191 node: node,
192 expiry: get_attr!(child, "expiry", optional),
193 jid: get_attr!(child, "jid", optional),
194 subid: get_attr!(child, "subid", optional),
195 subscription: get_attr!(child, "subscription", optional),
196 });
197 } else {
198 return Err(Error::ParseError("Unknown child in event element."));
199 }
200 }
201 Ok(payload.ok_or(Error::ParseError("No payload in event element."))?)
202 }
203}
204
205impl From<PubSubEvent> for Element {
206 fn from(event: PubSubEvent) -> Element {
207 let payload = match event {
208 PubSubEvent::Configuration { node, form } => {
209 Element::builder("configuration")
210 .ns(ns::PUBSUB_EVENT)
211 .attr("node", node)
212 .append(form)
213 .build()
214 },
215 PubSubEvent::Delete { node, redirect } => {
216 Element::builder("purge")
217 .ns(ns::PUBSUB_EVENT)
218 .attr("node", node)
219 .append(redirect.map(|redirect| {
220 Element::builder("redirect")
221 .ns(ns::PUBSUB_EVENT)
222 .attr("uri", redirect)
223 .build()
224 }))
225 .build()
226 },
227 PubSubEvent::EmptyItems { node } => {
228 Element::builder("items")
229 .ns(ns::PUBSUB_EVENT)
230 .attr("node", node)
231 .build()
232 },
233 PubSubEvent::PublishedItems { node, items } => {
234 Element::builder("items")
235 .ns(ns::PUBSUB_EVENT)
236 .attr("node", node)
237 .append(items)
238 .build()
239 },
240 PubSubEvent::RetractedItems { node, items } => {
241 Element::builder("items")
242 .ns(ns::PUBSUB_EVENT)
243 .attr("node", node)
244 .append(items)
245 .build()
246 },
247 PubSubEvent::Purge { node } => {
248 Element::builder("purge")
249 .ns(ns::PUBSUB_EVENT)
250 .attr("node", node)
251 .build()
252 },
253 PubSubEvent::Subscription { node, expiry, jid, subid, subscription } => {
254 Element::builder("subscription")
255 .ns(ns::PUBSUB_EVENT)
256 .attr("node", node)
257 .attr("expiry", expiry.map(|expiry| expiry.to_rfc3339()))
258 .attr("jid", jid)
259 .attr("subid", subid)
260 .attr("subscription", subscription)
261 .build()
262 },
263 };
264 Element::builder("event")
265 .ns(ns::PUBSUB_EVENT)
266 .append(payload)
267 .build()
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use std::str::FromStr;
275 use compare_elements::NamespaceAwareCompare;
276
277 #[test]
278 fn test_simple() {
279 let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='coucou'/></event>".parse().unwrap();
280 let event = PubSubEvent::try_from(elem).unwrap();
281 match event {
282 PubSubEvent::EmptyItems { node } => assert_eq!(node, String::from("coucou")),
283 _ => panic!(),
284 }
285 }
286
287 #[test]
288 fn test_simple_items() {
289 let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='coucou'><item id='test' node='huh?' publisher='test@coucou'/></items></event>".parse().unwrap();
290 let event = PubSubEvent::try_from(elem).unwrap();
291 match event {
292 PubSubEvent::PublishedItems { node, items } => {
293 assert_eq!(node, String::from("coucou"));
294 assert_eq!(items[0].id, Some(String::from("test")));
295 assert_eq!(items[0].node, Some(String::from("huh?")));
296 assert_eq!(items[0].publisher, Some(Jid::from_str("test@coucou").unwrap()));
297 assert_eq!(items[0].payload, None);
298 },
299 _ => panic!(),
300 }
301 }
302
303 #[test]
304 fn test_simple_pep() {
305 let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='something'><item><foreign xmlns='example:namespace'/></item></items></event>".parse().unwrap();
306 let event = PubSubEvent::try_from(elem).unwrap();
307 match event {
308 PubSubEvent::PublishedItems { node, items } => {
309 assert_eq!(node, String::from("something"));
310 assert_eq!(items[0].id, None);
311 assert_eq!(items[0].node, None);
312 assert_eq!(items[0].publisher, None);
313 match items[0].payload {
314 Some(ref elem) => assert!(elem.is("foreign", "example:namespace")),
315 _ => panic!(),
316 }
317 },
318 _ => panic!(),
319 }
320 }
321
322 #[test]
323 fn test_simple_retract() {
324 let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='something'><retract id='coucou'/><retract id='test'/></items></event>".parse().unwrap();
325 let event = PubSubEvent::try_from(elem).unwrap();
326 match event {
327 PubSubEvent::RetractedItems { node, items } => {
328 assert_eq!(node, String::from("something"));
329 assert_eq!(items[0], String::from("coucou"));
330 assert_eq!(items[1], String::from("test"));
331 },
332 _ => panic!(),
333 }
334 }
335
336 #[test]
337 fn test_simple_delete() {
338 let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><delete node='coucou'><redirect uri='hello'/></delete></event>".parse().unwrap();
339 let event = PubSubEvent::try_from(elem).unwrap();
340 match event {
341 PubSubEvent::Delete { node, redirect } => {
342 assert_eq!(node, String::from("coucou"));
343 assert_eq!(redirect, Some(String::from("hello")));
344 },
345 _ => panic!(),
346 }
347 }
348
349 #[test]
350 fn test_simple_purge() {
351 let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><purge node='coucou'/></event>".parse().unwrap();
352 let event = PubSubEvent::try_from(elem).unwrap();
353 match event {
354 PubSubEvent::Purge { node } => {
355 assert_eq!(node, String::from("coucou"));
356 },
357 _ => panic!(),
358 }
359 }
360
361 #[test]
362 fn test_simple_configure() {
363 let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><configuration node='coucou'><x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>http://jabber.org/protocol/pubsub#node_config</value></field></x></configuration></event>".parse().unwrap();
364 let event = PubSubEvent::try_from(elem).unwrap();
365 match event {
366 PubSubEvent::Configuration { node, form: _ } => {
367 assert_eq!(node, String::from("coucou"));
368 //assert_eq!(form.type_, Result_);
369 },
370 _ => panic!(),
371 }
372 }
373
374 #[test]
375 fn test_invalid() {
376 let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><coucou node='test'/></event>".parse().unwrap();
377 let error = PubSubEvent::try_from(elem).unwrap_err();
378 let message = match error {
379 Error::ParseError(string) => string,
380 _ => panic!(),
381 };
382 assert_eq!(message, "Unknown child in event element.");
383 }
384
385 #[test]
386 fn test_invalid_attribute() {
387 let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event' coucou=''/>".parse().unwrap();
388 let error = PubSubEvent::try_from(elem).unwrap_err();
389 let message = match error {
390 Error::ParseError(string) => string,
391 _ => panic!(),
392 };
393 assert_eq!(message, "Unknown attribute in event element.");
394 }
395
396 #[test]
397 fn test_ex221_subscription() {
398 let elem: Element = r#"
399<event xmlns='http://jabber.org/protocol/pubsub#event'>
400 <subscription
401 expiry='2006-02-28T23:59:59+00:00'
402 jid='francisco@denmark.lit'
403 node='princely_musings'
404 subid='ba49252aaa4f5d320c24d3766f0bdcade78c78d3'
405 subscription='subscribed'/>
406</event>
407"#.parse().unwrap();
408 let event = PubSubEvent::try_from(elem.clone()).unwrap();
409 match event.clone() {
410 PubSubEvent::Subscription { node, expiry, jid, subid, subscription } => {
411 assert_eq!(node, String::from("princely_musings"));
412 assert_eq!(subid, Some(String::from("ba49252aaa4f5d320c24d3766f0bdcade78c78d3")));
413 assert_eq!(subscription, Some(Subscription::Subscribed));
414 assert_eq!(jid, Some(Jid::from_str("francisco@denmark.lit").unwrap()));
415 assert_eq!(expiry, Some("2006-02-28T23:59:59Z".parse().unwrap()));
416 },
417 _ => panic!(),
418 }
419
420 let elem2: Element = event.into();
421 assert!(elem.compare_to(&elem2));
422 }
423}