Skip to main content

hl7_3_derive/
lib.rs

1//! Derive macro for [`hl7-3`](https://crates.io/crates/hl7-3): map a
2//! struct's fields to XML element attributes and children once, in the
3//! type definition, instead of writing the same accessor calls at every
4//! call site.
5//!
6//! This crate is not used directly. `hl7-3` re-exports the macro behind
7//! its `derive` feature, so the dependency to add is:
8//!
9//! ```toml
10//! hl7-3 = { version = "0.1", features = ["derive"] }
11//! ```
12//!
13//! Keeping the macro in a crate of their own is what lets the default
14//! build of `hl7-3` keep exactly one dependency: `syn` and `quote` are
15//! compiled only for callers who ask for the macro.
16//!
17//! ```ignore
18//! use hl7_3::FromElement;
19//! use hl7_3::rim::Act;
20//!
21//! #[derive(FromElement, Default)]
22//! struct Observation {
23//!     #[element("classCode")]     class_code: String,
24//!     #[element("moodCode")]      mood_code: String,
25//!     #[element(child = "note")]  note: Option<String>,
26//!     #[element(nested = "component")] component: Act, // its own FromElement
27//!     #[element(raw)]             raw: hl7_3::xml::Element, // the escape hatch
28//! }
29//! ```
30//!
31//! One attribute per field, and a field with none is `Default::default()`:
32//!
33//! | attribute | reads |
34//! |---|---|
35//! | `#[element("classCode")]` | the `classCode` attribute, via `FromElementValue::from_attribute` |
36//! | `#[element(child = "note")]` | the `note` child's text, via `FromElementValue::from_child_text` |
37//! | `#[element(nested = "component")]` | the `component` child, via the field type's own `FromElement` |
38//! | `#[element(raw)]` | the whole element (field type must be `hl7_3::xml::Element`) |
39//! | none | `Default::default()` |
40//!
41//! There is no `#[derive(ToElement)]`: `hl7-3` has no XML-writing
42//! capability yet (see its `spec/index.md` §1), so a write-direction macro
43//! would have nothing real to generate.
44//!
45//! ## When `hl7-3` is not called `hl7_3`
46//!
47//! The generated code names the crate absolutely, as `::hl7_3`, so that it
48//! works wherever the type is defined without the caller importing
49//! anything. A caller who renames the dependency —
50//! `hl7 = { package = "hl7-3" }` in `Cargo.toml`, or a workspace that
51//! aliases it — has no `::hl7_3` for the macro to reach, and the generated
52//! code stops compiling. Say where it is instead, once, on the struct:
53//!
54//! ```ignore
55//! #[derive(FromElement)]
56//! #[element(crate = hl7)]        // or `crate = "::some::path::to::hl7_3"`
57//! struct Author {
58//!     #[element("classCode")] class_code: String,
59//! }
60//! ```
61
62#![warn(missing_docs, clippy::pedantic)]
63
64use proc_macro::TokenStream;
65use quote::quote;
66use syn::spanned::Spanned;
67use syn::{Data, DeriveInput, Field, Fields, LitStr, Path, Token, parse_macro_input};
68
69/// Derive `FromElement`: read each annotated field from an XML element's
70/// attributes or children.
71///
72/// See the crate documentation for the attributes. Only structs with named
73/// fields are supported; a tuple struct has no field names to map, and an
74/// enum has no single element shape to read.
75#[proc_macro_derive(FromElement, attributes(element))]
76pub fn derive_from_element(input: TokenStream) -> TokenStream {
77    let input = parse_macro_input!(input as DeriveInput);
78    match from_element(&input) {
79        Ok(tokens) => tokens.into(),
80        Err(error) => error.to_compile_error().into(),
81    }
82}
83
84/// What one field's `#[element(...)]` attribute asked for.
85enum Mapping {
86    /// `#[element("classCode")]`
87    Attribute(LitStr),
88    /// `#[element(child = "note")]`
89    ChildText(LitStr),
90    /// `#[element(nested = "component")]`
91    Nested(LitStr),
92    /// `#[element(raw)]`
93    Raw,
94    /// No attribute at all.
95    None,
96}
97
98fn from_element(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
99    let name = &input.ident;
100    let krate = crate_path(input)?;
101    let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
102    let mut reads = Vec::new();
103    for field in named_fields(input)? {
104        let ident = field.ident.as_ref().expect("named fields");
105        let ty = &field.ty;
106        reads.push(match mapping(field)? {
107            Mapping::Attribute(name) => quote! {
108                #ident: <#ty as #krate::typed::FromElementValue>::from_attribute(
109                    element.attribute(#name)
110                )
111            },
112            Mapping::ChildText(name) => quote! {
113                #ident: <#ty as #krate::typed::FromElementValue>::from_child_text(
114                    element.child(#name).and_then(#krate::xml::Element::text_opt)
115                )
116            },
117            Mapping::Nested(name) => quote! {
118                #ident: element
119                    .child(#name)
120                    .map(<#ty as #krate::typed::FromElement>::from_element)
121                    .unwrap_or_default()
122            },
123            Mapping::Raw => quote! {
124                #ident: ::core::clone::Clone::clone(element)
125            },
126            Mapping::None => quote! {
127                #ident: ::core::default::Default::default()
128            },
129        });
130    }
131    Ok(quote! {
132        #[automatically_derived]
133        impl #impl_generics #krate::typed::FromElement for #name #type_generics #where_clause {
134            fn from_element(element: &#krate::xml::Element) -> Self {
135                #name { #(#reads),* }
136            }
137        }
138    })
139}
140
141/// The struct's named fields, or an error explaining what this macro maps.
142fn named_fields(input: &DeriveInput) -> syn::Result<impl Iterator<Item = &Field>> {
143    match &input.data {
144        Data::Struct(data) => match &data.fields {
145            Fields::Named(named) => Ok(named.named.iter()),
146            other => Err(syn::Error::new(
147                other.span(),
148                "hl7-3 derives map field names to element attributes and children, so the struct needs named fields",
149            )),
150        },
151        Data::Enum(_) | Data::Union(_) => Err(syn::Error::new(
152            input.ident.span(),
153            "hl7-3 derives apply to structs; an enum or union has no single element shape",
154        )),
155    }
156}
157
158/// Read one field's `#[element(...)]` attribute.
159/// Where the generated code should look for `hl7-3`.
160///
161/// `::hl7_3` unless the struct says otherwise with
162/// `#[element(crate = ...)]`, which is what a caller who renamed the
163/// dependency needs: the generated code names the crate absolutely so that
164/// it compiles wherever the type is defined, and an absolute name that does
165/// not exist is a compile error the caller cannot work around from their
166/// side.
167///
168/// The value is a path, written bare (`crate = hl7`) or quoted
169/// (`crate = "::vendor::hl7_3"`); the quoted form is there because that is
170/// how the rest of the ecosystem spells it.
171fn crate_path(input: &DeriveInput) -> syn::Result<Path> {
172    for attribute in &input.attrs {
173        if !attribute.path().is_ident("element") {
174            continue;
175        }
176        return attribute.parse_args_with(|stream: syn::parse::ParseStream| {
177            stream.parse::<Token![crate]>().map_err(|_| {
178                syn::Error::new(
179                    attribute.span(),
180                    "the only #[element(...)] option on a struct is `crate = ...`; \
181                     name attributes belong on fields",
182                )
183            })?;
184            stream.parse::<Token![=]>()?;
185            if stream.peek(LitStr) {
186                return stream.parse::<LitStr>()?.parse();
187            }
188            stream.parse()
189        });
190    }
191    Ok(syn::parse_quote!(::hl7_3))
192}
193
194fn mapping(field: &Field) -> syn::Result<Mapping> {
195    let mut found = Mapping::None;
196    for attribute in &field.attrs {
197        if !attribute.path().is_ident("element") {
198            continue;
199        }
200        if !matches!(found, Mapping::None) {
201            return Err(syn::Error::new(
202                attribute.span(),
203                "a field takes one #[element(...)] attribute",
204            ));
205        }
206        // Four spellings: an attribute-name literal, `child = "..."`,
207        // `nested = "..."`, or `raw`.
208        found = attribute.parse_args_with(|input: syn::parse::ParseStream| {
209            if input.peek(LitStr) {
210                return Ok(Mapping::Attribute(input.parse()?));
211            }
212            let word: syn::Ident = input.parse()?;
213            match word.to_string().as_str() {
214                "raw" => Ok(Mapping::Raw),
215                "child" => {
216                    input.parse::<syn::Token![=]>()?;
217                    Ok(Mapping::ChildText(input.parse()?))
218                }
219                "nested" => {
220                    input.parse::<syn::Token![=]>()?;
221                    Ok(Mapping::Nested(input.parse()?))
222                }
223                other => Err(syn::Error::new(
224                    word.span(),
225                    format!(
226                        "unknown #[element(...)] option {other:?}; expected an attribute \
227                         name such as #[element(\"classCode\")], `child = \"name\"`, \
228                         `nested = \"name\"`, or `raw`"
229                    ),
230                )),
231            }
232        })?;
233    }
234    Ok(found)
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use quote::ToTokens;
241
242    fn resolved(attributes: &str) -> String {
243        let input: DeriveInput = syn::parse_str(&format!("{attributes} struct S {{ f: u32 }}"))
244            .expect("test input parses");
245        crate_path(&input)
246            .expect("crate path resolves")
247            .to_token_stream()
248            .to_string()
249            .replace(' ', "")
250    }
251
252    #[test]
253    fn defaults_to_the_absolute_crate_name() {
254        assert_eq!(resolved(""), "::hl7_3");
255    }
256
257    #[test]
258    fn a_bare_path_is_taken_as_written() {
259        assert_eq!(resolved("#[element(crate = hl7)]"), "hl7");
260        assert_eq!(
261            resolved("#[element(crate = ::vendor::hl7_3)]"),
262            "::vendor::hl7_3"
263        );
264    }
265
266    #[test]
267    fn a_quoted_path_is_the_same_thing() {
268        assert_eq!(
269            resolved(r#"#[element(crate = "::vendor::hl7_3")]"#),
270            "::vendor::hl7_3"
271        );
272    }
273
274    #[test]
275    fn a_struct_attribute_that_is_not_crate_says_so() {
276        let input: DeriveInput = syn::parse_str("#[element(\"classCode\")] struct S { f: u32 }")
277            .expect("test input parses");
278        let Err(error) = crate_path(&input) else {
279            panic!("a name literal is not a struct option");
280        };
281        assert!(error.to_string().contains("belong on fields"), "{error}");
282    }
283}