diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index 2ea1b8d..5eaff2d 100644 --- a/parsers/src/util/macro_tests.rs +++ b/parsers/src/util/macro_tests.rs @@ -183,3 +183,48 @@ fn namespace_lit_roundtrip() { }; roundtrip_full::(""); } + +#[derive(FromXml, IntoXml, PartialEq, Debug, Clone)] +#[xml(namespace = NS1, name = "attr")] +struct RequiredAttribute { + #[xml(attribute)] + foo: String, +} + +#[test] +fn required_attribute_roundtrip() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::(""); +} + +#[test] +fn required_attribute_positive() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + let data = parse_str::("").unwrap(); + assert_eq!(data.foo, "bar"); +} + +#[test] +fn required_attribute_missing() { + #[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 field") && e.contains("missing") => + { + () + } + other => panic!("unexpected result: {:?}", other), + } +} diff --git a/xso-proc/src/compound.rs b/xso-proc/src/compound.rs index 397095f..42b51b3 100644 --- a/xso-proc/src/compound.rs +++ b/xso-proc/src/compound.rs @@ -7,29 +7,41 @@ //! Handling of the insides of compound structures (structs and enum variants) use proc_macro2::{Span, TokenStream}; -use quote::{quote, ToTokens}; +use quote::quote; use syn::*; +use crate::error_message::ParentRef; +use crate::field::{FieldBuilderPart, FieldDef, FieldIteratorPart, FieldTempInit}; +use crate::scope::{mangle_member, FromEventsScope, IntoEventsScope}; use crate::state::{FromEventsSubmachine, IntoEventsSubmachine, State}; use crate::types::qname_ty; /// A struct or enum variant's contents. -pub(crate) struct Compound; +pub(crate) struct Compound { + /// The fields of this compound. + fields: Vec, +} impl Compound { /// Construct a compound from fields. pub(crate) fn from_fields(compound_fields: &Fields) -> Result { - match compound_fields { - Fields::Unit => (), - other => { - return Err(Error::new_spanned( - other, - "cannot derive on non-unit struct (yet!)", - )) - } + let mut fields = Vec::with_capacity(compound_fields.len()); + for (i, field) in compound_fields.iter().enumerate() { + let index = match i.try_into() { + Ok(v) => v, + // we are converting to u32, are you crazy?! + // (u32, because syn::Member::Index needs that.) + Err(_) => { + return Err(Error::new_spanned( + field, + "okay, mate, that are way too many fields. get your life together.", + )) + } + }; + fields.push(FieldDef::from_field(field, index)?); } - Ok(Self) + Ok(Self { fields }) } /// Make and return a set of states which is used to construct the target @@ -40,9 +52,12 @@ impl Compound { pub(crate) fn make_from_events_statemachine( &self, state_ty_ident: &Ident, - output_cons: &Path, + output_name: &ParentRef, state_prefix: &str, ) -> Result { + let scope = FromEventsScope::new(); + let FromEventsScope { ref attrs, .. } = scope; + let default_state_ident = quote::format_ident!("{}Default", state_prefix); let builder_data_ident = quote::format_ident!("__data"); let builder_data_ty: Type = TypePath { @@ -52,9 +67,44 @@ impl Compound { .into(); let mut states = Vec::new(); - let readable_name = output_cons.to_token_stream().to_string(); - let unknown_attr_err = format!("Unknown attribute in {} element.", readable_name); - let unknown_child_err = format!("Unknown child in {} element.", readable_name); + let mut builder_data_def = TokenStream::default(); + let mut builder_data_init = TokenStream::default(); + let mut output_cons = TokenStream::default(); + + for field in self.fields.iter() { + let member = field.member(); + let builder_field_name = mangle_member(member); + let part = field.make_builder_part(&scope, &output_name)?; + + match part { + FieldBuilderPart::Init { + value: FieldTempInit { ty, init }, + } => { + builder_data_def.extend(quote! { + #builder_field_name: #ty, + }); + + builder_data_init.extend(quote! { + #builder_field_name: #init, + }); + + output_cons.extend(quote! { + #member: #builder_data_ident.#builder_field_name, + }); + } + } + } + + let unknown_attr_err = format!("Unknown attribute in {}.", output_name); + let unknown_child_err = format!("Unknown child in {}.", output_name); + + let output_cons = match output_name { + ParentRef::Named(ref path) => { + quote! { + #path { #output_cons } + } + } + }; states.push(State::new_with_builder( default_state_ident.clone(), @@ -86,18 +136,21 @@ impl Compound { Ok(FromEventsSubmachine { defs: quote! { - struct #builder_data_ty; + struct #builder_data_ty { + #builder_data_def + } }, states, init: quote! { - if attrs.len() > 0 { + let #builder_data_ident = #builder_data_ty { + #builder_data_init + }; + if #attrs.len() > 0 { return ::core::result::Result::Err(::xso::error::Error::Other( #unknown_attr_err, ).into()); } - ::core::result::Result::Ok(#state_ty_ident::#default_state_ident { - #builder_data_ident: #builder_data_ty, - }) + ::core::result::Result::Ok(#state_ty_ident::#default_state_ident { #builder_data_ident }) }, }) } @@ -116,23 +169,54 @@ impl Compound { input_name: &Path, state_prefix: &str, ) -> Result { + let scope = IntoEventsScope::new(); + let IntoEventsScope { ref attrs, .. } = scope; + let start_element_state_ident = quote::format_ident!("{}StartElement", state_prefix); let end_element_state_ident = quote::format_ident!("{}EndElement", state_prefix); let name_ident = quote::format_ident!("name"); let mut states = Vec::new(); + let mut init_body = TokenStream::default(); + let mut destructure = TokenStream::default(); + let mut start_init = TokenStream::default(); + states.push( State::new(start_element_state_ident.clone()) - .with_field(&name_ident, &qname_ty(Span::call_site())) - .with_impl(quote! { - ::core::option::Option::Some(::xso::exports::rxml::Event::StartElement( - ::xso::exports::rxml::parser::EventMetrics::zero(), - #name_ident, - ::xso::exports::rxml::AttrMap::new(), - )) - }), + .with_field(&name_ident, &qname_ty(Span::call_site())), ); + for field in self.fields.iter() { + let member = field.member(); + let bound_name = mangle_member(member); + let part = field.make_iterator_part(&scope, &bound_name)?; + + match part { + FieldIteratorPart::Header { setter } => { + destructure.extend(quote! { + #member: #bound_name, + }); + init_body.extend(setter); + start_init.extend(quote! { + #bound_name, + }); + states[0].add_field(&bound_name, field.ty()); + } + } + } + + states[0].set_impl(quote! { + { + let mut #attrs = ::xso::exports::rxml::AttrMap::new(); + #init_body + ::core::option::Option::Some(::xso::exports::rxml::Event::StartElement( + ::xso::exports::rxml::parser::EventMetrics::zero(), + #name_ident, + #attrs, + )) + } + }); + states.push( State::new(end_element_state_ident.clone()).with_impl(quote! { ::core::option::Option::Some(::xso::exports::rxml::Event::EndElement( @@ -145,10 +229,10 @@ impl Compound { defs: TokenStream::default(), states, destructure: quote! { - #input_name + #input_name { #destructure } }, init: quote! { - Self::#start_element_state_ident { #name_ident } + Self::#start_element_state_ident { #name_ident, #start_init } }, }) } diff --git a/xso-proc/src/error_message.rs b/xso-proc/src/error_message.rs new file mode 100644 index 0000000..b2913b5 --- /dev/null +++ b/xso-proc/src/error_message.rs @@ -0,0 +1,81 @@ +// 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/. + +//! Infrastructure for contextual error messages + +use std::fmt; + +use syn::*; + +/// Reference to a compound field's parent +/// +/// This reference can be converted to a hopefully-useful human-readable +/// string via [`std::fmt::Display`]. +#[derive(Clone, Debug)] +pub(super) enum ParentRef { + /// The parent is addressable by a path, e.g. a struct type or enum + /// variant. + Named(Path), +} + +impl From for ParentRef { + fn from(other: Path) -> Self { + Self::Named(other) + } +} + +impl From<&Path> for ParentRef { + fn from(other: &Path) -> Self { + Self::Named(other.clone()) + } +} + +impl fmt::Display for ParentRef { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Named(name) => { + let mut first = true; + for segment in name.segments.iter() { + if !first || name.leading_colon.is_some() { + write!(f, "::")?; + } + first = false; + write!(f, "{}", segment.ident)?; + } + write!(f, " element") + } + } + } +} + +/// Ephemeral struct to create a nice human-readable representation of +/// [`syn::Member`]. +/// +/// It implements [`std::fmt::Display`] for that purpose and is otherwise of +/// little use. +#[repr(transparent)] +struct FieldName<'x>(&'x Member); + +impl fmt::Display for FieldName<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.0 { + Member::Named(v) => write!(f, "field '{}'", v), + Member::Unnamed(v) => write!(f, "unnamed field {}", v.index), + } + } +} + +/// Create a string error message for a missing attribute. +/// +/// `parent_name` should point at the compound which is being parsed and +/// `field` should be the field to which the attribute belongs. +pub(super) fn on_missing_attribute(parent_name: &ParentRef, field: &Member) -> String { + format!( + "Required attribute {} on {} missing.", + FieldName(&field), + parent_name + ) +} diff --git a/xso-proc/src/field.rs b/xso-proc/src/field.rs new file mode 100644 index 0000000..adadd7d --- /dev/null +++ b/xso-proc/src/field.rs @@ -0,0 +1,215 @@ +// 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/. + +//! Compound (struct or enum variant) field types + +use proc_macro2::TokenStream; +use quote::quote; +use syn::{spanned::Spanned, *}; + +use rxml_validation::NcName; + +use crate::error_message::{self, ParentRef}; +use crate::meta::{NameRef, XmlFieldMeta}; +use crate::scope::{FromEventsScope, IntoEventsScope}; + +/// Code slices necessary for declaring and initializing a temporary variable +/// for parsing purposes. +pub(crate) struct FieldTempInit { + /// The type of the temporary variable. + pub(crate) ty: Type, + + /// The initializer for the temporary variable. + pub(crate) init: TokenStream, +} + +/// Describe how a struct or enum variant's member is parsed from XML data. +/// +/// This struct is returned from [`FieldDef::make_builder_part`] and +/// contains code snippets and instructions for +/// [`Compound::make_from_events_statemachine`][`crate::compound::Compound::make_from_events_statemachine`] +/// to parse the field's data from XML. +pub(crate) enum FieldBuilderPart { + /// Parse a field from the item's element's start event. + Init { + /// Expression and type which extracts the field's data from the + /// element's start event. + value: FieldTempInit, + }, +} + +/// Describe how a struct or enum variant's member is converted to XML data. +/// +/// This struct is returned from [`FieldDef::make_iterator_part`] and +/// contains code snippets and instructions for +/// [`Compound::make_into_events_statemachine`][`crate::compound::Compound::make_into_events_statemachine`] +/// to convert the field's data into XML. +pub(crate) enum FieldIteratorPart { + /// The field is emitted as part of StartElement. + Header { + /// A sequence of statements which updates the temporary variables + /// during the StartElement event's construction, consuming the + /// field's value. + setter: TokenStream, + }, +} + +/// Specify how the field is mapped to XML. +enum FieldKind { + /// The field maps to an attribute. + Attribute { + /// The XML name of the attribute. + xml_name: NameRef, + }, +} + +impl FieldKind { + /// Construct a new field implementation from the meta attributes. + /// + /// `field_ident` is, for some field types, used to infer an XML name if + /// it is not specified explicitly. + fn from_meta(meta: XmlFieldMeta, field_ident: Option<&Ident>) -> Result { + match meta { + XmlFieldMeta::Attribute { span } => { + let Some(field_ident) = field_ident else { + return Err(Error::new( + span, + "attribute extraction not supported on unnamed fields", + )); + }; + + let xml_name = match NcName::try_from(field_ident.to_string()) { + Ok(v) => v, + Err(e) => { + return Err(Error::new( + field_ident.span(), + format!("invalid XML attribute name: {}", e), + )) + } + }; + + Ok(Self::Attribute { + xml_name: NameRef::Literal { + span: field_ident.span(), + value: xml_name, + }, + }) + } + } + } +} + +/// Definition of a single field in a compound. +/// +/// See [`Compound`][`crate::compound::Compound`] for more information on +/// compounds in general. +pub(crate) struct FieldDef { + /// The member identifying the field. + member: Member, + + /// The type of the field. + ty: Type, + + /// The way the field is mapped to XML. + kind: FieldKind, +} + +impl FieldDef { + /// Create a new field definition from its declaration. + /// + /// The `index` must be the zero-based index of the field even for named + /// fields. + pub(crate) fn from_field(field: &syn::Field, index: u32) -> Result { + let field_span = field.span(); + let meta = XmlFieldMeta::parse_from_attributes(&field.attrs, &field_span)?; + + let (member, ident) = match field.ident.as_ref() { + Some(v) => (Member::Named(v.clone()), Some(v)), + None => ( + Member::Unnamed(Index { + index, + span: field_span, + }), + None, + ), + }; + + let ty = field.ty.clone(); + + Ok(Self { + member, + ty, + kind: FieldKind::from_meta(meta, ident)?, + }) + } + + /// Access the [`syn::Member`] identifying this field in the original + /// type. + pub(crate) fn member(&self) -> &Member { + &self.member + } + + /// Access the field's type. + pub(crate) fn ty(&self) -> &Type { + &self.ty + } + + /// Construct the builder pieces for this field. + /// + /// `container_name` must be a reference to the compound's type, so that + /// it can be used for error messages. + pub(crate) fn make_builder_part( + &self, + scope: &FromEventsScope, + container_name: &ParentRef, + ) -> Result { + match self.kind { + FieldKind::Attribute { ref xml_name } => { + let FromEventsScope { ref attrs, .. } = scope; + + let missing_msg = error_message::on_missing_attribute(container_name, &self.member); + + return Ok(FieldBuilderPart::Init { + value: FieldTempInit { + ty: self.ty.clone(), + init: quote! { + match #attrs.remove(::xso::exports::rxml::Namespace::none(), #xml_name) { + ::core::option::Option::Some(v) => v, + ::core::option::Option::None => return ::core::result::Result::Err(::xso::error::Error::Other(#missing_msg).into()), + } + }, + }, + }); + } + } + } + + /// Construct the iterator pieces for this field. + /// + /// `bound_name` must be the name to which the field's value is bound in + /// the iterator code. + pub(crate) fn make_iterator_part( + &self, + scope: &IntoEventsScope, + bound_name: &Ident, + ) -> Result { + match self.kind { + FieldKind::Attribute { ref xml_name } => { + let IntoEventsScope { ref attrs, .. } = scope; + + return Ok(FieldIteratorPart::Header { + setter: quote! { + #attrs.insert( + ::xso::exports::rxml::Namespace::NONE, + #xml_name.to_owned(), + #bound_name, + ); + }, + }); + } + } + } +} diff --git a/xso-proc/src/lib.rs b/xso-proc/src/lib.rs index 557a02f..cdf9362 100644 --- a/xso-proc/src/lib.rs +++ b/xso-proc/src/lib.rs @@ -26,7 +26,10 @@ use quote::quote; use syn::*; mod compound; +mod error_message; +mod field; mod meta; +mod scope; mod state; mod structs; mod types; diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs index 02a70a1..ebbfb74 100644 --- a/xso-proc/src/meta.rs +++ b/xso-proc/src/meta.rs @@ -11,7 +11,7 @@ use proc_macro2::{Span, TokenStream}; use quote::{quote, quote_spanned}; -use syn::{spanned::Spanned, *}; +use syn::{meta::ParseNestedMeta, spanned::Spanned, *}; use rxml_validation::NcName; @@ -194,3 +194,101 @@ impl XmlCompoundMeta { } } } + +/// Contents of an `#[xml(..)]` attribute on a struct or enum variant member. +#[derive(Debug)] +pub(crate) enum XmlFieldMeta { + Attribute { + /// The span of the `#[xml(attribute)]` meta from which this was parsed. + /// + /// This is useful for error messages. + span: Span, + }, +} + +impl XmlFieldMeta { + /// Parse a `#[xml(attribute(..))]` meta. + fn attribute_from_meta(meta: ParseNestedMeta<'_>) -> Result { + Ok(Self::Attribute { + span: meta.path.span(), + }) + } + + /// Parse [`Self`] from a nestd meta, switching on the identifier + /// of that nested meta. + fn parse_from_meta(meta: ParseNestedMeta<'_>) -> Result { + if meta.path.is_ident("attribute") { + Self::attribute_from_meta(meta) + } else { + Err(Error::new_spanned(meta.path, "unsupported field meta")) + } + } + + /// Parse an `#[xml(..)]` meta on a field. + /// + /// This switches based on the first identifier within the `#[xml(..)]` + /// meta and generates an enum variant accordingly. + /// + /// Only a single nested meta is allowed; more than one will be + /// rejected with an appropriate compile-time error. + /// + /// If no meta is contained at all, a compile-time error is generated. + /// + /// Undefined options or options with incompatible values are rejected + /// with an appropriate compile-time error. + pub(crate) fn parse_from_attribute(attr: &Attribute) -> Result { + let mut result: Option = None; + + attr.parse_nested_meta(|meta| { + if result.is_some() { + return Err(Error::new_spanned( + meta.path, + "multiple field type specifiers are not supported", + )); + } + + result = Some(Self::parse_from_meta(meta)?); + Ok(()) + })?; + + if let Some(result) = result { + Ok(result) + } else { + Err(Error::new_spanned( + attr, + "missing field type specifier within `#[xml(..)]`", + )) + } + } + + /// Find and parse a `#[xml(..)]` meta on a field. + /// + /// This invokes [`Self::parse_from_attribute`] internally on the first + /// encountered `#[xml(..)]` meta. + /// + /// If not exactly one `#[xml(..)]` meta is encountered, an error is + /// returned. The error is spanned to `err_span`. + pub(crate) fn parse_from_attributes(attrs: &[Attribute], err_span: &Span) -> Result { + let mut result: Option = None; + for attr in attrs { + if !attr.path().is_ident("xml") { + continue; + } + + if result.is_some() { + return Err(Error::new_spanned( + attr, + "only one #[xml(..)] attribute per field allowed.", + )); + } + + result = Some(Self::parse_from_attribute(attr)?); + } + + if let Some(result) = result { + Ok(result) + } else { + Err(Error::new(*err_span, "missing #[xml(..)] meta on field")) + } + } +} diff --git a/xso-proc/src/scope.rs b/xso-proc/src/scope.rs new file mode 100644 index 0000000..2a2315c --- /dev/null +++ b/xso-proc/src/scope.rs @@ -0,0 +1,73 @@ +// 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/. + +//! Identifiers used within generated code. + +use proc_macro2::Span; +use syn::*; + +/// Container struct for various identifiers used throughout the parser code. +/// +/// This struct is passed around from the [`crate::compound::Compound`] +/// downward to the code generators in order to ensure that everyone is on the +/// same page about which identifiers are used for what. +/// +/// The recommended usage is to bind the names which are needed into the local +/// scope like this: +/// +/// ```text +/// # let scope = FromEventsScope::new(); +/// let FromEventsScope { +/// ref attrs, +/// .. +/// } = scope; +/// ``` +pub(crate) struct FromEventsScope { + /// Accesses the `AttrMap` from code in + /// [`crate::field::FieldBuilderPart::Init`]. + pub(crate) attrs: Ident, +} + +impl FromEventsScope { + /// Create a fresh scope with all necessary identifiers. + pub(crate) fn new() -> Self { + // Sadly, `Ident::new` is not `const`, so we have to create even the + // well-known identifiers from scratch all the time. + Self { + attrs: Ident::new("attrs", Span::call_site()), + } + } +} + +/// Container struct for various identifiers used throughout the generator +/// code. +/// +/// This struct is passed around from the [`crate::compound::Compound`] +/// downward to the code generators in order to ensure that everyone is on the +/// same page about which identifiers are used for what. +/// +/// See [`FromEventsScope`] for recommendations on the usage. +pub(crate) struct IntoEventsScope { + /// Accesses the `AttrMap` from code in + /// [`crate::field::FieldIteratorPart::Header`]. + pub(crate) attrs: Ident, +} + +impl IntoEventsScope { + /// Create a fresh scope with all necessary identifiers. + pub(crate) fn new() -> Self { + Self { + attrs: Ident::new("attrs", Span::call_site()), + } + } +} + +pub(crate) fn mangle_member(member: &Member) -> Ident { + match member { + Member::Named(member) => quote::format_ident!("f{}", member), + Member::Unnamed(member) => quote::format_ident!("f_u{}", member.index), + } +} diff --git a/xso-proc/src/state.rs b/xso-proc/src/state.rs index 62cf8e8..0e35d5f 100644 --- a/xso-proc/src/state.rs +++ b/xso-proc/src/state.rs @@ -88,6 +88,13 @@ impl State { self.advance_body = body; self } + + /// Override the current `advance` implementation of this state. + /// + /// This is an in-place version of [`Self::with_impl`]. + pub(crate) fn set_impl(&mut self, body: TokenStream) { + self.advance_body = body; + } } /// A partial [`FromEventsStateMachine`] which only covers the builder for a diff --git a/xso-proc/src/structs.rs b/xso-proc/src/structs.rs index 8503b38..f6012e6 100644 --- a/xso-proc/src/structs.rs +++ b/xso-proc/src/structs.rs @@ -96,7 +96,7 @@ impl StructDef { .inner .make_from_events_statemachine( &state_ty_ident, - &target_ty_ident.clone().into(), + &Path::from(target_ty_ident.clone()).into(), "Struct", )? .with_augmented_init(|init| { diff --git a/xso/src/from_xml_doc.md b/xso/src/from_xml_doc.md index 3854d04..fd14be1 100644 --- a/xso/src/from_xml_doc.md +++ b/xso/src/from_xml_doc.md @@ -17,15 +17,20 @@ assert_eq!(foo, Foo); ## Attributes -The derive macros need to know which XML namespace and name the elements it -is supposed have. This must be specified via key-value pairs on the type the -derive macro is invoked on. These are specified as Rust attributes. In order -to disambiguate between XML attributes and Rust attributes, we are going to -refer to Rust attributes using the term *meta* instead, which is consistent -with the Rust language reference calling that syntax construct *meta*. +The derive macros need additional information, such as XML namespaces and +names to match. This must be specified via key-value pairs on the type or +fields the derive macro is invoked on. These key-value pairs are specified as +Rust attributes. In order to disambiguate between XML attributes and Rust +attributes, we are going to refer to Rust attributes using the term *meta* +instead, which is consistent with the Rust language reference calling that +syntax construct *meta*. All key-value pairs interpreted by these derive macros must be wrapped in a -`#[xml( ... )]` *meta*. The following keys are defined on structs: +`#[xml( ... )]` *meta*. + +### Struct meta + +The following keys are defined on structs: | Key | Value type | Description | | --- | --- | --- | @@ -43,16 +48,20 @@ and cannot be overridden. The following will thus not compile: struct Foo; ``` -## Limitations +### Field meta -Supports only empty structs currently. For example, the following will not -work: +For fields, the *meta* consists of a nested meta inside the `#[xml(..)]` meta, +the identifier of which controls *how* the field is mapped to XML, while the +contents control the parameters of that mapping. -```compile_fail -# use xso::FromXml; -#[derive(FromXml, Debug, PartialEq)] -#[xml(namespace = "urn:example", name = "foo")] -struct Foo { - some_field: String, -} -``` +The following mapping types are defined: + +| Type | Description | +| --- | --- | +| [`attribute`](#attribute-meta) | Map the field to an XML attribute on the struct's element | + +#### `attribute` meta + +The `attribute` meta does not support additional parameters. The field it is +used on is mapped to an XML attribute of the same name and must be of type +[`String`].