For codec-based error messages, this reduces the amount of errors per violation to one. For all others, it improves the placement of the error slightly, but we still get duplicates. I couldn't figure out the remaining discrepancies in the spans ...
921 lines
36 KiB
Rust
921 lines
36 KiB
Rust
// 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/.
|
||
|
||
//! Handling of the insides of compound structures (structs and enum variants)
|
||
|
||
use proc_macro2::{Span, TokenStream};
|
||
use quote::{quote, quote_spanned, ToTokens};
|
||
use syn::{spanned::Spanned, *};
|
||
|
||
use std::collections::{hash_map::Entry, HashMap};
|
||
|
||
use crate::error_message::{FieldName, ParentRef};
|
||
use crate::field::{FieldBuilderPart, FieldDef, FieldIteratorPart, FieldTempInit, NestedMatcher};
|
||
use crate::meta::{DiscardSpec, Flag, NameRef, NamespaceRef, QNameRef};
|
||
use crate::scope::{mangle_member, AsItemsScope, FromEventsScope};
|
||
use crate::state::{AsItemsSubmachine, FromEventsSubmachine, State};
|
||
use crate::types::{
|
||
default_fn, discard_builder_ty, feed_fn, namespace_ty, ncnamestr_cow_ty, phantom_lifetime_ty,
|
||
ref_ty, unknown_attribute_policy_path, unknown_child_policy_path,
|
||
};
|
||
|
||
fn resolve_policy(policy: Option<Ident>, mut enum_ref: Path) -> Expr {
|
||
match policy {
|
||
Some(ident) => {
|
||
enum_ref.segments.push(ident.into());
|
||
Expr::Path(ExprPath {
|
||
attrs: Vec::new(),
|
||
qself: None,
|
||
path: enum_ref,
|
||
})
|
||
}
|
||
None => {
|
||
let default_fn = default_fn(Type::Path(TypePath {
|
||
qself: None,
|
||
path: enum_ref,
|
||
}));
|
||
Expr::Call(ExprCall {
|
||
attrs: Vec::new(),
|
||
func: Box::new(default_fn),
|
||
paren_token: token::Paren::default(),
|
||
args: punctuated::Punctuated::new(),
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A struct or enum variant's contents.
|
||
pub(crate) struct Compound {
|
||
/// The fields of this compound.
|
||
fields: Vec<FieldDef>,
|
||
|
||
/// Policy defining how to handle unknown attributes.
|
||
unknown_attribute_policy: Expr,
|
||
|
||
/// Policy defining how to handle unknown children.
|
||
unknown_child_policy: Expr,
|
||
|
||
/// Attributes to discard.
|
||
discard_attr: Vec<(Option<NamespaceRef>, NameRef)>,
|
||
|
||
/// Text to discard.
|
||
discard_text: Flag,
|
||
|
||
/// Attribute qualified names which are selected by fields.
|
||
///
|
||
/// This is used to generate code which asserts, at compile time, that no
|
||
/// two fields select the same XML attribute.
|
||
selected_attributes: Vec<(QNameRef, Member)>,
|
||
}
|
||
|
||
impl Compound {
|
||
/// Construct a compound from processed field definitions.
|
||
pub(crate) fn from_field_defs<I: IntoIterator<Item = Result<FieldDef>>>(
|
||
compound_fields: I,
|
||
unknown_attribute_policy: Option<Ident>,
|
||
unknown_child_policy: Option<Ident>,
|
||
discard: Vec<DiscardSpec>,
|
||
) -> Result<Self> {
|
||
let unknown_attribute_policy = resolve_policy(
|
||
unknown_attribute_policy,
|
||
unknown_attribute_policy_path(Span::call_site()),
|
||
);
|
||
let unknown_child_policy = resolve_policy(
|
||
unknown_child_policy,
|
||
unknown_child_policy_path(Span::call_site()),
|
||
);
|
||
let compound_fields = compound_fields.into_iter();
|
||
let size_hint = compound_fields.size_hint();
|
||
let mut fields = Vec::with_capacity(size_hint.1.unwrap_or(size_hint.0));
|
||
let mut text_field = None;
|
||
let mut selected_attributes: HashMap<QNameRef, Member> = HashMap::new();
|
||
for field in compound_fields {
|
||
let field = field?;
|
||
|
||
if field.is_text_field() {
|
||
if let Some(other_field) = text_field.as_ref() {
|
||
let mut err = Error::new_spanned(
|
||
field.member(),
|
||
"only one `#[xml(text)]` field allowed per compound",
|
||
);
|
||
err.combine(Error::new(
|
||
*other_field,
|
||
"the other `#[xml(text)]` field is here",
|
||
));
|
||
return Err(err);
|
||
}
|
||
text_field = Some(field.member().span())
|
||
}
|
||
|
||
if let Some(qname) = field.captures_attribute() {
|
||
let span = field.span();
|
||
match selected_attributes.entry(qname) {
|
||
Entry::Occupied(o) => {
|
||
let mut err = Error::new(
|
||
span,
|
||
"this field XML field matches the same attribute as another field",
|
||
);
|
||
err.combine(Error::new(
|
||
o.get().span(),
|
||
"the other field matching the same attribute is here",
|
||
));
|
||
return Err(err);
|
||
}
|
||
Entry::Vacant(v) => {
|
||
v.insert(field.member().clone());
|
||
}
|
||
}
|
||
}
|
||
|
||
fields.push(field);
|
||
}
|
||
|
||
let mut discard_text = Flag::Absent;
|
||
let mut discard_attr = Vec::new();
|
||
for spec in discard {
|
||
match spec {
|
||
DiscardSpec::Text { span } => {
|
||
if let Some(field) = text_field.as_ref() {
|
||
let mut err = Error::new(
|
||
*field,
|
||
"cannot combine `#[xml(text)]` field with `discard(text)`",
|
||
);
|
||
err.combine(Error::new(
|
||
spec.span(),
|
||
"the discard(text) attribute is here",
|
||
));
|
||
return Err(err);
|
||
}
|
||
if let Flag::Present(other) = discard_text {
|
||
let mut err = Error::new(
|
||
span,
|
||
"only one `discard(text)` meta is allowed per compound",
|
||
);
|
||
err.combine(Error::new(other, "the discard(text) meta is here"));
|
||
return Err(err);
|
||
}
|
||
|
||
discard_text = Flag::Present(span);
|
||
}
|
||
|
||
DiscardSpec::Attribute {
|
||
qname: QNameRef { namespace, name },
|
||
span,
|
||
} => {
|
||
let xml_namespace = namespace;
|
||
let xml_name = match name {
|
||
Some(v) => v,
|
||
None => {
|
||
return Err(Error::new(
|
||
span,
|
||
"discard(attribute) must specify a name, e.g. via discard(attribute = \"some-name\")",
|
||
));
|
||
}
|
||
};
|
||
discard_attr.push((xml_namespace, xml_name));
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok(Self {
|
||
fields,
|
||
unknown_attribute_policy,
|
||
unknown_child_policy,
|
||
discard_attr,
|
||
discard_text,
|
||
selected_attributes: selected_attributes.into_iter().collect(),
|
||
})
|
||
}
|
||
|
||
/// Construct a compound from fields.
|
||
pub(crate) fn from_fields(
|
||
compound_fields: &Fields,
|
||
container_namespace: &NamespaceRef,
|
||
unknown_attribute_policy: Option<Ident>,
|
||
unknown_child_policy: Option<Ident>,
|
||
discard: Vec<DiscardSpec>,
|
||
) -> Result<Self> {
|
||
Self::from_field_defs(
|
||
compound_fields.iter().enumerate().map(|(i, field)| {
|
||
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.",
|
||
))
|
||
}
|
||
};
|
||
FieldDef::from_field(field, index, container_namespace)
|
||
}),
|
||
unknown_attribute_policy,
|
||
unknown_child_policy,
|
||
discard,
|
||
)
|
||
}
|
||
|
||
/// Generate code which, at compile time, asserts that all attributes
|
||
/// which are selected by this compound are disjunct.
|
||
///
|
||
/// NOTE: this needs rustc 1.83 or newer for `const_refs_to_static`.
|
||
fn assert_disjunct_attributes(&self) -> TokenStream {
|
||
let mut checks = TokenStream::default();
|
||
|
||
// Comparison is commutative, so we *could* reduce this to n^2/2
|
||
// comparisons instead of n*(n-1). However, by comparing every field
|
||
// with every other field and emitting check code for that, we can
|
||
// point at both fields in the error messages.
|
||
for (i, (qname_a, member_a)) in self.selected_attributes.iter().enumerate() {
|
||
for (j, (qname_b, member_b)) in self.selected_attributes.iter().enumerate() {
|
||
if i == j {
|
||
continue;
|
||
}
|
||
// Flip a and b around if a is later than b.
|
||
// This way, the error message is the same for both
|
||
// conflicting fields. Note that we always take the span of
|
||
// `a` though, so that the two errors point at different
|
||
// fields.
|
||
let span = member_a.span();
|
||
let (member_a, member_b) = if i > j {
|
||
(member_b, member_a)
|
||
} else {
|
||
(member_a, member_b)
|
||
};
|
||
if qname_a.namespace.is_some() != qname_b.namespace.is_some() {
|
||
// cannot ever match.
|
||
continue;
|
||
}
|
||
let Some((name_a, name_b)) = qname_a.name.as_ref().zip(qname_b.name.as_ref())
|
||
else {
|
||
panic!("selected attribute has no XML local name");
|
||
};
|
||
|
||
let mut check = quote! {
|
||
::xso::exports::const_str_eq(#name_a.as_str(), #name_b.as_str())
|
||
};
|
||
|
||
let namespaces = qname_a.namespace.as_ref().zip(qname_b.namespace.as_ref());
|
||
if let Some((ns_a, ns_b)) = namespaces {
|
||
check.extend(quote! {
|
||
&& ::xso::exports::const_str_eq(#ns_a, #ns_b)
|
||
});
|
||
};
|
||
|
||
let attr_a = if let Some(namespace_a) = qname_a.namespace.as_ref() {
|
||
format!("{{{}}}{}", namespace_a, name_a)
|
||
} else {
|
||
format!("{}", name_a)
|
||
};
|
||
|
||
let attr_b = if let Some(namespace_b) = qname_b.namespace.as_ref() {
|
||
format!("{{{}}}{}", namespace_b, name_b)
|
||
} else {
|
||
format!("{}", name_b)
|
||
};
|
||
|
||
// See TODO below for why we do the extra replace calls.
|
||
let attr_a = attr_a.replace('{', "{{").replace('}', "}}");
|
||
let attr_b = attr_b.replace('{', "{{").replace('}', "}}");
|
||
let field_a = FieldName(&member_a)
|
||
.to_string()
|
||
.replace('{', "{{")
|
||
.replace('}', "}}");
|
||
let field_b = FieldName(&member_b)
|
||
.to_string()
|
||
.replace('{', "{{")
|
||
.replace('}', "}}");
|
||
|
||
// By assigning the checks to a `const`, we ensure that they
|
||
// are in fact evaluated at compile time, even if that constant
|
||
// is never used.
|
||
checks.extend(quote_spanned! {span=>
|
||
const _: () = { if #check {
|
||
// TODO: Rust nightly from 2025-05-02 (or around that date) didn't like our fancy panic, so we use something simpler.
|
||
// Filed bug upstream: https://github.com/rust-lang/rust/issues/140585
|
||
// For now, we work around this because it breaks our docs builds...
|
||
//panic!("member {} and member {} match the same XML attribute: {} == {}", #field_a, #field_b, #attr_a, #attr_b);
|
||
// Note that we cannot replace it with concat!(..), because we may have `{` and `}` in the strings...
|
||
panic!(concat!("member ", #field_a, " and member ", #field_b, " match the same XML attribute: ", #attr_a, " == ", #attr_b));
|
||
} };
|
||
})
|
||
}
|
||
}
|
||
|
||
checks
|
||
}
|
||
|
||
/// Make and return a set of states which is used to construct the target
|
||
/// type from XML events.
|
||
///
|
||
/// The states are returned as partial state machine. See the return
|
||
/// type's documentation for details.
|
||
pub(crate) fn make_from_events_statemachine(
|
||
&self,
|
||
state_ty_ident: &Ident,
|
||
output_name: &ParentRef,
|
||
state_prefix: &str,
|
||
) -> Result<FromEventsSubmachine> {
|
||
let scope = FromEventsScope::new(state_ty_ident.clone());
|
||
let FromEventsScope {
|
||
ref attrs,
|
||
ref builder_data_ident,
|
||
ref text,
|
||
ref substate_data,
|
||
ref substate_result,
|
||
..
|
||
} = scope;
|
||
|
||
let default_state_ident = quote::format_ident!("{}Default", state_prefix);
|
||
let discard_state_ident = quote::format_ident!("{}Discard", state_prefix);
|
||
let builder_data_ty: Type = TypePath {
|
||
qself: None,
|
||
path: quote::format_ident!("{}Data{}", state_ty_ident, state_prefix).into(),
|
||
}
|
||
.into();
|
||
let mut states = Vec::new();
|
||
|
||
let mut builder_data_def = TokenStream::default();
|
||
let mut builder_data_init = TokenStream::default();
|
||
let mut output_cons = TokenStream::default();
|
||
let mut child_matchers = TokenStream::default();
|
||
let mut fallback_child_matcher = None;
|
||
let mut text_handler = if self.discard_text.is_set() {
|
||
Some(quote! {
|
||
::core::result::Result::Ok(::core::ops::ControlFlow::Break(
|
||
Self::#default_state_ident { #builder_data_ident }
|
||
))
|
||
})
|
||
} else {
|
||
None
|
||
};
|
||
let mut extra_defs = TokenStream::default();
|
||
let is_tuple = !output_name.is_path();
|
||
|
||
for (i, field) in self.fields.iter().enumerate() {
|
||
let member = field.member();
|
||
let builder_field_name = mangle_member(member);
|
||
let part = field.make_builder_part(&scope, output_name)?;
|
||
let state_name = quote::format_ident!("{}Field{}", state_prefix, i);
|
||
|
||
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,
|
||
});
|
||
|
||
if is_tuple {
|
||
output_cons.extend(quote! {
|
||
#builder_data_ident.#builder_field_name,
|
||
});
|
||
} else {
|
||
output_cons.extend(quote! {
|
||
#member: #builder_data_ident.#builder_field_name,
|
||
});
|
||
}
|
||
}
|
||
|
||
FieldBuilderPart::Text {
|
||
value: FieldTempInit { ty, init },
|
||
collect,
|
||
finalize,
|
||
} => {
|
||
if text_handler.is_some() {
|
||
// the existence of only one text handler is enforced
|
||
// by Compound's constructor(s).
|
||
panic!("more than one field attempts to collect text data");
|
||
}
|
||
|
||
builder_data_def.extend(quote! {
|
||
#builder_field_name: #ty,
|
||
});
|
||
builder_data_init.extend(quote! {
|
||
#builder_field_name: #init,
|
||
});
|
||
text_handler = Some(quote! {
|
||
#collect
|
||
::core::result::Result::Ok(::core::ops::ControlFlow::Break(
|
||
Self::#default_state_ident { #builder_data_ident }
|
||
))
|
||
});
|
||
|
||
if is_tuple {
|
||
output_cons.extend(quote! {
|
||
#finalize,
|
||
});
|
||
} else {
|
||
output_cons.extend(quote! {
|
||
#member: #finalize,
|
||
});
|
||
}
|
||
}
|
||
|
||
FieldBuilderPart::Nested {
|
||
extra_defs: field_extra_defs,
|
||
value: FieldTempInit { ty, init },
|
||
matcher,
|
||
builder,
|
||
collect,
|
||
finalize,
|
||
} => {
|
||
let feed = feed_fn(builder.clone());
|
||
|
||
let mut substate_data_ident = substate_data.clone();
|
||
substate_data_ident.set_span(ty.span());
|
||
states.push(State::new_with_builder(
|
||
state_name.clone(),
|
||
builder_data_ident,
|
||
&builder_data_ty,
|
||
).with_field(
|
||
&substate_data_ident,
|
||
&builder,
|
||
).with_mut(substate_data).with_impl(quote! {
|
||
match #feed(&mut #substate_data, ev, ctx)? {
|
||
::core::option::Option::Some(#substate_result) => {
|
||
#collect
|
||
::core::result::Result::Ok(::core::ops::ControlFlow::Break(Self::#default_state_ident {
|
||
#builder_data_ident,
|
||
}))
|
||
}
|
||
::core::option::Option::None => {
|
||
::core::result::Result::Ok(::core::ops::ControlFlow::Break(Self::#state_name {
|
||
#builder_data_ident,
|
||
#substate_data,
|
||
}))
|
||
}
|
||
}
|
||
}));
|
||
|
||
builder_data_def.extend(quote! {
|
||
#builder_field_name: #ty,
|
||
});
|
||
|
||
builder_data_init.extend(quote! {
|
||
#builder_field_name: #init,
|
||
});
|
||
|
||
match matcher {
|
||
NestedMatcher::Selective(matcher) => {
|
||
child_matchers.extend(quote! {
|
||
let (name, attrs) = match #matcher {
|
||
::core::result::Result::Err(::xso::error::FromEventsError::Mismatch { name, attrs }) => (name, attrs),
|
||
::core::result::Result::Err(::xso::error::FromEventsError::Invalid(e)) => return ::core::result::Result::Err(e),
|
||
::core::result::Result::Ok(#substate_data) => {
|
||
return ::core::result::Result::Ok(::core::ops::ControlFlow::Break(Self::#state_name {
|
||
#builder_data_ident,
|
||
#substate_data,
|
||
}))
|
||
}
|
||
};
|
||
});
|
||
}
|
||
NestedMatcher::Fallback(matcher) => {
|
||
if let Some((span, _)) = fallback_child_matcher.as_ref() {
|
||
let mut err = Error::new(
|
||
field.span(),
|
||
"more than one field is attempting to consume all unmatched child elements"
|
||
);
|
||
err.combine(Error::new(
|
||
*span,
|
||
"the previous field collecting all unmatched child elements is here"
|
||
));
|
||
return Err(err);
|
||
}
|
||
|
||
let matcher = quote! {
|
||
::core::result::Result::Ok(::core::ops::ControlFlow::Break(Self::#state_name {
|
||
#builder_data_ident,
|
||
#substate_data: { #matcher },
|
||
}))
|
||
};
|
||
|
||
fallback_child_matcher = Some((field.span(), matcher));
|
||
}
|
||
}
|
||
|
||
if is_tuple {
|
||
output_cons.extend(quote! {
|
||
#finalize,
|
||
});
|
||
} else {
|
||
output_cons.extend(quote! {
|
||
#member: #finalize,
|
||
});
|
||
}
|
||
|
||
extra_defs.extend(field_extra_defs);
|
||
}
|
||
}
|
||
}
|
||
|
||
// We always implicitly discard the `xml:lang` attribute. Its
|
||
// processing is handled using the `#[xml(language)]` meta.
|
||
let mut discard_attr = quote! {
|
||
let _ = #attrs.remove(::xso::exports::rxml::Namespace::xml(), "lang");
|
||
};
|
||
for (xml_namespace, xml_name) in self.discard_attr.iter() {
|
||
let xml_namespace = match xml_namespace {
|
||
Some(v) => v.to_token_stream(),
|
||
None => quote! {
|
||
::xso::exports::rxml::Namespace::none()
|
||
},
|
||
};
|
||
discard_attr.extend(quote! {
|
||
let _ = #attrs.remove(#xml_namespace, #xml_name);
|
||
});
|
||
}
|
||
|
||
let text_handler = match text_handler {
|
||
Some(v) => v,
|
||
None => quote! {
|
||
// note: u8::is_ascii_whitespace includes U+000C, which is not
|
||
// part of XML's white space definition.'
|
||
if !::xso::is_xml_whitespace(#text.as_bytes()) {
|
||
::core::result::Result::Err(::xso::error::Error::Other("Unexpected text content".into()))
|
||
} else {
|
||
::core::result::Result::Ok(::core::ops::ControlFlow::Break(
|
||
Self::#default_state_ident { #builder_data_ident }
|
||
))
|
||
}
|
||
},
|
||
};
|
||
|
||
let unknown_attr_err = format!("Unknown attribute in {}.", output_name);
|
||
let unknown_child_err = format!("Unknown child in {}.", output_name);
|
||
let unknown_child_policy = &self.unknown_child_policy;
|
||
|
||
let output_cons = match output_name {
|
||
ParentRef::Named(ref path) => {
|
||
quote! {
|
||
#path { #output_cons }
|
||
}
|
||
}
|
||
ParentRef::Unnamed { .. } => {
|
||
quote! {
|
||
( #output_cons )
|
||
}
|
||
}
|
||
};
|
||
|
||
let discard_builder_ty = discard_builder_ty(Span::call_site());
|
||
let discard_feed = feed_fn(discard_builder_ty.clone());
|
||
let child_fallback = match fallback_child_matcher {
|
||
Some((_, matcher)) => matcher,
|
||
None => quote! {
|
||
let _ = (name, attrs);
|
||
let _: () = #unknown_child_policy.apply_policy(#unknown_child_err)?;
|
||
::core::result::Result::Ok(::core::ops::ControlFlow::Break(Self::#discard_state_ident {
|
||
#builder_data_ident,
|
||
#substate_data: #discard_builder_ty::new(),
|
||
}))
|
||
},
|
||
};
|
||
|
||
states.push(State::new_with_builder(
|
||
discard_state_ident.clone(),
|
||
builder_data_ident,
|
||
&builder_data_ty,
|
||
).with_field(
|
||
substate_data,
|
||
&discard_builder_ty,
|
||
).with_mut(substate_data).with_impl(quote! {
|
||
match #discard_feed(&mut #substate_data, ev, ctx)? {
|
||
::core::option::Option::Some(#substate_result) => {
|
||
::core::result::Result::Ok(::core::ops::ControlFlow::Break(Self::#default_state_ident {
|
||
#builder_data_ident,
|
||
}))
|
||
}
|
||
::core::option::Option::None => {
|
||
::core::result::Result::Ok(::core::ops::ControlFlow::Break(Self::#discard_state_ident {
|
||
#builder_data_ident,
|
||
#substate_data,
|
||
}))
|
||
}
|
||
}
|
||
}));
|
||
|
||
states.push(State::new_with_builder(
|
||
default_state_ident.clone(),
|
||
builder_data_ident,
|
||
&builder_data_ty,
|
||
).with_impl(quote! {
|
||
match ev {
|
||
// EndElement in Default state -> done parsing.
|
||
::xso::exports::rxml::Event::EndElement(_) => {
|
||
::core::result::Result::Ok(::core::ops::ControlFlow::Continue(
|
||
#output_cons
|
||
))
|
||
}
|
||
::xso::exports::rxml::Event::StartElement(_, name, attrs) => {
|
||
#child_matchers
|
||
#child_fallback
|
||
}
|
||
::xso::exports::rxml::Event::Text(_, #text) => {
|
||
#text_handler
|
||
}
|
||
// we ignore these: a correct parser only generates
|
||
// them at document start, and there we want to indeed
|
||
// not worry about them being in front of the first
|
||
// element.
|
||
::xso::exports::rxml::Event::XmlDeclaration(_, ::xso::exports::rxml::XmlVersion::V1_0) => ::core::result::Result::Ok(::core::ops::ControlFlow::Break(
|
||
Self::#default_state_ident { #builder_data_ident }
|
||
))
|
||
}
|
||
}));
|
||
|
||
let unknown_attribute_policy = &self.unknown_attribute_policy;
|
||
|
||
let checks = self.assert_disjunct_attributes();
|
||
|
||
Ok(FromEventsSubmachine {
|
||
defs: quote! {
|
||
#extra_defs
|
||
#checks
|
||
|
||
struct #builder_data_ty {
|
||
#builder_data_def
|
||
}
|
||
},
|
||
states,
|
||
init: quote! {
|
||
let #builder_data_ident = #builder_data_ty {
|
||
#builder_data_init
|
||
};
|
||
#discard_attr
|
||
if #attrs.len() > 0 {
|
||
let _: () = #unknown_attribute_policy.apply_policy(#unknown_attr_err)?;
|
||
}
|
||
::core::result::Result::Ok(#state_ty_ident::#default_state_ident { #builder_data_ident })
|
||
},
|
||
})
|
||
}
|
||
|
||
/// Make and return a set of states which is used to destructure the
|
||
/// target type into XML events.
|
||
///
|
||
/// The states are returned as partial state machine. See the return
|
||
/// type's documentation for details.
|
||
///
|
||
/// **Important:** The returned submachine is not in functional state!
|
||
/// It's `init` must be modified so that a variable called `name` of type
|
||
/// `rxml::QName` is in scope.
|
||
pub(crate) fn make_as_item_iter_statemachine(
|
||
&self,
|
||
input_name: &ParentRef,
|
||
state_ty_ident: &Ident,
|
||
state_prefix: &str,
|
||
lifetime: &Lifetime,
|
||
) -> Result<AsItemsSubmachine> {
|
||
let scope = AsItemsScope::new(lifetime, state_ty_ident.clone());
|
||
|
||
let element_head_start_state_ident =
|
||
quote::format_ident!("{}ElementHeadStart", state_prefix);
|
||
let element_head_end_state_ident = quote::format_ident!("{}ElementHeadEnd", state_prefix);
|
||
let element_foot_state_ident = quote::format_ident!("{}ElementFoot", state_prefix);
|
||
let name_ident = quote::format_ident!("name");
|
||
let ns_ident = quote::format_ident!("ns");
|
||
let dummy_ident = quote::format_ident!("dummy");
|
||
let mut header_states = Vec::new();
|
||
let mut body_states = Vec::new();
|
||
|
||
let is_tuple = !input_name.is_path();
|
||
let mut destructure = TokenStream::default();
|
||
let mut start_init = TokenStream::default();
|
||
let mut extra_defs = TokenStream::default();
|
||
|
||
header_states.push(
|
||
State::new(element_head_start_state_ident.clone())
|
||
.with_field(&dummy_ident, &phantom_lifetime_ty(lifetime.clone()))
|
||
.with_field(&ns_ident, &namespace_ty(Span::call_site()))
|
||
.with_field(
|
||
&name_ident,
|
||
&ncnamestr_cow_ty(Span::call_site(), lifetime.clone()),
|
||
),
|
||
);
|
||
|
||
body_states.push((
|
||
None,
|
||
State::new(element_head_end_state_ident.clone()).with_impl(quote! {
|
||
::core::option::Option::Some(::xso::Item::ElementHeadEnd)
|
||
}),
|
||
));
|
||
|
||
for (i, field) in self.fields.iter().enumerate() {
|
||
let member = field.member();
|
||
let bound_name = mangle_member(member);
|
||
let part = field.make_iterator_part(&scope, input_name, &bound_name)?;
|
||
let state_name = quote::format_ident!("{}Field{}", state_prefix, i);
|
||
let ty = scope.borrow(field.ty().clone());
|
||
|
||
match part {
|
||
FieldIteratorPart::Header { generator } => {
|
||
// We have to make sure that we carry our data around in
|
||
// all the previous states.
|
||
// For header states, it is sufficient to do it here.
|
||
// For body states, we have to do it in a separate loop
|
||
// below to correctly handle the case when a field with a
|
||
// body state is placed before a field with a header
|
||
// state.
|
||
for state in header_states.iter_mut() {
|
||
state.add_field(&bound_name, &ty);
|
||
}
|
||
header_states.push(
|
||
State::new(state_name)
|
||
.with_field(&bound_name, &ty)
|
||
.with_impl(quote! {
|
||
#generator
|
||
}),
|
||
);
|
||
|
||
if is_tuple {
|
||
destructure.extend(quote! {
|
||
ref #bound_name,
|
||
});
|
||
} else {
|
||
destructure.extend(quote! {
|
||
#member: ref #bound_name,
|
||
});
|
||
}
|
||
start_init.extend(quote! {
|
||
#bound_name,
|
||
});
|
||
}
|
||
|
||
FieldIteratorPart::Text { generator } => {
|
||
// We have to make sure that we carry our data around in
|
||
// all the previous body states.
|
||
// We also have to make sure that our data is carried
|
||
// by all *header* states, but we can only do that once
|
||
// we have visited them all, so that happens at the bottom
|
||
// of the loop.
|
||
for (_, state) in body_states.iter_mut() {
|
||
state.add_field(&bound_name, &ty);
|
||
}
|
||
let state = State::new(state_name)
|
||
.with_field(&bound_name, &ty)
|
||
.with_impl(quote! {
|
||
#generator.map(|value| ::xso::Item::Text(
|
||
value,
|
||
))
|
||
});
|
||
if is_tuple {
|
||
destructure.extend(quote! {
|
||
#bound_name,
|
||
});
|
||
} else {
|
||
destructure.extend(quote! {
|
||
#member: #bound_name,
|
||
});
|
||
}
|
||
start_init.extend(quote! {
|
||
#bound_name,
|
||
});
|
||
body_states.push((Some((bound_name, ty)), state));
|
||
}
|
||
|
||
FieldIteratorPart::Content {
|
||
extra_defs: field_extra_defs,
|
||
value: FieldTempInit { ty, init },
|
||
generator,
|
||
} => {
|
||
// We have to make sure that we carry our data around in
|
||
// all the previous body states.
|
||
// We also have to make sure that our data is carried
|
||
// by all *header* states, but we can only do that once
|
||
// we have visited them all, so that happens at the bottom
|
||
// of the loop.
|
||
for (_, state) in body_states.iter_mut() {
|
||
state.add_field(&bound_name, &ty);
|
||
}
|
||
|
||
let state = State::new(state_name.clone())
|
||
.with_field(&bound_name, &ty)
|
||
.with_mut(&bound_name)
|
||
.with_impl(quote! {
|
||
#generator?
|
||
});
|
||
if is_tuple {
|
||
destructure.extend(quote! {
|
||
#bound_name,
|
||
});
|
||
} else {
|
||
destructure.extend(quote! {
|
||
#member: #bound_name,
|
||
});
|
||
}
|
||
start_init.extend(quote! {
|
||
#bound_name: #init,
|
||
});
|
||
|
||
extra_defs.extend(field_extra_defs);
|
||
body_states.push((Some((bound_name, ty)), state));
|
||
}
|
||
}
|
||
}
|
||
|
||
header_states[0].set_impl(quote! {
|
||
{
|
||
::core::option::Option::Some(::xso::Item::ElementHeadStart(
|
||
#ns_ident,
|
||
#name_ident,
|
||
))
|
||
}
|
||
});
|
||
|
||
for (data, _) in body_states.iter() {
|
||
if let Some((bound_name, ty)) = data.as_ref() {
|
||
for state in header_states.iter_mut() {
|
||
state.add_field(bound_name, ty);
|
||
}
|
||
}
|
||
}
|
||
|
||
header_states.extend(body_states.into_iter().map(|(_, state)| state));
|
||
let mut states = header_states;
|
||
|
||
states.push(
|
||
State::new(element_foot_state_ident.clone()).with_impl(quote! {
|
||
::core::option::Option::Some(::xso::Item::ElementFoot)
|
||
}),
|
||
);
|
||
|
||
let destructure = match input_name {
|
||
ParentRef::Named(ref input_path) => quote! {
|
||
#input_path { #destructure }
|
||
},
|
||
ParentRef::Unnamed { .. } => quote! {
|
||
( #destructure )
|
||
},
|
||
};
|
||
|
||
let checks = self.assert_disjunct_attributes();
|
||
extra_defs.extend(checks);
|
||
|
||
Ok(AsItemsSubmachine {
|
||
defs: extra_defs,
|
||
states,
|
||
destructure,
|
||
init: quote! {
|
||
Self::#element_head_start_state_ident { #dummy_ident: ::core::marker::PhantomData, #name_ident: name.1, #ns_ident: name.0, #start_init }
|
||
},
|
||
})
|
||
}
|
||
|
||
/// Return a reference to this compound's only field's type.
|
||
///
|
||
/// If the compound does not have exactly one field, this function returns
|
||
/// None.
|
||
pub(crate) fn single_ty(&self) -> Option<&Type> {
|
||
if self.fields.len() > 1 {
|
||
return None;
|
||
}
|
||
self.fields.first().map(|x| x.ty())
|
||
}
|
||
|
||
/// Construct a tuple type with this compound's field's types in the same
|
||
/// order as they appear in the compound.
|
||
pub(crate) fn to_tuple_ty(&self) -> TypeTuple {
|
||
TypeTuple {
|
||
paren_token: token::Paren::default(),
|
||
elems: self.fields.iter().map(|x| x.ty().clone()).collect(),
|
||
}
|
||
}
|
||
|
||
/// Construct a tuple type with this compound's field's types in the same
|
||
/// order as they appear in the compound.
|
||
pub(crate) fn to_single_or_tuple_ty(&self) -> Type {
|
||
match self.single_ty() {
|
||
None => self.to_tuple_ty().into(),
|
||
Some(v) => v.clone(),
|
||
}
|
||
}
|
||
|
||
/// Construct a tuple type with references to this compound's field's
|
||
/// types in the same order as they appear in the compound, with the given
|
||
/// lifetime.
|
||
pub(crate) fn to_ref_tuple_ty(&self, lifetime: &Lifetime) -> TypeTuple {
|
||
TypeTuple {
|
||
paren_token: token::Paren::default(),
|
||
elems: self
|
||
.fields
|
||
.iter()
|
||
.map(|x| ref_ty(x.ty().clone(), lifetime.clone()))
|
||
.collect(),
|
||
}
|
||
}
|
||
|
||
/// Return the number of fields in this compound.
|
||
pub(crate) fn field_count(&self) -> usize {
|
||
self.fields.len()
|
||
}
|
||
}
|