Skip to main content

haste_fhir_operation_error_derive/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{
4    Attribute, Data, DeriveInput, Expr, Ident, Lit, Meta, MetaList, Token, Type, Variant,
5    parse_macro_input, punctuated::Punctuated,
6};
7
8static FATAL: &str = "fatal";
9static ERROR: &str = "error";
10static WARNING: &str = "warning";
11static INFORMATION: &str = "information";
12
13fn get_issue_list(attrs: &[Attribute]) -> Vec<MetaList> {
14    let issues: Vec<MetaList> = attrs
15        .iter()
16        .filter_map(|attr| match &attr.meta {
17            Meta::List(meta_list)
18                if meta_list.path.is_ident(FATAL)
19                    || meta_list.path.is_ident(ERROR)
20                    || meta_list.path.is_ident(WARNING)
21                    || meta_list.path.is_ident(INFORMATION) =>
22            {
23                Some(meta_list.clone())
24            }
25            _ => None,
26        })
27        .collect();
28
29    issues
30}
31
32const CODES_ALLOWED: &[&str] = &[
33    "invalid",
34    "structure",
35    "required",
36    "value",
37    "invariant",
38    "security",
39    "login",
40    "unknown",
41    "expired",
42    "forbidden",
43    "suppressed",
44    "processing",
45    "not-supported",
46    "duplicate",
47    "multiple-matches",
48    "not-found",
49    "deleted",
50    "too-long",
51    "code-invalid",
52    "extension",
53    "too-costly",
54    "business-rule",
55    "conflict",
56    "transient",
57    "lock-error",
58    "no-store",
59    "exception",
60    "timeout",
61    "incomplete",
62    "throttled",
63    "informational",
64];
65
66fn get_expr_string(expr: &Expr) -> Option<String> {
67    if let Expr::Lit(lit) = expr
68        && let Lit::Str(lit_str) = &lit.lit
69    {
70        return Some(lit_str.value());
71    }
72
73    None
74}
75
76#[derive(Clone)]
77enum Severity {
78    Fatal,
79    Error,
80    Warning,
81    Information,
82}
83
84impl From<Severity> for String {
85    fn from(severity: Severity) -> Self {
86        match severity {
87            Severity::Fatal => "fatal",
88            Severity::Error => "error",
89            Severity::Warning => "warning",
90            Severity::Information => "information",
91        }
92        .to_string()
93    }
94}
95
96#[derive(Clone)]
97struct SimpleIssue {
98    severity: Severity,
99    code: String,
100    diagnostic: Option<String>,
101}
102
103fn get_severity(meta_list: &MetaList) -> Severity {
104    if meta_list.path.is_ident("fatal") {
105        Severity::Fatal
106    } else if meta_list.path.is_ident("error") {
107        Severity::Error
108    } else if meta_list.path.is_ident("warning") {
109        Severity::Warning
110    } else if meta_list.path.is_ident("information") {
111        Severity::Information
112    } else {
113        panic!(
114            "Unknown severity type: {}",
115            meta_list.path.get_ident().unwrap()
116        );
117    }
118}
119
120fn get_issue_attributes(attrs: &[Attribute]) -> Vec<SimpleIssue> {
121    let mut simple_issue = vec![];
122    let issue_attributes = get_issue_list(attrs);
123    for issues in issue_attributes {
124        let parsed_arguments = issues
125            .parse_args_with(Punctuated::<Expr, Token![,]>::parse_terminated)
126            .unwrap();
127
128        assert!(
129            parsed_arguments.len() <= 2,
130            "Expected exactly 2 arguments for issue attributes"
131        );
132
133        let severity = get_severity(&issues);
134        let mut code: Option<String> = None;
135        let mut diagnostic: Option<String> = None;
136
137        for expression in parsed_arguments {
138            match expression {
139                Expr::Assign(expr_assign) => match expr_assign.left.as_ref() {
140                    Expr::Path(path) => match path.path.get_ident().unwrap().to_string().as_str() {
141                        "code" => {
142                            code = get_expr_string(expr_assign.right.as_ref());
143                            if let Some(code) = code.as_ref() {
144                                assert!(
145                                    CODES_ALLOWED.contains(&code.as_str()),
146                                    "Invalid code: '{code}' Must be one of '{CODES_ALLOWED:?}'",
147                                );
148                            }
149                        }
150                        "diagnostic" => {
151                            diagnostic = get_expr_string(expr_assign.right.as_ref());
152                        }
153                        _ => panic!(
154                            "Unknown error attribute: {}",
155                            path.path.get_ident().unwrap()
156                        ),
157                    },
158                    _ => panic!("Expected an assignment expression"),
159                },
160                _ => {
161                    panic!("Expected an assignment expression");
162                }
163            }
164        }
165
166        simple_issue.push(SimpleIssue {
167            severity,
168            code: code.unwrap_or_else(|| "error".to_string()),
169            diagnostic,
170        });
171    }
172
173    simple_issue
174}
175
176/// Derive the operatiooutcome issues from the
177/// attributes issue, fatal, error, warning, information
178fn derive_operation_issues(v: &Variant) -> proc_macro2::TokenStream {
179    let issues = get_issue_attributes(&v.attrs);
180    let invariant_operation_outcome_issues = issues.iter().map(|simple_issue: &SimpleIssue| {
181        let severity_string: String = simple_issue.severity.clone().into();
182        let severity = quote!{ haste_fhir_model::r4::generated::terminology::BoundCode::<haste_fhir_model::r4::generated::terminology::IssueSeverity>::new(#severity_string).unwrap() };
183
184        let diagnostic = if let Some(diagnostic) = simple_issue.diagnostic.as_ref() {
185            quote! {
186                Some(Box::new(haste_fhir_model::r4::generated::types::FHIRString{
187                    id: None,
188                    extension: None,
189                    value: Some(format!(#diagnostic)),
190                }))
191            }
192        } else {
193            quote! {
194                None
195            }
196        };
197
198        let code_string = &simple_issue.code;
199        let code = quote! {
200            haste_fhir_model::r4::generated::terminology::BoundCode::<haste_fhir_model::r4::generated::terminology::IssueType>::new(#code_string).unwrap()
201        };
202
203        quote! {
204            haste_fhir_model::r4::generated::resources::OperationOutcomeIssue {
205                id: None,
206                extension: None,
207                modifierExtension: None,
208                severity: #severity,
209                code: #code,
210                details: None,
211                diagnostics: #diagnostic,
212                location: None,
213                expression: None,
214            }
215        }
216    });
217
218    quote! {
219        vec![#(#invariant_operation_outcome_issues),*]
220    }
221}
222
223fn get_arg_identifier(i: usize) -> Ident {
224    format_ident!("arg{}", i)
225}
226
227#[derive(Debug, Clone)]
228struct FromInformation {
229    variant: Variant,
230    from: usize,
231    error_type: Type,
232}
233
234/// Returns the argument identifier for the from variant.
235/// This should be an error.
236fn get_from_error(v: &Variant) -> Option<FromInformation> {
237    let from_fields: Vec<FromInformation> = v
238        .fields
239        .iter()
240        .enumerate()
241        .filter_map(|(i, field)| {
242            let from_attr = field.attrs.iter().find(|attr| attr.path().is_ident("from"));
243
244            if from_attr.is_some() {
245                if from_attr.is_some() {
246                    Some(FromInformation {
247                        variant: v.clone(),
248                        from: i,
249                        error_type: field.ty.clone(),
250                    })
251                } else {
252                    panic!("Expected a named field with 'from' attribute");
253                }
254            } else {
255                None
256            }
257        })
258        .collect();
259
260    assert!(
261        from_fields.len() <= 1,
262        "Expected only one field with 'from' attribute"
263    );
264
265    from_fields.first().cloned()
266}
267
268/// Instantiate the arguments for the variant
269/// This is used in formatting the error message.
270/// Format is arg0, arg1, arg2, ...
271fn instantiate_args(v: &Variant) -> proc_macro2::TokenStream {
272    let arg_identifiers = (0..v.fields.len())
273        .map(get_arg_identifier)
274        .collect::<Vec<_>>();
275    if arg_identifiers.is_empty() {
276        quote! {}
277    } else {
278        quote! {
279            (#(#arg_identifiers),*)
280        }
281    }
282}
283
284/// Derives the [`OperationOutcomeError`] trait for an enum.
285///
286/// # Panics
287///
288/// This macro will panic at compile time if it is applied to a struct or a union,
289/// as it can only be derived from an enum definition.
290#[proc_macro_derive(
291    OperationOutcomeError,
292    attributes(fatal, error, warning, information, from)
293)]
294pub fn operation_error(input: TokenStream) -> TokenStream {
295    // Parse the input tokens into a syntax tree
296    let input = parse_macro_input!(input as DeriveInput);
297
298    match input.data {
299        Data::Enum(data) => {
300            let name = input.ident;
301
302            // Errors to implement from trait for.
303            let mut from_information: Vec<FromInformation> = vec![];
304
305            let variants: Vec<proc_macro2::TokenStream> = data.variants.iter().map(|v| {
306                let ident = &v.ident;
307                let op_issues = derive_operation_issues(v);
308                let arg_instantiation = instantiate_args( v);
309
310                let from_error = if let Some(from_info) = get_from_error(v) {
311                    let arg_identifier = get_arg_identifier(from_info.from);
312                    from_information.push(from_info);
313                    quote!{ Some(#arg_identifier.into()) }
314                } else {
315                    quote! { None }
316                };
317
318
319                quote! {
320                    #ident #arg_instantiation => {
321
322                        let mut operation_outcome = haste_fhir_model::r4::generated::resources::OperationOutcome::default();
323                        operation_outcome.issue = #op_issues;
324
325                        haste_fhir_operation_error::OperationOutcomeError::new(#from_error, operation_outcome)
326                    }
327                }
328            }).collect();
329
330            let from_impl = from_information.into_iter().map(|from_info| {
331                let error_type = &from_info.error_type;
332                let from_variant = &from_info.variant.ident;
333
334                quote! {
335                    impl From<#error_type> for #name {
336                        fn from(error: #error_type) -> Self {
337                            #name::#from_variant(error)
338                        }
339                    }
340                }
341            });
342
343            let expanded = quote! {
344                impl From<#name> for haste_fhir_operation_error::OperationOutcomeError {
345                    fn from(value: #name) -> Self {
346                        match value {
347                            #(#name::#variants),*
348                        }
349                    }
350                }
351                #(#from_impl)*
352            };
353
354            // println!("{}", expanded.to_string());
355
356            expanded.into()
357        }
358        _ => {
359            panic!("Can only derive OperationOutcomeError from an enum.")
360        }
361    }
362}