#![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(FromHl7, attributes(hl7))]
pub fn derive_from_hl7(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match from_hl7(&input) {
Ok(tokens) => tokens.into(),
Err(error) => error.to_compile_error().into(),
}
}
#[proc_macro_derive(ToHl7, attributes(hl7))]
pub fn derive_to_hl7(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match to_hl7(&input) {
Ok(tokens) => tokens.into(),
Err(error) => error.to_compile_error().into(),
}
}
enum Mapping {
Path(LitStr),
Nested,
Raw,
None,
}
fn from_hl7(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::Path(path) => quote! {
#ident: <#ty as #krate::FromHl7Value>::from_hl7_value(message, #path)?
},
Mapping::Nested => quote! {
#ident: <#ty as #krate::FromHl7>::from_hl7(message)?
},
Mapping::Raw => quote! {
#ident: #krate::Raw::new(::core::clone::Clone::clone(message)).into()
},
Mapping::None => quote! {
#ident: ::core::default::Default::default()
},
});
}
Ok(quote! {
#[automatically_derived]
impl #impl_generics #krate::FromHl7 for #name #type_generics #where_clause {
fn from_hl7(message: &#krate::Message) -> ::core::result::Result<Self, #krate::Error> {
::core::result::Result::Ok(#name { #(#reads),* })
}
}
})
}
fn to_hl7(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 writes = Vec::new();
for field in named_fields(input)? {
let ident = field.ident.as_ref().expect("named fields");
let ty = &field.ty;
match mapping(field)? {
Mapping::Path(path) => writes.push(quote! {
<#ty as #krate::ToHl7Value>::to_hl7_value(&self.#ident, message, #path)?;
}),
Mapping::Nested => writes.push(quote! {
<#ty as #krate::ToHl7>::to_hl7(&self.#ident, message)?;
}),
Mapping::Raw | Mapping::None => {}
}
}
Ok(quote! {
#[automatically_derived]
impl #impl_generics #krate::ToHl7 for #name #type_generics #where_clause {
fn to_hl7(
&self,
message: &mut #krate::Message,
) -> ::core::result::Result<(), #krate::Error> {
#(#writes)*
::core::result::Result::Ok(())
}
}
})
}
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-2 derives map field names to HL7 paths, so the struct needs named fields",
)),
},
Data::Enum(_) | Data::Union(_) => Err(syn::Error::new(
input.ident.span(),
"hl7-2 derives apply to structs; an enum or union has no single message shape",
)),
}
}
fn crate_path(input: &DeriveInput) -> syn::Result<Path> {
for attribute in &input.attrs {
if !attribute.path().is_ident("hl7") {
continue;
}
return attribute.parse_args_with(|stream: syn::parse::ParseStream| {
stream.parse::<Token![crate]>().map_err(|_| {
syn::Error::new(
attribute.span(),
"the only #[hl7(...)] option on a struct is `crate = ...`; \
path attributes belong on fields",
)
})?;
stream.parse::<Token![=]>()?;
if stream.peek(LitStr) {
return stream.parse::<LitStr>()?.parse();
}
stream.parse()
});
}
Ok(syn::parse_quote!(::hl7_2))
}
fn mapping(field: &Field) -> syn::Result<Mapping> {
let mut found = Mapping::None;
for attribute in &field.attrs {
if !attribute.path().is_ident("hl7") {
continue;
}
if !matches!(found, Mapping::None) {
return Err(syn::Error::new(
attribute.span(),
"a field takes one #[hl7(...)] attribute",
));
}
found = attribute.parse_args_with(|input: syn::parse::ParseStream| {
if input.peek(LitStr) {
return Ok(Mapping::Path(input.parse()?));
}
let word: syn::Ident = input.parse()?;
match word.to_string().as_str() {
"nested" => Ok(Mapping::Nested),
"raw" => Ok(Mapping::Raw),
other => Err(syn::Error::new(
word.span(),
format!(
"unknown #[hl7(...)] option {other:?}; expected a path such as \
#[hl7(\"PID-5.1\")], or `nested`, 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_2");
}
#[test]
fn a_bare_path_is_taken_as_written() {
assert_eq!(resolved("#[hl7(crate = hl7)]"), "hl7");
assert_eq!(
resolved("#[hl7(crate = ::vendor::hl7_2)]"),
"::vendor::hl7_2"
);
assert_eq!(resolved("#[hl7(crate = crate::renamed)]"), "crate::renamed");
}
#[test]
fn a_quoted_path_is_the_same_thing() {
assert_eq!(
resolved(r#"#[hl7(crate = "::vendor::hl7_2")]"#),
"::vendor::hl7_2"
);
}
#[test]
fn a_struct_attribute_that_is_not_crate_says_so() {
let input: DeriveInput =
syn::parse_str("#[hl7(\"PID-5\")] struct S { f: u32 }").expect("test input parses");
let Err(error) = crate_path(&input) else {
panic!("a path literal is not a struct option");
};
assert!(error.to_string().contains("belong on fields"), "{error}");
}
}