// Copyright (c) 2024 Jonas Schäfer // // 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, Flag, NameRef, NamespaceRef, QNameRef, XmlFieldMeta}; use crate::scope::{AsItemsScope, FromEventsScope}; mod attribute; mod child; #[cfg(feature = "minidom")] mod element; mod text; use self::attribute::AttributeField; use self::child::{ChildField, ExtractDef}; #[cfg(feature = "minidom")] use self::element::ElementField; 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`, /// consuming `name: rxml::QName` and `attrs: rxml::AttrMap`. /// /// `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`. /// /// `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, 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; /// 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; /// Return true if and only if this field captures text content. fn captures_text(&self) -> bool { false } } fn default_name(span: Span, name: Option, field_ident: Option<&Ident>) -> Result { 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 inferrence 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> { match meta { XmlFieldMeta::Attribute { span, qname: QNameRef { namespace, name }, default_, type_, } => { 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 { xml_name, xml_namespace: namespace, default_, })) } 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, } => { let xml_namespace = namespace.unwrap_or_else(|| container_namespace.clone()); let xml_name = default_name(span, name, field_ident)?; let mut field = { let mut fields = fields.into_iter(); let Some(field) = fields.next() else { return Err(Error::new( span, "`#[xml(extract(..))]` must contain one `fields(..)` nested meta which contains at least one field meta." )); }; if let Some(field) = fields.next() { return Err(Error::new( field.span(), "more than one extracted piece of data is currently not supported", )); } field }; let amount = amount.unwrap_or(AmountConstraint::FixedSingle(Span::call_site())); let field_ty = match field.take_type() { Some(v) => v, None => match amount { // Only allow inferrence for single values: inferrence // for collections will always be wrong. AmountConstraint::FixedSingle(_) => field_ty.clone(), _ => { return Err(Error::new( field.span(), "extracted field must specify a type explicitly when extracting into a collection." )); } }, }; let parts = Compound::from_field_defs( [FieldDef::from_extract(field, 0, &field_ty, &xml_namespace)].into_iter(), )?; Ok(Box::new(ChildField { default_, amount, extract: Some(ExtractDef { xml_namespace, xml_name, parts, }), })) } #[cfg(feature = "minidom")] XmlFieldMeta::Element { span, amount } => { match amount { Some(AmountConstraint::Any(_)) => (), Some(AmountConstraint::FixedSingle(span)) => { return Err(Error::new( span, "only `n = ..` is supported for #[xml(element)]` currently", )) } None => return Err(Error::new(span, "`n` must be set to `..` currently")), } Ok(Box::new(ElementField)) } #[cfg(not(feature = "minidom"))] XmlFieldMeta::Element { span, amount } => { let _ = amount; Err(Error::new( span, "#[xml(element)] requires xso to be built with the \"minidom\" feature.", )) } } } /// 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, } 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 { let field_span = field.span(); let meta = XmlFieldMeta::parse_from_attributes(&field.attrs, &field_span)?; let (member, ident) = match field.ident.as_ref() { Some(v) => (Member::Named(v.clone()), Some(v)), None => ( Member::Unnamed(Index { index, span: field_span, }), None, ), }; 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 { 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 { 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 { 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 span which points at the field's definition.' pub(crate) fn span(&self) -> Span { self.span } }