#![forbid(unsafe_code)]
#![warn(missing_docs, clippy::pedantic)]
use proc_macro::TokenStream;
use quote::quote;
use syn::spanned::Spanned;
use syn::{Data, DeriveInput, Field, Fields, LitStr, Path, Token, parse_macro_input};
#[proc_macro_derive(FromElement, attributes(element))]
pub fn derive_from_element(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match from_element(&input) {
Ok(tokens) => tokens.into(),
Err(error) => error.to_compile_error().into(),
}
}
enum Mapping {
Attribute(LitStr),
ChildText(LitStr),
Nested(LitStr),
Raw,
None,
}
fn from_element(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let name = &input.ident;
let krate = crate_path(input)?;
let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
let mut reads = Vec::new();
for field in named_fields(input)? {
let ident = field.ident.as_ref().expect("named fields");
let ty = &field.ty;
reads.push(match mapping(field)? {
Mapping::Attribute(name) => quote! {
#ident: <#ty as #krate::typed::FromElementValue>::from_attribute(
element.attribute(#name)
)
},
Mapping::ChildText(name) => quote! {
#ident: <#ty as #krate::typed::FromElementValue>::from_child_text(
element.child(#name).and_then(#krate::xml::Element::text_opt)
)
},
Mapping::Nested(name) => quote! {
#ident: element
.child(#name)
.map(<#ty as #krate::typed::FromElement>::from_element)
.unwrap_or_default()
},
Mapping::Raw => quote! {
#ident: ::core::clone::Clone::clone(element)
},
Mapping::None => quote! {
#ident: ::core::default::Default::default()
},
});
}
Ok(quote! {
#[automatically_derived]
impl #impl_generics #krate::typed::FromElement for #name #type_generics #where_clause {
fn from_element(element: &#krate::xml::Element) -> Self {
#name { #(#reads),* }
}
}
})
}
fn named_fields(input: &DeriveInput) -> syn::Result<impl Iterator<Item = &Field>> {
match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(named) => Ok(named.named.iter()),
other => Err(syn::Error::new(
other.span(),
"hl7-3 derives map field names to element attributes and children, so the struct needs named fields",
)),
},
Data::Enum(_) | Data::Union(_) => Err(syn::Error::new(
input.ident.span(),
"hl7-3 derives apply to structs; an enum or union has no single element shape",
)),
}
}
fn crate_path(input: &DeriveInput) -> syn::Result<Path> {
for attribute in &input.attrs {
if !attribute.path().is_ident("element") {
continue;
}
return attribute.parse_args_with(|stream: syn::parse::ParseStream| {
stream.parse::<Token![crate]>().map_err(|_| {
syn::Error::new(
attribute.span(),
"the only #[element(...)] option on a struct is `crate = ...`; \
name attributes belong on fields",
)
})?;
stream.parse::<Token![=]>()?;
if stream.peek(LitStr) {
return stream.parse::<LitStr>()?.parse();
}
stream.parse()
});
}
Ok(syn::parse_quote!(::hl7_3))
}
fn mapping(field: &Field) -> syn::Result<Mapping> {
let mut found = Mapping::None;
for attribute in &field.attrs {
if !attribute.path().is_ident("element") {
continue;
}
if !matches!(found, Mapping::None) {
return Err(syn::Error::new(
attribute.span(),
"a field takes one #[element(...)] attribute",
));
}
found = attribute.parse_args_with(|input: syn::parse::ParseStream| {
if input.peek(LitStr) {
return Ok(Mapping::Attribute(input.parse()?));
}
let word: syn::Ident = input.parse()?;
match word.to_string().as_str() {
"raw" => Ok(Mapping::Raw),
"child" => {
input.parse::<syn::Token![=]>()?;
Ok(Mapping::ChildText(input.parse()?))
}
"nested" => {
input.parse::<syn::Token![=]>()?;
Ok(Mapping::Nested(input.parse()?))
}
other => Err(syn::Error::new(
word.span(),
format!(
"unknown #[element(...)] option {other:?}; expected an attribute \
name such as #[element(\"classCode\")], `child = \"name\"`, \
`nested = \"name\"`, or `raw`"
),
)),
}
})?;
}
Ok(found)
}
#[cfg(test)]
mod tests {
use super::*;
use quote::ToTokens;
fn resolved(attributes: &str) -> String {
let input: DeriveInput = syn::parse_str(&format!("{attributes} struct S {{ f: u32 }}"))
.expect("test input parses");
crate_path(&input)
.expect("crate path resolves")
.to_token_stream()
.to_string()
.replace(' ', "")
}
#[test]
fn defaults_to_the_absolute_crate_name() {
assert_eq!(resolved(""), "::hl7_3");
}
#[test]
fn a_bare_path_is_taken_as_written() {
assert_eq!(resolved("#[element(crate = hl7)]"), "hl7");
assert_eq!(
resolved("#[element(crate = ::vendor::hl7_3)]"),
"::vendor::hl7_3"
);
}
#[test]
fn a_quoted_path_is_the_same_thing() {
assert_eq!(
resolved(r#"#[element(crate = "::vendor::hl7_3")]"#),
"::vendor::hl7_3"
);
}
#[test]
fn a_struct_attribute_that_is_not_crate_says_so() {
let input: DeriveInput = syn::parse_str("#[element(\"classCode\")] struct S { f: u32 }")
.expect("test input parses");
let Err(error) = crate_path(&input) else {
panic!("a name literal is not a struct option");
};
assert!(error.to_string().contains("belong on fields"), "{error}");
}
}