xso: implement support for defaulting extracts

This commit is contained in:
Jonas Schäfer 2024-08-04 15:11:13 +02:00
commit c7c180feb6
7 changed files with 388 additions and 31 deletions

View file

@ -15,7 +15,7 @@ use crate::field::{FieldBuilderPart, FieldDef, FieldIteratorPart, FieldTempInit}
use crate::meta::NamespaceRef;
use crate::scope::{mangle_member, AsItemsScope, FromEventsScope};
use crate::state::{AsItemsSubmachine, FromEventsSubmachine, State};
use crate::types::{feed_fn, namespace_ty, ncnamestr_cow_ty, phantom_lifetime_ty};
use crate::types::{feed_fn, namespace_ty, ncnamestr_cow_ty, phantom_lifetime_ty, ref_ty};
/// A struct or enum variant's contents.
pub(crate) struct Compound {
@ -517,4 +517,27 @@ impl Compound {
},
})
}
/// 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 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(),
}
}
}

View file

@ -19,8 +19,8 @@ use crate::scope::{AsItemsScope, FromEventsScope};
use crate::types::{
as_optional_xml_text_fn, as_xml_iter_fn, as_xml_text_fn, default_fn, extend_fn, from_events_fn,
from_xml_builder_ty, from_xml_text_fn, into_iterator_into_iter_fn, into_iterator_item_ty,
into_iterator_iter_ty, item_iter_ty, option_ty, ref_ty, string_ty, text_codec_decode_fn,
text_codec_encode_fn, ty_from_ident,
into_iterator_iter_ty, item_iter_ty, option_as_xml_ty, option_ty, ref_ty, string_ty,
text_codec_decode_fn, text_codec_encode_fn, ty_from_ident,
};
/// Code slices necessary for declaring and initializing a temporary variable
@ -306,6 +306,7 @@ impl FieldKind {
XmlFieldMeta::Extract {
span,
default_,
qname: QNameRef { namespace, name },
amount,
fields,
@ -352,7 +353,7 @@ impl FieldKind {
)?;
Ok(Self::Child {
default_: Flag::Absent,
default_,
amount,
extract: Some(ExtractDef {
xml_namespace,
@ -565,12 +566,7 @@ impl FieldDef {
&Visibility::Inherited,
&from_xml_builder_ty_ident,
&state_ty_ident,
&Type::Tuple(TypeTuple {
paren_token: token::Paren::default(),
elems: [
element_ty.clone(),
].into_iter().collect(),
})
&parts.to_tuple_ty().into(),
)?;
let from_xml_builder_ty =
ty_from_ident(from_xml_builder_ty_ident.clone()).into();
@ -580,7 +576,31 @@ impl FieldDef {
(
extra_defs,
matcher,
quote! { #substate_result.0 },
// This little ".into()" here goes a long way. It
// relies on one of the most underrated trait
// implementations in the standard library:
// `impl From<T> for Option<T>`, which creates a
// `Some(_)` from a `T`. Why is it so great?
// Because there is also `impl From<Option<T>> for
// Option<T>` (obviously), which is just a move.
// So even without knowing the exact type of the
// substate result and the field, we can make an
// "downcast" to `Option<T>` if the field is of
// type `Option<T>`, and it does the right thing
// no matter whether the extracted field is of
// type `Option<T>` or `T`.
//
// And then, type inferrence does the rest: There
// is ambiguity there, of course, if we call
// `.into()` on a value of type `Option<T>`:
// Should Rust wrap it into another layer of
// `Option`, or should it just move the value? The
// answer lies in the type constraint imposed by
// the place the value is *used*, which is
// strictly bound by the field's type (so there
// is, in fact, no ambiguity). So this works all
// kinds of magic.
quote! { #substate_result.0.into() },
from_xml_builder_ty,
)
}
@ -736,7 +756,7 @@ impl FieldDef {
}
};
let (extra_defs, fetch, as_xml_iter, iter_ty) = match extract {
let (extra_defs, init, iter_ty) = match extract {
Some(ExtractDef {
ref xml_namespace,
ref xml_name,
@ -776,21 +796,25 @@ impl FieldDef {
.compile()
.render(
&Visibility::Inherited,
&Type::Tuple(TypeTuple {
paren_token: token::Paren::default(),
elems: [ref_ty(item_ty.clone(), lifetime.clone())]
.into_iter()
.collect(),
}),
&parts.to_ref_tuple_ty(lifetime).into(),
&state_ty_ident,
lifetime,
&item_iter_ty,
)?;
let item_iter_ty = option_as_xml_ty(item_iter_ty);
(
extra_defs,
quote! { (&#bound_name,) },
quote! { #item_iter_ty_ident::new },
// Again we exploit the extreme usefulness of the
// `impl From<T> for Option<T>`. We already wrote
// extensively about that in the FromXml
// implementation corresponding to this code
// above, and we will not repeat it here.
quote! {
::xso::OptionAsXml::new(::core::option::Option::from(#bound_name).map(|#bound_name| {
#item_iter_ty_ident::new((#bound_name,))
}).transpose()?)
},
item_iter_ty,
)
}
@ -800,8 +824,7 @@ impl FieldDef {
(
TokenStream::default(),
quote! { #bound_name },
quote! { #as_xml_iter },
quote! { #as_xml_iter(#bound_name)? },
item_iter,
)
}
@ -810,12 +833,7 @@ impl FieldDef {
match amount {
AmountConstraint::FixedSingle(_) => Ok(FieldIteratorPart::Content {
extra_defs,
value: FieldTempInit {
init: quote! {
#as_xml_iter(#fetch)?
},
ty: iter_ty,
},
value: FieldTempInit { init, ty: iter_ty },
generator: quote! {
#bound_name.next().transpose()
},
@ -853,7 +871,10 @@ impl FieldDef {
}
}
if let ::core::option::Option::Some(item) = #bound_name.0.next() {
#bound_name.1 = ::core::option::Option::Some(#as_xml_iter({ let #bound_name = item; #fetch })?)
#bound_name.1 = ::core::option::Option::Some({
let #bound_name = item;
#init
});
} else {
break ::core::result::Result::Ok(::core::option::Option::None)
}

View file

@ -680,6 +680,9 @@ pub(crate) enum XmlFieldMeta {
/// The `n` flag.
amount: Option<AmountConstraint>,
/// The `default` flag.
default_: Flag,
/// The `fields` nested meta.
fields: Vec<XmlFieldMeta>,
},
@ -859,8 +862,15 @@ impl XmlFieldMeta {
let mut qname = QNameRef::default();
let mut fields = None;
let mut amount = None;
let mut default_ = Flag::Absent;
meta.parse_nested_meta(|meta| {
if meta.path.is_ident("fields") {
if meta.path.is_ident("default") {
if default_.is_set() {
return Err(Error::new_spanned(meta.path, "duplicate `default` key"));
}
default_ = (&meta.path).into();
Ok(())
} else if meta.path.is_ident("fields") {
if let Some((fields_span, _)) = fields.as_ref() {
let mut error = Error::new_spanned(meta.path, "duplicate `fields` meta");
error.combine(Error::new(*fields_span, "previous `fields` meta was here"));
@ -889,6 +899,7 @@ impl XmlFieldMeta {
let fields = fields.map(|(_, x)| x).unwrap_or_else(Vec::new);
Ok(Self::Extract {
span: meta.path.span(),
default_,
qname,
fields,
amount,

View file

@ -753,3 +753,33 @@ pub(crate) fn ty_from_ident(ident: Ident) -> TypePath {
path: ident.into(),
}
}
/// Construct a [`syn::Type`] referring to `xso::OptionAsXml<#inner_ty>`.
pub(crate) fn option_as_xml_ty(inner_ty: Type) -> Type {
let span = inner_ty.span();
Type::Path(TypePath {
qself: None,
path: Path {
leading_colon: Some(syn::token::PathSep {
spans: [span, span],
}),
segments: [
PathSegment {
ident: Ident::new("xso", span),
arguments: PathArguments::None,
},
PathSegment {
ident: Ident::new("OptionAsXml", span),
arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments {
colon2_token: None,
lt_token: token::Lt { spans: [span] },
args: [GenericArgument::Type(inner_ty)].into_iter().collect(),
gt_token: token::Gt { spans: [span] },
}),
},
]
.into_iter()
.collect(),
},
})
}