use proc_macro::TokenStream;
use quote::quote;
use syn::spanned::Spanned;
use syn::{Data, DeriveInput, Field, Fields, LitStr, 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 (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 ::hl7_2::FromHl7Value>::from_hl7_value(message, #path)?
},
Mapping::Nested => quote! {
#ident: <#ty as ::hl7_2::FromHl7>::from_hl7(message)?
},
Mapping::Raw => quote! {
#ident: ::hl7_2::Raw::new(::core::clone::Clone::clone(message)).into()
},
Mapping::None => quote! {
#ident: ::core::default::Default::default()
},
});
}
Ok(quote! {
#[automatically_derived]
impl #impl_generics ::hl7_2::FromHl7 for #name #type_generics #where_clause {
fn from_hl7(message: &::hl7_2::Message) -> ::core::result::Result<Self, ::hl7_2::Error> {
::core::result::Result::Ok(#name { #(#reads),* })
}
}
})
}
fn to_hl7(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let name = &input.ident;
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 ::hl7_2::ToHl7Value>::to_hl7_value(&self.#ident, message, #path)?;
}),
Mapping::Nested => writes.push(quote! {
<#ty as ::hl7_2::ToHl7>::to_hl7(&self.#ident, message)?;
}),
Mapping::Raw | Mapping::None => {}
}
}
Ok(quote! {
#[automatically_derived]
impl #impl_generics ::hl7_2::ToHl7 for #name #type_generics #where_clause {
fn to_hl7(
&self,
message: &mut ::hl7_2::Message,
) -> ::core::result::Result<(), ::hl7_2::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 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)
}