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}