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