parsers: replace some more generate_element! calls with derive macros

Jonas SchΓ€fer created

Change summary

parsers/src/data_forms.rs    |  28 +++++++
parsers/src/disco.rs         |  17 ++--
parsers/src/legacy_omemo.rs  |  60 ++++++++++-------
parsers/src/muc/user.rs      |  85 +++++++++++++++++-------
parsers/src/pubsub/owner.rs  |  64 +++++++++---------
parsers/src/pubsub/pubsub.rs | 132 +++++++++++++++++++++++--------------
parsers/src/rsm.rs           |  28 +++++++
parsers/src/sm.rs            |  26 +++---
parsers/src/util/macros.rs   |  26 +++++++
parsers/src/vcard_update.rs  |  16 ++--
10 files changed, 317 insertions(+), 165 deletions(-)

Detailed changes

parsers/src/data_forms.rs πŸ”—

@@ -4,7 +4,11 @@
 // 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 xso::error::{Error, FromElementError};
+use xso::{
+    error::{Error, FromElementError, FromEventsError},
+    exports::rxml,
+    minidom_compat, AsXml, FromXml,
+};
 
 use crate::data_forms_validate::Validate;
 use crate::media_element::MediaElement;
@@ -358,6 +362,20 @@ impl TryFrom<Element> for DataForm {
     }
 }
 
+impl FromXml for DataForm {
+    type Builder = minidom_compat::FromEventsViaElement<DataForm>;
+
+    fn from_events(
+        qname: rxml::QName,
+        attrs: rxml::AttrMap,
+    ) -> Result<Self::Builder, FromEventsError> {
+        if qname.0 != crate::ns::DATA_FORMS || qname.1 != "x" {
+            return Err(FromEventsError::Mismatch { name: qname, attrs });
+        }
+        Self::Builder::new(qname, attrs)
+    }
+}
+
 impl From<DataForm> for Element {
     fn from(form: DataForm) -> Element {
         Element::builder("x", ns::DATA_FORMS)
@@ -381,6 +399,14 @@ impl From<DataForm> for Element {
     }
 }
 
+impl AsXml for DataForm {
+    type ItemIter<'x> = minidom_compat::AsItemsViaElement<'x>;
+
+    fn as_xml_iter(&self) -> Result<Self::ItemIter<'_>, Error> {
+        minidom_compat::AsItemsViaElement::new(self.clone())
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

parsers/src/disco.rs πŸ”—

@@ -185,20 +185,21 @@ impl From<DiscoInfoResult> for Element {
     }
 }
 
-generate_element!(
 /// Structure representing a `<query xmlns='http://jabber.org/protocol/disco#items'/>` element.
 ///
 /// It should only be used in an `<iq type='get'/>`, as it can only represent
 /// the request, and not a result.
-DiscoItemsQuery, "query", DISCO_ITEMS,
-attributes: [
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = ns::DISCO_ITEMS, name = "query")]
+pub struct DiscoItemsQuery {
     /// Node on which we are doing the discovery.
-    node: Option<String> = "node",
-],
-children: [
+    #[xml(attribute(default))]
+    pub node: Option<String>,
+
     /// Optional paging via Result Set Management
-    rsm: Option<crate::rsm::SetQuery> = ("set", RSM) => SetQuery,
-]);
+    #[xml(child(default))]
+    pub rsm: Option<SetQuery>,
+}
 
 impl IqGetPayload for DiscoItemsQuery {}
 

parsers/src/legacy_omemo.rs πŸ”—

@@ -88,20 +88,28 @@ pub struct PreKeyPublic {
     pub data: Vec<u8>,
 }
 
