Skip to main content

haste_fhir_ops_derive/
lib.rs

1use haste_fhir_model::r4::generated::resources::ResourceType;
2use proc_macro2::{Ident, TokenStream};
3use quote::{ToTokens, format_ident, quote};
4use syn::{
5    Attribute, Data, DataStruct, DeriveInput, Expr, Field, Fields, Lit, Meta, PathArguments,
6    PathSegment, Type, parse_macro_input,
7};
8
9fn get_attribute_value(attrs: &[Attribute], attribute: &str) -> Option<String> {
10    attrs.iter().find_map(|attr| match &attr.meta {
11        Meta::NameValue(name_value) => {
12            if name_value.path.is_ident(attribute) {
13                match &name_value.value {
14                    Expr::Lit(lit) => match &lit.lit {
15                        Lit::Str(lit) => Some(lit.value()),
16                        _ => panic!("Expected a string literal"),
17                    },
18                    _ => panic!("Expected a string literal"),
19                }
20            } else {
21                None
22            }
23        }
24        _ => None,
25    })
26}
27
28fn get_parameter_name(field: &Field) -> String {
29    if let Some(rename) = get_attribute_value(&field.attrs, "parameter_rename") {
30        rename
31    } else {
32        field.ident.as_ref().unwrap().to_string()
33    }
34}
35
36fn is_nested_parameter(attrs: &[Attribute]) -> bool {
37    attrs
38        .iter()
39        .any(|attr| attr.path().is_ident("parameter_nested"))
40}
41
42fn determine_is_vector(field: &Field) -> bool {
43    let inner_type = get_optional_type(field);
44
45    inner_type.ident == format_ident!("Vec")
46}
47
48fn strip_type_wrappers(segment: &PathSegment, ignore_cases: Vec<String>) -> PathSegment {
49    if ignore_cases.contains(&segment.ident.to_string()) {
50        match &segment.arguments {
51            PathArguments::AngleBracketed(args) => {
52                if let Some(syn::GenericArgument::Type(Type::Path(inner_path))) = args.args.first()
53                {
54                    let k = inner_path.path.segments.first().unwrap();
55                    strip_type_wrappers(k, ignore_cases)
56                } else {
57                    panic!("invalid");
58                }
59            }
60            _ => panic!("invalid"),
61        }
62    } else {
63        segment.clone()
64    }
65}
66
67/// Returns the inner type if it's between Options and Vecs etc..
68fn field_inner_type(field: &Field) -> PathSegment {
69    match &field.ty {
70        Type::Path(path) => {
71            let type_ = path.path.segments.first().unwrap();
72            strip_type_wrappers(type_, vec!["Option".to_string(), "Vec".to_string()])
73        }
74        _ => panic!("Unsupported field type for serialization"),
75    }
76}
77
78/// Returns the inner type if it's between Option
79fn get_optional_type(field: &Field) -> PathSegment {
80    match &field.ty {
81        Type::Path(path) => {
82            let type_ = path.path.segments.first().unwrap();
83            strip_type_wrappers(type_, vec!["Option".to_string()])
84        }
85        _ => panic!("Unsupported field type for serialization"),
86    }
87}
88
89fn is_optional(field: &Field) -> bool {
90    match &field.ty {
91        Type::Path(path) => {
92            let type_ = path.path.segments.first().unwrap();
93            type_.ident == format_ident!("Option")
94        }
95        _ => panic!("Unsupported field type for serialization"),
96    }
97}
98
99fn is_resource_type(field: &Field) -> bool {
100    let field_type = field_inner_type(field).ident;
101
102    ResourceType::try_from(field_type.to_string()).is_ok()
103}
104
105fn build_return_value(fields: &Fields) -> proc_macro2::TokenStream {
106    let field_setters = fields.iter().map(|field| {
107        let optional = is_optional(field);
108        let field = field.ident.as_ref().unwrap();
109        let field_name = field.to_string();
110
111        if optional {
112            quote!{
113                #field: #field
114            }
115        } else {
116            quote!{
117                #field: #field.ok_or_else(||
118                    OperationOutcomeError::error(
119                        haste_fhir_model::r4::generated::terminology::IssueType::invalid(), format!("Field '{}' is required.", stringify!(#field_name))))?
120             }
121        }
122
123    });
124
125    quote! {
126        Ok(Self {
127            #(#field_setters),*
128        })
129    }
130}
131
132#[proc_macro_derive(ToParameters, attributes(parameter_rename, parameter_nested))]
133pub fn haste_to_parameters(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
134    let input = parse_macro_input!(input as DeriveInput);
135
136    match input.data {
137        Data::Struct(data) => derive_to_parameters(&input.ident, &data).into(),
138        _ => syn::Error::new_spanned(input.ident, "ToParameters can only be derived for structs")
139            .to_compile_error()
140            .into(),
141    }
142}
143
144fn derive_to_parameters(struct_name: &Ident, data: &DataStruct) -> TokenStream {
145    let parameters_name = format_ident!("parameters");
146    let var_name = format_ident!("s");
147
148    let to_parameters = match data
149        .fields
150        .iter()
151        .map(generate_parameter_code)
152        .collect::<syn::Result<Vec<_>>>()
153    {
154        Ok(tokens) => tokens,
155        Err(err) => return err.to_compile_error(),
156    };
157
158    quote! {
159        impl From<#struct_name> for Vec<ParametersParameter> {
160            fn from(#var_name: #struct_name) -> Self {
161                let mut #parameters_name = vec![];
162                #(#to_parameters)*
163                #parameters_name
164            }
165        }
166    }
167}
168
169fn generate_parameter_code(field: &Field) -> syn::Result<proc_macro2::TokenStream> {
170    let field_name = field.ident.as_ref().ok_or_else(|| {
171        syn::Error::new_spanned(field, "ToParameters only supports named struct fields")
172    })?;
173
174    let tmp_name = format_ident!("tmp");
175    let var_name = format_ident!("s");
176
177    let mut body = generate_parameter_push(field, &tmp_name);
178
179    if determine_is_vector(field) {
180        body = quote! {
181            for #tmp_name in #tmp_name.into_iter() {
182                #body
183            }
184        };
185    }
186
187    let tokens = if is_optional(field) {
188        quote! {
189            if let Some(#tmp_name) = #var_name.#field_name {
190                #body
191            }
192        }
193    } else {
194        quote! {
195            let #tmp_name = #var_name.#field_name;
196            #body
197        }
198    };
199
200    Ok(tokens)
201}
202
203fn fhir_string() -> TokenStream {
204    quote! {
205        haste_fhir_model::r4::generated::types::FHIRString
206    }
207}
208
209fn generate_parameter_push(field: &Field, value: &Ident) -> proc_macro2::TokenStream {
210    let value_type = field_inner_type(field);
211    let parameter_name = get_parameter_name(field);
212
213    let fhir_string = fhir_string();
214
215    if is_nested_parameter(&field.attrs) {
216        quote! {
217            parameters.push(ParametersParameter {
218                name: Box::new(#fhir_string {
219                    value: Some(#parameter_name.to_string()),
220                    ..Default::default()
221                }),
222                part: Some(#value.into()),
223                ..Default::default()
224            });
225        }
226    } else if value_type.ident == format_ident!("Resource") {
227        quote! {
228            parameters.push(ParametersParameter {
229                name: Box::new(#fhir_string {
230                    value: Some(#parameter_name.to_string()),
231                    ..Default::default()
232                }),
233                resource: Some(Box::new(#value)),
234                ..Default::default()
235            });
236        }
237    } else if value_type.ident == format_ident!("ParametersParameterValueTypeChoice") {
238        quote! {
239            parameters.push(ParametersParameter {
240                name: Box::new(#fhir_string {
241                    value: Some(#parameter_name.to_string()),
242                    ..Default::default()
243                }),
244                value: Some(#value),
245                ..Default::default()
246            });
247        }
248    } else if is_resource_type(field) {
249        quote! {
250            parameters.push(ParametersParameter {
251                name: Box::new(#fhir_string {
252                    value: Some(#parameter_name.to_string()),
253                    ..Default::default()
254                }),
255                resource: Some(Box::new(Resource::#value_type(#value))),
256                ..Default::default()
257            });
258        }
259    } else {
260        let primitive = value_type.ident.to_string().replacen("FHIR", "", 1);
261        let parameter_value_type = format_ident!("{}", primitive);
262
263        quote! {
264            parameters.push(ParametersParameter {
265                name: Box::new(#fhir_string {
266                    value: Some(#parameter_name.to_string()),
267                    ..Default::default()
268                }),
269                value: Some(
270                    haste_fhir_model::r4::generated::resources::ParametersParameterValueTypeChoice::#parameter_value_type(
271                        Box::new(#value)
272                    )
273                ),
274                ..Default::default()
275            });
276        }
277    }
278}
279
280#[proc_macro_derive(FromParameters, attributes(parameter_rename, parameter_nested))]
281pub fn haste_from_parameters(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
282    let input = parse_macro_input!(input as DeriveInput);
283
284    match input.data {
285        Data::Struct(data) => derive_from_parameters(&input.ident, &data).into(),
286        _ => syn::Error::new_spanned(
287            input.ident,
288            "FromParameters can only be derived for structs",
289        )
290        .to_compile_error()
291        .into(),
292    }
293}
294
295fn derive_from_parameters(struct_name: &Ident, data: &DataStruct) -> proc_macro2::TokenStream {
296    let parameters_name = format_ident!("parameters");
297    let current_parameter = format_ident!("param");
298
299    let declare_fields = match data
300        .fields
301        .iter()
302        .map(generate_field_declaration)
303        .collect::<syn::Result<Vec<_>>>()
304    {
305        Ok(fields) => fields,
306        Err(err) => return err.to_compile_error(),
307    };
308
309    let set_fields = match data
310        .fields
311        .iter()
312        .map(generate_parameter_match_arm)
313        .collect::<syn::Result<Vec<_>>>()
314    {
315        Ok(fields) => fields,
316        Err(err) => return err.to_compile_error(),
317    };
318
319    let return_value = build_return_value(&data.fields);
320
321    quote! {
322        impl TryFrom<Vec<ParametersParameter>> for #struct_name {
323            type Error = OperationOutcomeError;
324
325            fn try_from(#parameters_name: Vec<ParametersParameter>) -> Result<Self, Self::Error> {
326                #(#declare_fields)*
327
328                for #current_parameter in #parameters_name {
329                    match #current_parameter.name.value.as_ref().map(|v| v.as_str()) {
330                        #(#set_fields),*
331
332                        Some(k) => {
333                            return Err(OperationOutcomeError::error(
334                                haste_fhir_model::r4::generated::terminology::IssueType::invalid(),
335                                format!("Parameter '{}' is not allowed.", k),
336                            ));
337                        }
338
339                        None => {
340                            return Err(OperationOutcomeError::error(
341                                haste_fhir_model::r4::generated::terminology::IssueType::invalid(),
342                                "Parameter must have a name on it".to_string(),
343                            ));
344                        }
345                    }
346                }
347
348                #return_value
349            }
350        }
351    }
352}
353
354fn generate_field_declaration(field: &Field) -> syn::Result<proc_macro2::TokenStream> {
355    let field_name = field.ident.as_ref().ok_or_else(|| {
356        syn::Error::new_spanned(field, "FromParameters only supports named struct fields")
357    })?;
358
359    let field_type = get_optional_type(field).to_token_stream();
360
361    Ok(quote! {
362        let mut #field_name: Option<#field_type> = None;
363    })
364}
365
366fn generate_parameter_match_arm(field: &Field) -> syn::Result<proc_macro2::TokenStream> {
367    let field_name = field.ident.as_ref().ok_or_else(|| {
368        syn::Error::new_spanned(field, "FromParameters only supports named struct fields")
369    })?;
370    let is_vector = determine_is_vector(field);
371    let expected_parameter_name = get_parameter_name(field);
372
373    let value_from_param = generate_value_extraction(field);
374
375    let setter = if is_vector {
376        quote! {
377            let tmp_value: Result<_, OperationOutcomeError> = #value_from_param;
378
379            if let Some(tmp_value) = tmp_value? {
380                if let Some(tmp_array) = #field_name.as_mut() {
381                    tmp_array.push(tmp_value);
382                } else {
383                    #field_name = Some(vec![tmp_value]);
384                }
385            }
386        }
387    } else {
388        quote! {
389            if #field_name.is_some() {
390                return Err(OperationOutcomeError::error(
391                    haste_fhir_model::r4::generated::terminology::IssueType::invalid(),
392                    format!("Parameter '{}' is not allowed to be repeated.", #expected_parameter_name)
393                ));
394            }
395
396            let tmp_value: Result<_, OperationOutcomeError> = #value_from_param;
397            #field_name = tmp_value?;
398        }
399    };
400
401    Ok(quote! {
402        Some(#expected_parameter_name) => {
403            #setter
404        }
405    })
406}
407
408fn generate_value_extraction(field: &Field) -> proc_macro2::TokenStream {
409    let value_type = field_inner_type(field);
410    let expected_parameter_name = get_parameter_name(field);
411    let current_parameter = format_ident!("param");
412
413    if is_nested_parameter(&field.attrs) {
414        quote! {
415            #value_type::try_from(
416                #current_parameter.part.unwrap_or_default()
417            )
418            .map(Some)
419        }
420    } else if value_type.ident == format_ident!("Resource") {
421        quote! {
422            Ok(#current_parameter.resource.map(|r| *r))
423        }
424    } else if value_type.ident == format_ident!("ParametersParameterValueTypeChoice") {
425        quote! {
426            Ok(#current_parameter.value)
427        }
428    } else if is_resource_type(field) {
429        quote! {
430            if let Some(Resource::#value_type(resource)) =
431                #current_parameter.resource.map(|r| *r)
432            {
433                Ok(Some(resource))
434            } else {
435                Err(OperationOutcomeError::error(
436                    haste_fhir_model::r4::generated::terminology::IssueType::invalid(),
437                    format!(
438                        "Parameter '{}' does not contain correct value type.",
439                        #expected_parameter_name
440                    )
441                ))
442            }
443        }
444    } else {
445        let primitive = value_type.ident.to_string().replacen("FHIR", "", 1);
446        let parameter_value_type = format_ident!("{}", primitive);
447
448        quote! {
449            if let Some(
450                haste_fhir_model::r4::generated::resources::ParametersParameterValueTypeChoice::#parameter_value_type(value)
451            ) = #current_parameter.value {
452                Ok(Some(*value))
453            } else {
454                Err(OperationOutcomeError::error(
455                    haste_fhir_model::r4::generated::terminology::IssueType::invalid(),
456                    format!(
457                        "Parameter '{}' does not contain correct value type.",
458                        #expected_parameter_name
459                    )
460                ))
461            }
462        }
463    }
464}