haste_fhir_operation_error_derive/
lib.rs1use 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
177fn 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
235fn 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
268fn 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 let input = parse_macro_input!(input as DeriveInput);
288
289 match input.data {
290 Data::Enum(data) => {
291 let name = input.ident;
292
293 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 expanded.into()
349 }
350 _ => {
351 panic!("Can only derive OperationOutcomeError from an enum.")
352 }
353 }
354}