parsers: port Iq to use the derive macros

Jonas SchΓ€fer created

Change summary

parsers/ChangeLog                          |  11 
parsers/src/iq.rs                          | 516 +++++++++++++++--------
tokio-xmpp/examples/download_avatars.rs    | 110 ++--
tokio-xmpp/examples/keep_connection.rs     |   3 
tokio-xmpp/src/client/iq.rs                |  82 ++-
tokio-xmpp/src/event.rs                    |   9 
tokio-xmpp/src/stanzastream/negotiation.rs |  47 +-
xmpp/src/iq/mod.rs                         |  20 
8 files changed, 486 insertions(+), 312 deletions(-)

Detailed changes

parsers/ChangeLog πŸ”—

@@ -85,6 +85,17 @@ XXXX-YY-ZZ RELEASER <admin@example.com>
            optional `parent` attribute.
       - presence::Presence has been ported to use xso (!477). It also uses the
         message::Lang newtype since !561.
+      - iq::Iq has been ported to use xso (!572). That entails a couple
+        breaking changes:
+
+        - The IqType type has been removed and loosely replaced by the
+          IqPayload type.
+        - The Iq type is now an enum. Access to `from`, `to` and `id` needs
+          to happen via the provided accessor functions, by `match`-ing the
+          enum variant or by splitting the Iq into its IqHeader and IqPayload
+          using the `split()` method.
+        - The inverse is possible through the `assemble()` methods on
+          IqPayload, IqHeader and Iq, whichever is more convenient.
     * New parsers/serialisers:
       - Stream Features (RFC 6120) (!400)
       - Spam Reporting (XEP-0377) (!506)

parsers/src/iq.rs πŸ”—

@@ -9,8 +9,7 @@ use crate::ns;
 use crate::stanza_error::StanzaError;
 use jid::Jid;
 use minidom::Element;
-use minidom::IntoAttributeValue;
-use xso::error::{Error, FromElementError};
+use xso::{AsXml, FromXml};
 
 /// Should be implemented on every known payload of an `<iq type='get'/>`.
 pub trait IqGetPayload: TryFrom<Element> + Into<Element> {}
@@ -21,228 +20,354 @@ pub trait IqSetPayload: TryFrom<Element> + Into<Element> {}
 /// Should be implemented on every known payload of an `<iq type='result'/>`.
 pub trait IqResultPayload: TryFrom<Element> + Into<Element> {}
 
