xmpp-rs/xso-proc/src/structs.rs

836 lines
29 KiB
Rust

/*!
# Processing of struct declarations
This module contains the main code for implementing the derive macros from
this crate on `struct` items.
It is thus the counterpart to [`crate::enums`].
*/
use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::{spanned::Spanned, *};
use crate::common::{build_prepare, build_validate, ItemDef};
use crate::compound::{Compound, DynCompound};
use crate::error_message::ParentRef;
use crate::meta::{
Flag, Name, NameRef, NamespaceRef, NodeFilterMeta, StaticNamespace, XmlCompoundMeta,
};
/// A XML namespace as declared on a struct.
#[derive(Debug)]
pub(crate) enum StructNamespace {
/// The namespace is a static string.
Static(
/// The namespace as [`Path`] pointing at the static string.
StaticNamespace,
),
/// Instead of a fixed namespace, the namespace is dynamic. The allowed
/// values are determined by a
/// [`NamespaceField`][`crate::field::namespace::NamespaceField`]
/// (declared using `#[xml(namespace)]`).
Dyn {
/// The `dyn` token from the `#[xml(namespace = dyn)]` meta.
#[allow(dead_code)]
dyn_tok: Token![dyn],
/// The type of the namespace field.
ty: Type,
/// The member of the namespace field.
member: Member,
},
}
/// Represent a selector for element-transparent structs.
///
/// See also [`StructInner::Element`].
#[derive(Debug)]
pub(crate) enum ElementSelector {
/// Any element will be accepted.
///
/// Corresponds to `#[xml(element)]`.
Any,
/// The element will be matched by XML name only.
///
/// Corresponds to `#[xml(element(name = ..))]`.
ByName(Name),
/// The element will be matched by XML namespace only.
///
/// Corresponds to `#[xml(element(namespace = ..))]`.
ByNamespace(StaticNamespace),
/// The element will be matched by XML namespace and name..
///
/// Corresponds to `#[xml(element(namespace = .., name = ..))]`.
Qualified {
/// The XML namespace to match.
namespace: StaticNamespace,
/// The XML name to match.
name: Name,
},
}
impl TryFrom<NodeFilterMeta> for ElementSelector {
type Error = Error;
fn try_from(other: NodeFilterMeta) -> Result<Self> {
let namespace = match other.namespace {
None => None,
Some(NamespaceRef::Static(ns)) => Some(ns),
Some(NamespaceRef::Dyn(ns)) => return Err(Error::new_spanned(
ns,
"namespace = dyn cannot be used with element-transparent structs or enum variants."
)),
Some(NamespaceRef::Super(ns)) => return Err(Error::new_spanned(
ns,
"namespace = super cannot be used with element-transparent structs or enum variants."
)),
};
let name = other.name.map(|x| Name::from(x));
match (namespace, name) {
(Some(namespace), Some(name)) => Ok(Self::Qualified { namespace, name }),
(Some(namespace), None) => Ok(Self::ByNamespace(namespace)),
(None, Some(name)) => Ok(Self::ByName(name)),
(None, None) => Ok(Self::Any),
}
}
}
impl ElementSelector {
/// Construct a token stream evaluating to bool.
///
/// If the `minidom::Element` in `residual` matches the selector, the
/// token stream will evaluate to true. Otherwise, it will evaluate to
/// false.
pub(crate) fn build_test(&self, residual: &Ident) -> TokenStream {
match self {
Self::Any => quote! { true },
Self::ByName(name) => quote! {
#residual.name() == #name
},
Self::ByNamespace(ns) => quote! {
#residual.ns() == #ns
},
Self::Qualified { namespace, name } => quote! {
#residual.is(#name, #namespace)
},
}
}
}
/// The inner parts of the struct.
///
/// This contains all data necessary for the matching logic, but does not
/// include validation/preparation of the data. The latter is handled by
/// [`StructDef`].
#[derive(Debug)]
pub(crate) enum StructInner {
/// Single-field tuple-like struct declared with `#[xml(transparent)]`.
///
/// Transparent struct delegate all parsing and serialising to their
/// single field, which is why they do not need to store a lot of
/// information and come with extra restrictions, such as:
///
/// - no XML namespace can be declared (it is determined by inner type)
/// - no XML name can be declared (it is determined by inner type)
/// - the fields must be unnamed
/// - there must be only exactly one field
/// - that field has no `#[xml]` attribute
Transparent {
/// Type of the only unnamed field.
ty: Type,
},
/// Single-field tuple-like struct declared with `#[xml(element)]`.
///
/// Element-transparent structs take the incoming XML element as-is, and
/// re-serialise it as-is.
Element {
/// Determines the set of acceptable XML elements. Elements which do
/// not match the selector will not be parsed.
selector: ElementSelector,
},
/// A compound of fields, *not* declared as transparent.
///
/// This can be a unit, tuple-like, or named struct.
Compound {
/// The XML namespace to match the struct against.
namespace: StructNamespace,
/// The XML name to match the struct against.
name: Name,
/// The contents of the struct.
inner: Compound,
},
}
impl StructInner {
/// Process the `meta` and `fields` into a [`StructInner`].
///
/// The `meta` must be "blank" except for the `transparent`, `namespace`
/// and `name` fields. If any other field has a non-`None` / non-`Absent`
/// value, this function panics!
pub(crate) fn new(meta: XmlCompoundMeta, fields: &Fields) -> Result<Self> {
// These must be taken out by the caller.
assert!(!meta.exhaustive.is_set());
assert!(meta.validate.is_none());
assert!(meta.prepare.is_none());
assert!(meta.normalize_with.is_none());
assert!(!meta.debug.is_set());
assert!(!meta.fallback.is_set());
assert!(meta.attribute.is_none());
assert!(meta.value.is_none());
if let Some(element) = meta.element {
if let Flag::Present(transparent) = meta.transparent {
return Err(Error::new(
transparent,
"transparent option conflicts with element option. pick one or the other.",
));
}
if let Some(namespace) = meta.namespace {
return Err(Error::new_spanned(
namespace,
"namespace option not allowed on element-transparent structs or enum variants",
));
}
if let Some(name) = meta.name {
return Err(Error::new_spanned(
name,
"name option not allowed on element-transparent structs or enum variants",
));
}
Self::new_element(element, fields)
} else if let Flag::Present(_) = meta.transparent {
if let Some(namespace) = meta.namespace {
return Err(Error::new_spanned(
namespace,
"namespace option not allowed on transparent structs or enum variants",
));
}
if let Some(name) = meta.name {
return Err(Error::new_spanned(
name,
"name option not allowed on transparent structs or enum variants",
));
}
Self::new_transparent(fields)
} else {
let Some(namespace) = meta.namespace else {
return Err(Error::new(
meta.span,
"`namespace` option is required on non-transparent structs or enum variants",
));
};
let Some(name) = meta.name else {
return Err(Error::new(
meta.span,
"`name` option is required on non-transparent structs or enum variants",
));
};
Self::new_compound(
namespace,
name,
meta.on_unknown_child,
meta.on_unknown_attribute,
fields,
)
}
}
/// Construct a new transparent struct with the given fields.
///
/// This function ensures that only a single, unnamed field is inside the
/// struct and causes a compile-time error otherwise.
fn new_transparent(fields: &Fields) -> Result<Self> {
let field = match fields {
Fields::Unit => {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must have exactly one field",
))
}
Fields::Named(_) => {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must be tuple-like",
))
}
Fields::Unnamed(fields) => {
if fields.unnamed.len() == 0 {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must have exactly one field",
));
} else if fields.unnamed.len() > 1 {
return Err(Error::new_spanned(
&fields.unnamed[1],
"transparent structs or enum variants must have exactly one field",
));
}
&fields.unnamed[0]
}
};
for attr in field.attrs.iter() {
if attr.path().is_ident("xml") {
return Err(Error::new_spanned(
attr.path(),
"the field inside a #[xml(transparent)] struct or enum variant cannot have an #[xml(..)] attribute."
));
}
}
Ok(Self::Transparent {
ty: field.ty.clone(),
})
}
/// Construct a new element-transparent struct with the given fields.
///
/// This function ensures that only a single, unnamed field is inside the
/// struct and causes a compile-time error otherwise.
fn new_element(node_filter: NodeFilterMeta, fields: &Fields) -> Result<Self> {
let field = match fields {
Fields::Unit => {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must have exactly one field",
))
}
Fields::Named(_) => {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must be tuple-like",
))
}
Fields::Unnamed(fields) => {
if fields.unnamed.len() == 0 {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must have exactly one field",
));
} else if fields.unnamed.len() > 1 {
return Err(Error::new_spanned(
&fields.unnamed[1],
"transparent structs or enum variants must have exactly one field",
));
}
&fields.unnamed[0]
}
};
for attr in field.attrs.iter() {
if attr.path().is_ident("xml") {
return Err(Error::new_spanned(
attr.path(),
"the field inside a #[xml(transparent)] struct or enum variant cannot have an #[xml(..)] attribute."
));
}
}
Ok(Self::Element {
selector: node_filter.try_into()?,
})
}
/// Construct a new compound-based struct with the given namespace, name
/// and fields.
fn new_compound(
namespace: NamespaceRef,
name: NameRef,
on_unknown_child: Option<Ident>,
on_unknown_attribute: Option<Ident>,
fields: &Fields,
) -> Result<Self> {
let inner = Compound::from_fields(on_unknown_child, on_unknown_attribute, fields)?;
let namespace_field = inner.namespace_field();
let namespace = match namespace {
NamespaceRef::Static(namespace) => {
if let Some((span, ..)) = namespace_field {
return Err(Error::new(
span,
"struct or enum variant must be declared with #[xml(namespace = dyn, ..)] to use a #[xml(namespace)] field."
));
}
StructNamespace::Static(namespace)
}
NamespaceRef::Dyn(namespace) => {
if let Some((_, ty, member)) = namespace_field {
StructNamespace::Dyn {
dyn_tok: namespace,
ty: ty.clone(),
member: member.clone(),
}
} else {
return Err(Error::new_spanned(
namespace,
"enum variant or struct declared with #[xml(namespace = dyn)] must have a field annotated with #[xml(namespace)]"
));
}
}
NamespaceRef::Super(ns) => {
return Err(Error::new_spanned(
ns,
"#[xml(namespace = super)] not allowed on enum variant or struct.",
));
}
};
Ok(Self::Compound {
namespace,
name: name.into(),
inner,
})
}
/// Construct an expression which consumes `residual` and evaluates to
/// `Result<T, Element>`.
///
/// - `struct_name` may contain either the path necessary to construct an
/// instance of the struct or a nested parent ref. In the latter case,
/// the struct is constructed as tuple instead of a struct.
///
/// - `residual` must be the identifier of the `minidom::Element` to
/// process.
///
/// If the element does not match the selectors of this struct, it is
/// returned in the `Err` variant for further probing.
pub(crate) fn build_try_from_element(
&self,
struct_name: &ParentRef,
residual: &Ident,
) -> Result<TokenStream> {
match self {
Self::Transparent { ty } => {
let cons = match struct_name {
ParentRef::Named(path) => quote! { #path },
ParentRef::Unnamed { .. } | ParentRef::Wrapper { .. } => quote! {},
};
let ty_from_tree = quote_spanned! {ty.span()=> <#ty as ::xso::FromXml>::from_tree};
Ok(quote! {
match #ty_from_tree(#residual) {
Ok(v) => Ok(#cons (v)),
Err(::xso::error::Error::TypeMismatch(_, _, #residual)) => Err(#residual),
Err(other) => return Err(other),
}
})
}
Self::Element { selector } => {
let test = selector.build_test(residual);
let cons = match struct_name {
ParentRef::Named(path) => quote! { #path },
ParentRef::Unnamed { .. } | ParentRef::Wrapper { .. } => quote! {},
};
Ok(quote! {
if #test {
Ok(#cons ( #residual ))
} else {
Err(#residual)
}
})
}
Self::Compound {
namespace,
name: xml_name,
inner,
} => {
let namespace_tempname = Ident::new("__struct_namespace", Span::call_site());
let namespace_expr = Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: namespace_tempname.clone().into(),
});
let body =
inner.build_try_from_element(struct_name, &namespace_expr, residual, &[])?;
match namespace {
StructNamespace::Dyn { ty, .. } => {
let ty_from_xml_text = quote_spanned! {ty.span()=> <#ty as ::xso::DynNamespaceEnum>::from_xml_text};
Ok(quote! {
match #ty_from_xml_text(&#residual.ns()) {
Ok(#namespace_tempname) => if #residual.name() == #xml_name {
Ok(#body)
} else {
Err(#residual)
}
Err(::xso::error::DynNamespaceError::Invalid) => {
return Err(::xso::error::Error::ParseError(
"Invalid namespace"
));
}
Err(::xso::error::DynNamespaceError::Mismatch) => Err(#residual),
}
})
}
StructNamespace::Static(xml_namespace) => Ok(quote! {
if #residual.is(#xml_name, #xml_namespace) {
let #namespace_tempname = #xml_namespace;
Ok(#body)
} else {
Err(#residual)
}
}),
}
}
}
}
/// Construct an expression which takes the fields as accessed through
/// `access_field` and converts them into a `minidom::Element`.
///
/// - `struct_name` is used primarily for diagnostic messages.s
///
/// - `access_field` must be a function which transforms a [`syn::Member`]
/// referring to a member of the struct to an expression under which the
/// member can be accessed.
pub(crate) fn build_into_element(
&self,
struct_name: &ParentRef,
mut access_field: impl FnMut(Member) -> Expr,
) -> Result<TokenStream> {
match self {
Self::Transparent { ty } => {
let ident = access_field(Member::Unnamed(Index {
index: 0,
span: Span::call_site(),
}));
let ty_into_tree = quote_spanned! {ty.span()=> <#ty as ::xso::IntoXml>::into_tree};
Ok(quote! {
#ty_into_tree(#ident).expect("inner element did not produce any data")
})
}
Self::Element { .. } => {
let ident = access_field(Member::Unnamed(Index {
index: 0,
span: Span::call_site(),
}));
Ok(quote! {
#ident
})
}
Self::Compound {
namespace,
name: xml_name,
inner,
} => {
let builder = Ident::new("builder", Span::call_site());
let (builder_init, namespace_expr) = match namespace {
StructNamespace::Dyn { ref member, ty, .. } => {
let expr = access_field(member.clone());
let ty_into_xml_text = quote_spanned! {ty.span()=> <#ty as ::xso::DynNamespaceEnum>::into_xml_text};
(
quote! {
::xso::exports::minidom::Element::builder(
#xml_name,
#ty_into_xml_text(#expr.clone()),
)
},
expr,
)
}
StructNamespace::Static(xml_namespace) => {
let expr = Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: xml_namespace.clone(),
});
(
quote! {
::xso::exports::minidom::Element::builder(
#xml_name,
#xml_namespace,
)
},
expr,
)
}
};
let body = inner.build_into_element(
struct_name,
&namespace_expr,
&builder,
&mut access_field,
)?;
Ok(quote! {
{
let mut #builder = #builder_init;
let #builder = #body;
#builder.build()
}
})
}
}
}
/// Return an iterator which returns the [`syn::Member`] structs to access
/// the struct's fields in declaration order.
pub(crate) fn iter_members(&self) -> Box<dyn Iterator<Item = Member> + '_> {
match self {
Self::Transparent { .. } | Self::Element { .. } => Box::new(
[Member::Unnamed(Index {
index: 0,
span: Span::call_site(),
})]
.into_iter(),
),
Self::Compound { inner, .. } => inner.iter_members(),
}
}
pub(crate) fn as_dyn(&self) -> Option<DynStructInner<'_>> {
match self {
Self::Transparent { .. } | Self::Element { .. } => None,
Self::Compound { ref inner, .. } => inner.as_dyn().map(|x| DynStructInner { inner: x }),
}
}
}
/// Reference to a [`StructInner`] which has proven that the struct is using
/// namespace = dyn.
///
/// This simplifies some checks here and there.
pub(crate) struct DynStructInner<'x> {
/// The compound with `namespace = dyn` asserted.
inner: DynCompound<'x>,
}
impl<'x> DynStructInner<'x> {
/// Return a reference to the [`Type`] of the field annotated with
/// `#[xml(namespace)]`.
pub(crate) fn namespace_ty(&self) -> &'x Type {
self.inner.namespace_ty()
}
/// Build the implementation of
/// `DynNamespace::namespace(&self) -> &Self::Namespace`.
pub(crate) fn build_get_namespace(
&self,
access_field: impl FnMut(Member) -> Expr,
) -> Result<TokenStream> {
self.inner.build_get_namespace(access_field)
}
/// Build the implementation of
/// `DynNamespace::set_namespace<T: Into<Self::Namespace>>(&mut self, ns: T)`.
pub(crate) fn build_set_namespace(
&self,
input: &Ident,
access_field: impl FnMut(Member) -> Expr,
) -> Result<TokenStream> {
self.inner.build_set_namespace(input, access_field)
}
}
/// Create an accessor function for struct fields.
///
/// `struct_path` must be the path under which the struct is accessible.
fn make_accessor(struct_path: Path) -> impl FnMut(Member) -> Expr {
move |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: struct_path.clone(),
})),
member,
})
}
}
/// Represent a struct.
#[derive(Debug)]
pub(crate) struct StructDef {
/// The `validate` value, if set on the struct.
///
/// This is called after the struct has been otherwise parsed successfully
/// with the struct value as mutable reference as only argument. It is
/// expected to return `Result<(), Error>`, the `Err(..)` variant of which
/// is forwarded correctly.
validate: Option<Path>,
/// The `prepare` value, if set on the struct.
///
/// This is called before the struct will be converted back into an XML
/// element with the struct value as mutable reference as only argument.
prepare: Option<Path>,
/// Structure of the struct.
inner: StructInner,
/// The `debug` flag if set on the struct.
#[cfg_attr(not(feature = "debug"), allow(dead_code))]
debug: Flag,
}
impl StructDef {
/// Construct a new struct from its `#[xml(..)]` attribute and the
/// fields.
fn new(mut meta: XmlCompoundMeta, fields: &Fields) -> Result<Self> {
if let Flag::Present(fallback) = meta.fallback.take() {
return Err(syn::Error::new(
fallback,
"`fallback` is not allowed on structs",
));
}
if let Flag::Present(exhaustive) = meta.exhaustive.take() {
return Err(syn::Error::new(
exhaustive,
"`exhaustive` is not allowed on structs",
));
}
if let Some(attribute) = meta.attribute.take() {
return Err(syn::Error::new_spanned(
attribute,
"`attribute` is not allowed on structs",
));
}
if let Some(value) = meta.value.take() {
return Err(syn::Error::new_spanned(
value,
"`value` is not allowed on structs",
));
}
if let Some(normalize_with) = meta.normalize_with.take() {
return Err(syn::Error::new_spanned(
normalize_with,
"`normalize_with` is not allowed on structs",
));
}
let validate = meta.validate.take();
let prepare = meta.prepare.take();
let debug = meta.debug.take();
Ok(Self {
validate,
prepare,
debug,
inner: StructInner::new(meta, fields)?,
})
}
}
impl ItemDef for StructDef {
fn build_try_from_element(
&self,
struct_name: &ParentRef,
residual: &Ident,
) -> Result<TokenStream> {
let validate = build_validate(self.validate.as_ref());
let try_from_impl = self.inner.build_try_from_element(struct_name, residual)?;
let result = quote! {
{
let mut result = match #try_from_impl {
Ok(v) => v,
Err(residual) => return Err(Self::Error::TypeMismatch("", "", residual)),
};
#validate;
Ok(result)
}
};
#[cfg(feature = "debug")]
if self.debug.is_set() {
println!("{}", result);
}
Ok(result)
}
fn build_into_element(
&self,
struct_name: &ParentRef,
value_ident: &Ident,
) -> Result<TokenStream> {
let prepare = build_prepare(self.prepare.as_ref(), value_ident);
let access_field = make_accessor(Path {
leading_colon: None,
segments: [PathSegment::from(value_ident.clone())]
.into_iter()
.collect(),
});
let into_impl = self.inner.build_into_element(struct_name, access_field)?;
let result = quote! {
{
#prepare
#into_impl
}
};
#[cfg(feature = "debug")]
if self.debug.is_set() {
println!("{}", result);
}
Ok(result)
}
fn build_dyn_namespace(&self) -> Result<TokenStream> {
let dyn_inner = match self.inner.as_dyn() {
Some(v) => v,
None => return Err(Error::new(
Span::call_site(),
"struct must have `namespace = dyn` and a `#[xml(namespace)]` field to derive DynNamespace"
)),
};
let set_namespace_input = Ident::new("ns", Span::call_site());
let mut accessor = make_accessor(Ident::new("self", Span::call_site()).into());
let ty = dyn_inner.namespace_ty();
let namespace_impl = dyn_inner.build_get_namespace(&mut accessor)?;
let set_namespace_impl =
dyn_inner.build_set_namespace(&set_namespace_input, &mut accessor)?;
Ok(quote! {
type Namespace = #ty;
fn namespace(&self) -> &Self::Namespace {
#namespace_impl
}
fn set_namespace<T: Into<Self::Namespace>>(&mut self, #set_namespace_input: T) {
let #set_namespace_input = #set_namespace_input.into();
#set_namespace_impl
}
})
}
}
pub(crate) fn parse_struct(item: &syn::ItemStruct) -> Result<Box<dyn ItemDef>> {
let mut meta = XmlCompoundMeta::parse_from_attributes(&item.attrs)?;
let wrapped_with = meta.wrapped_with.take();
let span = meta.span;
let mut def = Box::new(StructDef::new(meta, &item.fields)?) as Box<dyn ItemDef>;
if let Some(wrapped_with) = wrapped_with {
def = crate::wrapped::wrap(&span, wrapped_with, &item.ident, def)?;
}
Ok(def)
}