-generate_element!(
-    /// A collection of publicly accessible data that can be used to build a session with a device, namely its public IdentityKey, a signed PreKey with corresponding signature, and a list of (single use) PreKeys.
-    Bundle, "bundle", LEGACY_OMEMO,
-    children: [
-        /// SignedPreKey public key
-        signed_pre_key_public: Option<SignedPreKeyPublic> = ("signedPreKeyPublic", LEGACY_OMEMO) => SignedPreKeyPublic,
-        /// SignedPreKey signature
-        signed_pre_key_signature: Option<SignedPreKeySignature> = ("signedPreKeySignature", LEGACY_OMEMO) => SignedPreKeySignature,
-        /// IdentityKey public key
-        identity_key: Option<IdentityKey> = ("identityKey", LEGACY_OMEMO) => IdentityKey,
-        /// List of (single use) PreKeys
-        prekeys: Option<Prekeys> = ("prekeys", LEGACY_OMEMO) => Prekeys,
-    ]
-);
+/// A collection of publicly accessible data that can be used to build a
+/// session with a device, namely its public IdentityKey, a signed PreKey with
+/// corresponding signature, and a list of (single use) PreKeys.
+#[derive(FromXml, AsXml, Debug, PartialEq, Clone)]
+#[xml(namespace = ns::LEGACY_OMEMO, name = "bundle")]
+pub struct Bundle {
+    /// SignedPreKey public key
+    #[xml(child(default))]
+    pub signed_pre_key_public: Option<SignedPreKeyPublic>,
+
+    /// SignedPreKey signature
+    #[xml(child(default))]
+    pub signed_pre_key_signature: Option<SignedPreKeySignature>,
+
+    /// IdentityKey public key
+    #[xml(child(default))]
+    pub identity_key: Option<IdentityKey>,
+
+    /// List of (single use) PreKeys
+    #[xml(child(default))]
+    pub prekeys: Option<Prekeys>,
+}
 
 impl PubSubPayload for Bundle {}
 
@@ -167,17 +175,19 @@ pub struct Payload {
     pub data: Vec<u8>,
 }
 
-generate_element!(
-    /// An OMEMO element, which can be either a MessageElement (with payload),
-    /// or a KeyTransportElement (without payload).
-    Encrypted, "encrypted", LEGACY_OMEMO,
-    children: [
-        /// The header contains encrypted keys for a message
-        header: Required<Header> = ("header", LEGACY_OMEMO) => Header,
-        /// Payload for MessageElement
-        payload: Option<Payload> = ("payload", LEGACY_OMEMO) => Payload
-    ]
-);
+/// An OMEMO element, which can be either a MessageElement (with payload),
+/// or a KeyTransportElement (without payload).
+#[derive(FromXml, AsXml, Debug, PartialEq, Clone)]
+#[xml(namespace = ns::LEGACY_OMEMO, name = "encrypted")]
+pub struct Encrypted {
+    /// The header contains encrypted keys for a message
+    #[xml(child)]
+    pub header: Header,
+
+    /// Payload for MessageElement
+    #[xml(child(default))]
+    pub payload: Option<Payload>,
+}
 
 impl MessagePayload for Encrypted {}
 

parsers/src/muc/user.rs πŸ”—

@@ -6,8 +6,9 @@
 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 use xso::{
-    error::{Error, FromElementError},
-    AsXml, FromXml,
+    error::{Error, FromElementError, FromEventsError},
+    exports::rxml,
+    minidom_compat, AsXml, FromXml,
 };
 
 use crate::message::MessagePayload;
@@ -117,6 +118,20 @@ impl TryFrom<Element> for Actor {
     }
 }
 
+impl FromXml for Actor {
+    type Builder = minidom_compat::FromEventsViaElement<Actor>;
+
+    fn from_events(
+        qname: rxml::QName,
+        attrs: rxml::AttrMap,
+    ) -> Result<Self::Builder, FromEventsError> {
+        if qname.0 != crate::ns::MUC_USER || qname.1 != "actor" {
+            return Err(FromEventsError::Mismatch { name: qname, attrs });
+        }
+        Self::Builder::new(qname, attrs)
+    }
+}
+
 impl From<Actor> for Element {
     fn from(actor: Actor) -> Element {
         let elem = Element::builder("actor", ns::MUC_USER);
@@ -129,6 +144,14 @@ impl From<Actor> for Element {
     }
 }
 
+impl AsXml for Actor {
+    type ItemIter<'x> = minidom_compat::AsItemsViaElement<'x>;
+
+    fn as_xml_iter(&self) -> Result<Self::ItemIter<'_>, Error> {
+        minidom_compat::AsItemsViaElement::new(self.clone())
+    }
+}
+
 /// Used to continue a one-to-one discussion in a room, with more than one
 /// participant.
 #[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
@@ -190,31 +213,38 @@ generate_attribute!(
     }, Default = None
 );
 
