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
commit 212c5c4a83
10 changed files with 665 additions and 50 deletions

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