parsers: port extracts over to derive macros

Jonas Schäfer created

Change summary

parsers/ChangeLog               |   4 
parsers/src/data_forms.rs       |  28 ++--
parsers/src/mix.rs              | 109 +++++++++++-----------
parsers/src/util/macro_tests.rs | 170 +++++++++++++++++++++++++++++++++++
4 files changed, 243 insertions(+), 68 deletions(-)

Detailed changes

parsers/ChangeLog 🔗

@@ -5,6 +5,10 @@ XXXX-YY-ZZ RELEASER <admin@example.com>
         `xmpp_parsers::stanza_error::StanzaError` has been moved into the
         corresponding enum variants of the
         `xmpp_parsers::stanza_error::DefinedCondition` where it may occur.
+      - The `xmpp_parsers::mix::Participant::jid` and `..::Mix::jid` fields
+        are now of type `BareJid` (instead of `String`), as they should always
+        have been. This influences various other places, such as these
+        struct's constructors.
     * New parsers/serialisers:
         - Stream Features (RFC 6120) (!400)
 

parsers/src/data_forms.rs 🔗

@@ -15,18 +15,18 @@ use crate::media_element::MediaElement;
 use crate::ns;
 use minidom::Element;
 
-generate_element!(
-    /// Represents one of the possible values for a list- field.
-    Option_, "option", DATA_FORMS,
-    attributes: [
-        /// The optional label to be displayed to the user for this option.
-        label: Option<String> = "label"
-    ],
-    children: [
-        /// The value returned to the server when selecting this option.
-        value: Required<String> = ("value", DATA_FORMS) => String
-    ]
-);
+/// Represents one of the possible values for a list- field.
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = ns::DATA_FORMS, name = "option")]
+pub struct Option_ {
+    /// The optional label to be displayed to the user for this option.
+    #[xml(attribute(default))]
+    pub label: Option<String>,
+
+    /// The value returned to the server when selecting this option.
+    #[xml(extract(namespace = ns::DATA_FORMS, name = "value", fields(text)))]
+    pub value: String,
+}
 
 generate_attribute!(
     /// The type of a [field](struct.Field.html) element.
@@ -588,7 +588,7 @@ mod tests {
             FromElementError::Invalid(Error::Other(string)) => string,
             _ => panic!(),
         };
-        assert_eq!(message, "Missing child value in option element.");
+        assert_eq!(message, "Missing child field 'value' in Option_ element.");
 
         let elem: Element = "<option xmlns='jabber:x:data' label='Coucou !'><value>coucou</value><value>error</value></option>".parse().unwrap();
         let error = Option_::try_from(elem).unwrap_err();
@@ -598,7 +598,7 @@ mod tests {
         };
         assert_eq!(
             message,
-            "Element option must not have more than one value child."
+            "Option_ element must not have more than one child in field 'value'."
         );
     }
 

parsers/src/mix.rs 🔗

@@ -32,25 +32,25 @@ generate_id!(
     ChannelId
 );
 
