diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index c08c5919141efe48046992b896ca62a133ba1b9d..1d8eb64da1d296c52a9ca7157ccf6884b4cfd73c 100644 --- a/parsers/src/util/macro_tests.rs +++ b/parsers/src/util/macro_tests.rs @@ -1922,3 +1922,45 @@ fn extract_ignore_unknown_stuff_roundtrip() { }; roundtrip_full::("hello world") } + +#[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::("") { + 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::(""); +} + +#[test] +fn flag_absent_roundtrip() { + #[allow(unused_imports)] + use core::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::(""); +} diff --git a/xso-proc/src/error_message.rs b/xso-proc/src/error_message.rs index fbf307fb8710d414730ddfff86e8400c4a68d7f8..da1cd72e9291621769f0aa12862b08846eea09fd 100644 --- a/xso-proc/src/error_message.rs +++ b/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 { diff --git a/xso-proc/src/field/flag.rs b/xso-proc/src/field/flag.rs new file mode 100644 index 0000000000000000000000000000000000000000..77dd122fa0a68c17ef50c18dd93ea97516892f6f --- /dev/null +++ b/xso-proc/src/field/flag.rs @@ -0,0 +1,137 @@ +// Copyright (c) 2024 Jonas Schäfer +// +// 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 { + 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 { + 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!(), + } + } + }, + }) + } +} diff --git a/xso-proc/src/field/mod.rs b/xso-proc/src/field/mod.rs index 572f556a5a272216ad6f03468aa8327a1bce81c1..069a7fe40b57988adb7c0c9377a360662f80f349 100644 --- a/xso-proc/src/field/mod.rs +++ b/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, + })) + } } } diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs index fc033b43384b3eaf8b45c6e31fca68cfd0b1e49f..849ea61a605e9768a6c409a2a3a6342f2636acf6 100644 --- a/xso-proc/src/meta.rs +++ b/xso-proc/src/meta.rs @@ -755,6 +755,17 @@ pub(crate) enum XmlFieldMeta { /// The `n` flag. amount: Option, }, + + /// `#[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 { + 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 { @@ -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, } } diff --git a/xso-proc/src/types.rs b/xso-proc/src/types.rs index 2392a22c11ce9018cc2d1f82868965f4354bdb22..ad6901a77c1bde7d001fc2b7dd4523e471a94481 100644 --- a/xso-proc/src/types.rs +++ b/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(), + }, + }) +} diff --git a/xso/ChangeLog b/xso/ChangeLog index b7cdc2bc6837635d822b51e92d00fe26b95262b1..4562cef625386aaeab4c55dcd9fae7213dc39050 100644 --- a/xso/ChangeLog +++ b/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 diff --git a/xso/src/from_xml_doc.md b/xso/src/from_xml_doc.md index ecf0a1f3b5966b6d2129fefec774ac57b99c75a5..99ae6e30cdfdebe01150dd307701ef77ee239629 100644 --- a/xso/src/from_xml_doc.md +++ b/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"").unwrap(); +assert_eq!(foo, Foo { + flag: true, +}); + +let foo: Foo = xso::from_bytes(b"").unwrap(); +assert_eq!(foo, Foo { + flag: false, +}); +``` + ### `text` meta The `text` meta causes the field to be mapped to the text content of the diff --git a/xso/src/fromxml.rs b/xso/src/fromxml.rs index 431a2355e096e0a532e65e3d6630f111ef2ed26e..4e3ac85430a3cdb2cac71ddbbbed78359630c525 100644 --- a/xso/src/fromxml.rs +++ b/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, 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 { + if attr.len() > 0 { + return Err(Error::Other(self.attributeerr)); + } + Ok(EmptyBuilder { + childerr: self.childerr, + texterr: self.texterr, + }) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/xso/src/lib.rs b/xso/src/lib.rs index 3417936ead9c07c6038e2c647f2ced599b60db88..5fee3065654fe31e3b258989c65c768544452db3 100644 --- a/xso/src/lib.rs +++ b/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::{