Implement `#[xml(flag)]` meta

Jonas Schรคfer created

Change summary

parsers/src/util/macro_tests.rs |  42 ++++++++++
xso-proc/src/error_message.rs   |   2 
xso-proc/src/field/flag.rs      | 137 +++++++++++++++++++++++++++++++++++
xso-proc/src/field/mod.rs       |  14 +++
xso-proc/src/meta.rs            |  27 ++++++
xso-proc/src/types.rs           |  92 +++++++++++++++++++++++
xso/ChangeLog                   |   2 
xso/src/from_xml_doc.md         |  44 ++++++++++
xso/src/fromxml.rs              |  52 +++++++++++++
xso/src/lib.rs                  |  13 +++
10 files changed, 423 insertions(+), 2 deletions(-)

Detailed changes

parsers/src/util/macro_tests.rs ๐Ÿ”—

@@ -1922,3 +1922,45 @@ fn extract_ignore_unknown_stuff_roundtrip() {
     };
     roundtrip_full::<ExtractIgnoreUnknownStuff>("<parent xmlns='urn:example:ns1'><child><grandchild>hello world</grandchild></child></parent>")
 }
+
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "foo")]
+struct Flag {
+    #[xml(flag(namespace = NS1, name = "flag"))]
+    flag: bool,
+}
+
+#[test]
+fn flag_parse_present_as_true() {
+    #[allow(unused_imports)]
+    use core::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<Flag>("<foo xmlns='urn:example:ns1'><flag/></foo>") {
+        Ok(Flag { flag }) => {
+            assert!(flag);
+        }
+        other => panic!("unexpected result: {:?}", other),
+    }
+}
+
+#[test]
+fn flag_present_roundtrip() {
+    #[allow(unused_imports)]
+    use core::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<Flag>("<foo xmlns='urn:example:ns1'><flag/></foo>");
+}
+
+#[test]
+fn flag_absent_roundtrip() {
+    #[allow(unused_imports)]
+    use core::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<Flag>("<foo xmlns='urn:example:ns1'/>");
+}

xso-proc/src/error_message.rs ๐Ÿ”—

@@ -102,7 +102,7 @@ impl ParentRef {
 /// It implements [`core::fmt::Display`] for that purpose and is otherwise of
 /// little use.
 #[repr(transparent)]
-struct FieldName<'x>(&'x Member);
+pub(crate) struct FieldName<'x>(pub &'x Member);
 
 impl fmt::Display for FieldName<'_> {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {

xso-proc/src/field/flag.rs ๐Ÿ”—