-generate_element!(
-    /// An item representing a user in a room.
-    Item, "item", MUC_USER, attributes: [
-        /// The affiliation of this user with the room.
-        affiliation: Required<Affiliation> = "affiliation",
+/// An item representing a user in a room.
+#[derive(FromXml, AsXml, Debug, PartialEq, Clone)]
+#[xml(namespace = ns::MUC_USER, name = "item")]
+pub struct Item {
+    /// The affiliation of this user with the room.
+    #[xml(attribute)]
+    affiliation: Affiliation,
 
-        /// The real JID of this user, if you are allowed to see it.
-        jid: Option<FullJid> = "jid",
+    /// The real JID of this user, if you are allowed to see it.
+    #[xml(attribute(default))]
+    jid: Option<FullJid>,
 
-        /// The current nickname of this user.
-        nick: Option<String> = "nick",
+    /// The current nickname of this user.
+    #[xml(attribute(default))]
+    nick: Option<String>,
 
-        /// The current role of this user.
-        role: Required<Role> = "role",
-    ], children: [
-        /// The actor affected by this item.
-        actor: Option<Actor> = ("actor", MUC_USER) => Actor,
+    /// The current role of this user.
+    #[xml(attribute)]
+    role: Role,
 
-        /// Whether this continues a one-to-one discussion.
-        continue_: Option<Continue> = ("continue", MUC_USER) => Continue,
+    /// The actor affected by this item.
+    #[xml(child(default))]
+    actor: Option<Actor>,
 
-        /// A reason for this item.
-        reason: Option<Reason> = ("reason", MUC_USER) => Reason
-    ]
-);
+    /// Whether this continues a one-to-one discussion.
+    #[xml(child(default))]
+    continue_: Option<Continue>,
+
+    /// A reason for this item.
+    #[xml(child(default))]
+    reason: Option<Reason>,
+}
 
 impl Item {
     /// Creates a new item with the given affiliation and role.
@@ -590,6 +620,8 @@ mod tests {
     #[test]
     fn test_item_invalid_attr() {
         let elem: Element = "<item xmlns='http://jabber.org/protocol/muc#user'
+                  affiliation='member'
+                  role='moderator'
                   foo='bar'/>"
             .parse()
             .unwrap();
@@ -598,7 +630,7 @@ mod tests {
             FromElementError::Invalid(Error::Other(string)) => string,
             _ => panic!(),
         };
-        assert_eq!(message, "Unknown attribute in item element.".to_owned());
+        assert_eq!(message, "Unknown attribute in Item element.".to_owned());
     }
 
     #[test]
@@ -622,7 +654,10 @@ mod tests {
             FromElementError::Invalid(Error::Other(string)) => string,
             _ => panic!(),
         };
-        assert_eq!(message, "Required attribute 'role' missing.".to_owned());
+        assert_eq!(
+            message,
+            "Required attribute field 'role' on Item element missing.".to_owned()
+        );
     }
 
     #[test]
@@ -652,7 +687,7 @@ mod tests {
         };
         assert_eq!(
             message,
-            "Required attribute 'affiliation' missing.".to_owned()
+            "Required attribute field 'affiliation' on Item element missing.".to_owned()
         );
     }
 

parsers/src/pubsub/owner.rs πŸ”—

@@ -41,40 +41,40 @@ pub struct Affiliation {
     affiliation: AffiliationAttribute,
 }
 
-generate_element!(
-    /// Request to configure a node.
-    Configure, "configure", PUBSUB_OWNER,
-    attributes: [
-        /// The node to be configured.
-        node: Option<NodeName> = "node",
-    ],
-    children: [
-        /// The form to configure it.
-        form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
-    ]
-);
+/// Request to configure a node.
+#[derive(FromXml, AsXml, Debug, PartialEq, Clone)]
+#[xml(namespace = ns::PUBSUB_OWNER, name = "configure")]
+pub struct Configure {
+    /// The node to be configured.
+    #[xml(attribute(default))]
+    pub node: Option<NodeName>,
 
-generate_element!(
-    /// Request to change default configuration.
-    Default, "default", PUBSUB_OWNER,
-    children: [
-        /// The form to configure it.
-        form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
-    ]
-);
+    /// The form to configure it.
+    #[xml(child(default))]
+    pub form: Option<DataForm>,
+}
 
