// 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 quote::{quote, ToTokens}; use syn::{spanned::Spanned, *}; use rxml_validation::NcName; use crate::compound::Compound; use crate::error_message::{self, ParentRef}; use crate::meta::{AmountConstraint, Flag, NameRef, NamespaceRef, QNameRef, XmlFieldMeta}; 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_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 /// 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, } /// 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, /// Expression which evaluates to `Result`, /// consuming `name: rxml::QName` and `attrs: rxml::AttrMap`. /// /// `T` must be the type specified in the /// [`Self::Nested::builder`] field. matcher: TokenStream, /// 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, }, } /// Definition of what to extract from a child element. struct ExtractDef { /// The XML namespace of the child to extract data from. xml_namespace: NamespaceRef, /// The XML name of the child to extract data from. xml_name: NameRef, /// Compound which contains the arguments of the `extract(..)` meta /// (except the `from`), 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, } /// Specify how the field is mapped to XML. enum FieldKind { /// The field maps to an attribute. Attribute { /// The optional XML namespace of the attribute. xml_namespace: Option, /// The XML name of the attribute. xml_name: NameRef, /// Flag indicating whether the value should be defaulted if the /// attribute is absent. default_: Flag, }, /// The field maps to the character data of the element. Text { /// Optional codec to use codec: Option, }, /// The field maps to a child Child { /// Flag indicating whether the value should be defaulted if the /// child is absent. default_: Flag, /// Number of child elements allowed. amount: AmountConstraint, /// If set, the child element is not parsed as a field implementing /// `FromXml` / `AsXml`, but instead its contents are extracted. extract: Option, }, } 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), )), }, }, } } impl FieldKind { /// 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 from_meta( 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(Self::Attribute { 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(Self::Text { 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(Self::Child { 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(Self::Child { default_, amount, extract: Some(ExtractDef { xml_namespace, xml_name, parts, }), }) } } } } /// Definition of a single field in a compound. /// /// See [`Compound`][`crate::compound::Compound`] for more information on /// compounds in general. pub(crate) struct FieldDef { /// The member identifying the field. member: Member, /// The type of the field. ty: Type, /// The way the field is mapped to XML. kind: FieldKind, } 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 { kind: FieldKind::from_meta(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 { member: Member::Unnamed(Index { index, span }), ty: ty.clone(), kind: FieldKind::from_meta(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 { match self.kind { FieldKind::Attribute { ref xml_name, ref xml_namespace, ref default_, } => { let FromEventsScope { ref attrs, .. } = scope; let ty = self.ty.clone(); let missing_msg = error_message::on_missing_attribute(container_name, &self.member); let xml_namespace = match xml_namespace { Some(v) => v.to_token_stream(), None => quote! { ::xso::exports::rxml::Namespace::none() }, }; let from_xml_text = from_xml_text_fn(ty.clone()); let on_absent = match default_ { Flag::Absent => quote! { return ::core::result::Result::Err(::xso::error::Error::Other(#missing_msg).into()) }, Flag::Present(_) => { let default_ = default_fn(ty.clone()); quote! { #default_() } } }; Ok(FieldBuilderPart::Init { value: FieldTempInit { init: quote! { match #attrs.remove(#xml_namespace, #xml_name).map(#from_xml_text).transpose()? { ::core::option::Option::Some(v) => v, ::core::option::Option::None => #on_absent, } }, ty: self.ty.clone(), }, }) } FieldKind::Text { ref codec } => { let FromEventsScope { ref text, .. } = scope; let field_access = scope.access_field(&self.member); let finalize = match codec { Some(codec) => { let decode = text_codec_decode_fn(self.ty.clone()); quote! { #decode(&#codec, #field_access)? } } None => { let from_xml_text = from_xml_text_fn(self.ty.clone()); quote! { #from_xml_text(#field_access)? } } }; Ok(FieldBuilderPart::Text { value: FieldTempInit { init: quote! { ::std::string::String::new() }, ty: string_ty(Span::call_site()), }, collect: quote! { #field_access.push_str(#text.as_str()); }, finalize, }) } FieldKind::Child { ref default_, ref amount, ref extract, } => { let FromEventsScope { ref substate_result, .. } = scope; let field_access = scope.access_field(&self.member); let element_ty = match amount { AmountConstraint::FixedSingle(_) => self.ty.clone(), AmountConstraint::Any(_) => into_iterator_item_ty(self.ty.clone()), }; let (extra_defs, matcher, fetch, builder) = match extract { Some(ExtractDef { ref xml_namespace, ref xml_name, ref parts, }) => { let from_xml_builder_ty_ident = scope.make_member_type_name(&self.member, "FromXmlBuilder"); let state_ty_ident = quote::format_ident!("{}State", from_xml_builder_ty_ident,); let extra_defs = parts.make_from_events_statemachine( &state_ty_ident, &container_name.child(self.member.clone()), "", )?.with_augmented_init(|init| quote! { if name.0 == #xml_namespace && name.1 == #xml_name { #init } else { ::core::result::Result::Err(::xso::error::FromEventsError::Mismatch { name, attrs }) } }).compile().render( &Visibility::Inherited, &from_xml_builder_ty_ident, &state_ty_ident, &parts.to_tuple_ty().into(), )?; let from_xml_builder_ty = ty_from_ident(from_xml_builder_ty_ident.clone()).into(); let matcher = quote! { #state_ty_ident::new(name, attrs).map(|x| #from_xml_builder_ty_ident(::core::option::Option::Some(x))) }; ( extra_defs, matcher, // This little ".into()" here goes a long way. It // relies on one of the most underrated trait // implementations in the standard library: // `impl From for Option`, which creates a // `Some(_)` from a `T`. Why is it so great? // Because there is also `impl From> for // Option` (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` if the field is of // type `Option`, and it does the right thing // no matter whether the extracted field is of // type `Option` 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`: // 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, ) } None => { let from_events = from_events_fn(element_ty.clone()); let from_xml_builder = from_xml_builder_ty(element_ty.clone()); let matcher = quote! { #from_events(name, attrs) }; let builder = from_xml_builder; ( TokenStream::default(), matcher, quote! { #substate_result }, builder, ) } }; match amount { AmountConstraint::FixedSingle(_) => { let missing_msg = error_message::on_missing_child(container_name, &self.member); let duplicate_msg = error_message::on_duplicate_child(container_name, &self.member); let on_absent = match default_ { Flag::Absent => quote! { return ::core::result::Result::Err(::xso::error::Error::Other(#missing_msg).into()) }, Flag::Present(_) => { let default_ = default_fn(element_ty.clone()); quote! { #default_() } } }; Ok(FieldBuilderPart::Nested { extra_defs, value: FieldTempInit { init: quote! { ::core::option::Option::None }, ty: option_ty(self.ty.clone()), }, matcher: quote! { match #matcher { ::core::result::Result::Ok(v) => if #field_access.is_some() { ::core::result::Result::Err(::xso::error::FromEventsError::Invalid(::xso::error::Error::Other(#duplicate_msg))) } else { ::core::result::Result::Ok(v) }, ::core::result::Result::Err(e) => ::core::result::Result::Err(e), } }, builder, collect: quote! { #field_access = ::core::option::Option::Some(#fetch); }, finalize: quote! { match #field_access { ::core::option::Option::Some(value) => value, ::core::option::Option::None => #on_absent, } }, }) } AmountConstraint::Any(_) => { let ty_extend = extend_fn(self.ty.clone(), element_ty.clone()); let ty_default = default_fn(self.ty.clone()); Ok(FieldBuilderPart::Nested { extra_defs, value: FieldTempInit { init: quote! { #ty_default() }, ty: self.ty.clone(), }, matcher, builder, collect: quote! { #ty_extend(&mut #field_access, [#fetch]); }, finalize: quote! { #field_access }, }) } } } } } /// 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 { match self.kind { FieldKind::Attribute { ref xml_name, ref xml_namespace, .. } => { let xml_namespace = match xml_namespace { Some(v) => quote! { ::xso::exports::rxml::Namespace::from(#v) }, None => quote! { ::xso::exports::rxml::Namespace::NONE }, }; let as_optional_xml_text = as_optional_xml_text_fn(self.ty.clone()); Ok(FieldIteratorPart::Header { generator: quote! { #as_optional_xml_text(#bound_name)?.map(|#bound_name| ::xso::Item::Attribute( #xml_namespace, ::std::borrow::Cow::Borrowed(#xml_name), #bound_name, )); }, }) } FieldKind::Text { ref codec } => { let generator = match codec { Some(codec) => { let encode = text_codec_encode_fn(self.ty.clone()); quote! { #encode(&#codec, #bound_name)? } } None => { let as_xml_text = as_xml_text_fn(self.ty.clone()); quote! { ::core::option::Option::Some(#as_xml_text(#bound_name)?) } } }; Ok(FieldIteratorPart::Text { generator }) } FieldKind::Child { default_: _, ref amount, ref extract, } => { let AsItemsScope { ref lifetime, .. } = scope; let item_ty = match amount { AmountConstraint::FixedSingle(_) => self.ty.clone(), AmountConstraint::Any(_) => { // This should give us the type of element stored in the // collection. into_iterator_item_ty(self.ty.clone()) } }; let (extra_defs, init, iter_ty) = match extract { Some(ExtractDef { ref xml_namespace, ref xml_name, ref parts, }) => { let item_iter_ty_ident = scope.make_member_type_name(&self.member, "AsXmlIterator"); let state_ty_ident = quote::format_ident!("{}State", item_iter_ty_ident,); let mut item_iter_ty = ty_from_ident(item_iter_ty_ident.clone()); item_iter_ty.path.segments[0].arguments = PathArguments::AngleBracketed(AngleBracketedGenericArguments { colon2_token: None, lt_token: token::Lt::default(), args: [GenericArgument::Lifetime(lifetime.clone())] .into_iter() .collect(), gt_token: token::Gt::default(), }); let item_iter_ty = item_iter_ty.into(); let extra_defs = parts .make_as_item_iter_statemachine( &container_name.child(self.member.clone()), &state_ty_ident, "", lifetime, )? .with_augmented_init(|init| { quote! { let name = ( ::xso::exports::rxml::Namespace::from(#xml_namespace), ::std::borrow::Cow::Borrowed(#xml_name), ); #init } }) .compile() .render( &Visibility::Inherited, &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, // Again we exploit the extreme usefulness of the // `impl From for Option`. 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, ) } None => { let as_xml_iter = as_xml_iter_fn(item_ty.clone()); let item_iter = item_iter_ty(item_ty.clone(), lifetime.clone()); ( TokenStream::default(), quote! { #as_xml_iter(#bound_name)? }, item_iter, ) } }; match amount { AmountConstraint::FixedSingle(_) => Ok(FieldIteratorPart::Content { extra_defs, value: FieldTempInit { init, ty: iter_ty }, generator: quote! { #bound_name.next().transpose() }, }), AmountConstraint::Any(_) => { // This is the collection type we actually work // with -- as_xml_iter uses references after all. let ty = ref_ty(self.ty.clone(), lifetime.clone()); // But the iterator for iterating over the elements // inside the collection must use the ref type. let element_iter = into_iterator_iter_ty(ty.clone()); // And likewise the into_iter impl. let into_iter = into_iterator_into_iter_fn(ty.clone()); let state_ty = Type::Tuple(TypeTuple { paren_token: token::Paren::default(), elems: [element_iter, option_ty(iter_ty)].into_iter().collect(), }); Ok(FieldIteratorPart::Content { extra_defs, value: FieldTempInit { init: quote! { (#into_iter(#bound_name), ::core::option::Option::None) }, ty: state_ty, }, generator: quote! { loop { if let ::core::option::Option::Some(current) = #bound_name.1.as_mut() { if let ::core::option::Option::Some(item) = current.next() { break ::core::option::Option::Some(item).transpose(); } } if let ::core::option::Option::Some(item) = #bound_name.0.next() { #bound_name.1 = ::core::option::Option::Some({ let #bound_name = item; #init }); } else { break ::core::result::Result::Ok(::core::option::Option::None) } } }, }) } } } } } /// Return true if this field's parsing consumes text data. pub(crate) fn is_text_field(&self) -> bool { matches!(self.kind, FieldKind::Text { .. }) } }