// 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/. //! This module concerns the processing of attributes. //! //! In particular, it provides the `#[xml(attribute)]` implementation. use proc_macro2::Span; use quote::{quote, quote_spanned, ToTokens}; use syn::{spanned::Spanned, *}; use std::borrow::Cow; use crate::error_message::{self, ParentRef}; use crate::meta::{Flag, NameRef, NamespaceRef, QNameRef, XMLNS_XML}; use crate::scope::{AsItemsScope, FromEventsScope}; use crate::types::{ as_optional_xml_text_fn, default_fn, from_xml_text_fn, text_codec_decode_fn, text_codec_encode_fn, }; use super::{Field, FieldBuilderPart, FieldIteratorPart, FieldTempInit}; /// Subtype for attribute-matching fields. pub(super) enum AttributeFieldKind { /// Matches any attribute Generic { /// The optional XML namespace of the attribute. xml_namespace: Option, /// The XML name of the attribute. xml_name: NameRef, }, /// Matches `xml:lang` XmlLang, } impl AttributeFieldKind { fn matcher(&self) -> (Cow<'_, Option>, Cow<'_, NameRef>) { match self { Self::Generic { ref xml_namespace, ref xml_name, } => (Cow::Borrowed(xml_namespace), Cow::Borrowed(xml_name)), Self::XmlLang => ( Cow::Owned(Some(NamespaceRef::fudge(XMLNS_XML, Span::call_site()))), Cow::Owned(NameRef::fudge( rxml_validation::NcName::try_from("lang").unwrap(), Span::call_site(), )), ), } } fn qname_ref(&self) -> QNameRef { let (namespace, name) = self.matcher(); QNameRef { namespace: namespace.into_owned(), name: Some(name.into_owned()), } } } /// The field maps to an attribute. pub(super) struct AttributeField { /// Subtype pub(super) kind: AttributeFieldKind, /// Flag indicating whether the value should be defaulted if the /// attribute is absent. pub(super) default_: Flag, /// Optional codec to use. pub(super) codec: Option, } impl Field for AttributeField { fn make_builder_part( &self, scope: &FromEventsScope, container_name: &ParentRef, member: &Member, ty: &Type, ) -> Result { let FromEventsScope { ref attrs, .. } = scope; let ty = ty.clone(); let fetch = match self.kind { AttributeFieldKind::Generic { ref xml_namespace, ref xml_name, } => { let xml_namespace = match xml_namespace { Some(v) => v.to_token_stream(), None => quote! { ::xso::exports::rxml::Namespace::none() }, }; quote! { #attrs.remove(#xml_namespace, #xml_name) } } AttributeFieldKind::XmlLang => { quote! { ctx.language().map(::xso::exports::alloc::borrow::ToOwned::to_owned) } } }; let finalize = match self.codec { Some(ref codec) => { let span = codec.span(); let decode = text_codec_decode_fn(ty.clone(), span); quote_spanned! { span=> |value| #decode(&#codec, value) } } None => { let from_xml_text = from_xml_text_fn(ty.clone()); quote! { #from_xml_text } } }; let missing_msg = error_message::on_missing_attribute(container_name, member); let on_absent = match self.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 #fetch.map(#finalize).transpose()? { ::core::option::Option::Some(v) => v, ::core::option::Option::None => #on_absent, } }, ty: ty.clone(), }, }) } fn make_iterator_part( &self, _scope: &AsItemsScope, _container_name: &ParentRef, bound_name: &Ident, _member: &Member, ty: &Type, ) -> Result { let (xml_namespace, xml_name) = self.kind.matcher(); let xml_namespace = match xml_namespace.as_ref() { Some(ref v) => quote! { ::xso::exports::rxml::Namespace::from(#v) }, None => quote! { ::xso::exports::rxml::Namespace::NONE }, }; let generator = match self.codec { Some(ref codec) => { let span = codec.span(); let encode = text_codec_encode_fn(ty.clone(), span); // NOTE: We need to fudge the span of `bound_name` here, // because its span points outside the macro (the identifier // of the field), which means that quote_spanned will not // override it, which would make the error message ugly. let mut bound_name = bound_name.clone(); bound_name.set_span(span); quote_spanned! { span=> #encode(&#codec, #bound_name)? } } None => { let as_optional_xml_text = as_optional_xml_text_fn(ty.clone()); quote! { #as_optional_xml_text(#bound_name)? } } }; Ok(FieldIteratorPart::Header { generator: quote! { #generator.map(|#bound_name| ::xso::Item::Attribute( #xml_namespace, ::xso::exports::alloc::borrow::Cow::Borrowed(#xml_name), #bound_name, )); }, }) } fn captures_attribute(&self) -> Option { Some(self.kind.qname_ref()) } }