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'/>");
|
||||
}
|
||||
|
||||
#[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)
|
||||
|
||||
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 => {
|
||||
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(
|
||||
other,
|
||||
"cannot derive on non-unit struct (yet!)",
|
||||
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,22 +169,53 @@ 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! {
|
||||
.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,
|
||||
::xso::exports::rxml::AttrMap::new(),
|
||||
#attrs,
|
||||
))
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
states.push(
|
||||
State::new(end_element_state_ident.clone()).with_impl(quote! {
|
||||
|
@ -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 }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
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::*;
|
||||
|
||||
mod compound;
|
||||
mod error_message;
|
||||
mod field;
|
||||
mod meta;
|
||||
mod scope;
|
||||
mod state;
|
||||
mod structs;
|
||||
mod types;
|
||||
|
|
|
@ -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
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
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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`].
|
||||
|
|
Loading…
Reference in a new issue