-generate_element!(
-    /// Represents a participant in a MIX channel, usually returned on the
-    /// urn:xmpp:mix:nodes:participants PubSub node.
-    Participant, "participant", MIX_CORE,
-    children: [
-        /// The nick of this participant.
-        nick: Required<String> = ("nick", MIX_CORE) => String,
-
-        /// The bare JID of this participant.
-        // TODO: should be a BareJid!
-        jid: Required<String> = ("jid", MIX_CORE) => String
-    ]
-);
+/// Represents a participant in a MIX channel, usually returned on the
+/// urn:xmpp:mix:nodes:participants PubSub node.
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = ns::MIX_CORE, name = "participant")]
+pub struct Participant {
+    /// The nick of this participant.
+    #[xml(extract(namespace = ns::MIX_CORE, name = "nick", fields(text)))]
+    pub nick: String,
+
+    /// The bare JID of this participant.
+    #[xml(extract(namespace = ns::MIX_CORE, name = "jid", fields(text)))]
+    pub jid: BareJid,
+}
 
 impl PubSubPayload for Participant {}
 
 impl Participant {
     /// Create a new MIX participant.
-    pub fn new<J: Into<String>, N: Into<String>>(jid: J, nick: N) -> Participant {
+    pub fn new<J: Into<BareJid>, N: Into<String>>(jid: J, nick: N) -> Participant {
         Participant {
             nick: nick.into(),
             jid: jid.into(),
@@ -76,21 +76,22 @@ impl Subscribe {
     }
 }
 
-generate_element!(
-    /// A request from a user’s server to join a MIX channel.
-    Join, "join", MIX_CORE,
-    attributes: [
-        /// The participant identifier returned by the MIX service on successful join.
-        id: Option<ParticipantId> = "id",
-    ],
-    children: [
-        /// The nick requested by the user or set by the service.
-        nick: Required<String> = ("nick", MIX_CORE) => String,
-
-        /// Which MIX nodes to subscribe to.
-        subscribes: Vec<Subscribe> = ("subscribe", MIX_CORE) => Subscribe
-    ]
-);
+/// A request from a user’s server to join a MIX channel.
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = ns::MIX_CORE, name = "join")]
+pub struct Join {
+    /// The participant identifier returned by the MIX service on successful join.
+    #[xml(attribute(default))]
+    pub id: Option<ParticipantId>,
+
+    /// The nick requested by the user or set by the service.
+    #[xml(extract(namespace = ns::MIX_CORE, name = "nick", fields(text)))]
+    pub nick: String,
+
+    /// Which MIX nodes to subscribe to.
+    #[xml(child(n = ..))]
+    pub subscribes: Vec<Subscribe>,
+}
 
 impl IqSetPayload for Join {}
 impl IqResultPayload for Join {}
@@ -158,14 +159,14 @@ pub struct Leave;
 impl IqSetPayload for Leave {}
 impl IqResultPayload for Leave {}
 
-generate_element!(
-    /// A request to change the user’s nick.
-    SetNick, "setnick", MIX_CORE,
-    children: [
-        /// The new requested nick.
-        nick: Required<String> = ("nick", MIX_CORE) => String
-    ]
-);
+/// A request to change the user’s nick.
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = ns::MIX_CORE, name = "setnick")]
+pub struct SetNick {
+    /// The new requested nick.
+    #[xml(extract(namespace = ns::MIX_CORE, name = "nick", fields(text)))]
+    pub nick: String,
+}
 
 impl IqSetPayload for SetNick {}
 impl IqResultPayload for SetNick {}
@@ -177,25 +178,25 @@ impl SetNick {
     }
 }
 
-generate_element!(
-    /// Message payload describing who actually sent the message, since unlike in MUC, all messages
-    /// are sent from the channel’s JID.
-    Mix, "mix", MIX_CORE,
-    children: [
-        /// The nick of the user who said something.
-        nick: Required<String> = ("nick", MIX_CORE) => String,
-
-        /// The JID of the user who said something.
-        // TODO: should be a BareJid!
-        jid: Required<String> = ("jid", MIX_CORE) => String
-    ]
-);
+/// Message payload describing who actually sent the message, since unlike in MUC, all messages
+/// are sent from the channel’s JID.
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = ns::MIX_CORE, name = "mix")]
+pub struct Mix {
+    /// The nick of the user who said something.
+    #[xml(extract(namespace = ns::MIX_CORE, name = "nick", fields(text)))]
+    pub nick: String,
+
+    /// The JID of the user who said something.
+    #[xml(extract(namespace = ns::MIX_CORE, name = "jid", fields(text)))]
+    pub jid: BareJid,
+}
 
 impl MessagePayload for Mix {}
 
 impl Mix {
     /// Create a new Mix element.
-    pub fn new<N: Into<String>, J: Into<String>>(nick: N, jid: J) -> Mix {
+    pub fn new<N: Into<String>, J: Into<BareJid>>(nick: N, jid: J) -> Mix {
         Mix {
             nick: nick.into(),
             jid: jid.into(),
@@ -263,7 +264,7 @@ mod tests {
             .unwrap();
         let participant = Participant::try_from(elem).unwrap();
         assert_eq!(participant.nick, "coucou");
-        assert_eq!(participant.jid, "foo@bar");
+        assert_eq!(participant.jid.as_str(), "foo@bar");
     }
 
     #[test]
@@ -316,7 +317,7 @@ mod tests {
                 .unwrap();
         let mix = Mix::try_from(elem).unwrap();
         assert_eq!(mix.nick, "coucou");
-        assert_eq!(mix.jid, "foo@bar");
+        assert_eq!(mix.jid.as_str(), "foo@bar");
     }
 
     #[test]
@@ -362,7 +363,7 @@ mod tests {
             "<setnick xmlns='urn:xmpp:mix:core:1'><nick>coucou</nick></setnick>"
         );
 
-        let elem: Element = Mix::new("coucou", "coucou@example").into();
+        let elem: Element = Mix::new("coucou", "coucou@example".parse::<BareJid>().unwrap()).into();
         let xml = String::from(&elem);
         assert_eq!(
             xml,

parsers/src/util/macro_tests.rs 🔗

@@ -852,6 +852,78 @@ fn text_extract_positive() {
     }
 }
 
+#[test]
+fn text_extract_negative_absent_child() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<TextExtract>("<parent xmlns='urn:example:ns1'/>") {
+        Err(xso::error::FromElementError::Invalid(xso::error::Error::Other(e)))
+            if e.contains("Missing child field") =>
+        {
+            ()
+        }
+        other => panic!("unexpected result: {:?}", other),
+    }
+}
+
+#[test]
+fn text_extract_negative_unexpected_attribute_in_child() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<TextExtract>("<parent xmlns='urn:example:ns1'><child foo='bar'/></parent>") {
+        Err(xso::error::FromElementError::Invalid(xso::error::Error::Other(e)))
+            if e.contains("Unknown attribute") =>
+        {
+            ()
+        }
+        other => panic!("unexpected result: {:?}", other),
+    }
+}
+
+#[test]
+fn text_extract_negative_unexpected_child_in_child() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<TextExtract>(
+        "<parent xmlns='urn:example:ns1'><child><quak/></child></parent>",
+    ) {
+        Err(xso::error::FromElementError::Invalid(xso::error::Error::Other(e)))
+            if e.contains("Unknown child in extraction") =>
+        {
+            ()
+        }
+        other => panic!("unexpected result: {:?}", other),
+    }
+}
+
+#[test]
+fn text_extract_negative_duplicate_child() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<TextExtract>(
+        "<parent xmlns='urn:example:ns1'><child>hello world</child><child>more</child></parent>",
+    ) {
+        Err(xso::error::FromElementError::Invalid(xso::error::Error::Other(e)))
+            if e.contains("must not have more than one") =>
+        {
+            ()
+        }
+        other => panic!("unexpected result: {:?}", other),
+    }
+}
+
 #[test]
 fn text_extract_roundtrip() {
     #[allow(unused_imports)]
@@ -888,6 +960,42 @@ fn attribute_extract_positive() {
     }
 }
 
