diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index 7a9ecd3d2388a6cbc4e6515229ffb823b5e665aa..7cd690fcfefc936dcd47531df6c170188bcb3b6b 100644 --- a/parsers/src/util/macro_tests.rs +++ b/parsers/src/util/macro_tests.rs @@ -1570,3 +1570,33 @@ fn element_catchall_roundtrip() { "", ) } + +#[derive(FromXml, AsXml, PartialEq, Debug, Clone)] +#[xml(transparent)] +struct TransparentStruct(RequiredAttribute); + +#[test] +fn transparent_struct_roundtrip() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::(""); +} + +#[derive(FromXml, AsXml, PartialEq, Debug, Clone)] +#[xml(transparent)] +struct TransparentStructNamed { + foo: RequiredAttribute, +} + +#[test] +fn transparent_struct_named_roundtrip() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::(""); +} diff --git a/xso-proc/src/enums.rs b/xso-proc/src/enums.rs index 404eb0ee5b067aa360d5e42fa97ad400378d1661..221ca1f000c5df09343d67c3018252a94de4772a 100644 --- a/xso-proc/src/enums.rs +++ b/xso-proc/src/enums.rs @@ -44,6 +44,7 @@ impl NameVariant { debug, builder, iterator, + transparent, } = XmlCompoundMeta::parse_from_attributes(&decl.attrs)?; reject_key!(debug flag not on "enum variants" only on "enums and structs"); @@ -51,6 +52,7 @@ impl NameVariant { reject_key!(namespace not on "enum variants" only on "enums and structs"); reject_key!(builder not on "enum variants" only on "enums and structs"); reject_key!(iterator not on "enum variants" only on "enums and structs"); + reject_key!(transparent flag not on "named enum variants" only on "structs"); let Some(name) = name else { return Err(Error::new(meta_span, "`name` is required on enum variants")); @@ -179,9 +181,11 @@ impl EnumDef { debug, builder, iterator, + transparent, } = meta; reject_key!(name not on "enums" only on "their variants"); + reject_key!(transparent flag not on "enums" only on "structs"); let Some(namespace) = namespace else { return Err(Error::new(meta_span, "`namespace` is required on enums")); diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs index f64de0268cf785bc672f07f7aadfd1c7c068467a..69b4abc2a1e3b2cbb3e5e6131ecb300b1efe7e59 100644 --- a/xso-proc/src/meta.rs +++ b/xso-proc/src/meta.rs @@ -23,7 +23,7 @@ pub const XMLNS_XML: &str = "http://www.w3.org/XML/1998/namespace"; pub const XMLNS_XMLNS: &str = "http://www.w3.org/2000/xmlns/"; macro_rules! reject_key { - ($key:ident not on $not_allowed_on:literal only on $only_allowed_on:literal) => { + ($key:ident not on $not_allowed_on:literal $(only on $only_allowed_on:literal)?) => { if let Some($key) = $key { return Err(Error::new_spanned( $key, @@ -32,15 +32,17 @@ macro_rules! reject_key { stringify!($key), "` is not allowed on ", $not_allowed_on, - " (only on ", - $only_allowed_on, - ")" + $( + " (only on ", + $only_allowed_on, + ")", + )? ), )); } }; - ($key:ident flag not on $not_allowed_on:literal only on $only_allowed_on:literal) => { + ($key:ident flag not on $not_allowed_on:literal $(only on $only_allowed_on:literal)?) => { if let Flag::Present($key) = $key { return Err(Error::new( $key, @@ -49,9 +51,11 @@ macro_rules! reject_key { stringify!($key), "` is not allowed on ", $not_allowed_on, - " (only on ", - $only_allowed_on, - ")" + $( + " (only on ", + $only_allowed_on, + ")", + )? ), )); } @@ -252,6 +256,13 @@ impl Flag { Self::Present(_) => true, } } + + /// Like `Option::take`, but for flags. + pub(crate) fn take(&mut self) -> Self { + let mut result = Flag::Absent; + core::mem::swap(&mut result, self); + result + } } impl From for Flag { @@ -340,6 +351,9 @@ pub(crate) struct XmlCompoundMeta { /// The exhaustive flag. pub(crate) exhaustive: Flag, + + /// The transparent flag. + pub(crate) transparent: Flag, } impl XmlCompoundMeta { @@ -353,6 +367,7 @@ impl XmlCompoundMeta { let mut iterator = None; let mut debug = Flag::Absent; let mut exhaustive = Flag::Absent; + let mut transparent = Flag::Absent; attr.parse_nested_meta(|meta| { if meta.path.is_ident("debug") { @@ -379,6 +394,12 @@ impl XmlCompoundMeta { } exhaustive = (&meta.path).into(); Ok(()) + } else if meta.path.is_ident("transparent") { + if transparent.is_set() { + return Err(Error::new_spanned(meta.path, "duplicate `transparent` key")); + } + transparent = (&meta.path).into(); + Ok(()) } else { match qname.parse_incremental_from_meta(meta)? { None => Ok(()), @@ -394,6 +415,7 @@ impl XmlCompoundMeta { builder, iterator, exhaustive, + transparent, }) } diff --git a/xso-proc/src/structs.rs b/xso-proc/src/structs.rs index c0d8b78432eed53789ce78e78702c21b7cd2ecfe..42fcc8b2b97c078ef4f04ccd8a53542e059b80f7 100644 --- a/xso-proc/src/structs.rs +++ b/xso-proc/src/structs.rs @@ -6,42 +6,59 @@ //! Handling of structs -use proc_macro2::Span; +use proc_macro2::{Span, TokenStream}; use quote::quote; -use syn::*; +use syn::{spanned::Spanned, *}; use crate::common::{AsXmlParts, FromXmlParts, ItemDef}; use crate::compound::Compound; +use crate::error_message::ParentRef; use crate::meta::{reject_key, Flag, NameRef, NamespaceRef, QNameRef, XmlCompoundMeta}; -use crate::types::{ref_ty, ty_from_ident}; +use crate::state::{AsItemsSubmachine, FromEventsSubmachine, State}; +use crate::types::{ + as_xml_iter_fn, feed_fn, from_events_fn, from_xml_builder_ty, item_iter_ty, ref_ty, + ty_from_ident, +}; -/// Definition of a struct and how to parse it. -pub(crate) struct StructDef { - /// The XML namespace of the element to map the struct to. - namespace: NamespaceRef, - - /// The XML name of the element to map the struct to. - name: NameRef, +/// The inner parts of the struct. +/// +/// This contains all data necessary for the matching logic. +enum StructInner { + /// Single-field struct declared with `#[xml(transparent)]`. + /// + /// Transparent struct delegate all parsing and serialising to their + /// only field, which is why they do not need to store a lot of + /// information and come with extra restrictions, such as: + /// + /// - no XML namespace can be declared (it is determined by inner type) + /// - no XML name can be declared (it is determined by inner type) + /// - there must be only exactly one field + /// - that field has no `#[xml]` attribute + Transparent { + /// The member identifier of the only field. + member: Member, - /// The field(s) of this struct. - inner: Compound, + /// Type of the only field. + ty: Type, + }, - /// Name of the target type. - target_ty_ident: Ident, + /// A compound of fields, *not* declared as transparent. + /// + /// This can be a unit, tuple-like, or named struct. + Compound { + /// The XML namespace of the element to map the struct to. + xml_namespace: NamespaceRef, - /// Name of the builder type. - builder_ty_ident: Ident, - - /// Name of the iterator type. - item_iter_ty_ident: Ident, + /// The XML name of the element to map the struct to. + xml_name: NameRef, - /// Flag whether debug mode is enabled. - debug: bool, + /// The field(s) of this struct. + inner: Compound, + }, } -impl StructDef { - /// Create a new struct from its name, meta, and fields. - pub(crate) fn new(ident: &Ident, meta: XmlCompoundMeta, fields: &Fields) -> Result { +impl StructInner { + fn new(meta: XmlCompoundMeta, fields: &Fields) -> Result { // We destructure here so that we get informed when new fields are // added and can handle them, either by processing them or raising // an error if they are present. @@ -52,32 +69,268 @@ impl StructDef { debug, builder, iterator, + transparent, } = meta; + // These must've been cleared by the caller. Because these being set + // is a programming error (in xso-proc) and not a usage error, we + // assert here instead of using reject_key!. + assert!(builder.is_none()); + assert!(iterator.is_none()); + assert!(!debug.is_set()); + reject_key!(exhaustive flag not on "structs" only on "enums"); - let Some(namespace) = namespace else { - return Err(Error::new(meta_span, "`namespace` is required on structs")); - }; + if let Flag::Present(_) = transparent { + reject_key!(namespace not on "transparent structs"); + reject_key!(name not on "transparent structs"); - let Some(name) = name else { - return Err(Error::new(meta_span, "`name` is required on structs")); - }; + let fields_span = fields.span(); + let fields = match fields { + Fields::Unit => { + return Err(Error::new( + fields_span, + "transparent structs or enum variants must have exactly one field", + )) + } + Fields::Named(FieldsNamed { + named: ref fields, .. + }) + | Fields::Unnamed(FieldsUnnamed { + unnamed: ref fields, + .. + }) => fields, + }; + + if fields.len() != 1 { + return Err(Error::new( + fields_span, + "transparent structs or enum variants must have exactly one field", + )); + } + + let field = &fields[0]; + for attr in field.attrs.iter() { + if attr.meta.path().is_ident("xml") { + return Err(Error::new_spanned( + attr, + "#[xml(..)] attributes are not allowed inside transparent structs", + )); + } + } + let member = match field.ident.as_ref() { + Some(v) => Member::Named(v.clone()), + None => Member::Unnamed(Index { + span: field.ty.span(), + index: 0, + }), + }; + let ty = field.ty.clone(); + Ok(Self::Transparent { ty, member }) + } else { + let Some(xml_namespace) = namespace else { + return Err(Error::new( + meta_span, + "`namespace` is required on non-transparent structs", + )); + }; + + let Some(xml_name) = name else { + return Err(Error::new( + meta_span, + "`name` is required on non-transparent structs", + )); + }; + + Ok(Self::Compound { + inner: Compound::from_fields(fields, &xml_namespace)?, + xml_namespace, + xml_name, + }) + } + } + + fn make_from_events_statemachine( + &self, + state_ty_ident: &Ident, + output_name: &ParentRef, + state_prefix: &str, + ) -> Result { + match self { + Self::Transparent { ty, member } => { + let from_xml_builder_ty = from_xml_builder_ty(ty.clone()); + let from_events_fn = from_events_fn(ty.clone()); + let feed_fn = feed_fn(from_xml_builder_ty.clone()); + + let output_cons = match output_name { + ParentRef::Named(ref path) => quote! { + #path { #member: result } + }, + ParentRef::Unnamed { .. } => quote! { + ( result, ) + }, + }; + + let state_name = quote::format_ident!("{}Default", state_prefix); + let builder_data_ident = quote::format_ident!("__xso_data"); + + // Here, we generate a partial statemachine which really only + // proxies the FromXmlBuilder implementation of the inner + // type. + Ok(FromEventsSubmachine { + defs: TokenStream::default(), + states: vec![ + State::new_with_builder( + state_name.clone(), + &builder_data_ident, + &from_xml_builder_ty, + ) + .with_impl(quote! { + match #feed_fn(&mut #builder_data_ident, ev)? { + ::core::option::Option::Some(result) => { + ::core::result::Result::Ok(::core::ops::ControlFlow::Continue(#output_cons)) + } + ::core::option::Option::None => { + ::core::result::Result::Ok(::core::ops::ControlFlow::Break(Self::#state_name { + #builder_data_ident, + })) + } + } + }) + ], + init: quote! { + #from_events_fn(name, attrs).map(|#builder_data_ident| Self::#state_name { #builder_data_ident }) + }, + }) + } + + Self::Compound { + ref inner, + ref xml_namespace, + ref xml_name, + } => Ok(inner + .make_from_events_statemachine(state_ty_ident, output_name, state_prefix)? + .with_augmented_init(|init| { + quote! { + if name.0 != #xml_namespace || name.1 != #xml_name { + ::core::result::Result::Err(::xso::error::FromEventsError::Mismatch { + name, + attrs, + }) + } else { + #init + } + } + })), + } + } + + fn make_as_item_iter_statemachine( + &self, + input_name: &ParentRef, + state_ty_ident: &Ident, + state_prefix: &str, + item_iter_ty_lifetime: &Lifetime, + ) -> Result { + match self { + Self::Transparent { ty, member } => { + let item_iter_ty = item_iter_ty(ty.clone(), item_iter_ty_lifetime.clone()); + let as_xml_iter_fn = as_xml_iter_fn(ty.clone()); + + let state_name = quote::format_ident!("{}Default", state_prefix); + let iter_ident = quote::format_ident!("__xso_data"); - let builder_ty_ident = match builder { + let destructure = match input_name { + ParentRef::Named(ref path) => quote! { + #path { #member: #iter_ident } + }, + ParentRef::Unnamed { .. } => quote! { + (#iter_ident, ) + }, + }; + + // Here, we generate a partial statemachine which really only + // proxies the AsXml iterator implementation from the inner + // type. + Ok(AsItemsSubmachine { + defs: TokenStream::default(), + states: vec![State::new_with_builder( + state_name.clone(), + &iter_ident, + &item_iter_ty, + ) + .with_mut(&iter_ident) + .with_impl(quote! { + #iter_ident.next().transpose()? + })], + destructure, + init: quote! { + #as_xml_iter_fn(#iter_ident).map(|#iter_ident| Self::#state_name { #iter_ident })? + }, + }) + } + + Self::Compound { + ref inner, + ref xml_namespace, + ref xml_name, + } => Ok(inner + .make_as_item_iter_statemachine( + input_name, + state_ty_ident, + state_prefix, + item_iter_ty_lifetime, + )? + .with_augmented_init(|init| { + quote! { + let name = ( + ::xso::exports::rxml::Namespace::from(#xml_namespace), + ::std::borrow::Cow::Borrowed(#xml_name), + ); + #init + } + })), + } + } +} + +/// Definition of a struct and how to parse it. +pub(crate) struct StructDef { + /// Name of the target type. + target_ty_ident: Ident, + + /// Name of the builder type. + builder_ty_ident: Ident, + + /// Name of the iterator type. + item_iter_ty_ident: Ident, + + /// Flag whether debug mode is enabled. + debug: bool, + + /// The matching logic and contents of the struct. + inner: StructInner, +} + +impl StructDef { + /// Create a new struct from its name, meta, and fields. + pub(crate) fn new(ident: &Ident, mut meta: XmlCompoundMeta, fields: &Fields) -> Result { + let builder_ty_ident = match meta.builder.take() { Some(v) => v, None => quote::format_ident!("{}FromXmlBuilder", ident.to_string()), }; - let item_iter_ty_ident = match iterator { + let item_iter_ty_ident = match meta.iterator.take() { Some(v) => v, None => quote::format_ident!("{}AsXmlIterator", ident.to_string()), }; + let debug = meta.debug.take(); + + let inner = StructInner::new(meta, fields)?; + Ok(Self { - inner: Compound::from_fields(fields, &namespace)?, - namespace, - name, + inner, target_ty_ident: ident.clone(), builder_ty_ident, item_iter_ty_ident, @@ -93,9 +346,6 @@ impl ItemDef for StructDef { name_ident: &Ident, attrs_ident: &Ident, ) -> Result { - let xml_namespace = &self.namespace; - let xml_name = &self.name; - let target_ty_ident = &self.target_ty_ident; let builder_ty_ident = &self.builder_ty_ident; let state_ty_ident = quote::format_ident!("{}State", builder_ty_ident); @@ -107,18 +357,6 @@ impl ItemDef for StructDef { &Path::from(target_ty_ident.clone()).into(), "Struct", )? - .with_augmented_init(|init| { - quote! { - if name.0 != #xml_namespace || name.1 != #xml_name { - ::core::result::Result::Err(::xso::error::FromEventsError::Mismatch { - name, - attrs, - }) - } else { - #init - } - } - }) .compile() .render( vis, @@ -141,9 +379,6 @@ impl ItemDef for StructDef { } fn make_as_xml_iter(&self, vis: &Visibility) -> Result { - let xml_namespace = &self.namespace; - let xml_name = &self.name; - let target_ty_ident = &self.target_ty_ident; let item_iter_ty_ident = &self.item_iter_ty_ident; let item_iter_ty_lifetime = Lifetime { @@ -183,15 +418,6 @@ impl ItemDef for StructDef { "Struct", &item_iter_ty_lifetime, )? - .with_augmented_init(|init| { - quote! { - let name = ( - ::xso::exports::rxml::Namespace::from(#xml_namespace), - ::std::borrow::Cow::Borrowed(#xml_name), - ); - #init - } - }) .compile() .render( vis, diff --git a/xso/ChangeLog b/xso/ChangeLog index 3f41058392d3ebb0b3e4dd182787c1c3fdb59d2d..108d4c6deba397ba1db5bebc48d7555bbe72e276 100644 --- a/xso/ChangeLog +++ b/xso/ChangeLog @@ -22,6 +22,7 @@ Version NEXT: structs. - Support for collecting all unknown children in a single field as collection of `minidom::Element`. + - Support for "transparent" structs (newtype-like patterns for XSO). 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 23c5e787b3b4f327b56433f3938d587d6c3061d8..0e3090d87ebf820f241dd0961b375c433a27f263 100644 --- a/xso/src/from_xml_doc.md +++ b/xso/src/from_xml_doc.md @@ -50,6 +50,7 @@ The following keys are defined on structs: | --- | --- | --- | | `namespace` | *string literal* or *path* | The XML element namespace to match. If it is a *path*, it must point at a `&'static str`. | | `name` | *string literal* or *path* | The XML element name to match. If it is a *path*, it must point at a `&'static NcNameStr`. | +| `transparent` | *flag* | If present, declares the struct as *transparent* struct (see below) | | `builder` | optional *ident* | The name to use for the generated builder type. | | `iterator` | optional *ident* | The name to use for the generated iterator type. | @@ -74,6 +75,12 @@ By default, the builder type uses the type's name suffixed with `FromXmlBuilder` and the iterator type uses the type's name suffixed with `AsXmlIterator`. +If the struct is marked as `transparent`, it must not have a `namespace` or +`name` set and it must have exactly one field. That field's type must +implement [`FromXml`] in order to derive `FromXml` and [`AsXml`] in order to +derive `AsXml`. The struct will be (de-)serialised exactly like the type of +that single field. This allows a newtype-like pattern for XSO structs. + ### Enum meta The following keys are defined on enums: