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]) -> 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
176fn 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
234fn 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
268fn 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#[proc_macro_derive(
291 OperationOutcomeError,
292 attributes(fatal, error, warning, information, from)
293)]
294pub fn operation_error(input: TokenStream) -> TokenStream {
295 let input = parse_macro_input!(input as DeriveInput);
297
298 match input.data {
299 Data::Enum(data) => {
300 let name = input.ident;
301
302 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 expanded.into()
357 }
358 _ => {
359 panic!("Can only derive OperationOutcomeError from an enum.")
360 }
361 }
362}