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]) -> Option<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    Some(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        if let Lit::Str(lit_str) = &lit.lit {
69            return Some(lit_str.value());
70        }
71    }
72    None
73}
74
75#[derive(Clone)]
76enum Severity {
77    Fatal,
78    Error,
79    Warning,
80    Information,
81}
82
83impl Into<String> for Severity {
84    fn into(self) -> String {
85        match self {
86            Severity::Fatal => "fatal".to_string(),
87            Severity::Error => "error".to_string(),
88            Severity::Warning => "warning".to_string(),
89            Severity::Information => "information".to_string(),
90        }
91    }
92}
93
94#[derive(Clone)]
95struct SimpleIssue {
96    severity: Severity,
97    code: String,
98    diagnostic: Option<String>,
99}
100
101fn get_severity(meta_list: &MetaList) -> Severity {
102    if meta_list.path.is_ident("fatal") {
103        Severity::Fatal
104    } else if meta_list.path.is_ident("error") {
105        Severity::Error
106    } else if meta_list.path.is_ident("warning") {
107        Severity::Warning
108    } else if meta_list.path.is_ident("information") {
109        Severity::Information
110    } else {
111        panic!(
112            "Unknown severity type: {}",
113            meta_list.path.get_ident().unwrap()
114        );
115    }
116}
117
118fn get_issue_attributes(attrs: &[Attribute]) -> Option<Vec<SimpleIssue>> {
119    let mut simple_issue = vec![];
120    if let Some(issue_attributes) = get_issue_list(&attrs) {
121        for issues in issue_attributes {
122            let parsed_arguments = issues
123                .parse_args_with(Punctuated::<Expr, Token![,]>::parse_terminated)
124                .unwrap();
125
126            if parsed_arguments.len() > 2 {
127                panic!("Expected exactly 2 arguments for issue attributes");
128            }
129
130            let severity = get_severity(&issues);
131            let mut code: Option<String> = None;
132            let mut diagnostic: Option<String> = None;
133
134            for expression in parsed_arguments {
135                match expression {
136                    Expr::Assign(expr_assign) => match expr_assign.left.as_ref() {
137                        Expr::Path(path) => {
138                            match path.path.get_ident().unwrap().to_string().as_str() {
139                                "code" => {
140                                    code = get_expr_string(expr_assign.right.as_ref());
141                                    if let Some(code) = code.as_ref() {
142                                        if !CODES_ALLOWED.contains(&code.as_str()) {
143                                            panic!(
144                                                "Invalid code: '{}' Must be one of '{:?}'",
145                                                code, CODES_ALLOWED
146                                            );
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                        }
159                        _ => panic!("Expected an assignment expression"),
160                    },
161                    _ => {
162                        panic!("Expected an assignment expression");
163                    }
164                }
165            }
166
167            simple_issue.push(SimpleIssue {
168                severity,
169                code: code.unwrap_or_else(|| "error".to_string()),
170                diagnostic: diagnostic,
171            });
172        }
173    }
174
175    Some(simple_issue)
176}
177
178/// Derive the operatiooutcome issues from the
179/// attributes issue, fatal, error, warning, information
180fn derive_operation_issues(v: &Variant) -> proc_macro2::TokenStream {
181    let issues = get_issue_attributes(&v.attrs).unwrap_or(vec![]);
182    let invariant_operation_outcome_issues = issues.iter().map(|simple_issue: &SimpleIssue| {
183        let severity_string: String = simple_issue.severity.clone().into();
184        let severity = quote!{ Box::new(haste_fhir_model::r4::generated::terminology::IssueSeverity::try_from(#severity_string.to_string()).unwrap()) };
185        
186        let diagnostic = if let Some(diagnostic) = simple_issue.diagnostic.as_ref() {
187            quote! {
188                Some(Box::new(haste_fhir_model::r4::generated::types::FHIRString{
189                    id: None,
190                    extension: None,
191                    value: Some(format!(#diagnostic)),
192                }))
193            }
194        } else {
195            quote! {
196                None
197            }
198        };
199
200        let code_string = &simple_issue.code;
201        let code = quote! {
202            Box::new(haste_fhir_model::r4::generated::terminology::IssueType::try_from(#code_string.to_string()).unwrap())
203        };
204
205        quote! {
206            haste_fhir_model::r4::generated::resources::OperationOutcomeIssue {
207                id: None,
208                extension: None,
209                modifierExtension: None,
210                severity: #severity,
211                code: #code,
212                details: None,
213                diagnostics: #diagnostic,
214                location: None,
215                expression: None,
216            }
217        }
218    });
219
220    quote! {
221        vec![#(#invariant_operation_outcome_issues),*]
222    }
223}
224
225fn get_arg_identifier(i: usize) -> Ident {
226    format_ident!("arg{}", i)
227}
228
229#[derive(Debug, Clone)]
230struct FromInformation {
231    variant: Variant,
232    from: usize,
233    error_type: Type,
234}
235
236/// Returns the argument identifier for the from variant.
237/// This should be an error.
238fn get_from_error(v: &Variant) -> Option<FromInformation> {
239    let from_fields: Vec<FromInformation> = v
240        .fields
241        .iter()
242        .enumerate()
243        .filter_map(|(i, field)| {
244            let from_attr = field.attrs.iter().find(|attr| {
245                let p = attr.path().is_ident("from");
246                p
247            });
248
249            if from_attr.is_some() {
250                if from_attr.is_some() {
251                    Some(FromInformation {
252                        variant: v.clone(),
253                        from: i,
254                        error_type: field.ty.clone(),
255                    })
256                } else {
257                    panic!("Expected a named field with 'from' attribute");
258                }
259            } else {
260                None
261            }
262        })
263        .collect();
264
265    if from_fields.len() > 1 {
266        panic!("Expected only one field with 'from' attribute");
267    }
268
269    from_fields.get(0).cloned()
270}
271
272/// Instantiate the arguments for the variant
273/// This is used in formatting the error message.
274/// Format is arg0, arg1, arg2, ...
275fn instantiate_args(v: &Variant) -> proc_macro2::TokenStream {
276    let arg_identifiers = (0..v.fields.len())
277        .map(|i| get_arg_identifier(i))
278        .collect::<Vec<_>>();
279    if arg_identifiers.is_empty() {
280        quote! {}
281    } else {
282        quote! {
283            (#(#arg_identifiers),*)
284        }
285    }
286}
287
288#[proc_macro_derive(
289    OperationOutcomeError,
290    attributes(fatal, error, warning, information, from)
291)]
292pub fn operation_error(input: TokenStream) -> TokenStream {
293    // Parse the input tokens into a syntax tree
294    let input = parse_macro_input!(input as DeriveInput);
295
296    match input.data {
297        Data::Enum(data) => {
298            let name = input.ident;
299
300            // Errors to implement from trait for.
301            let mut from_information: Vec<FromInformation> = vec![];
302
303            let variants: Vec<proc_macro2::TokenStream> = data.variants.iter().map(|v| {
304                let ident = &v.ident;
305                let op_issues = derive_operation_issues(v);
306                let arg_instantiation = instantiate_args( v);
307
308                let from_error = if let Some(from_info) = get_from_error(v) {
309                    let arg_identifier = get_arg_identifier(from_info.from);
310                    from_information.push(from_info);
311                    quote!{ Some(#arg_identifier.into()) }
312                } else {
313                    quote! { None }
314                };
315
316
317                quote! {
318                    #ident #arg_instantiation => {
319                        
320                        let mut operation_outcome = haste_fhir_model::r4::generated::resources::OperationOutcome::default();
321                        operation_outcome.issue = #op_issues;
322                        
323                        haste_fhir_operation_error::OperationOutcomeError::new(#from_error, operation_outcome)
324                    }
325                }
326            }).collect();
327
328            let from_impl = from_information.into_iter().map(|from_info| {
329                let error_type = &from_info.error_type;
330                let from_variant = &from_info.variant.ident;
331
332                quote! {
333                    impl From<#error_type> for #name {
334                        fn from(error: #error_type) -> Self {
335                            #name::#from_variant(error)
336                        }
337                    }
338                }
339            });
340
341            let expanded = quote! {
342                impl From<#name> for haste_fhir_operation_error::OperationOutcomeError {
343                    fn from(value: #name) -> Self {
344                        match value {
345                            #(#name::#variants),*
346                        }
347                    }
348                }
349                #(#from_impl)*
350            };
351
352            // println!("{}", expanded.to_string());
353
354            expanded.into()
355        }
356        _ => {
357            panic!("Can only derive OperationOutcomeError from an enum.")
358        }
359    }
360}