mirror of
https://gitlab.com/xmpp-rs/xmpp-rs.git
synced 2024-07-12 22:21:53 +00:00
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:
parent
183bef5cf6
commit
212c5c4a83
10 changed files with 665 additions and 50 deletions
|
@ -183,3 +183,48 @@ fn namespace_lit_roundtrip() {
|
||||||
};
|
};
|
||||||
roundtrip_full::<NamespaceLit>("<baz xmlns='urn:example:ns2'/>");
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,29 +7,41 @@
|
||||||
//! Handling of the insides of compound structures (structs and enum variants)
|
//! Handling of the insides of compound structures (structs and enum variants)
|
||||||
|
|
||||||
use proc_macro2::{Span, TokenStream};
|
use proc_macro2::{Span, TokenStream};
|
||||||
use quote::{quote, ToTokens};
|
use quote::quote;
|
||||||
use syn::*;
|
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::state::{FromEventsSubmachine, IntoEventsSubmachine, State};
|
||||||
use crate::types::qname_ty;
|
use crate::types::qname_ty;
|
||||||
|
|
||||||
/// A struct or enum variant's contents.
|
/// 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 {
|
impl Compound {
|
||||||
/// Construct a compound from fields.
|
/// Construct a compound from fields.
|
||||||
pub(crate) fn from_fields(compound_fields: &Fields) -> Result<Self> {
|
pub(crate) fn from_fields(compound_fields: &Fields) -> Result<Self> {
|
||||||
match compound_fields {
|
let mut fields = Vec::with_capacity(compound_fields.len());
|
||||||
Fields::Unit => (),
|
for (i, field) in compound_fields.iter().enumerate() {
|
||||||
other => {
|
let index = match i.try_into() {
|
||||||
return Err(Error::new_spanned(
|
Ok(v) => v,
|
||||||
other,
|
// we are converting to u32, are you crazy?!
|
||||||
"cannot derive on non-unit struct (yet!)",
|
// (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
|
/// 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(
|
pub(crate) fn make_from_events_statemachine(
|
||||||
&self,
|
&self,
|
||||||
state_ty_ident: &Ident,
|
state_ty_ident: &Ident,
|
||||||
output_cons: &Path,
|
output_name: &ParentRef,
|
||||||
state_prefix: &str,
|
state_prefix: &str,
|
||||||
) -> Result<FromEventsSubmachine> {
|
) -> Result<FromEventsSubmachine> {
|
||||||
|
let scope = FromEventsScope::new();
|
||||||
|
let FromEventsScope { ref attrs, .. } = scope;
|
||||||
|
|
||||||
let default_state_ident = quote::format_ident!("{}Default", state_prefix);
|
let default_state_ident = quote::format_ident!("{}Default", state_prefix);
|
||||||
let builder_data_ident = quote::format_ident!("__data");
|
let builder_data_ident = quote::format_ident!("__data");
|
||||||
let builder_data_ty: Type = TypePath {
|
let builder_data_ty: Type = TypePath {
|
||||||
|
@ -52,9 +67,44 @@ impl Compound {
|
||||||
.into();
|
.into();
|
||||||
let mut states = Vec::new();
|
let mut states = Vec::new();
|
||||||
|
|
||||||
let readable_name = output_cons.to_token_stream().to_string();
|
let mut builder_data_def = TokenStream::default();
|
||||||
let unknown_attr_err = format!("Unknown attribute in {} element.", readable_name);
|
let mut builder_data_init = TokenStream::default();
|
||||||
let unknown_child_err = format!("Unknown child in {} element.", readable_name);
|
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(
|
states.push(State::new_with_builder(
|
||||||
default_state_ident.clone(),
|
default_state_ident.clone(),
|
||||||
|
@ -86,18 +136,21 @@ impl Compound {
|
||||||
|
|
||||||
Ok(FromEventsSubmachine {
|
Ok(FromEventsSubmachine {
|
||||||
defs: quote! {
|
defs: quote! {
|
||||||
struct #builder_data_ty;
|
struct #builder_data_ty {
|
||||||
|
#builder_data_def
|
||||||
|
}
|
||||||
},
|
},
|
||||||
states,
|
states,
|
||||||
init: quote! {
|
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(
|
return ::core::result::Result::Err(::xso::error::Error::Other(
|
||||||
#unknown_attr_err,
|
#unknown_attr_err,
|
||||||
).into());
|
).into());
|
||||||
}
|
}
|
||||||
::core::result::Result::Ok(#state_ty_ident::#default_state_ident {
|
::core::result::Result::Ok(#state_ty_ident::#default_state_ident { #builder_data_ident })
|
||||||
#builder_data_ident: #builder_data_ty,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -116,23 +169,54 @@ impl Compound {
|
||||||
input_name: &Path,
|
input_name: &Path,
|
||||||
state_prefix: &str,
|
state_prefix: &str,
|
||||||
) -> Result<IntoEventsSubmachine> {
|
) -> Result<IntoEventsSubmachine> {
|
||||||
|
let scope = IntoEventsScope::new();
|
||||||
|
let IntoEventsScope { ref attrs, .. } = scope;
|
||||||
|
|
||||||
let start_element_state_ident = quote::format_ident!("{}StartElement", state_prefix);
|
let start_element_state_ident = quote::format_ident!("{}StartElement", state_prefix);
|
||||||
let end_element_state_ident = quote::format_ident!("{}EndElement", state_prefix);
|
let end_element_state_ident = quote::format_ident!("{}EndElement", state_prefix);
|
||||||
let name_ident = quote::format_ident!("name");
|
let name_ident = quote::format_ident!("name");
|
||||||
let mut states = Vec::new();
|
let mut states = Vec::new();
|
||||||
|
|
||||||
|
let mut init_body = TokenStream::default();
|
||||||
|
let mut destructure = TokenStream::default();
|
||||||
|
let mut start_init = TokenStream::default();
|
||||||
|
|
||||||
states.push(
|
states.push(
|
||||||
State::new(start_element_state_ident.clone())
|
State::new(start_element_state_ident.clone())
|
||||||
.with_field(&name_ident, &qname_ty(Span::call_site()))
|
.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(),
|
|
||||||
))
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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(
|
states.push(
|
||||||
State::new(end_element_state_ident.clone()).with_impl(quote! {
|
State::new(end_element_state_ident.clone()).with_impl(quote! {
|
||||||
::core::option::Option::Some(::xso::exports::rxml::Event::EndElement(
|
::core::option::Option::Some(::xso::exports::rxml::Event::EndElement(
|
||||||
|
@ -145,10 +229,10 @@ impl Compound {
|
||||||
defs: TokenStream::default(),
|
defs: TokenStream::default(),
|
||||||
states,
|
states,
|
||||||
destructure: quote! {
|
destructure: quote! {
|
||||||
#input_name
|
#input_name { #destructure }
|
||||||
},
|
},
|
||||||
init: quote! {
|
init: quote! {
|
||||||
Self::#start_element_state_ident { #name_ident }
|
Self::#start_element_state_ident { #name_ident, #start_init }
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
81
xso-proc/src/error_message.rs
Normal file
81
xso-proc/src/error_message.rs
Normal 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
215
xso-proc/src/field.rs
Normal 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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,7 +26,10 @@ use quote::quote;
|
||||||
use syn::*;
|
use syn::*;
|
||||||
|
|
||||||
mod compound;
|
mod compound;
|
||||||
|
mod error_message;
|
||||||
|
mod field;
|
||||||
mod meta;
|
mod meta;
|
||||||
|
mod scope;
|
||||||
mod state;
|
mod state;
|
||||||
mod structs;
|
mod structs;
|
||||||
mod types;
|
mod types;
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
use proc_macro2::{Span, TokenStream};
|
use proc_macro2::{Span, TokenStream};
|
||||||
use quote::{quote, quote_spanned};
|
use quote::{quote, quote_spanned};
|
||||||
use syn::{spanned::Spanned, *};
|
use syn::{meta::ParseNestedMeta, spanned::Spanned, *};
|
||||||
|
|
||||||
use rxml_validation::NcName;
|
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
73
xso-proc/src/scope.rs
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
|
@ -88,6 +88,13 @@ impl State {
|
||||||
self.advance_body = body;
|
self.advance_body = body;
|
||||||
self
|
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
|
/// A partial [`FromEventsStateMachine`] which only covers the builder for a
|
||||||
|
|
|
@ -96,7 +96,7 @@ impl StructDef {
|
||||||
.inner
|
.inner
|
||||||
.make_from_events_statemachine(
|
.make_from_events_statemachine(
|
||||||
&state_ty_ident,
|
&state_ty_ident,
|
||||||
&target_ty_ident.clone().into(),
|
&Path::from(target_ty_ident.clone()).into(),
|
||||||
"Struct",
|
"Struct",
|
||||||
)?
|
)?
|
||||||
.with_augmented_init(|init| {
|
.with_augmented_init(|init| {
|
||||||
|
|
|
@ -17,15 +17,20 @@ assert_eq!(foo, Foo);
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
The derive macros need to know which XML namespace and name the elements it
|
The derive macros need additional information, such as XML namespaces and
|
||||||
is supposed have. This must be specified via key-value pairs on the type the
|
names to match. This must be specified via key-value pairs on the type or
|
||||||
derive macro is invoked on. These are specified as Rust attributes. In order
|
fields the derive macro is invoked on. These key-value pairs are specified as
|
||||||
to disambiguate between XML attributes and Rust attributes, we are going to
|
Rust attributes. In order to disambiguate between XML attributes and Rust
|
||||||
refer to Rust attributes using the term *meta* instead, which is consistent
|
attributes, we are going to refer to Rust attributes using the term *meta*
|
||||||
with the Rust language reference calling that syntax construct *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
|
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 |
|
| Key | Value type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
@ -43,16 +48,20 @@ and cannot be overridden. The following will thus not compile:
|
||||||
struct Foo;
|
struct Foo;
|
||||||
```
|
```
|
||||||
|
|
||||||
## Limitations
|
### Field meta
|
||||||
|
|
||||||
Supports only empty structs currently. For example, the following will not
|
For fields, the *meta* consists of a nested meta inside the `#[xml(..)]` meta,
|
||||||
work:
|
the identifier of which controls *how* the field is mapped to XML, while the
|
||||||
|
contents control the parameters of that mapping.
|
||||||
|
|
||||||
```compile_fail
|
The following mapping types are defined:
|
||||||
# use xso::FromXml;
|
|
||||||
#[derive(FromXml, Debug, PartialEq)]
|
| Type | Description |
|
||||||
#[xml(namespace = "urn:example", name = "foo")]
|
| --- | --- |
|
||||||
struct Foo {
|
| [`attribute`](#attribute-meta) | Map the field to an XML attribute on the struct's element |
|
||||||
some_field: String,
|
|
||||||
}
|
#### `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`].
|
||||||
|
|
Loading…
Reference in a new issue