From 1e0bccc50417140f21085b487537b959d83d1ebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Sat, 3 Aug 2024 13:45:57 +0200 Subject: [PATCH] parsers: port extracts over to derive macros --- parsers/ChangeLog | 4 + parsers/src/data_forms.rs | 28 +++--- parsers/src/mix.rs | 103 +++++++++---------- parsers/src/util/macro_tests.rs | 170 ++++++++++++++++++++++++++++++++ 4 files changed, 240 insertions(+), 65 deletions(-) diff --git a/parsers/ChangeLog b/parsers/ChangeLog index 242fc2b9..9ea610c7 100644 --- a/parsers/ChangeLog +++ b/parsers/ChangeLog @@ -5,6 +5,10 @@ XXXX-YY-ZZ RELEASER `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) diff --git a/parsers/src/data_forms.rs b/parsers/src/data_forms.rs index cc444ac1..b1add0db 100644 --- a/parsers/src/data_forms.rs +++ b/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 = "label" - ], - children: [ - /// The value returned to the server when selecting this option. - value: Required = ("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, + + /// 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 = "".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'." ); } diff --git a/parsers/src/mix.rs b/parsers/src/mix.rs index bb6704f0..ac2ef6b2 100644 --- a/parsers/src/mix.rs +++ b/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 = ("nick", 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. - // TODO: should be a BareJid! - jid: Required = ("jid", MIX_CORE) => 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, N: Into>(jid: J, nick: N) -> Participant { + pub fn new, N: Into>(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 = "id", - ], - children: [ - /// The nick requested by the user or set by the service. - nick: Required = ("nick", MIX_CORE) => String, +/// 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, - /// Which MIX nodes to subscribe to. - subscribes: Vec = ("subscribe", MIX_CORE) => Subscribe - ] -); + /// 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, +} 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 = ("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 = ("nick", 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. - // TODO: should be a BareJid! - jid: Required = ("jid", MIX_CORE) => 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, J: Into>(nick: N, jid: J) -> Mix { + pub fn new, J: Into>(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 { "coucou" ); - let elem: Element = Mix::new("coucou", "coucou@example").into(); + let elem: Element = Mix::new("coucou", "coucou@example".parse::().unwrap()).into(); let xml = String::from(&elem); assert_eq!( xml, diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index 561a6e2d..1d1c4f15 100644 --- a/parsers/src/util/macro_tests.rs +++ b/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::("") { + 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::("") { + 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::( + "", + ) { + 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::( + "hello worldmore", + ) { + 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::("") { + 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::( + "fnord", + ) { + 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, +} + +#[test] +fn optional_attribute_extract_positive_present() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + match parse_str::( + "", + ) { + 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::("") + { + 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::( + "", + ) +} + +#[test] +fn optional_attribute_extract_roundtrip_absent() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::("") +} + #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct ChildExtract {