@@ -0,0 +1,137 @@
+// Copyright (c) 2024 Jonas Schรคfer <jonas@zombofant.net>
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+//! This module concerns the processing of flag-style children.
+//!
+//! In particular, it provides the `#[xml(flag)]` implementation.
+
+use proc_macro2::{Span, TokenStream};
+use quote::quote;
+use syn::*;
+
+use crate::error_message::{FieldName, ParentRef};
+use crate::meta::{NameRef, NamespaceRef};
+use crate::scope::{AsItemsScope, FromEventsScope};
+use crate::types::{bool_ty, empty_builder_ty, u8_ty};
+
+use super::{Field, FieldBuilderPart, FieldIteratorPart, FieldTempInit, NestedMatcher};
+
+/// The field maps to a child element, the presence of which is represented as boolean.
+pub(super) struct FlagField {
+    /// The XML namespace of the child element.
+    pub(super) xml_namespace: NamespaceRef,
+
+    /// The XML name of the child element.
+    pub(super) xml_name: NameRef,
+}
+
+impl Field for FlagField {
+    fn make_builder_part(
+        &self,
+        scope: &FromEventsScope,
+        container_name: &ParentRef,
+        member: &Member,
+        _ty: &Type,
+    ) -> Result<FieldBuilderPart> {
+        let field_access = scope.access_field(member);
+
+        let unknown_attr_err = format!(
+            "Unknown attribute in flag child {} in {}.",
+            FieldName(&member),
+            container_name
+        );
+        let unknown_child_err = format!(
+            "Unknown child in flag child {} in {}.",
+            FieldName(&member),
+            container_name
+        );
+        let unknown_text_err = format!(
+            "Unexpected text in flag child {} in {}.",
+            FieldName(&member),
+            container_name
+        );
+
+        let xml_namespace = &self.xml_namespace;
+        let xml_name = &self.xml_name;
+
+        Ok(FieldBuilderPart::Nested {
+            extra_defs: TokenStream::new(),
+            value: FieldTempInit {
+                ty: bool_ty(Span::call_site()),
+                init: quote! { false },
+            },
+            matcher: NestedMatcher::Selective(quote! {
+                if name.0 == #xml_namespace && name.1 == #xml_name {
+                    ::xso::fromxml::Empty {
+                        attributeerr: #unknown_attr_err,
+                        childerr: #unknown_child_err,
+                        texterr: #unknown_text_err,
+                    }.start(attrs).map_err(
+                        ::xso::error::FromEventsError::Invalid
+                    )
+                } else {
+                    ::core::result::Result::Err(::xso::error::FromEventsError::Mismatch {
+                        name,
+                        attrs,
+                    })
+                }
+            }),
+            builder: empty_builder_ty(Span::call_site()),
+            collect: quote! {
+                #field_access = true;
+            },
+            finalize: quote! {
+                #field_access
+            },
+        })
+    }
+
+    fn make_iterator_part(
+        &self,
+        _scope: &AsItemsScope,
+        _container_name: &ParentRef,
+        bound_name: &Ident,
+        _member: &Member,
+        _ty: &Type,
+    ) -> Result<FieldIteratorPart> {
+        let xml_namespace = &self.xml_namespace;
+        let xml_name = &self.xml_name;
+
+        Ok(FieldIteratorPart::Content {
+            extra_defs: TokenStream::new(),
+            value: FieldTempInit {
+                init: quote! {
+                    if *#bound_name {
+                        3
+                    } else {
+                        1
+                    }
+                },
+                ty: u8_ty(Span::call_site()),
+            },
+            generator: quote! {
+                {
+                    // using wrapping_sub will make the match below crash
+                    // with unreachable!() in case we messed up somewhere.
+                    #bound_name = #bound_name.wrapping_sub(1);
+                    match #bound_name {
+                        0 => ::core::result::Result::<_, ::xso::error::Error>::Ok(::core::option::Option::None),
+                        1 => ::core::result::Result::Ok(::core::option::Option::Some(
+                            ::xso::Item::ElementFoot
+                        )),
+                        2 => ::core::result::Result::Ok(::core::option::Option::Some(
+                            ::xso::Item::ElementHeadStart(
+                                ::xso::exports::rxml::Namespace::from(#xml_namespace),
+                                ::std::borrow::Cow::Borrowed(#xml_name),
+                            )
+                        )),
+                        _ => unreachable!(),
+                    }
+                }
+            },
+        })
+    }
+}

xso-proc/src/field/mod.rs ๐Ÿ”—

@@ -20,12 +20,14 @@ mod attribute;
 mod child;
 #[cfg(feature = "minidom")]
 mod element;
+mod flag;
 mod text;
 
 use self::attribute::AttributeField;
 use self::child::{ChildField, ExtractDef};
 #[cfg(feature = "minidom")]
 use self::element::ElementField;
+use self::flag::FlagField;
 use self::text::TextField;
 
 /// Code slices necessary for declaring and initializing a temporary variable
@@ -406,6 +408,18 @@ fn new_field(
                 "#[xml(element)] requires xso to be built with the \"minidom\" feature.",
             ))
         }
+
+        XmlFieldMeta::Flag {
+            span,
+            qname: QNameRef { namespace, name },
+        } => {
+            let xml_namespace = namespace.unwrap_or_else(|| container_namespace.clone());
+            let xml_name = default_name(span, name, field_ident)?;
+            Ok(Box::new(FlagField {
+                xml_namespace,
+                xml_name,
+            }))
+        }
     }
 }
 

xso-proc/src/meta.rs ๐Ÿ”—

@@ -755,6 +755,17 @@ pub(crate) enum XmlFieldMeta {
         /// The `n` flag.
         amount: Option<AmountConstraint>,
     },
+
+    /// `#[xml(flag)]
+    Flag {
+        /// The span of the `#[xml(flag)]` meta from which this was parsed.
+        ///
+        /// This is useful for error messages.
+        span: Span,
+
+        /// The namespace/name keys.
+        qname: QNameRef,
+    },
 }
 
 impl XmlFieldMeta {
@@ -1017,6 +1028,19 @@ impl XmlFieldMeta {
         })
     }
 
+    /// Parse a `#[xml(flag)]` meta.
+    fn flag_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
+        let mut qname = QNameRef::default();
+        meta.parse_nested_meta(|meta| match qname.parse_incremental_from_meta(meta)? {
+            None => Ok(()),
+            Some(meta) => Err(Error::new_spanned(meta.path, "unsupported key")),
+        })?;
+        Ok(Self::Flag {
+            span: meta.path.span(),
+            qname,
+        })
+    }
+
     /// Parse [`Self`] from a nestd meta, switching on the identifier
     /// of that nested meta.
     fn parse_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
