Skip to main content

xso_proc/field/
attribute.rs

1// Copyright (c) 2024 Jonas Schäfer <jonas@zombofant.net>
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7//! This module concerns the processing of attributes.
8//!
9//! In particular, it provides the `#[xml(attribute)]` implementation.
10
11use proc_macro2::Span;
12use quote::{quote, quote_spanned, ToTokens};
13use syn::{spanned::Spanned, *};
14
15use std::borrow::Cow;
16
17use crate::common::GenericsInfo;
18use crate::error_message::{self, ParentRef};
19use crate::meta::{Flag, NameRef, NamespaceRef, QNameRef, XMLNS_XML};
20use crate::scope::{AsItemsScope, FromEventsScope};
21use crate::types::{
22    as_optional_xml_text_fn, as_optional_xml_text_trait, default_fn, from_xml_text_fn,
23    from_xml_text_trait, text_codec_decode_fn, text_codec_encode_fn,
24};
25
26use super::{Field, FieldBuilderPart, FieldIteratorPart, FieldTempInit};
27
28/// Subtype for attribute-matching fields.
29pub(super) enum AttributeFieldKind {
30    /// Matches any attribute
31    Generic {
32        /// The optional XML namespace of the attribute.
33        xml_namespace: Option<NamespaceRef>,
34
35        /// The XML name of the attribute.
36        xml_name: NameRef,
37    },
38
39    /// Matches `xml:lang`
40    XmlLang,
41}
42
43impl AttributeFieldKind {
44    fn matcher(&self) -> (Cow<'_, Option<NamespaceRef>>, Cow<'_, NameRef>) {
45        match self {
46            Self::Generic {
47                ref xml_namespace,
48                ref xml_name,
49            } => (Cow::Borrowed(xml_namespace), Cow::Borrowed(xml_name)),
50            Self::XmlLang => (
51                Cow::Owned(Some(NamespaceRef::fudge(XMLNS_XML, Span::call_site()))),
52                Cow::Owned(NameRef::fudge(
53                    rxml_validation::NcName::try_from("lang").unwrap(),
54                    Span::call_site(),
55                )),
56            ),
57        }
58    }
59
60    fn qname_ref(&self) -> QNameRef {
61        let (namespace, name) = self.matcher();
62        QNameRef {
63            namespace: namespace.into_owned(),
64            name: Some(name.into_owned()),
65        }
66    }
67}
68
69/// The field maps to an attribute.
70pub(super) struct AttributeField {
71    /// Subtype
72    pub(super) kind: AttributeFieldKind,
73
74    /// Flag indicating whether the value should be defaulted if the
75    /// attribute is absent.
76    pub(super) default_: Flag,
77
78    /// Optional codec to use.
79    pub(super) codec: Option<Expr>,
80}
81
82impl Field for AttributeField {
83    fn make_builder_part(
84        &self,
85        scope: &FromEventsScope,
86        generics: &mut GenericsInfo,
87        container_name: &ParentRef,
88        member: &Member,
89        ty: &Type,
90    ) -> Result<FieldBuilderPart> {
91        let FromEventsScope { ref attrs, .. } = scope;
92        let ty = ty.clone();
93
94        let fetch = match self.kind {
95            AttributeFieldKind::Generic {
96                ref xml_namespace,
97                ref xml_name,
98            } => {
99                let xml_namespace = match xml_namespace {
100                    Some(v) => v.to_token_stream(),
101                    None => quote! {
102                        ::xso::exports::rxml::Namespace::none()
103                    },
104                };
105
106                quote! {
107                    #attrs.remove(#xml_namespace, #xml_name)
108                }
109            }
110
111            AttributeFieldKind::XmlLang => {
112                quote! {
113                    ctx.language().map(::xso::exports::alloc::borrow::ToOwned::to_owned)
114                }
115            }
116        };
117
118        let finalize = match self.codec {
119            Some(ref codec) => {
120                let span = codec.span();
121                let decode = text_codec_decode_fn(ty.clone(), span);
122                quote_spanned! { span=>
123                    |value| #decode(&#codec, value)
124                }
125            }
126            None => {
127                generics.add_bound_if_generic(&ty, from_xml_text_trait(Span::call_site()));
128                let from_xml_text = from_xml_text_fn(ty.clone());
129                quote! { #from_xml_text }
130            }
131        };
132
133        let missing_msg = error_message::on_missing_attribute(container_name, member);
134        let on_absent = match self.default_ {
135            Flag::Absent => quote! {
136                return ::core::result::Result::Err(::xso::error::Error::Other(#missing_msg).into())
137            },
138            Flag::Present(_) => {
139                let default_ = default_fn(ty.clone());
140                quote! {
141                    #default_()
142                }
143            }
144        };
145
146        Ok(FieldBuilderPart::Init {
147            value: FieldTempInit {
148                init: quote! {
149                    match #fetch.map(#finalize).transpose()? {
150                        ::core::option::Option::Some(v) => v,
151                        ::core::option::Option::None => #on_absent,
152                    }
153                },
154                ty: ty.clone(),
155            },
156        })
157    }
158
159    fn make_iterator_part(
160        &self,
161        _scope: &AsItemsScope,
162        generics: &mut GenericsInfo,
163        _container_name: &ParentRef,
164        bound_name: &Ident,
165        _member: &Member,
166        ty: &Type,
167    ) -> Result<FieldIteratorPart> {
168        let (xml_namespace, xml_name) = self.kind.matcher();
169        let xml_namespace = match xml_namespace.as_ref() {
170            Some(ref v) => quote! { ::xso::exports::rxml::Namespace::from(#v) },
171            None => quote! {
172                ::xso::exports::rxml::Namespace::NONE
173            },
174        };
175
176        let generator = match self.codec {
177            Some(ref codec) => {
178                let span = codec.span();
179                let encode = text_codec_encode_fn(ty.clone(), span);
180                // NOTE: We need to fudge the span of `bound_name` here,
181                // because its span points outside the macro (the identifier
182                // of the field), which means that quote_spanned will not
183                // override it, which would make the error message ugly.
184                let mut bound_name = bound_name.clone();
185                bound_name.set_span(span);
186                quote_spanned! { span=> #encode(&#codec, #bound_name)? }
187            }
188            None => {
189                generics.add_bound_if_generic(ty, as_optional_xml_text_trait(Span::call_site()));
190                let as_optional_xml_text = as_optional_xml_text_fn(ty.clone());
191                quote! { #as_optional_xml_text(#bound_name)? }
192            }
193        };
194
195        Ok(FieldIteratorPart::Header {
196            generator: quote! {
197                #generator.map(|#bound_name| ::xso::Item::Attribute(
198                    #xml_namespace,
199                    ::xso::exports::alloc::borrow::Cow::Borrowed(#xml_name),
200                    #bound_name,
201                ));
202            },
203        })
204    }
205
206    fn captures_attribute(&self) -> Option<QNameRef> {
207        Some(self.kind.qname_ref())
208    }
209}