-generate_element!(
-    /// Request to delete a node.
-    Delete, "delete", PUBSUB_OWNER,
-    attributes: [
-        /// The node to be configured.
-        node: Required<NodeName> = "node",
-    ],
-    children: [
-        /// Redirection to replace the deleted node.
-        redirect: Option<Redirect> = ("redirect", PUBSUB_OWNER) => Redirect
-    ]
-);
+/// Request to retrieve default configuration.
+#[derive(FromXml, AsXml, Debug, PartialEq, Clone)]
+#[xml(namespace = ns::PUBSUB_OWNER, name = "default")]
+pub struct Default {
+    /// The form to configure it.
+    #[xml(child(default))]
+    pub form: Option<DataForm>,
+}
+
+/// Request to delete a node.
+#[derive(FromXml, AsXml, Debug, PartialEq, Clone)]
+#[xml(namespace = ns::PUBSUB_OWNER, name = "delete")]
+pub struct Delete {
+    /// The node to be deleted.
+    #[xml(attribute)]
+    pub node: NodeName,
+
+    /// Redirection to replace the deleted node.
+    #[xml(child(default))]
+    pub redirect: Option<Redirect>,
+}
 
 /// A redirect element.
 #[derive(FromXml, AsXml, PartialEq, Debug, Clone)]

parsers/src/pubsub/pubsub.rs πŸ”—

@@ -5,8 +5,9 @@
 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 use xso::{
-    error::{Error, FromElementError},
-    AsXml, FromXml,
+    error::{Error, FromElementError, FromEventsError},
+    exports::rxml,
+    minidom_compat, AsXml, FromXml,
 };
 
 use crate::data_forms::DataForm;
@@ -46,14 +47,14 @@ pub struct Affiliation {
     pub affiliation: AffiliationAttribute,
 }
 
-generate_element!(
-    /// Request to configure a new node.
-    Configure, "configure", PUBSUB,
-    children: [
-        /// The form to configure it.
-        form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
-    ]
-);
+/// Request to configure a new node.
+#[derive(FromXml, AsXml, Debug, PartialEq, Clone)]
+#[xml(namespace = ns::PUBSUB, name = "configure")]
+pub struct Configure {
+    /// The form to configure it.
+    #[xml(child(default))]
+    pub form: Option<DataForm>,
+}
 
 /// Request to create a new node.
 #[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
@@ -114,24 +115,26 @@ pub struct Item(pub PubSubItem);
 
 impl_pubsub_item!(Item, PUBSUB);
 
-generate_element!(
-    /// The options associated to a subscription request.
-    Options, "options", PUBSUB,
-    attributes: [
-        /// The JID affected by this request.
-        jid: Required<Jid> = "jid",
+/// The options associated to a subscription request.
+#[derive(FromXml, AsXml, Debug, PartialEq, Clone)]
+#[xml(namespace = ns::PUBSUB, name = "options")]
+pub struct Options {
+    /// The JID affected by this request.
+    #[xml(attribute)]
+    pub jid: Jid,
 
-        /// The node affected by this request.
-        node: Option<NodeName> = "node",
+    /// The node affected by this request.
+    #[xml(attribute(default))]
+    pub node: Option<NodeName>,
 
-        /// The subscription identifier affected by this request.
-        subid: Option<SubscriptionId> = "subid",
-    ],
-    children: [
-        /// The form describing the subscription.
-        form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
-    ]
-);
+    /// The subscription identifier affected by this request.
+    #[xml(attribute(default))]
+    pub subid: Option<SubscriptionId>,
+
+    /// The form describing the subscription.
+    #[xml(child(default))]
+    pub form: Option<DataForm>,
+}
 
 generate_element!(
     /// Request to publish items to a node.
@@ -146,14 +149,14 @@ generate_element!(
     ]
 );
 
-generate_element!(
-    /// The options associated to a publish request.
-    PublishOptions, "publish-options", PUBSUB,
-    children: [
-        /// The form describing these options.
-        form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
-    ]
-);
+/// The options associated to a publish request.
+#[derive(FromXml, AsXml, Debug, PartialEq, Clone)]
+#[xml(namespace = ns::PUBSUB, name = "publish-options")]
+pub struct PublishOptions {
+    /// The form describing these options.
+    #[xml(child(default))]
+    pub form: Option<DataForm>,
+}
 
 generate_attribute!(
     /// Whether a retract request should notify subscribers or not.
@@ -209,6 +212,20 @@ impl TryFrom<Element> for SubscribeOptions {
     }
 }
 