-/// Represents one of the four possible iq types.
-#[derive(Debug, Clone, PartialEq)]
-pub enum IqType {
-    /// This is a request for accessing some data.
+/// Metadata of an IQ stanza.
+pub struct IqHeader {
+    /// The sender JID.
+    pub from: Option<Jid>,
+
+    /// The reciepient JID.
+    pub to: Option<Jid>,
+
+    /// The stanza's ID.
+    pub id: String,
+}
+
+impl IqHeader {
+    /// Combine a header with [`IqPayload`] to create a full [`Iq`] stanza.
+    pub fn assemble(self, data: IqPayload) -> Iq {
+        data.assemble(self)
+    }
+}
+
+/// Payload of an IQ stanza, by type.
+pub enum IqPayload {
+    /// Payload of a type='get' stanza.
     Get(Element),
 
-    /// This is a request for modifying some data.
+    /// Payload of a type='set' stanza.
     Set(Element),
 
-    /// This is a result containing some data.
+    /// Payload of a type='result' stanza.
     Result(Option<Element>),
 
-    /// A get or set request failed.
+    /// The error carride in a type='error' stanza.
     Error(StanzaError),
 }
 
-impl IntoAttributeValue for &IqType {
-    fn into_attribute_value(self) -> Option<String> {
-        Some(
-            match *self {
-                IqType::Get(_) => "get",
-                IqType::Set(_) => "set",
-                IqType::Result(_) => "result",
-                IqType::Error(_) => "error",
-            }
-            .to_owned(),
-        )
+impl IqPayload {
+    /// Combine the data with an [`IqHeader`] to create a full [`Iq`] stanza.
+    pub fn assemble(self, IqHeader { from, to, id }: IqHeader) -> Iq {
+        match self {
+            Self::Get(payload) => Iq::Get {
+                from,
+                to,
+                id,
+                payload,
+            },
+            Self::Set(payload) => Iq::Set {
+                from,
+                to,
+                id,
+                payload,
+            },
+            Self::Result(payload) => Iq::Result {
+                from,
+                to,
+                id,
+                payload,
+            },
+            Self::Error(error) => Iq::Error {
+                from,
+                to,
+                id,
+                payload: None,
+                error,
+            },
+        }
     }
 }
 
 /// The main structure representing the `<iq/>` stanza.
-#[derive(Debug, Clone, PartialEq)]
-pub struct Iq {
-    /// The JID emitting this stanza.
-    pub from: Option<Jid>,
-
-    /// The recipient of this stanza.
-    pub to: Option<Jid>,
-
-    /// The @id attribute of this stanza, which is required in order to match a
-    /// request with its result/error.
-    pub id: String,
-
-    /// The payload content of this stanza.
-    pub payload: IqType,
+#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
+#[xml(namespace = ns::DEFAULT_NS, name = "iq", attribute = "type", exhaustive)]
+pub enum Iq {
+    /// An `<iq type='get'/>` stanza.
+    #[xml(value = "get")]
+    Get {
+        /// The JID emitting this stanza.
+        #[xml(attribute(default))]
+        from: Option<Jid>,
+
+        /// The recipient of this stanza.
+        #[xml(attribute(default))]
+        to: Option<Jid>,
+
+        /// The @id attribute of this stanza, which is required in order to match a
+        /// request with its result/error.
+        #[xml(attribute)]
+        id: String,
+
+        /// The payload content of this stanza.
+        #[xml(element(n = 1))]
+        payload: Element,
+    },
+
+    /// An `<iq type='set'/>` stanza.
+    #[xml(value = "set")]
+    Set {
+        /// The JID emitting this stanza.
+        #[xml(attribute(default))]
+        from: Option<Jid>,
+
+        /// The recipient of this stanza.
+        #[xml(attribute(default))]
+        to: Option<Jid>,
+
+        /// The @id attribute of this stanza, which is required in order to match a
+        /// request with its result/error.
+        #[xml(attribute)]
+        id: String,
+
+        /// The payload content of this stanza.
+        #[xml(element(n = 1))]
+        payload: Element,
+    },
+
+    /// An `<iq type='result'/>` stanza.
+    #[xml(value = "result")]
+    Result {
+        /// The JID emitting this stanza.
+        #[xml(attribute(default))]
+        from: Option<Jid>,
+
+        /// The recipient of this stanza.
+        #[xml(attribute(default))]
+        to: Option<Jid>,
+
+        /// The @id attribute of this stanza, which is required in order to match a
+        /// request with its result/error.
+        #[xml(attribute)]
+        id: String,
+
+        /// The payload content of this stanza.
+        #[xml(element(n = 1, default))]
+        payload: Option<Element>,
+    },
+
+    /// An `<iq type='error'/>` stanza.
+    #[xml(value = "error")]
+    Error {
+        /// The JID emitting this stanza.
+        #[xml(attribute(default))]
+        from: Option<Jid>,
+
+        /// The recipient of this stanza.
+        #[xml(attribute(default))]
+        to: Option<Jid>,
+
+        /// The @id attribute of this stanza, which is required in order to match a
+        /// request with its result/error.
+        #[xml(attribute)]
+        id: String,
+
+        /// The error carried by this stanza.
+        #[xml(child)]
+        error: StanzaError,
+
+        /// The optional payload content which caused the error.
+        ///
+        /// As per
+        /// [RFC 6120 Β§ 8.3.1](https://datatracker.ietf.org/doc/html/rfc6120#section-8.3.1),
+        /// the emitter of an error stanza MAY include the original XML which
+        /// caused the error. However, recipients MUST NOT rely on this.
+        #[xml(element(n = 1, default))]
+        payload: Option<Element>,
+    },
 }
 
 impl Iq {
+    /// Assemble a new Iq stanza from an [`IqHeader`] and the given
+    /// [`IqPayload`].
+    pub fn assemble(header: IqHeader, data: IqPayload) -> Self {
+        data.assemble(header)
+    }
+
     /// Creates an `<iq/>` stanza containing a get request.
     pub fn from_get<S: Into<String>>(id: S, payload: impl IqGetPayload) -> Iq {
-        Iq {
+        Iq::Get {
             from: None,
             to: None,
             id: id.into(),
-            payload: IqType::Get(payload.into()),
+            payload: payload.into(),
         }
     }
 
     /// Creates an `<iq/>` stanza containing a set request.
     pub fn from_set<S: Into<String>>(id: S, payload: impl IqSetPayload) -> Iq {
-        Iq {
+        Iq::Set {
             from: None,
             to: None,
             id: id.into(),
-            payload: IqType::Set(payload.into()),
+            payload: payload.into(),
         }
     }
 
     /// Creates an empty `<iq type="result"/>` stanza.
     pub fn empty_result<S: Into<String>>(to: Jid, id: S) -> Iq {
-        Iq {
+        Iq::Result {
             from: None,
             to: Some(to),
             id: id.into(),
-            payload: IqType::Result(None),
+            payload: None,
         }
     }
 
     /// Creates an `<iq/>` stanza containing a result.
     pub fn from_result<S: Into<String>>(id: S, payload: Option<impl IqResultPayload>) -> Iq {
-        Iq {
+        Iq::Result {
             from: None,
             to: None,
             id: id.into(),
-            payload: IqType::Result(payload.map(Into::into)),
+            payload: payload.map(Into::into),
         }
     }
 
     /// Creates an `<iq/>` stanza containing an error.
     pub fn from_error<S: Into<String>>(id: S, payload: StanzaError) -> Iq {
-        Iq {
+        Iq::Error {
             from: None,
             to: None,
             id: id.into(),
-            payload: IqType::Error(payload),
+            error: payload,
+            payload: None,
         }
     }
 
     /// Sets the recipient of this stanza.
     pub fn with_to(mut self, to: Jid) -> Iq {
-        self.to = Some(to);
+        *self.to_mut() = Some(to);
         self
     }
 
     /// Sets the emitter of this stanza.
     pub fn with_from(mut self, from: Jid) -> Iq {
-        self.from = Some(from);
+        *self.from_mut() = Some(from);
         self
     }
 
     /// Sets the id of this stanza, in order to later match its response.
     pub fn with_id(mut self, id: String) -> Iq {
-        self.id = id;
+        *self.id_mut() = id;
         self
     }
-}
 
-impl TryFrom<Element> for Iq {
-    type Error = FromElementError;
-
-    fn try_from(root: Element) -> Result<Iq, FromElementError> {
-        check_self!(root, "iq", DEFAULT_NS);
-        let from = get_attr!(root, "from", Option);
-        let to = get_attr!(root, "to", Option);
-        let id = get_attr!(root, "id", Required);
-        let type_: String = get_attr!(root, "type", Required);
-
-        let mut payload = None;
-        let mut error_payload = None;
-        for elem in root.children() {
-            if payload.is_some() {
-                return Err(Error::Other("Wrong number of children in iq element.").into());
-            }
-            if type_ == "error" {
-                if elem.is("error", ns::DEFAULT_NS) {
-                    if error_payload.is_some() {
-                        return Err(Error::Other("Wrong number of children in iq element.").into());
-                    }
-                    error_payload = Some(StanzaError::try_from(elem.clone())?);
-                } else if root.children().count() != 2 {
-                    return Err(Error::Other("Wrong number of children in iq element.").into());
-                }
-            } else {
-                payload = Some(elem.clone());
-            }
+    /// Access the sender address.
+    pub fn from(&self) -> Option<&Jid> {
+        match self {
+            Self::Get { from, .. }
+            | Self::Set { from, .. }
+            | Self::Result { from, .. }
+            | Self::Error { from, .. } => from.as_ref(),
         }
+    }
 
-        let type_ = if type_ == "get" {
-            if let Some(payload) = payload {
-                IqType::Get(payload)
-            } else {
-                return Err(Error::Other("Wrong number of children in iq element.").into());
-            }
-        } else if type_ == "set" {
-            if let Some(payload) = payload {
-                IqType::Set(payload)
-            } else {
-                return Err(Error::Other("Wrong number of children in iq element.").into());
-            }
-        } else if type_ == "result" {
-            if let Some(payload) = payload {
-                IqType::Result(Some(payload))
-            } else {
-                IqType::Result(None)
-            }
-        } else if type_ == "error" {
-            if let Some(payload) = error_payload {
-                IqType::Error(payload)
-            } else {
-                return Err(Error::Other("Wrong number of children in iq element.").into());
-            }
-        } else {
-            return Err(Error::Other("Unknown iq type.").into());
-        };
+    /// Access the sender address, mutably.
+    pub fn from_mut(&mut self) -> &mut Option<Jid> {
+        match self {
+            Self::Get { ref mut from, .. }
+            | Self::Set { ref mut from, .. }
+            | Self::Result { ref mut from, .. }
+            | Self::Error { ref mut from, .. } => from,
+        }
+    }
 
-        Ok(Iq {
-            from,
-            to,
-            id,
-            payload: type_,
-        })
+    /// Access the recipient address.
+    pub fn to(&self) -> Option<&Jid> {
+        match self {
+            Self::Get { to, .. }
+            | Self::Set { to, .. }
+            | Self::Result { to, .. }
+            | Self::Error { to, .. } => to.as_ref(),
+        }
     }
-}
 
-impl From<Iq> for Element {
-    fn from(iq: Iq) -> Element {
-        let mut stanza = Element::builder("iq", ns::DEFAULT_NS)
-            .attr("from", iq.from)
-            .attr("to", iq.to)
-            .attr("id", iq.id)
-            .attr("type", &iq.payload)
-            .build();
-        let elem = match iq.payload {
-            IqType::Get(elem) | IqType::Set(elem) | IqType::Result(Some(elem)) => elem,
-            IqType::Error(error) => error.into(),
-            IqType::Result(None) => return stanza,
-        };
-        stanza.append_child(elem);
-        stanza
+    /// Access the recipient address, mutably.
+    pub fn to_mut(&mut self) -> &mut Option<Jid> {
+        match self {
+            Self::Get { ref mut to, .. }
+            | Self::Set { ref mut to, .. }
+            | Self::Result { ref mut to, .. }
+            | Self::Error { ref mut to, .. } => to,
+        }
     }
-}
 
-impl ::xso::FromXml for Iq {
-    type Builder = ::xso::minidom_compat::FromEventsViaElement<Iq>;
+    /// Access the id.
+    pub fn id(&self) -> &str {
+        match self {
+            Self::Get { id, .. }
+            | Self::Set { id, .. }
+            | Self::Result { id, .. }
+            | Self::Error { id, .. } => id.as_str(),
+        }
+    }
 
-    fn from_events(
-        qname: ::xso::exports::rxml::QName,
-        attrs: ::xso::exports::rxml::AttrMap,
-        _ctx: &::xso::Context<'_>,
-    ) -> Result<Self::Builder, ::xso::error::FromEventsError> {
-        if qname.0 != crate::ns::DEFAULT_NS || qname.1 != "iq" {
-            return Err(::xso::error::FromEventsError::Mismatch { name: qname, attrs });
+    /// Access the id mutably.
+    pub fn id_mut(&mut self) -> &mut String {
+        match self {
+            Self::Get { ref mut id, .. }
+            | Self::Set { ref mut id, .. }
+            | Self::Result { ref mut id, .. }
+            | Self::Error { ref mut id, .. } => id,
         }
-        Self::Builder::new(qname, attrs)
     }
-}
 
-impl ::xso::AsXml for Iq {
-    type ItemIter<'x> = ::xso::minidom_compat::AsItemsViaElement<'x>;
+    /// Split the IQ stanza in its metadata and data.
+    ///
+    /// Note that this discards the optional original error-inducing
+    /// [`payload`][`Self::Error::payload`] of the
+    /// [`Iq::Error`][`Self::Error`] variant.
+    pub fn split(self) -> (IqHeader, IqPayload) {
+        match self {
+            Self::Get {
+                from,
+                to,
+                id,
+                payload,
+            } => (IqHeader { from, to, id }, IqPayload::Get(payload)),
+            Self::Set {
+                from,
+                to,
+                id,
+                payload,
+            } => (IqHeader { from, to, id }, IqPayload::Set(payload)),
+            Self::Result {
+                from,
+                to,
+                id,
+                payload,
+            } => (IqHeader { from, to, id }, IqPayload::Result(payload)),
+            Self::Error {
+                from,
+                to,
+                id,
+                error,
+                payload: _,
+            } => (IqHeader { from, to, id }, IqPayload::Error(error)),
+        }
+    }
 
-    fn as_xml_iter(&self) -> Result<Self::ItemIter<'_>, ::xso::error::Error> {
-        ::xso::minidom_compat::AsItemsViaElement::new(self.clone())
+    /// Return the [`IqHeader`] of this stanza, discarding the payload.
+    pub fn into_header(self) -> IqHeader {
+        self.split().0
+    }
+
+    /// Return the [`IqPayload`] of this stanza, discarding the header.
+    ///
+    /// Note that this also discards the optional original error-inducing
+    /// [`payload`][`Self::Error::payload`] of the
+    /// [`Iq::Error`][`Self::Error`] variant.
+    pub fn into_payload(self) -> IqPayload {
+        self.split().1
     }
 }
 
@@ -251,19 +376,22 @@ mod tests {
     use super::*;
     use crate::disco::DiscoInfoQuery;
     use crate::stanza_error::{DefinedCondition, ErrorType};
+    use xso::error::{Error, FromElementError};
 
     #[cfg(target_pointer_width = "32")]
     #[test]
     fn test_size() {
-        assert_size!(IqType, 108);
-        assert_size!(Iq, 152);
+        assert_size!(IqHeader, 44);
+        assert_size!(IqPayload, 108);
+        assert_size!(Iq, 212);
     }
 
     #[cfg(target_pointer_width = "64")]
     #[test]
     fn test_size() {
-        assert_size!(IqType, 216);
-        assert_size!(Iq, 304);
+        assert_size!(IqHeader, 88);
+        assert_size!(IqPayload, 216);
+        assert_size!(Iq, 424);
     }
 
     #[test]
@@ -277,20 +405,29 @@ mod tests {
             FromElementError::Invalid(Error::Other(string)) => string,
             _ => panic!(),
         };
-        assert_eq!(message, "Required attribute 'id' missing.");
+        assert_eq!(message, "Missing discriminator attribute.");
+    }
 
-        #[cfg(not(feature = "component"))]
-        let elem: Element = "<iq xmlns='jabber:client' id='coucou'/>".parse().unwrap();
-        #[cfg(feature = "component")]
-        let elem: Element = "<iq xmlns='jabber:component:accept' id='coucou'/>"
-            .parse()
-            .unwrap();
-        let error = Iq::try_from(elem).unwrap_err();
-        let message = match error {
-            FromElementError::Invalid(Error::Other(string)) => string,
-            _ => panic!(),
-        };
-        assert_eq!(message, "Required attribute 'type' missing.");
+    #[test]
+    fn test_require_id() {
+        for type_ in ["get", "set", "result", "error"] {
+            #[cfg(not(feature = "component"))]
+            let elem: Element = format!("<iq xmlns='jabber:client' type='{}'/>", type_)
+                .parse()
+                .unwrap();
+            #[cfg(feature = "component")]
+            let elem: Element = format!("<iq xmlns='jabber:component:accept' type='{}'/>", type_)
+                .parse()
+                .unwrap();
+            let error = Iq::try_from(elem).unwrap_err();
+            let message = match error {
+                FromElementError::Invalid(Error::Other(string)) => string,
+                _ => panic!(),
+            };
+            // Slicing here, because the rest of the error message is specific
+            // about the enum variant.
+            assert_eq!(&message[..33], "Required attribute field 'id' on ");
+        }
     }
 
     #[test]
@@ -309,11 +446,11 @@ mod tests {
             .unwrap();
         let iq = Iq::try_from(elem).unwrap();
         let query: Element = "<foo xmlns='bar'/>".parse().unwrap();
-        assert_eq!(iq.from, None);
-        assert_eq!(iq.to, None);
-        assert_eq!(&iq.id, "foo");
-        assert!(match iq.payload {
-            IqType::Get(element) => element == query,
+        assert_eq!(iq.from(), None);
+        assert_eq!(iq.to(), None);
+        assert_eq!(iq.id(), "foo");
+        assert!(match iq {
+            Iq::Get { payload, .. } => payload == query,
             _ => false,
         });
     }
@@ -334,11 +471,11 @@ mod tests {
             .unwrap();
         let iq = Iq::try_from(elem).unwrap();
         let vcard: Element = "<vCard xmlns='vcard-temp'/>".parse().unwrap();
-        assert_eq!(iq.from, None);
-        assert_eq!(iq.to, None);
-        assert_eq!(&iq.id, "vcard");
-        assert!(match iq.payload {
-            IqType::Set(element) => element == vcard,
+        assert_eq!(iq.from(), None);
+        assert_eq!(iq.to(), None);
+        assert_eq!(iq.id(), "vcard");
+        assert!(match iq {
+            Iq::Set { payload, .. } => payload == vcard,
             _ => false,
         });
     }
@@ -354,11 +491,11 @@ mod tests {
             .parse()
             .unwrap();
         let iq = Iq::try_from(elem).unwrap();
-        assert_eq!(iq.from, None);
-        assert_eq!(iq.to, None);
-        assert_eq!(&iq.id, "res");
-        assert!(match iq.payload {
-            IqType::Result(None) => true,
+        assert_eq!(iq.from(), None);
+        assert_eq!(iq.to(), None);
+        assert_eq!(iq.id(), "res");
+        assert!(match iq {
+            Iq::Result { payload: None, .. } => true,
             _ => false,
         });
     }
@@ -381,11 +518,14 @@ mod tests {
         let query: Element = "<query xmlns='http://jabber.org/protocol/disco#items'/>"
             .parse()
             .unwrap();
-        assert_eq!(iq.from, None);
-        assert_eq!(iq.to, None);
-        assert_eq!(&iq.id, "res");
-        assert!(match iq.payload {
-            IqType::Result(Some(element)) => element == query,
+        assert_eq!(iq.from(), None);
+        assert_eq!(iq.to(), None);
+        assert_eq!(iq.id(), "res");
+        assert!(match iq {
+            Iq::Result {
+                payload: Some(element),
+                ..
+            } => element == query,
             _ => false,
         });
     }
@@ -411,11 +551,11 @@ mod tests {
             .parse()
             .unwrap();
         let iq = Iq::try_from(elem).unwrap();
-        assert_eq!(iq.from, None);
-        assert_eq!(iq.to, None);
-        assert_eq!(iq.id, "err1");
-        match iq.payload {
-            IqType::Error(error) => {
+        assert_eq!(iq.from(), None);
+        assert_eq!(iq.to(), None);
+        assert_eq!(iq.id(), "err1");
+        match iq {
+            Iq::Error { error, .. } => {
                 assert_eq!(error.type_, ErrorType::Cancel);
                 assert_eq!(error.by, None);
                 assert_eq!(
@@ -444,7 +584,7 @@ mod tests {
             FromElementError::Invalid(Error::Other(string)) => string,
             _ => panic!(),
         };
-        assert_eq!(message, "Wrong number of children in iq element.");
+        assert_eq!(message, "Missing child field 'error' in Iq::Error element.");
     }
 
     #[test]
@@ -457,11 +597,11 @@ mod tests {
         let elem: Element = "<iq xmlns='jabber:component:accept' type='result' id='res'/>"
             .parse()
             .unwrap();
-        let iq2 = Iq {
+        let iq2 = Iq::Result {
             from: None,
             to: None,
             id: String::from("res"),
-            payload: IqType::Result(None),
+            payload: None,
         };
         let elem2 = iq2.into();
         assert_eq!(elem, elem2);
@@ -474,8 +614,8 @@ mod tests {
         #[cfg(feature = "component")]
         let elem: Element = "<iq xmlns='jabber:component:accept' type='get' id='disco'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>".parse().unwrap();
         let iq = Iq::try_from(elem).unwrap();
-        let disco_info = match iq.payload {
-            IqType::Get(payload) => DiscoInfoQuery::try_from(payload).unwrap(),
+        let disco_info = match iq {
+            Iq::Get { payload, .. } => DiscoInfoQuery::try_from(payload).unwrap(),
             _ => panic!(),
         };
         assert!(disco_info.node.is_none());

tokio-xmpp/examples/download_avatars.rs πŸ”—

@@ -11,7 +11,7 @@ use xmpp_parsers::{
     caps::{compute_disco, hash_caps, Caps},
     disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity},
     hashes::Algo,
-    iq::{Iq, IqType},
+    iq::Iq,
     jid::{BareJid, Jid},
     ns,
     presence::{Presence, Type as PresenceType},
@@ -54,63 +54,41 @@ async fn main() {
             client.send_stanza(presence.into()).await.unwrap();
         } else if let Some(stanza) = event.into_stanza() {
             match stanza {
-                Stanza::Iq(iq) => {
-                    if let IqType::Get(payload) = iq.payload {
-                        if payload.is("query", ns::DISCO_INFO) {
-                            let query = DiscoInfoQuery::try_from(payload);
-                            match query {
-                                Ok(query) => {
-                                    let mut disco = disco_info.clone();
-                                    disco.node = query.node;
-                                    let iq = Iq::from_result(iq.id, Some(disco))
-                                        .with_to(iq.from.unwrap());
-                                    client.send_stanza(iq.into()).await.unwrap();
-                                }
-                                Err(err) => {
-                                    client
-                                        .send_stanza(
-                                            make_error(
-                                                iq.from.unwrap(),
-                                                iq.id,
-                                                ErrorType::Modify,
-                                                DefinedCondition::BadRequest,
-                                                &format!("{}", err),
-                                            )
-                                            .into(),
-                                        )
-                                        .await
-                                        .unwrap();
-                                }
+                Stanza::Iq(Iq::Get {
+                    payload, id, from, ..
+                }) => {
+                    if payload.is("query", ns::DISCO_INFO) {
+                        let query = DiscoInfoQuery::try_from(payload);
+                        match query {
+                            Ok(query) => {
+                                let mut disco = disco_info.clone();
+                                disco.node = query.node;
+                                let iq = Iq::from_result(id, Some(disco)).with_to(from.unwrap());
+                                client.send_stanza(iq.into()).await.unwrap();
                             }
-                        } else {
-                            // We MUST answer unhandled get iqs with a service-unavailable error.
-                            client
-                                .send_stanza(
-                                    make_error(
-                                        iq.from.unwrap(),
-                                        iq.id,
-                                        ErrorType::Cancel,
-                                        DefinedCondition::ServiceUnavailable,
-                                        "No handler defined for this kind of iq.",
+                            Err(err) => {
+                                client
+                                    .send_stanza(
+                                        make_error(
+                                            from.unwrap(),
+                                            id,
+                                            ErrorType::Modify,
+                                            DefinedCondition::BadRequest,
+                                            &format!("{}", err),
+                                        )
+                                        .into(),
                                     )
-                                    .into(),
-                                )
-                                .await
-                                .unwrap();
-                        }
-                    } else if let IqType::Result(Some(payload)) = iq.payload {
-                        if payload.is("pubsub", ns::PUBSUB) {
-                            let pubsub = PubSub::try_from(payload).unwrap();
-                            let from = iq.from.clone().unwrap_or(jid.clone().into());
-                            handle_iq_result(pubsub, &from);
+                                    .await
+                                    .unwrap();
+                            }
                         }
-                    } else if let IqType::Set(_) = iq.payload {
-                        // We MUST answer unhandled set iqs with a service-unavailable error.
+                    } else {
+                        // We MUST answer unhandled get iqs with a service-unavailable error.
                         client
                             .send_stanza(
                                 make_error(
-                                    iq.from.unwrap(),
-                                    iq.id,
+                                    from.unwrap(),
+                                    id,
                                     ErrorType::Cancel,
                                     DefinedCondition::ServiceUnavailable,
                                     "No handler defined for this kind of iq.",
@@ -121,6 +99,34 @@ async fn main() {
                             .unwrap();
                     }
                 }
+                Stanza::Iq(Iq::Result {
+                    payload: Some(payload),
+                    from,
+                    ..
+                }) => {
+                    if payload.is("pubsub", ns::PUBSUB) {
+                        let pubsub = PubSub::try_from(payload).unwrap();
+                        let from = from.unwrap_or(jid.clone().into());
+                        handle_iq_result(pubsub, &from);
+                    }
+                }
+                Stanza::Iq(Iq::Set { from, id, .. }) => {
+                    // We MUST answer unhandled set iqs with a service-unavailable error.
+                    client
+                        .send_stanza(
+                            make_error(
+                                from.unwrap(),
+                                id,
+                                ErrorType::Cancel,
+                                DefinedCondition::ServiceUnavailable,
+                                "No handler defined for this kind of iq.",
+                            )
+                            .into(),
+                        )
+                        .await
+                        .unwrap();
+                }
+                Stanza::Iq(Iq::Error { .. }) | Stanza::Iq(Iq::Result { payload: None, .. }) => (),
                 Stanza::Message(message) => {
                     let from = message.from.clone().unwrap();
                     if let Some(body) = message.get_best_body(vec!["en"]) {

tokio-xmpp/examples/keep_connection.rs πŸ”—

@@ -88,8 +88,7 @@ async fn main() {
             _ = ping_timer.tick() => {
                 log::info!("sending ping for fun & profit");
                 ping_ctr = ping_ctr.wrapping_add(1);
-                let mut iq = Iq::from_get(format!("ping-{}", ping_ctr), ping::Ping);
-                iq.to = Some(domain.clone());
+                let iq = Iq::from_get(format!("ping-{}", ping_ctr), ping::Ping).with_to(domain.clone());
                 stream.send(Box::new(iq.into())).await;
             }
             ev = stream.next() => match ev {

tokio-xmpp/src/client/iq.rs πŸ”—

@@ -18,10 +18,7 @@ use std::sync::Mutex;
 use futures::Stream;
 use tokio::sync::oneshot;
 
-use xmpp_parsers::{
-    iq::{Iq, IqType},
-    stanza_error::StanzaError,
-};
+use xmpp_parsers::{iq::Iq, stanza_error::StanzaError};
 
 use crate::{
     event::make_id,
@@ -40,11 +37,21 @@ pub enum IqRequest {
     Set(Element),
 }
 
-impl From<IqRequest> for IqType {
-    fn from(other: IqRequest) -> IqType {
-        match other {
-            IqRequest::Get(v) => Self::Get(v),
-            IqRequest::Set(v) => Self::Set(v),
+impl IqRequest {
+    fn into_iq(self, from: Option<Jid>, to: Option<Jid>, id: String) -> Iq {
+        match self {
+            Self::Get(payload) => Iq::Get {
+                from,
+                to,
+                id,
+                payload,
+            },
+            Self::Set(payload) => Iq::Set {
+                from,
+                to,
+                id,
+                payload,
+            },
         }
     }
 }
@@ -59,11 +66,22 @@ pub enum IqResponse {
     Error(StanzaError),
 }
 
-impl From<IqResponse> for IqType {
-    fn from(other: IqResponse) -> IqType {
-        match other {
-            IqResponse::Result(v) => Self::Result(v),
-            IqResponse::Error(v) => Self::Error(v),
+impl IqResponse {
+    fn into_iq(self, from: Option<Jid>, to: Option<Jid>, id: String) -> Iq {
+        match self {
+            Self::Error(error) => Iq::Error {
+                from,
+                to,
+                id,
+                error,
+                payload: None,
+            },
+            Self::Result(payload) => Iq::Result {
+                from,
+                to,
+                id,
+                payload,
+            },
         }
     }
 }
@@ -251,22 +269,28 @@ impl IqResponseTracker {
     /// Returns the IQ stanza unharmed if it is not an IQ response matching
     /// any request which is still being tracked.
     pub fn handle_iq(&self, iq: Iq) -> ControlFlow<(), Iq> {
-        let payload = match iq.payload {
-            IqType::Error(error) => IqResponse::Error(error),
-            IqType::Result(result) => IqResponse::Result(result),
+        let (from, to, id, payload) = match iq {
+            Iq::Error {
+                from,
+                to,
+                id,
+                error,
+                payload: _,
+            } => (from, to, id, IqResponse::Error(error)),
+            Iq::Result {
+                from,
+                to,
+                id,
+                payload,
+            } => (from, to, id, IqResponse::Result(payload)),
             _ => return ControlFlow::Continue(iq),
         };
-        let key = (iq.from, iq.id);
+        let key = (from, id);
         let mut map = self.map.lock().unwrap();
         match map.remove(&key) {
             None => {
                 log::trace!("not handling IQ response from {:?} with id {:?}: no active tracker for this tuple", key.0, key.1);
-                ControlFlow::Continue(Iq {
-                    from: key.0,
-                    id: key.1,
-                    to: iq.to,
-                    payload: payload.into(),
-                })
+                ControlFlow::Continue(payload.into_iq(key.0, to, key.1))
             }
             Some(sink) => {
                 sink.complete(payload);
@@ -298,14 +322,6 @@ impl IqResponseTracker {
             inner: rx,
         };
         map.insert(key.clone(), sink);
-        (
-            Iq {
-                from,
-                to: key.0,
-                id: key.1,
-                payload: req.into(),
-            },
-            token,
-        )
+        (req.into_iq(from, key.0, key.1), token)
     }
 }

tokio-xmpp/src/event.rs πŸ”—

@@ -43,10 +43,11 @@ impl Stanza {
     pub fn ensure_id(&mut self) -> &str {
         match self {
             Self::Iq(iq) => {
-                if iq.id.is_empty() {
-                    iq.id = make_id();
+                let id = iq.id_mut();
+                if id.is_empty() {
+                    *id = make_id();
                 }
-                &iq.id
+                id
             }
             Self::Message(message) => message.id.get_or_insert_with(|| Id(make_id())).0.as_ref(),
             Self::Presence(presence) => presence.id.get_or_insert_with(make_id),
@@ -97,7 +98,7 @@ impl TryFrom<Stanza> for Presence {
 impl TryFrom<Stanza> for Iq {
     type Error = Stanza;
 
-    fn try_from(other: Stanza) -> Result<Self, Self::Error> {
+    fn try_from(other: Stanza) -> Result<Self, Stanza> {
         match other {
             Stanza::Iq(st) => Ok(st),
             other => Err(other),

tokio-xmpp/src/stanzastream/negotiation.rs πŸ”—

@@ -13,7 +13,7 @@ use futures::{ready, Sink, Stream};
 
 use xmpp_parsers::{
     bind::{BindQuery, BindResponse},
-    iq::{Iq, IqType},
+    iq::Iq,
     jid::{FullJid, Jid},
     sm,
     stream_error::{DefinedCondition, StreamError},
@@ -200,31 +200,34 @@ impl NegotiationState {
 
                 match item {
                     Ok(XmppStreamElement::Stanza(data)) => match data {
-                        Stanza::Iq(iq) if iq.id == BIND_REQ_ID => {
-                            let error = match iq.payload {
-                                IqType::Result(Some(payload)) => {
-                                    match BindResponse::try_from(payload) {
-                                        Ok(v) => {
-                                            let bound_jid = v.into();
-                                            if *sm_supported {
-                                                *self = Self::SendSmRequest {
+                        Stanza::Iq(iq) if iq.id() == BIND_REQ_ID => {
+                            let error = match iq {
+                                Iq::Result {
+                                    payload: Some(payload),
+                                    ..
+                                } => match BindResponse::try_from(payload) {
+                                    Ok(v) => {
+                                        let bound_jid = v.into();
+                                        if *sm_supported {
+                                            *self = Self::SendSmRequest {
+                                                sm_state: None,
+                                                bound_jid: Some(bound_jid),
+                                            };
+                                            return Poll::Ready(Continue(None));
+                                        } else {
+                                            return Poll::Ready(Break(
+                                                NegotiationResult::StreamReset {
                                                     sm_state: None,
-                                                    bound_jid: Some(bound_jid),
-                                                };
-                                                return Poll::Ready(Continue(None));
-                                            } else {
-                                                return Poll::Ready(Break(
-                                                    NegotiationResult::StreamReset {
-                                                        sm_state: None,
-                                                        bound_jid: Jid::from(bound_jid),
-                                                    },
-                                                ));
-                                            }
+                                                    bound_jid: Jid::from(bound_jid),
+                                                },
+                                            ));
                                         }
-                                        Err(e) => e.to_string(),
                                     }
+                                    Err(e) => e.to_string(),
+                                },
+                                Iq::Result { payload: None, .. } => {
+                                    "Bind response has no payload".to_owned()
                                 }
-                                IqType::Result(None) => "Bind response has no payload".to_owned(),
                                 _ => "Unexpected IQ type in response to bind request".to_owned(),
                             };
                             log::warn!("Received IQ matching the bind request, but parsing failed ({error})! Emitting stream error.");

xmpp/src/iq/mod.rs πŸ”—

@@ -4,7 +4,7 @@
 // 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 tokio_xmpp::parsers::iq::{Iq, IqType};
+use tokio_xmpp::parsers::iq::{Iq, IqHeader, IqPayload};
 
 use crate::{Agent, Event};
 
@@ -14,16 +14,14 @@ pub mod set;
 
 pub async fn handle_iq(agent: &mut Agent, iq: Iq) -> Vec<Event> {
     let mut events = vec![];
-    let from = iq
-        .from
-        .clone()
-        .unwrap_or_else(|| agent.client.bound_jid().unwrap().to_bare().into());
-    if let IqType::Get(payload) = iq.payload {
-        get::handle_iq_get(agent, &mut events, from, iq.to, iq.id, payload).await;
-    } else if let IqType::Result(Some(payload)) = iq.payload {
-        result::handle_iq_result(agent, &mut events, from, iq.to, iq.id, payload).await;
-    } else if let IqType::Set(payload) = iq.payload {
-        set::handle_iq_set(agent, &mut events, from, iq.to, iq.id, payload).await;
+    let (IqHeader { from, to, id }, data) = iq.split();
+    let from = from.unwrap_or_else(|| agent.client.bound_jid().unwrap().to_bare().into());
+    if let IqPayload::Get(payload) = data {
+        get::handle_iq_get(agent, &mut events, from, to, id, payload).await;
+    } else if let IqPayload::Result(Some(payload)) = data {
+        result::handle_iq_result(agent, &mut events, from, to, id, payload).await;
+    } else if let IqPayload::Set(payload) = data {
+        set::handle_iq_set(agent, &mut events, from, to, id, payload).await;
     }
     events
 }