event.rs

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