xmpp-rs/xso-proc/src/field/attribute.rs
Jonas Schäfer de39f410d5 xso-proc: improve spans for error messages
For codec-based error messages, this reduces the amount of errors per
violation to one. For all others, it improves the placement of the error
slightly, but we still get duplicates.

I couldn't figure out the remaining discrepancies in the spans ...
2025-05-02 17:16:59 +02:00

204 lines
6.5 KiB
Rust

// 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.
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<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()),
}
}
}
/// 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<Expr>,
}
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();
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<FieldIteratorPart> {
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<QNameRef> {
Some(self.kind.qname_ref())
}
}