diff --git a/src/macros.rs b/src/macros.rs index 0a2648d5..81a7f82d 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -101,6 +101,38 @@ macro_rules! generate_attribute { } } ); + ($elem:ident, $name:tt, bool) => ( + #[derive(Debug, Clone, PartialEq)] + pub enum $elem { + /// True value, represented by either 'true' or '1'. + True, + /// False value, represented by either 'false' or '0'. + False, + } + impl ::std::str::FromStr for $elem { + type Err = ::error::Error; + fn from_str(s: &str) -> Result { + Ok(match s { + "true" | "1" => $elem::True, + "false" | "0" => $elem::False, + _ => return Err(::error::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))), + }) + } + } + impl ::minidom::IntoAttributeValue for $elem { + fn into_attribute_value(self) -> Option { + match self { + $elem::True => Some(String::from("true")), + $elem::False => None + } + } + } + impl ::std::default::Default for $elem { + fn default() -> $elem { + $elem::False + } + } + ); } macro_rules! generate_element_enum { @@ -392,6 +424,9 @@ macro_rules! generate_element_with_text { } macro_rules! generate_element_with_children { + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_type:ty = $attr_name:tt => $attr_action:tt),*,], children: [$($(#[$child_meta:meta])* $child_ident:ident: Vec<$child_type:ty> = ($child_name:tt, $child_ns:ident) => $child_constructor:ident),+]) => ( + generate_element_with_children!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_type = $attr_name => $attr_action),*], children: [$($(#[$child_meta])* $child_ident: Vec<$child_type> = ($child_name, $child_ns) => $child_constructor),+]); + ); ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_type:ty = $attr_name:tt => $attr_action:tt),*], children: [$($(#[$child_meta:meta])* $child_ident:ident: Vec<$child_type:ty> = ($child_name:tt, $child_ns:ident) => $child_constructor:ident),+]) => ( $(#[$meta])* #[derive(Debug, Clone)] @@ -448,4 +483,107 @@ macro_rules! generate_element_with_children { } } ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, child: ($(#[$child_meta:meta])* $child_ident:ident: Option<$child_type:ty> = ($child_name:tt, $child_ns:ident) => $child_constructor:ident)) => ( + generate_element_with_children!($(#[$meta])* $elem, $name, $ns, attributes: [], child: ($(#[$child_meta])* $child_ident: Option<$child_type> = ($child_name, $child_ns) => $child_constructor)); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_type:ty = $attr_name:tt => $attr_action:tt),*,], child: ($(#[$child_meta:meta])* $child_ident:ident: Option<$child_type:ty> = ($child_name:tt, $child_ns:ident) => $child_constructor:ident)) => ( + generate_element_with_children!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_type = $attr_name => $attr_action),*], child: ($(#[$child_meta])* $child_ident: Option<$child_type> = ($child_name, $child_ns) => $child_constructor)); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_type:ty = $attr_name:tt => $attr_action:tt),*], child: ($(#[$child_meta:meta])* $child_ident:ident: Option<$child_type:ty> = ($child_name:tt, $child_ns:ident) => $child_constructor:ident)) => ( + $(#[$meta])* + #[derive(Debug, Clone)] + pub struct $elem { + $( + $(#[$attr_meta])* + pub $attr: $attr_type, + )* + $(#[$child_meta])* + pub $child_ident: Option<$child_type>, + } + + impl ::try_from::TryFrom<::minidom::Element> for $elem { + type Err = ::error::Error; + + fn try_from(elem: ::minidom::Element) -> Result<$elem, ::error::Error> { + check_self!(elem, $name, $ns); + check_no_unknown_attributes!(elem, $name, [$($attr_name),*]); + let mut parsed_child = None; + for child in elem.children() { + if child.is($child_name, ::ns::$child_ns) { + parsed_child = Some($child_constructor::try_from(child.clone())?); + continue; + } + return Err(::error::Error::ParseError(concat!("Unknown child in ", $name, " element."))); + } + Ok($elem { + $( + $attr: get_attr!(elem, $attr_name, $attr_action), + )* + $child_ident: parsed_child, + }) + } + } + + impl From<$elem> for ::minidom::Element { + fn from(elem: $elem) -> ::minidom::Element { + ::minidom::Element::builder($name) + .ns(::ns::$ns) + $( + .attr($attr_name, elem.$attr) + )* + .append(elem.$child_ident) + .build() + } + } + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, child: ($(#[$child_meta:meta])* $child_ident:ident: $child_type:ty = ($child_name:tt, $child_ns:ident) => $child_constructor:ident)) => ( + generate_element_with_children!($(#[$meta])* $elem, $name, $ns, attributes: [], child: ($(#[$child_meta])* $child_ident: $child_type = ($child_name, $child_ns) => $child_constructor)); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_type:ty = $attr_name:tt => $attr_action:tt),*], child: ($(#[$child_meta:meta])* $child_ident:ident: $child_type:ty = ($child_name:tt, $child_ns:ident) => $child_constructor:ident)) => ( + $(#[$meta])* + #[derive(Debug, Clone)] + pub struct $elem { + $( + $(#[$attr_meta])* + pub $attr: $attr_type, + )* + $(#[$child_meta])* + pub $child_ident: $child_type, + } + + impl ::try_from::TryFrom<::minidom::Element> for $elem { + type Err = ::error::Error; + + fn try_from(elem: ::minidom::Element) -> Result<$elem, ::error::Error> { + check_self!(elem, $name, $ns); + check_no_unknown_attributes!(elem, $name, [$($attr_name),*]); + let mut parsed_child = None; + for child in elem.children() { + if child.is($child_name, ::ns::$child_ns) { + parsed_child = Some($child_constructor::try_from(child.clone())?); + continue; + } + return Err(::error::Error::ParseError(concat!("Unknown child in ", $name, " element."))); + } + Ok($elem { + $( + $attr: get_attr!(elem, $attr_name, $attr_action), + )* + $child_ident: parsed_child.ok_or(::error::Error::ParseError(concat!("Missing child ", $child_name, " in ", $name, " element.")))?, + }) + } + } + + impl From<$elem> for ::minidom::Element { + fn from(elem: $elem) -> ::minidom::Element { + ::minidom::Element::builder($name) + .ns(::ns::$ns) + $( + .attr($attr_name, elem.$attr) + )* + .append(elem.$child_ident) + .build() + } + } + ); } diff --git a/src/pubsub/mod.rs b/src/pubsub/mod.rs index 735fb9c6..e12d0c1a 100644 --- a/src/pubsub/mod.rs +++ b/src/pubsub/mod.rs @@ -5,8 +5,10 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. pub mod event; +pub mod pubsub; pub use self::event::PubSubEvent; +pub use self::pubsub::PubSub; generate_id!(NodeName); generate_id!(ItemId); diff --git a/src/pubsub/pubsub.rs b/src/pubsub/pubsub.rs new file mode 100644 index 00000000..ce1160d8 --- /dev/null +++ b/src/pubsub/pubsub.rs @@ -0,0 +1,490 @@ +// Copyright (c) 2018 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use try_from::TryFrom; + +use minidom::Element; +use jid::Jid; + +use error::Error; + +use ns; + +use data_forms::DataForm; + +use pubsub::{NodeName, ItemId, Subscription, SubscriptionId}; + +// TODO: a better solution would be to split this into a query and a result elements, like for +// XEP-0030. +generate_element_with_children!( + Affiliations, "affiliations", PUBSUB, + attributes: [ + node: Option = "node" => optional, + ], + children: [ + affiliations: Vec = ("affiliation", PUBSUB) => Affiliation + ] +); + +generate_attribute!( + AffiliationAttribute, "affiliation", { + Member => "member", + None => "none", + Outcast => "outcast", + Owner => "owner", + Publisher => "publisher", + PublishOnly => "publish-only", + } +); + +generate_element_with_only_attributes!( + Affiliation, "affiliation", PUBSUB, [ + node: NodeName = "node" => required, + affiliation: AffiliationAttribute = "affiliation" => required, + ] +); + +generate_element_with_children!( + Configure, "configure", PUBSUB, + child: ( + form: Option = ("x", DATA_FORMS) => DataForm + ) +); + +generate_element_with_only_attributes!( + Create, "create", PUBSUB, [ + node: Option = "node" => optional, + ] +); + +generate_element_with_only_attributes!( + Default, "default", PUBSUB, [ + node: Option = "node" => optional, + // TODO: do we really want to support collection nodes? + // type: String = "type" => optional, + ] +); + +generate_element_with_children!( + Items, "items", PUBSUB, + attributes: [ + // TODO: should be an xs:positiveInteger, that is, an unbounded int ≥ 1. + max_items: Option = "max_items" => optional, + node: NodeName = "node" => required, + subid: Option = "subid" => optional, + ], + children: [ + items: Vec = ("item", PUBSUB) => Item + ] +); + +#[derive(Debug, Clone)] +pub struct Item { + payload: Option, + id: Option, +} + +impl TryFrom for Item { + type Err = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "item", PUBSUB); + check_no_unknown_attributes!(elem, "item", ["id"]); + let mut payloads = elem.children().cloned().collect::>(); + let payload = payloads.pop(); + if !payloads.is_empty() { + return Err(Error::ParseError("More than a single payload in item element.")); + } + Ok(Item { + payload, + id: get_attr!(elem, "id", optional), + }) + } +} + +impl From for Element { + fn from(item: Item) -> Element { + Element::builder("item") + .ns(ns::PUBSUB) + .attr("id", item.id) + .append(item.payload) + .build() + } +} + +generate_element_with_children!( + Options, "options", PUBSUB, + attributes: [ + jid: Jid = "jid" => required, + node: Option = "node" => optional, + subid: Option = "subid" => optional, + ], + child: ( + form: Option = ("x", DATA_FORMS) => DataForm + ) +); + +generate_element_with_children!( + Publish, "publish", PUBSUB, + attributes: [ + node: NodeName = "node" => required, + ], + children: [ + items: Vec = ("item", PUBSUB) => Item + ] +); + +generate_element_with_children!( + PublishOptions, "publish-options", PUBSUB, + child: ( + form: DataForm = ("x", DATA_FORMS) => DataForm + ) +); + +generate_attribute!(Notify, "notify", bool); + +generate_element_with_children!( + Retract, "retract", PUBSUB, + attributes: [ + node: NodeName = "node" => required, + notify: Notify = "notify" => default, + ], + children: [ + items: Vec = ("item", PUBSUB) => Item + ] +); + +#[derive(Debug, Clone)] +pub struct SubscribeOptions { + required: bool, +} + +impl TryFrom for SubscribeOptions { + type Err = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "subscribe-options", PUBSUB); + check_no_attributes!(elem, "subscribe-options"); + let mut required = false; + for child in elem.children() { + if child.is("required", ns::PUBSUB) { + if required { + return Err(Error::ParseError("More than one required element in subscribe-options.")); + } + required = true; + } else { + return Err(Error::ParseError("Unknown child in subscribe-options element.")); + } + } + Ok(SubscribeOptions { required }) + } +} + +impl From for Element { + fn from(subscribe_options: SubscribeOptions) -> Element { + Element::builder("subscribe-options") + .ns(ns::PUBSUB) + .append(if subscribe_options.required { + vec!(Element::builder("required") + .ns(ns::PUBSUB) + .build()) + } else { + vec!() + }) + .build() + } +} + +generate_element_with_only_attributes!( + Subscribe, "subscribe", PUBSUB, [ + jid: Jid = "jid" => required, + node: Option = "node" => optional, + ] +); + +generate_element_with_children!( + Subscriptions, "subscriptions", PUBSUB, + attributes: [ + node: Option = "node" => optional, + ], + children: [ + subscription: Vec = ("subscription", PUBSUB) => SubscriptionElem + ] +); + +generate_element_with_children!( + SubscriptionElem, "subscription", PUBSUB, + attributes: [ + jid: Jid = "jid" => required, + node: Option = "node" => optional, + subid: Option = "subid" => optional, + subscription: Option = "subscription" => optional, + ], + child: ( + subscribe_options: Option = ("subscribe-options", PUBSUB) => SubscribeOptions + ) +); + +generate_element_with_only_attributes!( + Unsubscribe, "unsubscribe", PUBSUB, [ + jid: Jid = "jid" => required, + node: Option = "node" => optional, + subid: Option = "subid" => optional, + ] +); + +#[derive(Debug, Clone)] +pub enum PubSub { + Create { + create: Create, + configure: Option + }, + Publish { + publish: Publish, + publish_options: Option + }, + Affiliations(Affiliations), + Default(Default), + Items(Items), + Retract(Retract), + Subscription(SubscriptionElem), + Subscriptions(Subscriptions), + Unsubscribe(Unsubscribe), +} + +impl TryFrom for PubSub { + type Err = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "pubsub", PUBSUB); + check_no_attributes!(elem, "pubsub"); + + let mut payload = None; + for child in elem.children() { + if child.is("create", ns::PUBSUB) { + if payload.is_some() { + return Err(Error::ParseError("…")); + } + let create = Create::try_from(child.clone())?; + payload = Some(PubSub::Create { create, configure: None }); + } else { + return Err(Error::ParseError("Unknown child in pubsub element.")); + } + } + Ok(payload.ok_or(Error::ParseError("No payload in pubsub element."))?) + } +} + +impl From for Element { + fn from(pubsub: PubSub) -> Element { + Element::builder("pubsub") + .ns(ns::PUBSUB) + .append(match pubsub { + PubSub::Create { create, configure } => { + let mut elems = vec!(Element::from(create)); + if let Some(configure) = configure { + elems.push(Element::from(configure)); + } + elems + }, + PubSub::Publish { publish, publish_options } => { + let mut elems = vec!(Element::from(publish)); + if let Some(publish_options) = publish_options { + elems.push(Element::from(publish_options)); + } + elems + }, + PubSub::Affiliations(affiliations) => vec!(Element::from(affiliations)), + PubSub::Default(default) => vec!(Element::from(default)), + PubSub::Items(items) => vec!(Element::from(items)), + PubSub::Retract(retract) => vec!(Element::from(retract)), + PubSub::Subscription(subscription) => vec!(Element::from(subscription)), + PubSub::Subscriptions(subscriptions) => vec!(Element::from(subscriptions)), + PubSub::Unsubscribe(unsubscribe) => vec!(Element::from(unsubscribe)), + }) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use compare_elements::NamespaceAwareCompare; + + #[test] + fn invalid_empty_pubsub() { + let elem: Element = "".parse().unwrap(); + let error = PubSub::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "No payload in pubsub element."); + /* + match pubsub { + PubSub::EmptyItems { node } => assert_eq!(node, NodeName(String::from("coucou"))), + _ => panic!(), + } + */ + } + + #[test] + fn publish_option() { + let elem: Element = "http://jabber.org/protocol/pubsub#publish-options".parse().unwrap(); + let publish_options = PublishOptions::try_from(elem).unwrap(); + assert_eq!(&publish_options.form.form_type.unwrap(), "http://jabber.org/protocol/pubsub#publish-options"); + } + + #[test] + fn subscribe_options() { + let elem1: Element = "".parse().unwrap(); + let subscribe_options1 = SubscribeOptions::try_from(elem1).unwrap(); + assert_eq!(subscribe_options1.required, false); + + let elem2: Element = "".parse().unwrap(); + let subscribe_options2 = SubscribeOptions::try_from(elem2).unwrap(); + assert_eq!(subscribe_options2.required, true); + } + + /* + #[test] + fn test_simple_items() { + let elem: Element = "".parse().unwrap(); + let event = PubSub::try_from(elem).unwrap(); + match event { + PubSub::PublishedItems { node, items } => { + assert_eq!(node, NodeName(String::from("coucou"))); + assert_eq!(items[0].id, Some(ItemId(String::from("test")))); + assert_eq!(items[0].node, Some(NodeName(String::from("huh?")))); + assert_eq!(items[0].publisher, Some(Jid::from_str("test@coucou").unwrap())); + assert_eq!(items[0].payload, None); + }, + _ => panic!(), + } + } + + #[test] + fn test_simple_pep() { + let elem: Element = "".parse().unwrap(); + let event = PubSub::try_from(elem).unwrap(); + match event { + PubSub::PublishedItems { node, items } => { + assert_eq!(node, NodeName(String::from("something"))); + assert_eq!(items[0].id, None); + assert_eq!(items[0].node, None); + assert_eq!(items[0].publisher, None); + match items[0].payload { + Some(ref elem) => assert!(elem.is("foreign", "example:namespace")), + _ => panic!(), + } + }, + _ => panic!(), + } + } + + #[test] + fn test_simple_retract() { + let elem: Element = "".parse().unwrap(); + let event = PubSub::try_from(elem).unwrap(); + match event { + PubSub::RetractedItems { node, items } => { + assert_eq!(node, NodeName(String::from("something"))); + assert_eq!(items[0], ItemId(String::from("coucou"))); + assert_eq!(items[1], ItemId(String::from("test"))); + }, + _ => panic!(), + } + } + + #[test] + fn test_simple_delete() { + let elem: Element = "".parse().unwrap(); + let event = PubSub::try_from(elem).unwrap(); + match event { + PubSub::Delete { node, redirect } => { + assert_eq!(node, NodeName(String::from("coucou"))); + assert_eq!(redirect, Some(String::from("hello"))); + }, + _ => panic!(), + } + } + + #[test] + fn test_simple_purge() { + let elem: Element = "".parse().unwrap(); + let event = PubSub::try_from(elem).unwrap(); + match event { + PubSub::Purge { node } => { + assert_eq!(node, NodeName(String::from("coucou"))); + }, + _ => panic!(), + } + } + + #[test] + fn test_simple_configure() { + let elem: Element = "http://jabber.org/protocol/pubsub#node_config".parse().unwrap(); + let event = PubSub::try_from(elem).unwrap(); + match event { + PubSub::Configuration { node, form: _ } => { + assert_eq!(node, NodeName(String::from("coucou"))); + //assert_eq!(form.type_, Result_); + }, + _ => panic!(), + } + } + + #[test] + fn test_invalid() { + let elem: Element = "".parse().unwrap(); + let error = PubSub::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in event element."); + } + + #[test] + fn test_invalid_attribute() { + let elem: Element = "".parse().unwrap(); + let error = PubSub::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in event element."); + } + + #[test] + fn test_ex221_subscription() { + let elem: Element = r#" + + + +"#.parse().unwrap(); + let event = PubSub::try_from(elem.clone()).unwrap(); + match event.clone() { + PubSub::Subscription { node, expiry, jid, subid, subscription } => { + assert_eq!(node, NodeName(String::from("princely_musings"))); + assert_eq!(subid, Some(SubscriptionId(String::from("ba49252aaa4f5d320c24d3766f0bdcade78c78d3")))); + assert_eq!(subscription, Some(Subscription::Subscribed)); + assert_eq!(jid, Some(Jid::from_str("francisco@denmark.lit").unwrap())); + assert_eq!(expiry, Some("2006-02-28T23:59:59Z".parse().unwrap())); + }, + _ => panic!(), + } + + let elem2: Element = event.into(); + assert!(elem.compare_to(&elem2)); + } + */ +}