haste_fhir_operation_error_derive/
lib.rs

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