1#![warn(missing_docs, clippy::pedantic)]
57
58use proc_macro::TokenStream;
59use quote::quote;
60use syn::spanned::Spanned;
61use syn::{Data, DeriveInput, Field, Fields, LitStr, Path, Token, parse_macro_input};
62
63#[proc_macro_derive(FromHl7, attributes(hl7))]
69pub fn derive_from_hl7(input: TokenStream) -> TokenStream {
70 let input = parse_macro_input!(input as DeriveInput);
71 match from_hl7(&input) {
72 Ok(tokens) => tokens.into(),
73 Err(error) => error.to_compile_error().into(),
74 }
75}
76
77#[proc_macro_derive(ToHl7, attributes(hl7))]
83pub fn derive_to_hl7(input: TokenStream) -> TokenStream {
84 let input = parse_macro_input!(input as DeriveInput);
85 match to_hl7(&input) {
86 Ok(tokens) => tokens.into(),
87 Err(error) => error.to_compile_error().into(),
88 }
89}
90
91enum Mapping {
93 Path(LitStr),
95 Nested,
97 Raw,
99 None,
101}
102
103fn from_hl7(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
104 let name = &input.ident;
105 let krate = crate_path(input)?;
106 let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
107 let mut reads = Vec::new();
108 for field in named_fields(input)? {
109 let ident = field.ident.as_ref().expect("named fields");
110 let ty = &field.ty;
111 reads.push(match mapping(field)? {
112 Mapping::Path(path) => quote! {
113 #ident: <#ty as #krate::FromHl7Value>::from_hl7_value(message, #path)?
114 },
115 Mapping::Nested => quote! {
116 #ident: <#ty as #krate::FromHl7>::from_hl7(message)?
117 },
118 Mapping::Raw => quote! {
119 #ident: #krate::Raw::new(::core::clone::Clone::clone(message)).into()
120 },
121 Mapping::None => quote! {
122 #ident: ::core::default::Default::default()
123 },
124 });
125 }
126 Ok(quote! {
127 #[automatically_derived]
128 impl #impl_generics #krate::FromHl7 for #name #type_generics #where_clause {
129 fn from_hl7(message: &#krate::Message) -> ::core::result::Result<Self, #krate::Error> {
130 ::core::result::Result::Ok(#name { #(#reads),* })
131 }
132 }
133 })
134}
135
136fn to_hl7(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
137 let name = &input.ident;
138 let krate = crate_path(input)?;
139 let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
140 let mut writes = Vec::new();
141 for field in named_fields(input)? {
142 let ident = field.ident.as_ref().expect("named fields");
143 let ty = &field.ty;
144 match mapping(field)? {
145 Mapping::Path(path) => writes.push(quote! {
146 <#ty as #krate::ToHl7Value>::to_hl7_value(&self.#ident, message, #path)?;
147 }),
148 Mapping::Nested => writes.push(quote! {
149 <#ty as #krate::ToHl7>::to_hl7(&self.#ident, message)?;
150 }),
151 Mapping::Raw | Mapping::None => {}
154 }
155 }
156 Ok(quote! {
157 #[automatically_derived]
158 impl #impl_generics #krate::ToHl7 for #name #type_generics #where_clause {
159 fn to_hl7(
160 &self,
161 message: &mut #krate::Message,
162 ) -> ::core::result::Result<(), #krate::Error> {
163 #(#writes)*
164 ::core::result::Result::Ok(())
165 }
166 }
167 })
168}
169
170fn named_fields(input: &DeriveInput) -> syn::Result<impl Iterator<Item = &Field>> {
172 match &input.data {
173 Data::Struct(data) => match &data.fields {
174 Fields::Named(named) => Ok(named.named.iter()),
175 other => Err(syn::Error::new(
176 other.span(),
177 "hl7-2 derives map field names to HL7 paths, so the struct needs named fields",
178 )),
179 },
180 Data::Enum(_) | Data::Union(_) => Err(syn::Error::new(
181 input.ident.span(),
182 "hl7-2 derives apply to structs; an enum or union has no single message shape",
183 )),
184 }
185}
186
187fn crate_path(input: &DeriveInput) -> syn::Result<Path> {
200 for attribute in &input.attrs {
201 if !attribute.path().is_ident("hl7") {
202 continue;
203 }
204 return attribute.parse_args_with(|stream: syn::parse::ParseStream| {
205 stream.parse::<Token![crate]>().map_err(|_| {
206 syn::Error::new(
207 attribute.span(),
208 "the only #[hl7(...)] option on a struct is `crate = ...`; \
209 path attributes belong on fields",
210 )
211 })?;
212 stream.parse::<Token![=]>()?;
213 if stream.peek(LitStr) {
214 return stream.parse::<LitStr>()?.parse();
215 }
216 stream.parse()
217 });
218 }
219 Ok(syn::parse_quote!(::hl7_2))
220}
221
222fn mapping(field: &Field) -> syn::Result<Mapping> {
223 let mut found = Mapping::None;
224 for attribute in &field.attrs {
225 if !attribute.path().is_ident("hl7") {
226 continue;
227 }
228 if !matches!(found, Mapping::None) {
229 return Err(syn::Error::new(
230 attribute.span(),
231 "a field takes one #[hl7(...)] attribute",
232 ));
233 }
234 found = attribute.parse_args_with(|input: syn::parse::ParseStream| {
236 if input.peek(LitStr) {
237 return Ok(Mapping::Path(input.parse()?));
238 }
239 let word: syn::Ident = input.parse()?;
240 match word.to_string().as_str() {
241 "nested" => Ok(Mapping::Nested),
242 "raw" => Ok(Mapping::Raw),
243 other => Err(syn::Error::new(
244 word.span(),
245 format!(
246 "unknown #[hl7(...)] option {other:?}; expected a path such as \
247 #[hl7(\"PID-5.1\")], or `nested`, or `raw`"
248 ),
249 )),
250 }
251 })?;
252 }
253 Ok(found)
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use quote::ToTokens;
260
261 fn resolved(attributes: &str) -> String {
262 let input: DeriveInput = syn::parse_str(&format!("{attributes} struct S {{ f: u32 }}"))
263 .expect("test input parses");
264 crate_path(&input)
265 .expect("crate path resolves")
266 .to_token_stream()
267 .to_string()
268 .replace(' ', "")
269 }
270
271 #[test]
272 fn defaults_to_the_absolute_crate_name() {
273 assert_eq!(resolved(""), "::hl7_2");
274 }
275
276 #[test]
277 fn a_bare_path_is_taken_as_written() {
278 assert_eq!(resolved("#[hl7(crate = hl7)]"), "hl7");
279 assert_eq!(
280 resolved("#[hl7(crate = ::vendor::hl7_2)]"),
281 "::vendor::hl7_2"
282 );
283 assert_eq!(resolved("#[hl7(crate = crate::renamed)]"), "crate::renamed");
284 }
285
286 #[test]
287 fn a_quoted_path_is_the_same_thing() {
288 assert_eq!(
289 resolved(r#"#[hl7(crate = "::vendor::hl7_2")]"#),
290 "::vendor::hl7_2"
291 );
292 }
293
294 #[test]
295 fn a_struct_attribute_that_is_not_crate_says_so() {
296 let input: DeriveInput =
297 syn::parse_str("#[hl7(\"PID-5\")] struct S { f: u32 }").expect("test input parses");
298 let Err(error) = crate_path(&input) else {
299 panic!("a path literal is not a struct option");
300 };
301 assert!(error.to_string().contains("belong on fields"), "{error}");
302 }
303}