xmpp-rs/xso-proc/src/field/mod.rs
Jonas Schäfer 67f3ffaced xso: re-implement #[xml(lang)] on top of #[xml(attribute)]
This gives us all the goodies of `default`, `type_` and `codec` without
having to duplicate lots of code (and I think the `match`-iness of the
new macro code is still within limits).

However, we still keep them as separate `#[xml(..)]` attributes, because
their semantics are very different and it is sensible to make them stand
out.

skip-changelog, because `#[xml(lang)]` was introduced in this version.
2025-04-27 14:19:31 +02:00

612 lines
20 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/.
//! Compound (struct or enum variant) field types
use proc_macro2::{Span, TokenStream};
use syn::{spanned::Spanned, *};
use rxml_validation::NcName;
use crate::compound::Compound;
use crate::error_message::ParentRef;
use crate::meta::{
AmountConstraint, AttributeKind, Flag, NameRef, NamespaceRef, QNameRef, XmlFieldMeta,
};
use crate::scope::{AsItemsScope, FromEventsScope};
mod attribute;
mod child;
#[cfg(feature = "minidom")]
mod element;
mod flag;
mod text;
use self::attribute::{AttributeField, AttributeFieldKind};
use self::child::{ChildField, ExtractDef};
#[cfg(feature = "minidom")]
use self::element::ElementField;
use self::flag::FlagField;
use self::text::TextField;
/// 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,
}
/// Configure how a nested field builder selects child elements.
pub(crate) enum NestedMatcher {
/// Matches a specific child element fallabily.
Selective(
/// Expression which evaluates to `Result<T, FromEventsError>`,
/// consuming `name: rxml::QName` and `attrs: rxml::AttrMap`.
///
/// If the `name` and `attrs` allow starting to parse the child
/// element as a value of this field, `Ok(_)` must be returned. If
/// the `name` and `attrs` are those of an element which *could*
/// be a value of this field, but they have invalid contents,
/// `Err(FromEventsError::Invalid(_))` must be returned. Depending
/// on the field kind, it may also be acceptable to return the
/// `Invalid` variant if the data is valid, but no further child
/// element can be accepted into the value.
///
/// Otherwise, the `name` and `attrs` must be returned *unchanged* in
/// a `FromEventsError::Mismatch { .. }` variant. In that case, the
/// implementation in `Compound` will let the next field attempt to
/// parse the child element.
///
/// `T` must be the type specified in the
/// [`FieldBuilderPart::Nested::builder`] field.
TokenStream,
),
#[cfg_attr(not(feature = "minidom"), allow(dead_code))]
/// Matches any child element not matched by another matcher.
///
/// Only a single field may use this variant, otherwise an error is
/// raised during execution of the proc macro.
Fallback(
/// Expression which evaluates to `T` (or `return`s an error),
/// consuming `name: rxml::QName` and `attrs: rxml::AttrMap`.
///
/// Unlike the [`Selective`][`Self::Selective`] variant, this
/// expression must always evaluate to an instance of `T`. If that is
/// not possible, the expression must diverge, most commonly using
/// `return` with a `Err::<_, xso::error::Error>(_)`.
///
/// `T` must be the type specified in the
/// [`FieldBuilderPart::Nested::builder`] field.
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,
},
/// Parse a field from text events.
Text {
/// Expression and type which initializes a buffer to use during
/// parsing.
value: FieldTempInit,
/// Statement which takes text and accumulates it into the temporary
/// value declared via `value`.
collect: TokenStream,
/// Expression which evaluates to the field's type, consuming the
/// temporary value.
finalize: TokenStream,
},
/// Parse a field from child element events.
Nested {
/// Additional definition items which need to be inserted at module
/// level for the rest of the implementation to work.
extra_defs: TokenStream,
/// Expression and type which initializes a buffer to use during
/// parsing.
value: FieldTempInit,
/// Configure child matching behaviour for this field. See
/// [`NestedMatcher`] for options.
matcher: NestedMatcher,
/// Type implementing `xso::FromEventsBuilder` which parses the child
/// element.
///
/// This type is returned by the expressions in
/// [`matcher`][`Self::Nested::matcher`].
builder: Type,
/// Expression which consumes the value stored in the identifier
/// [`crate::common::FromEventsScope::substate_result`][`FromEventsScope::substate_result`]
/// and somehow collects it into the field declared with
/// [`value`][`Self::Nested::value`].
collect: TokenStream,
/// Expression which consumes the data from the field declared with
/// [`value`][`Self::Nested::value`] and converts it into the field's
/// type.
finalize: TokenStream,
},
}
/// 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 {
/// An expression which consumes the field's value and returns a
/// `Item`.
generator: TokenStream,
},
/// The field is emitted as text item.
Text {
/// An expression which consumes the field's value and returns a
/// String, which is then emitted as text data.
generator: TokenStream,
},
/// The field is emitted as series of items which form a child element.
Content {
/// Additional definition items which need to be inserted at module
/// level for the rest of the implementation to work.
extra_defs: TokenStream,
/// Expression and type which initializes the nested iterator.
///
/// Note that this is evaluated at construction time of the iterator.
/// Fields of this variant do not get access to their original data,
/// unless they carry it in the contents of this `value`.
value: FieldTempInit,
/// An expression which uses the value (mutably) and evaluates to
/// a Result<Option<Item>, Error>. Once the state returns None, the
/// processing will advance to the next state.
generator: TokenStream,
},
}
trait Field {
/// 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.
///
/// `member` and `ty` refer to the field itself.
fn make_builder_part(
&self,
scope: &FromEventsScope,
container_name: &ParentRef,
member: &Member,
ty: &Type,
) -> Result<FieldBuilderPart>;
/// 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.
///
/// `member` and `ty` refer to the field itself.
///
/// `bound_name` is the name under which the field's value is accessible
/// in the various parts of the code.
fn make_iterator_part(
&self,
scope: &AsItemsScope,
container_name: &ParentRef,
bound_name: &Ident,
member: &Member,
ty: &Type,
) -> Result<FieldIteratorPart>;
/// Return true if and only if this field captures text content.
fn captures_text(&self) -> bool {
false
}
/// Return a QNameRef if the field captures an attribute.
fn captures_attribute(&self) -> Option<QNameRef> {
None
}
}
fn default_name(span: Span, name: Option<NameRef>, field_ident: Option<&Ident>) -> Result<NameRef> {
match name {
Some(v) => Ok(v),
None => match field_ident {
None => Err(Error::new(
span,
"name must be explicitly specified with the `name` key on unnamed fields",
)),
Some(field_ident) => match NcName::try_from(field_ident.to_string()) {
Ok(value) => Ok(NameRef::Literal {
span: field_ident.span(),
value,
}),
Err(e) => Err(Error::new(
field_ident.span(),
format!("invalid XML name: {}", e),
)),
},
},
}
}
/// 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.
///
/// `field_ty` is needed for type inference on extracted fields.
///
/// `container_namespace` is used in some cases to insert a default
/// namespace.
fn new_field(
meta: XmlFieldMeta,
field_ident: Option<&Ident>,
field_ty: &Type,
container_namespace: &NamespaceRef,
) -> Result<Box<dyn Field>> {
match meta {
XmlFieldMeta::Attribute {
span,
kind: AttributeKind::Generic(QNameRef { name, namespace }),
default_,
type_,
codec,
} => {
let xml_name = default_name(span, name, field_ident)?;
// This would've been taken via `XmlFieldMeta::take_type` if
// this field was within an extract where a `type_` is legal
// to have.
if let Some(type_) = type_ {
return Err(Error::new_spanned(
type_,
"specifying `type_` on fields inside structs and enum variants is redundant and not allowed."
));
}
Ok(Box::new(AttributeField {
kind: AttributeFieldKind::Generic {
xml_name,
xml_namespace: namespace,
},
default_,
codec,
}))
}
XmlFieldMeta::Attribute {
span: _,
kind: AttributeKind::XmlLang,
default_,
type_,
codec,
} => {
// This would've been taken via `XmlFieldMeta::take_type` if
// this field was within an extract where a `type_` is legal
// to have.
if let Some(type_) = type_ {
return Err(Error::new_spanned(
type_,
"specifying `type_` on fields inside structs and enum variants is redundant and not allowed."
));
}
Ok(Box::new(AttributeField {
kind: AttributeFieldKind::XmlLang,
default_,
codec,
}))
}
XmlFieldMeta::Text {
span: _,
codec,
type_,
} => {
// This would've been taken via `XmlFieldMeta::take_type` if
// this field was within an extract where a `type_` is legal
// to have.
if let Some(type_) = type_ {
return Err(Error::new_spanned(
type_,
"specifying `type_` on fields inside structs and enum variants is redundant and not allowed."
));
}
Ok(Box::new(TextField { codec }))
}
XmlFieldMeta::Child {
span: _,
default_,
amount,
} => {
if let Some(AmountConstraint::Any(ref amount_span)) = amount {
if let Flag::Present(ref flag_span) = default_ {
let mut err =
Error::new(*flag_span, "`default` has no meaning for child collections");
err.combine(Error::new(
*amount_span,
"the field is treated as a collection because of this `n` value",
));
return Err(err);
}
}
Ok(Box::new(ChildField {
default_,
amount: amount.unwrap_or(AmountConstraint::FixedSingle(Span::call_site())),
extract: None,
}))
}
XmlFieldMeta::Extract {
span,
default_,
qname: QNameRef { namespace, name },
amount,
fields,
on_unknown_attribute,
on_unknown_child,
} => {
let xml_namespace = namespace.unwrap_or_else(|| container_namespace.clone());
let xml_name = default_name(span, name, field_ident)?;
let amount = amount.unwrap_or(AmountConstraint::FixedSingle(Span::call_site()));
match amount {
AmountConstraint::Any(ref amount) => {
if let Flag::Present(default_) = default_ {
let mut err = Error::new(
default_,
"default cannot be set when collecting into a collection",
);
err.combine(Error::new(
*amount,
"`n` was set to a non-1 value here, which enables collection logic",
));
return Err(err);
}
}
AmountConstraint::FixedSingle(_) => (),
}
let mut field_defs = Vec::new();
let allow_inference =
matches!(amount, AmountConstraint::FixedSingle(_)) && fields.len() == 1;
for (i, mut field) in fields.into_iter().enumerate() {
let field_ty = match field.take_type() {
Some(v) => v,
None => {
if allow_inference {
field_ty.clone()
} else {
return Err(Error::new(
field.span(),
"extracted field must specify a type explicitly when extracting into a collection or when extracting more than one field."
));
}
}
};
field_defs.push(FieldDef::from_extract(
field,
i as u32,
&field_ty,
&xml_namespace,
));
}
let parts = Compound::from_field_defs(
field_defs,
on_unknown_attribute,
on_unknown_child,
vec![],
)?;
Ok(Box::new(ChildField {
default_,
amount,
extract: Some(ExtractDef {
xml_namespace,
xml_name,
parts,
}),
}))
}
#[cfg(feature = "minidom")]
XmlFieldMeta::Element {
span,
default_,
amount,
} => Ok(Box::new(ElementField {
default_,
amount: amount.unwrap_or(AmountConstraint::FixedSingle(span)),
})),
#[cfg(not(feature = "minidom"))]
XmlFieldMeta::Element {
span,
amount,
default_,
} => {
let _ = amount;
let _ = default_;
Err(Error::new(
span,
"#[xml(element)] requires xso to be built with the \"minidom\" feature.",
))
}
XmlFieldMeta::Flag {
span,
qname: QNameRef { namespace, name },
} => {
let xml_namespace = namespace.unwrap_or_else(|| container_namespace.clone());
let xml_name = default_name(span, name, field_ident)?;
Ok(Box::new(FlagField {
xml_namespace,
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 {
/// A span which refers to the field's definition.
span: Span,
/// The member identifying the field.
member: Member,
/// The type of the field.
ty: Type,
/// The way the field is mapped to XML.
inner: Box<dyn Field>,
}
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,
container_namespace: &NamespaceRef,
) -> Result<Self> {
let (member, ident) = match field.ident.as_ref() {
Some(v) => (Member::Named(v.clone()), Some(v)),
None => (
Member::Unnamed(Index {
index,
// We use the type's span here, because `field.span()`
// will visually point at the `#[xml(..)]` meta, which is
// not helpful when glancing at error messages referring
// to the field itself.
span: field.ty.span(),
}),
None,
),
};
// This will either be the field's identifier's span (for named
// fields) or the field's type (for unnamed fields), which should give
// the user a good visual feedback about which field an error message
// is.
let field_span = member.span();
let meta = XmlFieldMeta::parse_from_attributes(&field.attrs, &field_span)?;
let ty = field.ty.clone();
Ok(Self {
span: field_span,
inner: new_field(meta, ident, &ty, container_namespace)?,
member,
ty,
})
}
/// 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_extract(
meta: XmlFieldMeta,
index: u32,
ty: &Type,
container_namespace: &NamespaceRef,
) -> Result<Self> {
let span = meta.span();
Ok(Self {
span,
member: Member::Unnamed(Index { index, span }),
ty: ty.clone(),
inner: new_field(meta, None, ty, container_namespace)?,
})
}
/// 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> {
self.inner
.make_builder_part(scope, container_name, &self.member, &self.ty)
}
/// 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: &AsItemsScope,
container_name: &ParentRef,
bound_name: &Ident,
) -> Result<FieldIteratorPart> {
self.inner
.make_iterator_part(scope, container_name, bound_name, &self.member, &self.ty)
}
/// Return true if this field's parsing consumes text data.
pub(crate) fn is_text_field(&self) -> bool {
self.inner.captures_text()
}
/// Return a QNameRef if the field captures an attribute.
pub(crate) fn captures_attribute(&self) -> Option<QNameRef> {
self.inner.captures_attribute()
}
/// Return a span which points at the field's definition.
pub(crate) fn span(&self) -> Span {
self.span
}
}