xso-proc: add support for parsing attributes into Strings

This is bare-bones and is missing many features which we intend to add
in future commits, such as parsing from attributes whose names differ
from the field names and parsing into non-String types.
This commit is contained in:
Jonas Schäfer 2024-06-23 09:06:32 +02:00
parent 183bef5cf6
commit 212c5c4a83
10 changed files with 665 additions and 50 deletions

View file

@ -183,3 +183,48 @@ fn namespace_lit_roundtrip() {
};
roundtrip_full::<NamespaceLit>("<baz xmlns='urn:example:ns2'/>");
}
#[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::<RequiredAttribute>("<attr xmlns='urn:example:ns1' foo='bar'/>");
}
#[test]
fn required_attribute_positive() {
#[allow(unused_imports)]
use std::{
option::Option::{None, Some},
result::Result::{Err, Ok},
};
let data = parse_str::<RequiredAttribute>("<attr xmlns='urn:example:ns1' foo='bar'/>").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::<RequiredAttribute>("<attr xmlns='urn:example:ns1'/>") {
Err(::xso::error::FromElementError::Invalid(::xso::error::Error::Other(e)))
if e.contains("Required attribute field") && e.contains("missing") =>
{
()
}
other => panic!("unexpected result: {:?}", other),
}
}

View file

@ -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<FieldDef>,
}
impl Compound {
/// Construct a compound from fields.
pub(crate) fn from_fields(compound_fields: &Fields) -> Result<Self> {
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<FromEventsSubmachine> {
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<IntoEventsSubmachine> {
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 }
},
})
}

View file

@ -0,0 +1,81 @@
// 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/.
//! 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<Path> 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
)
}

215
xso-proc/src/field.rs Normal file
View file

@ -0,0 +1,215 @@
// 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/.
//! 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<Self> {
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<Self> {
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<FieldBuilderPart> {
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<FieldIteratorPart> {
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,
);
},
});
}
}
}
}

View file

@ -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;

View file

@ -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<Self> {
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<Self> {
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<Self> {
let mut result: Option<Self> = 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<Self> {
let mut result: Option<Self> = 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"))
}
}
}

73
xso-proc/src/scope.rs Normal file
View file

@ -0,0 +1,73 @@
// 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/.
//! 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),
}
}

View file

@ -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

View file

@ -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| {

View file

@ -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`].