haste_fhir_operation_error_derive/
lib.rs1use 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
178fn 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
236fn 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
272fn 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 let input = parse_macro_input!(input as DeriveInput);
295
296 match input.data {
297 Data::Enum(data) => {
298 let name = input.ident;
299
300 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 expanded.into()
355 }
356 _ => {
357 panic!("Can only derive OperationOutcomeError from an enum.")
358 }
359 }
360}