1
0
mirror of https://gitlab.com/xmpp-rs/xmpp-rs.git synced 2024-06-18 21:55:57 +02:00
xmpp-rs/parsers-macros/src/field/child.rs
Jonas Schäfer d623b6ab9b parsers-macros: wrap Extend trait into TryExtend for flexibility
This allows containers to reject items based on their content, without
having to resort to a panic.

The use case is containers which are polymorphic, but don't support
different item types, as encountered e.g. in XEP-0060 PubSub publish vs.
retract events.
2024-04-05 15:53:48 +02:00

703 lines
29 KiB
Rust

//! Infrastructure for parsing fields from child elements while destructuring
//! their contents.
use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::{spanned::Spanned, *};
use crate::compound::Compound;
use crate::error_message::{self, ParentRef};
use crate::meta::{Flag, FlagOr, NameRef, NamespaceRef, XmlFieldMeta};
use super::{ChildMode, Field, FieldDef, FieldNamespace, FieldParsePart};
/// Definition of a child data extraction.
///
/// This is used to implement fields annotated with
/// `#[xml(child(.., extract(..))]` or `#[xml(children(.., extract(..)))]`.
#[derive(Debug)]
pub(super) struct ExtractDef {
namespace: FieldNamespace,
name: NameRef,
/// Compound which contains the arguments of the `extract(..)` attribute,
/// transformed into a struct with unnamed fields.
///
/// This is used to generate the parsing/serialisation code, by
/// essentially "declaring" a shim struct, as if it were a real Rust
/// struct, and using the result of the parsing process directly for the
/// field on which the `extract(..)` option was used, instead of putting
/// it into a Rust struct.
parts: Compound,
}
impl ExtractDef {
fn expand_namespace(&self, container_namespace_expr: &Expr) -> Expr {
match self.namespace {
FieldNamespace::Static(ref ns) => Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: ns.clone().into(),
}),
FieldNamespace::Super(_) => container_namespace_expr.clone(),
}
}
/// Construct an `ExtractDef`.
///
/// The `namespace` and `name` identify the XML element this `ExtractDef`
/// works on, i.e. the child element to match.
///
/// `parts` contains the pieces of data to extract from the child in the
/// order they are extracted.
///
/// Finally, `single_extract_type` should be passed if the extract is used
/// in the context of a `#[xml(child)]` field (i.e. not for a container)
/// and it should then be the type of that field. This allows defaulting
/// the type of the extract's field to that type if it has not been
/// specified explicitly by the user.
fn new(
span: Span,
namespace: FieldNamespace,
name: NameRef,
parts: Vec<Box<XmlFieldMeta>>,
mut single_extract_type: Option<Type>,
) -> Result<Self> {
if parts.len() != 1 {
single_extract_type = None;
}
let parts = Compound::new(
None,
None,
parts.into_iter().enumerate().map(|(i, x)| {
FieldDef::from_extract(span.clone(), *x, i as u32, single_extract_type.take())
}),
)?;
Ok(Self {
namespace,
name,
parts,
})
}
/// Construct a token stream containing an expression which tries to
/// process the child element at the identifier `residual`.
///
/// The value of the expression is a `Result<T, Element>`. If the
/// element in `residual` does not match the XML namespace and name of
/// this extract definition, it is returned as `Err(#residual)`.
///
/// Otherwise, the element is destructured according to the extraction
/// specification contained in `self`. If this extract consists of a
/// single field, that field is returned after an unbounded call to
/// `Into::into`. The call to `into` allows assignment to `Option<T>`.
///
/// If the extract consists of more than one field, these fields are
/// returned as tuple without any further conversion.
///
/// The `parent_namespace_expr`, if given, is evaluated, cloned and
/// latched into a local variable (in the generated code) and that is
/// passed on to the inner call to [`Compound::build_into_element`].
fn build_extract(
&self,
container_name: &ParentRef,
container_namespace_expr: &Expr,
residual: &Ident,
) -> Result<TokenStream> {
let namespace_expr = self.expand_namespace(container_namespace_expr);
let xml_name = &self.name;
let nfields = self.parts.field_count();
let repack = if nfields == 1 {
quote! { data.0.into() }
} else {
quote! { data }
};
let parse =
self.parts
.build_try_from_element(container_name, &namespace_expr, residual, &[])?;
let test_expr = match self.namespace {
FieldNamespace::Static(ref xml_namespace) => quote! {
#residual.is(#xml_name, #xml_namespace)
},
FieldNamespace::Super(_) => quote! {
::std::cmp::PartialEq::eq(&#namespace_expr, #residual.ns().as_str()) && #residual.name() == #xml_name
},
};
Ok(quote! {
if #test_expr {
let data = #parse;
Ok(#repack)
} else {
Err(#residual)
}
})
}
/// Construct a token stream containing an expression evaluating to a
/// `Option<minidom::Element>`.
///
/// This is the reverse operation of
/// [`build_extract`][`Self::build_extract`], and like in that function,
/// there are weird edge cases in here.
///
/// `field` must be the expression which contains the extracted field's
/// data. It is evaluated exactly once.
///
/// The `parent_namespace_expr`, if given, is evaluated, cloned and
/// latched into a local variable (in the generated code) and that is
/// passed on to the inner call to [`Compound::build_into_element`].
fn build_assemble(
&self,
container_name: &ParentRef,
container_namespace_expr: &Expr,
field: &Expr,
) -> Result<TokenStream> {
let xml_namespace = self.expand_namespace(container_namespace_expr);
let xml_name = &self.name;
let nfields = self.parts.field_count();
let ident = Ident::new("__extract_data", Span::call_site());
let repack = if nfields == 1 {
quote! { let #ident = (#ident,); }
} else {
quote! { let #ident = #ident; }
};
let builder = Ident::new("builder", Span::call_site());
let builder_init = match self.namespace {
FieldNamespace::Static(ref xml_namespace) => quote! {
::xmpp_parsers_core::exports::minidom::Element::builder(
#xml_name,
#xml_namespace
)
},
FieldNamespace::Super(_) => quote! {
::xmpp_parsers_core::exports::minidom::Element::builder(
#xml_name,
::xmpp_parsers_core::DynNamespaceEnum::into_xml_text(#xml_namespace.clone()),
)
},
};
let build =
self.parts
.build_into_element(container_name, &xml_namespace, &builder, |member| {
Expr::Field(ExprField {
attrs: Vec::new(),
dot_token: syn::token::Dot {
spans: [Span::call_site()],
},
base: Box::new(Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: Path::from(ident.clone()),
})),
member,
})
})?;
Ok(quote! {
{
let #ident = #field;
#repack
let #builder = #builder_init;
let #builder = #build;
#builder.build()
}
})
}
/// Return the type of the only field, if this extract has exactly one
/// field, or None otherwise.
fn inner_type(&self) -> Option<&Type> {
self.parts.single_type()
}
}
/// A field parsed from an XML child, destructured into a Rust data structure.
///
/// Maps to `#[xml(child)]` and `#[xml(children)]`.
#[derive(Debug)]
pub(crate) struct ChildField {
/// Determines whether one or more matching child elements are expected.
///
/// This is basically the difference between `#[xml(child(..))]` and
/// `#[xml(children(..))]`.
mode: ChildMode,
/// If set, the field's value will be obtained by destructuring the child
/// element using the given [`ExtractDef`], instead of parsing it using
/// `FromXml`.
extract: Option<ExtractDef>,
/// If set, `extract` must be None, the child's type must implement
/// `DynNamespace` and the compound must use `namespace = dyn`.
super_namespace: Flag,
/// If set, the field's value will be generated using
/// [`std::default::Default`] or the given path if no matching child can
/// be found, instead of aborting parsing with an error.
default_: FlagOr<Path>,
/// If set, it must point to a function. That function will be called with
/// an immutable reference to the field's value and must return a boolean.
/// If that boolean is true, the child will not be emitted.
skip_if: Option<Path>,
/// If set, it must point to a type. The `FromXml`/`IntoXml`
/// implementations of that type will be used instead, and the type must
/// implement `ElementCodec<T>`, where `T` is the type of the field.
codec: Option<Path>,
}
impl ChildField {
/// Construct a new `#[xml(child)]` or `#[xml(children)]` field.
///
/// `mode` distinguishes between `#[xml(child(..))]` and
/// `#[xml(children(..))]` fields.
///
/// If the child is going to be extracted, it `namespace` and `name` must
/// identify the target child's XML namespace and name and `extract` must
/// be the extraction parts to process.
///
/// Otherwise, if no extract is intended, `namespace` and `name` must be
/// `None` and `extract` must be empty.
///
/// The `default_` flag stored, see [`Self::default_`] for semantics.
///
/// `field_type` must be the type of the field. It is used to configure
/// the extract correctly, if it is specified and the mode is single.
///
/// `attr_span` is used for emitting error messages when no better span
/// can be constructed. This should point at the `#[xml(..)]` meta of the
/// field or another closely-related object.
pub(super) fn new(
attr_span: &Span,
mode: ChildMode,
namespace: Option<NamespaceRef>,
name: Option<NameRef>,
extract: Vec<Box<XmlFieldMeta>>,
default_: FlagOr<Path>,
skip_if: Option<Path>,
codec: Option<Path>,
field_type: &Type,
) -> Result<Self> {
if extract.len() > 0 {
let namespace = match namespace {
None => {
return Err(Error::new(
attr_span.clone(),
"namespace must be specified on extracted fields",
))
}
Some(NamespaceRef::Static(ns)) => FieldNamespace::Static(ns),
Some(NamespaceRef::Dyn(ns)) => {
return Err(Error::new_spanned(
ns,
"extracted fields cannot use dynamic namespaces",
))
}
Some(NamespaceRef::Super(ns)) => FieldNamespace::Super(ns),
};
let Some(name) = name else {
return Err(Error::new(
attr_span.clone(),
"name must be specified on extracted fields",
));
};
if let Some(codec) = codec {
return Err(Error::new_spanned(
codec,
"codec = .. cannot be combined with extract(..)",
));
}
let single_extract_type = match mode {
ChildMode::Single => {
if extract.len() > 1 {
return Err(Error::new(
attr_span.clone(),
"extracting multiple texts from children is only on collection fields",
));
};
Some(field_type.clone())
}
ChildMode::Collection => None,
};
Ok(Self {
mode,
extract: Some(ExtractDef::new(
attr_span.clone(),
namespace,
name.into(),
extract,
single_extract_type,
)?),
skip_if,
default_,
super_namespace: Flag::Absent,
codec: None,
})
} else {
let super_namespace = match namespace {
None => Flag::Absent,
Some(NamespaceRef::Super(ns)) => Flag::Present(ns.span),
Some(namespace) => {
return Err(Error::new_spanned(
namespace,
"namespace declaration not allowed on non-extracted child fields",
));
}
};
if let Some(name) = name {
return Err(Error::new_spanned(
name,
"name declaration not allowed on non-extracted child fields",
));
}
Ok(Self {
mode,
extract: None,
default_,
skip_if,
super_namespace,
codec,
})
}
}
}
impl Field for ChildField {
fn build_try_from_element(
&self,
container_name: &ParentRef,
container_namespace_expr: &Expr,
tempname: Ident,
member: &Member,
ty: &Type,
) -> Result<FieldParsePart> {
let ty_span = ty.span();
let ty_default = quote_spanned! {ty_span=> <#ty as std::default::Default>::default};
match self.mode {
ChildMode::Single => {
let missingerr = error_message::on_missing_child(container_name, &member);
let duperr = error_message::on_duplicate_child(container_name, &member);
let on_missing = match self.default_ {
FlagOr::Absent => {
quote! {
return Err(::xmpp_parsers_core::error::Error::ParseError(#missingerr));
}
}
FlagOr::Present(_) => {
quote! {
#ty_default()
}
}
FlagOr::Value { ref value, .. } => {
quote! {
#value()
}
}
};
match self.extract {
Some(ref extract) => {
let extract = extract.build_extract(
&container_name.child(member.clone()),
container_namespace_expr,
&Ident::new("residual", Span::call_site()),
)?;
Ok(FieldParsePart {
tempinit: quote! {
let mut #tempname: Option<#ty> = None;
},
childiter: quote! {
residual = match #extract {
Ok(v) => {
if #tempname.is_some() {
return Err(::xmpp_parsers_core::error::Error::ParseError(#duperr));
}
#tempname = Some(v);
continue;
},
Err(residual) => residual,
};
},
value: quote! {
if let Some(v) = #tempname {
v
} else {
#on_missing
}
},
..FieldParsePart::default()
})
}
None => {
let ns_test = match self.super_namespace {
Flag::Absent => quote! { true },
Flag::Present(_) => quote! {
::std::cmp::PartialEq::eq(&#container_namespace_expr, residual.ns().as_str())
},
};
let codec_ty = match self.codec {
Some(ref ty) => Type::Path(TypePath {
qself: None,
path: ty.clone(),
}),
None => ty.clone(),
};
let field_ty = ty;
let codec_ty_span = codec_ty.span();
let codec_ty_from_tree = quote_spanned! {codec_ty_span=> <#codec_ty as ::xmpp_parsers_core::FromXml>::from_tree};
let codec_ty_absent = quote_spanned! {codec_ty_span=> <#codec_ty as ::xmpp_parsers_core::FromXml>::absent};
let codec_ty_decode = quote_spanned! {codec_ty_span=> <#codec_ty as ::xmpp_parsers_core::ElementCodec::<#field_ty>>::decode};
Ok(FieldParsePart {
tempinit: quote! {
let mut #tempname: Option<#field_ty> = None;
},
childiter: quote! {
let mut residual = if #ns_test {
match #codec_ty_from_tree(residual) {
Ok(v) => {
let v = #codec_ty_decode(v);
if #tempname.is_some() {
return Err(::xmpp_parsers_core::error::Error::ParseError(#duperr));
}
#tempname = Some(v);
continue
}
Err(::xmpp_parsers_core::error::Error::TypeMismatch(_, _, e)) => e,
Err(other) => return Err(other),
}
} else {
residual
};
},
value: quote! {
if let Some(v) = #tempname {
v
} else if let Some(v) = #codec_ty_absent().map(#codec_ty_decode) {
v
} else {
#on_missing
}
},
..FieldParsePart::default()
})
}
}
}
ChildMode::Collection => {
let item_ty: Type = syn::parse2(quote_spanned! {ty_span=>
<#ty as IntoIterator>::Item
})
.expect("failed to construct item type");
let ty_try_extend = quote_spanned! {ty_span=> <#ty as ::xmpp_parsers_core::TryExtend<#item_ty>>::try_extend};
match self.extract {
Some(ref extract) => {
let extract = extract.build_extract(
&container_name.child(member.clone()),
container_namespace_expr,
&Ident::new("residual", Span::call_site()),
)?;
Ok(FieldParsePart {
tempinit: quote! {
let mut #tempname = #ty_default();
},
childiter: quote! {
residual = match #extract {
Ok(v) => {
#ty_try_extend(&mut #tempname, [v])?;
continue;
},
Err(residual) => residual,
};
},
value: quote! { #tempname },
..FieldParsePart::default()
})
}
None => {
if let Flag::Present(span) = self.super_namespace {
return Err(Error::new(
span,
"#[xml(namespace = dyn)] not supported for #[xml(children)]",
));
}
let codec_ty = match self.codec {
Some(ref ty) => Type::Path(TypePath {
qself: None,
path: ty.clone(),
}),
None => item_ty.clone(),
};
let codec_ty_span = codec_ty.span();
let codec_ty_from_tree = quote_spanned! {codec_ty_span=> <#codec_ty as ::xmpp_parsers_core::FromXml>::from_tree};
let codec_ty_decode = quote_spanned! {codec_ty_span=> <#codec_ty as ::xmpp_parsers_core::ElementCodec::<#item_ty>>::decode};
Ok(FieldParsePart {
tempinit: quote! {
let mut #tempname = #ty_default();
},
childiter: quote! {
let mut residual = match #codec_ty_from_tree(residual) {
Ok(item) => {
let item = #codec_ty_decode(item);
#ty_try_extend(&mut #tempname, [item])?;
continue;
},
Err(::xmpp_parsers_core::error::Error::TypeMismatch(_, _, e)) => e,
Err(other) => return Err(other),
};
},
value: quote! { #tempname },
..FieldParsePart::default()
})
}
}
}
}
}
fn build_set_namespace(&self, input: &Ident, ty: &Type, access: Expr) -> Result<TokenStream> {
match self.mode {
ChildMode::Single => match self.extract {
Some(_) => Ok(quote! {}),
None => match self.super_namespace {
Flag::Absent => Ok(quote! {}),
Flag::Present(_) => {
let ty_span = ty.span();
// using quote_spanned in this way here causes the "the trait `DynNamespace` is not implemented for `..`" error message appear on member_ty instead of on the derive macro invocation.
let method = quote_spanned! {ty_span=> <#ty as ::xmpp_parsers_core::DynNamespace>::set_namespace};
Ok(quote! {
#method(&mut #access, #input.clone());
})
}
},
},
_ => Ok(quote! {}),
}
}
fn build_into_element(
&self,
container_name: &ParentRef,
container_namespace_expr: &Expr,
member: &Member,
ty: &Type,
access: Expr,
) -> Result<TokenStream> {
let temp_ident = Ident::new("__data", Span::call_site());
let skip_map = match self.skip_if {
Some(ref callable) => quote! {
match #callable(&#temp_ident) {
false => Some(#temp_ident),
true => None,
}
},
None => quote! { Some(#temp_ident) },
};
match self.mode {
ChildMode::Single => match self.extract {
Some(ref extract) => {
let temp_expr = Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: temp_ident.clone().into(),
});
let inner_ty = extract
.inner_type()
.expect("child extract can only have one field!")
.clone();
let assemble = extract.build_assemble(
&container_name.child(member.clone()),
container_namespace_expr,
&temp_expr,
)?;
Ok(quote! {
match Option::<#inner_ty>::from(#access).and_then(|#temp_ident| #skip_map) {
Some(#temp_ident) => builder.append(::xmpp_parsers_core::exports::minidom::Node::Element(#assemble)),
None => builder,
}
})
}
None => {
let codec_ty = match self.codec {
Some(ref ty) => Type::Path(TypePath {
qself: None,
path: ty.clone(),
}),
None => ty.clone(),
};
let field_ty = ty;
let codec_ty_span = codec_ty.span();
let codec_ty_into_tree = quote_spanned! {codec_ty_span=> <#codec_ty as ::xmpp_parsers_core::IntoXml>::into_tree};
let codec_ty_encode = quote_spanned! {codec_ty_span=> <#codec_ty as ::xmpp_parsers_core::ElementCodec::<#field_ty>>::encode};
Ok(quote! {
{
let #temp_ident = #access;
match #skip_map.map(#codec_ty_encode).and_then(#codec_ty_into_tree) {
Some(#temp_ident) => builder.append(::xmpp_parsers_core::exports::minidom::Node::Element(#temp_ident)),
None => builder,
}
}
})
}
},
ChildMode::Collection => match self.extract {
Some(ref extract) => {
let assemble = extract.build_assemble(
&container_name.child(member.clone()),
container_namespace_expr,
&Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: temp_ident.clone().into(),
}),
)?;
Ok(quote! {
builder.append_all(
#access.into_iter().filter_map(|#temp_ident| {
match #skip_map {
Some(#temp_ident) => Some(#assemble),
None => None,
}
})
)
})
}
None => {
let ty_span = ty.span();
let item_ty: Type = syn::parse2(quote_spanned! {ty_span=>
<#ty as IntoIterator>::Item
})
.expect("failed to construct item type");
let codec_ty = match self.codec {
Some(ref ty) => Type::Path(TypePath {
qself: None,
path: ty.clone(),
}),
None => item_ty.clone(),
};
let codec_ty_span = codec_ty.span();
let codec_ty_into_tree = quote_spanned! {codec_ty_span=> <#codec_ty as ::xmpp_parsers_core::IntoXml>::into_tree};
let codec_ty_encode = quote_spanned! {codec_ty_span=> <#codec_ty as ::xmpp_parsers_core::ElementCodec::<#item_ty>>::encode};
Ok(quote! {
builder.append_all(#access.into_iter().filter_map(|#temp_ident| {
#skip_map.map(#codec_ty_encode).and_then(#codec_ty_into_tree).map(|el| ::xmpp_parsers_core::exports::minidom::Node::Element(el))
}))
})
}
},
}
}
}