+#[test]
+fn attribute_extract_negative_absent_attribute() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<AttributeExtract>("<parent xmlns='urn:example:ns1'><child/></parent>") {
+        Err(xso::error::FromElementError::Invalid(xso::error::Error::Other(e)))
+            if e.contains("Required attribute") =>
+        {
+            ()
+        }
+        other => panic!("unexpected result: {:?}", other),
+    }
+}
+
+#[test]
+fn attribute_extract_negative_unexpected_text_in_child() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<AttributeExtract>(
+        "<parent xmlns='urn:example:ns1'><child foo='hello world'>fnord</child></parent>",
+    ) {
+        Err(xso::error::FromElementError::Invalid(xso::error::Error::Other(e)))
+            if e.contains("Unexpected text") =>
+        {
+            ()
+        }
+        other => panic!("unexpected result: {:?}", other),
+    }
+}
+
 #[test]
 fn attribute_extract_roundtrip() {
     #[allow(unused_imports)]
@@ -900,6 +1008,68 @@ fn attribute_extract_roundtrip() {
     )
 }
 
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "parent")]
+struct OptionalAttributeExtract {
+    #[xml(extract(namespace = NS1, name = "child", fields(attribute(name = "foo", default))))]
+    contents: ::std::option::Option<String>,
+}
+
+#[test]
+fn optional_attribute_extract_positive_present() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<OptionalAttributeExtract>(
+        "<parent xmlns='urn:example:ns1'><child foo='hello world'/></parent>",
+    ) {
+        Ok(OptionalAttributeExtract {
+            contents: Some(contents),
+        }) => {
+            assert_eq!(contents, "hello world");
+        }
+        other => panic!("unexpected result: {:?}", other),
+    }
+}
+
+#[test]
+fn optional_attribute_extract_positive_absent() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<OptionalAttributeExtract>("<parent xmlns='urn:example:ns1'><child/></parent>")
+    {
+        Ok(OptionalAttributeExtract { contents: None }) => (),
+        other => panic!("unexpected result: {:?}", other),
+    }
+}
+
+#[test]
+fn optional_attribute_extract_roundtrip_present() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<OptionalAttributeExtract>(
+        "<parent xmlns='urn:example:ns1'><child foo='hello world'/></parent>",
+    )
+}
+
+#[test]
+fn optional_attribute_extract_roundtrip_absent() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<OptionalAttributeExtract>("<parent xmlns='urn:example:ns1'><child/></parent>")
+}
+
 #[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
 #[xml(namespace = NS1, name = "parent")]
 struct ChildExtract {