+impl FromXml for SubscribeOptions {
+    type Builder = minidom_compat::FromEventsViaElement<SubscribeOptions>;
+
+    fn from_events(
+        qname: rxml::QName,
+        attrs: rxml::AttrMap,
+    ) -> Result<Self::Builder, FromEventsError> {
+        if qname.0 != crate::ns::PUBSUB || qname.1 != "subscribe-options" {
+            return Err(FromEventsError::Mismatch { name: qname, attrs });
+        }
+        Self::Builder::new(qname, attrs)
+    }
+}
+
 impl From<SubscribeOptions> for Element {
     fn from(subscribe_options: SubscribeOptions) -> Element {
         Element::builder("subscribe-options", ns::PUBSUB)
@@ -221,6 +238,14 @@ impl From<SubscribeOptions> for Element {
     }
 }
 
+impl AsXml for SubscribeOptions {
+    type ItemIter<'x> = minidom_compat::AsItemsViaElement<'x>;
+
+    fn as_xml_iter(&self) -> Result<Self::ItemIter<'_>, Error> {
+        minidom_compat::AsItemsViaElement::new(self.clone())
+    }
+}
+
 /// A request to subscribe a JID to a node.
 #[derive(FromXml, AsXml, Debug, PartialEq, Clone)]
 #[xml(namespace = ns::PUBSUB, name = "subscribe")]
@@ -247,27 +272,30 @@ generate_element!(
     ]
 );
 
-generate_element!(
-    /// A subscription element, describing the state of a subscription.
-    SubscriptionElem, "subscription", PUBSUB,
-    attributes: [
-        /// The JID affected by this subscription.
-        jid: Required<Jid> = "jid",
+/// A subscription element, describing the state of a subscription.
+#[derive(FromXml, AsXml, Debug, PartialEq, Clone)]
+#[xml(namespace = ns::PUBSUB, name = "subscription")]
+pub struct SubscriptionElem {
+    /// The JID affected by this subscription.
+    #[xml(attribute)]
+    jid: Jid,
 
-        /// The node affected by this subscription.
-        node: Option<NodeName> = "node",
+    /// The node affected by this subscription.
+    #[xml(attribute(default))]
+    node: Option<NodeName>,
 
-        /// The subscription identifier for this subscription.
-        subid: Option<SubscriptionId> = "subid",
+    /// The subscription identifier for this subscription.
+    #[xml(attribute(default))]
+    subid: Option<SubscriptionId>,
 
-        /// The state of the subscription.
-        subscription: Option<Subscription> = "subscription",
-    ],
-    children: [
-        /// The options related to this subscription.
-        subscribe_options: Option<SubscribeOptions> = ("subscribe-options", PUBSUB) => SubscribeOptions
-    ]
-);
+    /// The state of the subscription.
+    #[xml(attribute(default))]
+    subscription: Option<Subscription>,
+
+    /// The options related to this subscription.
+    #[xml(child(default))]
+    subscribe_options: Option<SubscribeOptions>,
+}
 
 /// An unsubscribe request.
 #[derive(FromXml, AsXml, Debug, PartialEq, Clone)]

parsers/src/rsm.rs πŸ”—

@@ -6,7 +6,11 @@
 
 use crate::ns;
 use minidom::Element;
-use xso::error::{Error, FromElementError};
+use xso::{
+    error::{Error, FromElementError, FromEventsError},
+    exports::rxml,
+    minidom_compat, AsXml, FromXml,
+};
 
 /// Requests paging through a potentially big set of items (represented by an
 /// UID).
@@ -67,6 +71,20 @@ impl TryFrom<Element> for SetQuery {
     }
 }
 
