haste_fhir_operation_error_derive/
lib.rs1use haste_fhir_model::r4::generated::terminology::{IssueType, ValueSetDef};
7use proc_macro::TokenStream;
8use proc_macro2::Span;
9use quote::{format_ident, quote};
10use syn::{
11 Attribute, Data, DeriveInput, Fields, Ident, LitStr, Type, Variant, parse_macro_input,
12 spanned::Spanned,
13};
14
15const SEVERITIES: [&str; 4] = ["fatal", "error", "warning", "information"];
16
17struct Issue {
20 severity: Ident,
21 code: Ident,
22 diagnostic: Option<LitStr>,
23}
24
25fn parse_issue(attr: &Attribute) -> syn::Result<Option<Issue>> {
27 let Some(severity) = attr
28 .path()
29 .get_ident()
30 .filter(|ident| SEVERITIES.contains(&ident.to_string().as_str()))
31 else {
32 return Ok(None);
33 };
34
35 let mut code = None;
36 let mut diagnostic = None;
37 attr.parse_nested_meta(|meta| {
38 if meta.path.is_ident("code") {
39 let literal: LitStr = meta.value()?.parse()?;
40 let value = literal.value();
41 if IssueType::CODES.binary_search(&value.as_str()).is_err() {
42 return Err(syn::Error::new(
43 literal.span(),
44 format!(
45 "'{value}' is not a FHIR issue type; expected one of {:?}",
46 IssueType::CODES
47 ),
48 ));
49 }
50 code = Some(Ident::new(&value.replace('-', "_"), literal.span()));
51 } else if meta.path.is_ident("diagnostic") {
52 diagnostic = Some(meta.value()?.parse()?);
53 } else {
54 return Err(meta.error("expected `code` or `diagnostic`"));
55 }
56 Ok(())
57 })?;
58
59 let Some(code) = code else {
60 return Err(syn::Error::new(attr.span(), "an issue needs a `code`"));
61 };
62 Ok(Some(Issue {
63 severity: severity.clone(),
64 code,
65 diagnostic,
66 }))
67}
68
69fn issue_expr(issue: &Issue) -> proc_macro2::TokenStream {
72 let Issue {
73 severity,
74 code,
75 diagnostic,
76 } = issue;
77 let diagnostics = diagnostic.as_ref().map_or_else(
78 || quote! { None },
79 |text| {
80 quote! {
81 Some(Box::new(haste_fhir_model::r4::generated::types::FHIRString {
82 value: Some(format!(#text)),
83 ..Default::default()
84 }))
85 }
86 },
87 );
88 quote! {
89 haste_fhir_model::r4::generated::resources::OperationOutcomeIssue {
90 severity: haste_fhir_model::r4::generated::terminology::IssueSeverity::#severity(),
91 code: haste_fhir_model::r4::generated::terminology::IssueType::#code(),
92 diagnostics: #diagnostics,
93 ..Default::default()
94 }
95 }
96}
97
98fn variant_arm(
100 name: &Ident,
101 variant: &Variant,
102) -> syn::Result<(proc_macro2::TokenStream, Option<Type>)> {
103 let issues = variant
104 .attrs
105 .iter()
106 .map(parse_issue)
107 .filter_map(Result::transpose)
108 .collect::<syn::Result<Vec<_>>>()?;
109 if issues.is_empty() {
110 return Err(syn::Error::new(
111 variant.span(),
112 "a variant needs a `fatal`, `error`, `warning` or `information` attribute",
113 ));
114 }
115
116 let fields: Vec<_> = match &variant.fields {
117 Fields::Unit => Vec::new(),
118 Fields::Unnamed(unnamed) => unnamed.unnamed.iter().collect(),
119 Fields::Named(named) => {
120 return Err(syn::Error::new(
121 named.span(),
122 "named fields are not supported; use a tuple variant",
123 ));
124 }
125 };
126 let args: Vec<Ident> = (0..fields.len()).map(|i| format_ident!("arg{i}")).collect();
127
128 let from_positions: Vec<usize> = fields
129 .iter()
130 .enumerate()
131 .filter(|(_, field)| field.attrs.iter().any(|attr| attr.path().is_ident("from")))
132 .map(|(i, _)| i)
133 .collect();
134 if from_positions.len() > 1 {
135 return Err(syn::Error::new(
136 variant.span(),
137 "only one field can be `#[from]`",
138 ));
139 }
140 if !from_positions.is_empty() && fields.len() != 1 {
141 return Err(syn::Error::new(
142 variant.span(),
143 "a variant with a `#[from]` field has no other fields",
144 ));
145 }
146 let from = from_positions.first().map(|&i| fields[i].ty.clone());
147 let source = from_positions.first().map_or_else(
148 || quote! { None },
149 |&i| {
150 let arg = &args[i];
151 quote! { Some(#arg.into()) }
152 },
153 );
154
155 let ident = &variant.ident;
156 let pattern = if args.is_empty() {
157 quote! { #name::#ident }
158 } else {
159 quote! { #name::#ident(#(#args),*) }
160 };
161 let issues = issues.iter().map(issue_expr);
162 let arm = quote! {
164 #pattern => {
165 let outcome = haste_fhir_model::r4::generated::resources::OperationOutcome {
166 issue: vec![#(#issues),*],
167 ..Default::default()
168 };
169 haste_fhir_operation_error::OperationOutcomeError::new(#source, outcome)
170 }
171 };
172 Ok((arm, from))
173}
174
175fn expand(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
176 let Data::Enum(data) = &input.data else {
177 return Err(syn::Error::new(
178 Span::call_site(),
179 "OperationOutcomeError can only be derived for an enum",
180 ));
181 };
182 let name = &input.ident;
183
184 let mut arms = Vec::with_capacity(data.variants.len());
185 let mut from_impls = Vec::new();
186 for variant in &data.variants {
187 let (arm, from) = variant_arm(name, variant)?;
188 arms.push(arm);
189 if let Some(ty) = from {
190 let ident = &variant.ident;
191 from_impls.push(quote! {
192 impl From<#ty> for #name {
193 fn from(error: #ty) -> Self {
194 #name::#ident(error)
195 }
196 }
197 });
198 }
199 }
200
201 Ok(quote! {
203 impl From<#name> for haste_fhir_operation_error::OperationOutcomeError {
204 #[track_caller]
205 fn from(value: #name) -> Self {
206 match value {
207 #(#arms),*
208 }
209 }
210 }
211 #(#from_impls)*
212 })
213}
214
215#[proc_macro_derive(
223 OperationOutcomeError,
224 attributes(fatal, error, warning, information, from)
225)]
226pub fn operation_error(input: TokenStream) -> TokenStream {
227 let input = parse_macro_input!(input as DeriveInput);
228 expand(&input)
229 .unwrap_or_else(syn::Error::into_compile_error)
230 .into()
231}