2024-08-04 15:54:46 +02:00
|
|
|
// Copyright (c) 2024 Jonas Schäfer <jonas@zombofant.net>
|
|
|
|
|
//
|
|
|
|
|
// 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.
|
|
|
|
|
|
2025-04-27 10:32:17 +02:00
|
|
|
use proc_macro2::Span;
|
2025-05-02 16:56:22 +02:00
|
|
|
use quote::{quote, quote_spanned, ToTokens};
|
|
|
|
|
use syn::{spanned::Spanned, *};
|
2024-08-04 15:54:46 +02:00
|
|
|
|
2025-04-27 10:32:17 +02:00
|
|
|
use std::borrow::Cow;
|
|
|
|
|
|
2024-08-04 15:54:46 +02:00
|
|
|
use crate::error_message::{self, ParentRef};
|
2025-04-27 10:32:17 +02:00
|
|
|
use crate::meta::{Flag, NameRef, NamespaceRef, QNameRef, XMLNS_XML};
|
2024-08-04 15:54:46 +02:00
|
|
|
use crate::scope::{AsItemsScope, FromEventsScope};
|
2025-01-25 16:21:22 +01:00
|
|
|
use crate::types::{
|
|
|
|
|
as_optional_xml_text_fn, default_fn, from_xml_text_fn, text_codec_decode_fn,
|
|
|
|
|
text_codec_encode_fn,
|
|
|
|
|
};
|
2024-08-04 15:54:46 +02:00
|
|
|
|
|
|
|
|
use super::{Field, FieldBuilderPart, FieldIteratorPart, FieldTempInit};
|
|
|
|
|
|
2025-04-27 10:32:17 +02:00
|
|
|
/// Subtype for attribute-matching fields.
|
|
|
|
|
pub(super) enum AttributeFieldKind {
|
|
|
|
|
/// Matches any attribute
|
|
|
|
|
Generic {
|
|
|
|
|
/// The optional XML namespace of the attribute.
|
|
|
|
|
xml_namespace: Option<NamespaceRef>,
|
|
|
|
|
|
|
|
|
|
/// The XML name of the attribute.
|
|
|
|
|
xml_name: NameRef,
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/// Matches `xml:lang`
|
|
|
|
|
XmlLang,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl AttributeFieldKind {
|
|
|
|
|
fn matcher(&self) -> (Cow<'_, Option<NamespaceRef>>, 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()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-04 15:54:46 +02:00
|
|
|
/// The field maps to an attribute.
|
|
|
|
|
pub(super) struct AttributeField {
|
2025-04-27 10:32:17 +02:00
|
|
|
/// Subtype
|
|
|
|
|
pub(super) kind: AttributeFieldKind,
|
2024-08-04 15:54:46 +02:00
|
|
|
|
|
|
|
|
/// Flag indicating whether the value should be defaulted if the
|
|
|
|
|
/// attribute is absent.
|
|
|
|
|
pub(super) default_: Flag,
|
2025-01-25 16:21:22 +01:00
|
|
|
|
|
|
|
|
/// Optional codec to use.
|
|
|
|
|
pub(super) codec: Option<Expr>,
|
2024-08-04 15:54:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Field for AttributeField {
|
|
|
|
|
fn make_builder_part(
|
|
|
|
|
&self,
|
|
|
|
|
scope: &FromEventsScope,
|
|
|
|
|
container_name: &ParentRef,
|
|
|
|
|
member: &Member,
|
|
|
|
|
ty: &Type,
|
|
|
|
|
) -> Result<FieldBuilderPart> {
|
|
|
|
|
let FromEventsScope { ref attrs, .. } = scope;
|
|
|
|
|
let ty = ty.clone();
|
|
|
|
|
|
2025-04-27 10:32:17 +02:00
|
|
|
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()
|
|
|
|
|
},
|
|
|
|
|
};
|
2024-08-04 15:54:46 +02:00
|
|
|
|
2025-04-27 10:32:17 +02:00
|
|
|
quote! {
|
|
|
|
|
#attrs.remove(#xml_namespace, #xml_name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AttributeFieldKind::XmlLang => {
|
|
|
|
|
quote! {
|
|
|
|
|
ctx.language().map(::xso::exports::alloc::borrow::ToOwned::to_owned)
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-08-04 15:54:46 +02:00
|
|
|
};
|
|
|
|
|
|
2025-01-25 16:21:22 +01:00
|
|
|
let finalize = match self.codec {
|
|
|
|
|
Some(ref codec) => {
|
2025-05-02 16:56:22 +02:00
|
|
|
let span = codec.span();
|
|
|
|
|
let decode = text_codec_decode_fn(ty.clone(), span);
|
|
|
|
|
quote_spanned! { span=>
|
2025-01-25 16:21:22 +01:00
|
|
|
|value| #decode(&#codec, value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
let from_xml_text = from_xml_text_fn(ty.clone());
|
|
|
|
|
quote! { #from_xml_text }
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-08-04 15:54:46 +02:00
|
|
|
|
2025-04-27 10:32:17 +02:00
|
|
|
let missing_msg = error_message::on_missing_attribute(container_name, member);
|
2024-08-04 15:54:46 +02:00
|
|
|
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! {
|
2025-04-27 10:32:17 +02:00
|
|
|
match #fetch.map(#finalize).transpose()? {
|
2024-08-04 15:54:46 +02:00
|
|
|
::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<FieldIteratorPart> {
|
2025-04-27 10:32:17 +02:00
|
|
|
let (xml_namespace, xml_name) = self.kind.matcher();
|
|
|
|
|
let xml_namespace = match xml_namespace.as_ref() {
|
2024-08-04 15:54:46 +02:00
|
|
|
Some(ref v) => quote! { ::xso::exports::rxml::Namespace::from(#v) },
|
|
|
|
|
None => quote! {
|
|
|
|
|
::xso::exports::rxml::Namespace::NONE
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2025-01-25 16:21:22 +01:00
|
|
|
let generator = match self.codec {
|
|
|
|
|
Some(ref codec) => {
|
2025-05-02 16:56:22 +02:00
|
|
|
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)? }
|
2025-01-25 16:21:22 +01:00
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
let as_optional_xml_text = as_optional_xml_text_fn(ty.clone());
|
|
|
|
|
quote! { #as_optional_xml_text(#bound_name)? }
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-08-04 15:54:46 +02:00
|
|
|
|
|
|
|
|
Ok(FieldIteratorPart::Header {
|
|
|
|
|
generator: quote! {
|
2025-01-25 16:21:22 +01:00
|
|
|
#generator.map(|#bound_name| ::xso::Item::Attribute(
|
2024-08-04 15:54:46 +02:00
|
|
|
#xml_namespace,
|
2025-01-26 11:18:03 +01:00
|
|
|
::xso::exports::alloc::borrow::Cow::Borrowed(#xml_name),
|
2024-08-04 15:54:46 +02:00
|
|
|
#bound_name,
|
|
|
|
|
));
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
xso: reject attempts to match the same XML attribute in different fields
This was a bit tricky to build, because it is possible to have an
indirection through a `static` there. Thanks to Rust's extensive
const-fn capabilities, though, it's in fact possible to cover all cases.
We still do two different checks to improve user experience. If we can,
from within the proc macro, determine that two fields refer to the same
XML attribute (because their namespace/name values use the same Rust
tokens), then we reject the fields with a clear error message pointing
at both fields.
In the other case, when there's e.g. `#[xml(lang)]` and
`#[xml(attribute(namespace = rxml::XMLNS_XML, name = "lang"))]`, the
macro cannot be sure that XMLNS_XML is in fact the XML namespace. For
that case, we generate code which is evaluated at compile time (and
has no runtime impact) which panics if the namespace and name of two
attribute-matching fields is the same.
The error message will be less clear (because it contains extra,
unchangeable wording like "evaluation of constant value failed" and "the
evaluated program panicked at", which may be a bit confusing) than the
message generated by the macros themselves, but it's a price we have to
pay unfortunately.
Note that this check may seem cosmetic and purely for better user
experience, but it is in fact needed to avoid generating not-well-formed
and/or not-namespace-well-formed XML: As `AsXml` generates `xso::Item`,
where each attribute is emitted separated (and not aggregated in a
map structure), a naive (and efficient) implementation of a writer might
not double-check that no duplicate attributes are generated.
2025-04-27 09:26:56 +02:00
|
|
|
|
|
|
|
|
fn captures_attribute(&self) -> Option<QNameRef> {
|
2025-04-27 10:32:17 +02:00
|
|
|
Some(self.kind.qname_ref())
|
xso: reject attempts to match the same XML attribute in different fields
This was a bit tricky to build, because it is possible to have an
indirection through a `static` there. Thanks to Rust's extensive
const-fn capabilities, though, it's in fact possible to cover all cases.
We still do two different checks to improve user experience. If we can,
from within the proc macro, determine that two fields refer to the same
XML attribute (because their namespace/name values use the same Rust
tokens), then we reject the fields with a clear error message pointing
at both fields.
In the other case, when there's e.g. `#[xml(lang)]` and
`#[xml(attribute(namespace = rxml::XMLNS_XML, name = "lang"))]`, the
macro cannot be sure that XMLNS_XML is in fact the XML namespace. For
that case, we generate code which is evaluated at compile time (and
has no runtime impact) which panics if the namespace and name of two
attribute-matching fields is the same.
The error message will be less clear (because it contains extra,
unchangeable wording like "evaluation of constant value failed" and "the
evaluated program panicked at", which may be a bit confusing) than the
message generated by the macros themselves, but it's a price we have to
pay unfortunately.
Note that this check may seem cosmetic and purely for better user
experience, but it is in fact needed to avoid generating not-well-formed
and/or not-namespace-well-formed XML: As `AsXml` generates `xso::Item`,
where each attribute is emitted separated (and not aggregated in a
map structure), a naive (and efficient) implementation of a writer might
not double-check that no duplicate attributes are generated.
2025-04-27 09:26:56 +02:00
|
|
|
}
|
2024-08-04 15:54:46 +02:00
|
|
|
}
|