@@ -1030,6 +1054,8 @@ impl XmlFieldMeta {
             Self::extract_from_meta(meta)
         } else if meta.path.is_ident("element") {
             Self::element_from_meta(meta)
+        } else if meta.path.is_ident("flag") {
+            Self::flag_from_meta(meta)
         } else {
             Err(Error::new_spanned(meta.path, "unsupported field meta"))
         }
@@ -1112,6 +1138,7 @@ impl XmlFieldMeta {
             Self::Text { ref span, .. } => *span,
             Self::Extract { ref span, .. } => *span,
             Self::Element { ref span, .. } => *span,
+            Self::Flag { ref span, .. } => *span,
         }
     }
 

xso-proc/src/types.rs ๐Ÿ”—

@@ -890,3 +890,95 @@ pub(crate) fn discard_builder_ty(span: Span) -> Type {
         },
     })
 }
+
+/// Construct a [`syn::Type`] referring to the built-in `bool` type.
+///
+/// Note that we go through `xso::exports::CoreBool` for that, because there seems
+/// to be no way to access built-in types once they have been shadowed in a
+/// scope.
+pub(crate) fn bool_ty(span: Span) -> Type {
+    Type::Path(TypePath {
+        qself: None,
+        path: Path {
+            leading_colon: Some(syn::token::PathSep {
+                spans: [span, span],
+            }),
+            segments: [
+                PathSegment {
+                    ident: Ident::new("xso", span),
+                    arguments: PathArguments::None,
+                },
+                PathSegment {
+                    ident: Ident::new("exports", span),
+                    arguments: PathArguments::None,
+                },
+                PathSegment {
+                    ident: Ident::new("CoreBool", span),
+                    arguments: PathArguments::None,
+                },
+            ]
+            .into_iter()
+            .collect(),
+        },
+    })
+}
+
+/// Construct a [`syn::Type`] referring to the built-in `u8` type.
+///
+/// Note that we go through `xso::exports::CoreU8` for that, because there seems
+/// to be no way to access built-in types once they have been shadowed in a
+/// scope.
+pub(crate) fn u8_ty(span: Span) -> Type {
+    Type::Path(TypePath {
+        qself: None,
+        path: Path {
+            leading_colon: Some(syn::token::PathSep {
+                spans: [span, span],
+            }),
+            segments: [
+                PathSegment {
+                    ident: Ident::new("xso", span),
+                    arguments: PathArguments::None,
+                },
+                PathSegment {
+                    ident: Ident::new("exports", span),
+                    arguments: PathArguments::None,
+                },
+                PathSegment {
+                    ident: Ident::new("CoreU8", span),
+                    arguments: PathArguments::None,
+                },
+            ]
+            .into_iter()
+            .collect(),
+        },
+    })
+}
+
+/// Construct a [`syn::Type`] referring to `::xso::fromxml::EmptyBuilder`.
+pub(crate) fn empty_builder_ty(span: Span) -> Type {
+    Type::Path(TypePath {
+        qself: None,
+        path: Path {
+            leading_colon: Some(syn::token::PathSep {
+                spans: [span, span],
+            }),
+            segments: [
+                PathSegment {
+                    ident: Ident::new("xso", span),
+                    arguments: PathArguments::None,
+                },
+                PathSegment {
+                    ident: Ident::new("fromxml", span),
+                    arguments: PathArguments::None,
+                },
+                PathSegment {
+                    ident: Ident::new("EmptyBuilder", span),
+                    arguments: PathArguments::None,
+                },
+            ]
+            .into_iter()
+            .collect(),
+        },
+    })
+}

xso/ChangeLog ๐Ÿ”—

@@ -25,6 +25,8 @@ Version NEXT:
       - Support for "transparent" structs (newtype-like patterns for XSO).
       - FromXmlText and AsXmlText are now implemented for jid::NodePart,
         jid::DomainPart, and jid::ResourcePart (!485)
+      - Support for optional child elements, the presence of which are
+        translated into a boolean (`#[xml(flag)]`).
 
 Version 0.1.2:
 2024-07-26 Jonas Schรคfer <jonas@zombofant.net>

xso/src/from_xml_doc.md ๐Ÿ”—