+impl FromXml for SetQuery {
+    type Builder = minidom_compat::FromEventsViaElement<SetQuery>;
+
+    fn from_events(
+        qname: rxml::QName,
+        attrs: rxml::AttrMap,
+    ) -> Result<Self::Builder, FromEventsError> {
+        if qname.0 != crate::ns::RSM || qname.1 != "set" {
+            return Err(FromEventsError::Mismatch { name: qname, attrs });
+        }
+        Self::Builder::new(qname, attrs)
+    }
+}
+
 impl From<SetQuery> for Element {
     fn from(set: SetQuery) -> Element {
         Element::builder("set", ns::RSM)
@@ -93,6 +111,14 @@ impl From<SetQuery> for Element {
     }
 }
 
+impl AsXml for SetQuery {
+    type ItemIter<'x> = minidom_compat::AsItemsViaElement<'x>;
+
+    fn as_xml_iter(&self) -> Result<Self::ItemIter<'_>, Error> {
+        minidom_compat::AsItemsViaElement::new(self.clone())
+    }
+}
+
 /// Describes the paging result of a [query](struct.SetQuery.html).
 #[derive(Debug, Clone, PartialEq)]
 pub struct SetResult {

parsers/src/sm.rs πŸ”—

@@ -93,19 +93,19 @@ pub struct Enabled {
     pub resume: ResumeAttr,
 }
 
-generate_element!(
-    /// A stream management error happened.
-    Failed, "failed", SM,
-    attributes: [
-        /// The last handled stanza.
-        h: Option<u32> = "h",
-    ],
-    children: [
-        /// The error returned.
-        // XXX: implement the * handling.
-        error: Option<DefinedCondition> = ("*", XMPP_STANZAS) => DefinedCondition
-    ]
-);
+/// A stream management error happened.
+#[derive(FromXml, AsXml, Debug, PartialEq, Clone)]
+#[xml(namespace = ns::SM, name = "failed")]
+pub struct Failed {
+    /// The last handled stanza.
+    #[xml(attribute)]
+    pub h: Option<u32>,
+
+    /// The error returned.
+    // XXX: implement the * handling.
+    #[xml(child(default))]
+    pub error: Option<DefinedCondition>,
+}
 
 /// Requests the currently received stanzas by the other party.
 #[derive(FromXml, AsXml, PartialEq, Debug, Clone)]

parsers/src/util/macros.rs πŸ”—

@@ -300,6 +300,24 @@ macro_rules! generate_element_enum {
                 })
             }
         }
+
+        impl ::xso::FromXml for $elem {
+            type Builder = ::xso::minidom_compat::FromEventsViaElement<$elem>;
+
+            fn from_events(
+                qname: ::xso::exports::rxml::QName,
+                attrs: ::xso::exports::rxml::AttrMap,
+            ) -> Result<Self::Builder, ::xso::error::FromEventsError> {
+                if qname.0 != crate::ns::$ns || qname.1 != $name {
+                    return Err(::xso::error::FromEventsError::Mismatch {
+                        name: qname,
+                        attrs,
+                    })
+                }
+                Self::Builder::new(qname, attrs)
+            }
+        }
+
         impl From<$elem> for minidom::Element {
             fn from(elem: $elem) -> minidom::Element {
                 minidom::Element::builder(
@@ -311,6 +329,14 @@ macro_rules! generate_element_enum {
                     .build()
             }
         }
+
+        impl ::xso::AsXml for $elem {
+            type ItemIter<'x> = ::xso::minidom_compat::AsItemsViaElement<'x>;
+
+            fn as_xml_iter(&self) -> Result<Self::ItemIter<'_>, ::xso::error::Error> {
+                ::xso::minidom_compat::AsItemsViaElement::new(self.clone())
+            }
+        }
     );
 }
 

parsers/src/vcard_update.rs πŸ”—

@@ -16,14 +16,14 @@ use xso::{text::FixedHex, AsXml, FromXml};
 
 use crate::ns;
 
-generate_element!(
-    /// The presence payload for an avatar VCard update
-    VCardUpdate, "x", VCARD_UPDATE,
-    children: [
-        /// The photo element. Is empty if "a client is not yet ready to advertise an image".
-        photo: Option<Photo> = ("photo", VCARD_UPDATE) => Photo,
-    ]
-);
+/// The presence payload for an avatar VCard update
+#[derive(FromXml, AsXml, Debug, PartialEq, Clone)]
+#[xml(namespace = ns::VCARD_UPDATE, name = "x")]
+pub struct VCardUpdate {
+    /// The photo element. Is empty if "a client is not yet ready to advertise an image".
+    #[xml(child(default))]
+    pub photo: Option<Photo>,
+}
 
 /// The photo element containing the avatar metadata
 #[derive(FromXml, AsXml, PartialEq, Debug, Clone)]