@@ -27,7 +27,8 @@ assert_eq!(foo, Foo);
     2. [`child` meta](#child-meta)
     3. [`element` meta](#element-meta)
     4. [`extract` meta](#extract-meta)
-    5. [`text` meta](#text-meta)
+    5. [`flag` meta](#flag-meta)
+    6. [`text` meta](#text-meta)
 
 ## Attributes
 
@@ -557,6 +558,47 @@ assert_eq!(foo, Foo {
 });
 ```
 
+### `flag` meta
+
+The `flag` meta causes the field to be mapped to a single, optional child element. Absence of the child is equivalent to the value `false`, presence
+of the child element is equivalent to the value `true`.
+
+The following keys can be used inside the `#[xml(flag(..))]` meta:
+
+| Key | Value type | Description |
+| --- | --- | --- |
+| `namespace` | *string literal* or *path* | The optional namespace of the XML attribute to match. If it is a *path*, it must point at a `&'static str`. |
+| `name` | *string literal* or *path* | The name of the XML attribute to match. If it is a *path*, it must point at a `&'static NcNameStr`. |
+
+The field on which the `flag` meta is used must be of type `bool`.
+
+If `namespace` is not set, it defaults to the namespace of the surrounding
+container. If `name` is not set, it defaults to the field's name, if available.
+If `name` is not set and the field is unnamed, a compile-time error is raised.
+
+When parsing, any contents within the child element generate a parse error.
+
+#### Example
+```rust
+# use xso::FromXml;
+#[derive(FromXml, Debug, PartialEq)]
+#[xml(namespace = "urn:example", name = "foo")]
+struct Foo {
+    #[xml(flag(name = "flag"))]
+    flag: bool,
+};
+
+let foo: Foo = xso::from_bytes(b"<foo xmlns='urn:example'><flag/></foo>").unwrap();
+assert_eq!(foo, Foo {
+    flag: true,
+});
+
+let foo: Foo = xso::from_bytes(b"<foo xmlns='urn:example'/>").unwrap();
+assert_eq!(foo, Foo {
+    flag: false,
+});
+```
+
 ### `text` meta
 
 The `text` meta causes the field to be mapped to the text content of the

xso/src/fromxml.rs ๐Ÿ”—

@@ -269,6 +269,58 @@ impl FromEventsBuilder for Discard {
     }
 }
 
+/// Builder which discards the contents (or raises on unexpected contents).
+///
+/// This builder is only to be used from within the proc macros and is not
+/// stable, public API.
+#[doc(hidden)]
+#[cfg(feature = "macros")]
+pub struct EmptyBuilder {
+    childerr: &'static str,
+    texterr: &'static str,
+}
+
+#[cfg(feature = "macros")]
+impl FromEventsBuilder for EmptyBuilder {
+    type Output = ();
+
+    fn feed(&mut self, ev: rxml::Event) -> Result<Option<Self::Output>, Error> {
+        match ev {
+            rxml::Event::EndElement(..) => Ok(Some(())),
+            rxml::Event::StartElement(..) => Err(Error::Other(self.childerr)),
+            rxml::Event::Text(..) => Err(Error::Other(self.texterr)),
+            _ => Err(Error::Other(
+                "unexpected content in supposed-to-be-empty element",
+            )),
+        }
+    }
+}
+
+/// Precursor struct for [`EmptyBuilder`].
+///
+/// This struct is only to be used from within the proc macros and is not
+/// stable, public API.
+#[doc(hidden)]
+#[cfg(feature = "macros")]
+pub struct Empty {
+    pub attributeerr: &'static str,
+    pub childerr: &'static str,
+    pub texterr: &'static str,
+}
+
+#[cfg(feature = "macros")]
+impl Empty {
+    pub fn start(self, attr: rxml::AttrMap) -> Result<EmptyBuilder, Error> {
+        if attr.len() > 0 {
+            return Err(Error::Other(self.attributeerr));
+        }
+        Ok(EmptyBuilder {
+            childerr: self.childerr,
+            texterr: self.texterr,
+        })
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

xso/src/lib.rs ๐Ÿ”—

@@ -39,10 +39,23 @@ mod rxml_util;
 pub mod text;
 
 #[doc(hidden)]
+#[cfg(feature = "macros")]
 pub mod exports {
     #[cfg(feature = "minidom")]
     pub use minidom;
     pub use rxml;
+
+    /// The built-in `bool` type.
+    ///
+    /// This is re-exported for use by macros in cases where we cannot rely on
+    /// people not having done `type bool = str` or some similar shenanigans.
+    pub type CoreBool = bool;
+
+    /// The built-in `u8` type.
+    ///
+    /// This is re-exported for use by macros in cases where we cannot rely on
+    /// people not having done `type u8 = str` or some similar shenanigans.
+    pub type CoreU8 = u8;
 }
 
 use alloc::{