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 generate_parameter_push(field: &Field, value: &Ident) -> proc_macro2::TokenStream {
204    let value_type = field_inner_type(field);
205    let parameter_name = get_parameter_name(field);
206
207    if is_nested_parameter(&field.attrs) {
208        quote! {
209            parameters.push(ParametersParameter {
210                name: Box::new(FHIRString {
211                    value: Some(#parameter_name.to_string()),
212                    ..Default::default()
213                }),
214                part: Some(#value.into()),
215                ..Default::default()
216            });
217        }
218    } else if value_type.ident == format_ident!("Resource") {
219        quote! {
220            parameters.push(ParametersParameter {
221                name: Box::new(FHIRString {
222                    value: Some(#parameter_name.to_string()),
223                    ..Default::default()
224                }),
225                resource: Some(Box::new(#value)),
226                ..Default::default()
227            });
228        }
229    } else if value_type.ident == format_ident!("ParametersParameterValueTypeChoice") {
230        quote! {
231            parameters.push(ParametersParameter {
232                name: Box::new(FHIRString {
233                    value: Some(#parameter_name.to_string()),
234                    ..Default::default()
235                }),
236                value: Some(#value),
237                ..Default::default()
238            });
239        }
240    } else if is_resource_type(field) {
241        quote! {
242            parameters.push(ParametersParameter {
243                name: Box::new(FHIRString {
244                    value: Some(#parameter_name.to_string()),
245                    ..Default::default()
246                }),
247                resource: Some(Box::new(Resource::#value_type(#value))),
248                ..Default::default()
249            });
250        }
251    } else {
252        let primitive = value_type.ident.to_string().replacen("FHIR", "", 1);
253        let parameter_value_type = format_ident!("{}", primitive);
254
255        quote! {
256            parameters.push(ParametersParameter {
257                name: Box::new(FHIRString {
258                    value: Some(#parameter_name.to_string()),
259                    ..Default::default()
260                }),
261                value: Some(
262                    haste_fhir_model::r4::generated::resources::ParametersParameterValueTypeChoice::#parameter_value_type(
263                        Box::new(#value)
264                    )
265                ),
266                ..Default::default()
267            });
268        }
269    }
270}
271
272#[proc_macro_derive(FromParameters, attributes(parameter_rename, parameter_nested))]
273pub fn haste_from_parameters(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
274    let input = parse_macro_input!(input as DeriveInput);
275
276    match input.data {
277        Data::Struct(data) => derive_from_parameters(&input.ident, &data).into(),
278        _ => syn::Error::new_spanned(
279            input.ident,
280            "FromParameters can only be derived for structs",
281        )
282        .to_compile_error()
283        .into(),
284    }
285}
286
287fn derive_from_parameters(struct_name: &Ident, data: &DataStruct) -> proc_macro2::TokenStream {
288    let parameters_name = format_ident!("parameters");
289    let current_parameter = format_ident!("param");
290
291    let declare_fields = match data
292        .fields
293        .iter()
294        .map(generate_field_declaration)
295        .collect::<syn::Result<Vec<_>>>()
296    {
297        Ok(fields) => fields,
298        Err(err) => return err.to_compile_error(),
299    };
300
301    let set_fields = match data
302        .fields
303        .iter()
304        .map(generate_parameter_match_arm)
305        .collect::<syn::Result<Vec<_>>>()
306    {
307        Ok(fields) => fields,
308        Err(err) => return err.to_compile_error(),
309    };
310
311    let return_value = build_return_value(&data.fields);
312
313    quote! {
314        impl TryFrom<Vec<ParametersParameter>> for #struct_name {
315            type Error = OperationOutcomeError;
316
317            fn try_from(#parameters_name: Vec<ParametersParameter>) -> Result<Self, Self::Error> {
318                #(#declare_fields)*
319
320                for #current_parameter in #parameters_name {
321                    match #current_parameter.name.value.as_ref().map(|v| v.as_str()) {
322                        #(#set_fields),*
323
324                        Some(k) => {
325                            return Err(OperationOutcomeError::error(
326                                haste_fhir_model::r4::generated::terminology::IssueType::invalid(),
327                                format!("Parameter '{}' is not allowed.", k),
328                            ));
329                        }
330
331                        None => {
332                            return Err(OperationOutcomeError::error(
333                                haste_fhir_model::r4::generated::terminology::IssueType::invalid(),
334                                "Parameter must have a name on it".to_string(),
335                            ));
336                        }
337                    }
338                }
339
340                #return_value
341            }
342        }
343    }
344}
345
346fn generate_field_declaration(field: &Field) -> syn::Result<proc_macro2::TokenStream> {
347    let field_name = field.ident.as_ref().ok_or_else(|| {
348        syn::Error::new_spanned(field, "FromParameters only supports named struct fields")
349    })?;
350
351    let field_type = get_optional_type(field).to_token_stream();
352
353    Ok(quote! {
354        let mut #field_name: Option<#field_type> = None;
355    })
356}
357
358fn generate_parameter_match_arm(field: &Field) -> syn::Result<proc_macro2::TokenStream> {
359    let field_name = field.ident.as_ref().ok_or_else(|| {
360        syn::Error::new_spanned(field, "FromParameters only supports named struct fields")
361    })?;
362    let is_vector = determine_is_vector(field);
363    let expected_parameter_name = get_parameter_name(field);
364
365    let value_from_param = generate_value_extraction(field);
366
367    let setter = if is_vector {
368        quote! {
369            let tmp_value: Result<_, OperationOutcomeError> = #value_from_param;
370
371            if let Some(tmp_value) = tmp_value? {
372                if let Some(tmp_array) = #field_name.as_mut() {
373                    tmp_array.push(tmp_value);
374                } else {
375                    #field_name = Some(vec![tmp_value]);
376                }
377            }
378        }
379    } else {
380        quote! {
381            if #field_name.is_some() {
382                return Err(OperationOutcomeError::error(
383                    haste_fhir_model::r4::generated::terminology::IssueType::invalid(),
384                    format!("Parameter '{}' is not allowed to be repeated.", #expected_parameter_name)
385                ));
386            }
387
388            let tmp_value: Result<_, OperationOutcomeError> = #value_from_param;
389            #field_name = tmp_value?;
390        }
391    };
392
393    Ok(quote! {
394        Some(#expected_parameter_name) => {
395            #setter
396        }
397    })
398}
399
400fn generate_value_extraction(field: &Field) -> proc_macro2::TokenStream {
401    let value_type = field_inner_type(field);
402    let expected_parameter_name = get_parameter_name(field);
403    let current_parameter = format_ident!("param");
404
405    if is_nested_parameter(&field.attrs) {
406        quote! {
407            #value_type::try_from(
408                #current_parameter.part.unwrap_or_default()
409            )
410            .map(Some)
411        }
412    } else if value_type.ident == format_ident!("Resource") {
413        quote! {
414            Ok(#current_parameter.resource.map(|r| *r))
415        }
416    } else if value_type.ident == format_ident!("ParametersParameterValueTypeChoice") {
417        quote! {
418            Ok(#current_parameter.value)
419        }
420    } else if is_resource_type(field) {
421        quote! {
422            if let Some(Resource::#value_type(resource)) =
423                #current_parameter.resource.map(|r| *r)
424            {
425                Ok(Some(resource))
426            } else {
427                Err(OperationOutcomeError::error(
428                    haste_fhir_model::r4::generated::terminology::IssueType::invalid(),
429                    format!(
430                        "Parameter '{}' does not contain correct value type.",
431                        #expected_parameter_name
432                    )
433                ))
434            }
435        }
436    } else {
437        let primitive = value_type.ident.to_string().replacen("FHIR", "", 1);
438        let parameter_value_type = format_ident!("{}", primitive);
439
440        quote! {
441            if let Some(
442                haste_fhir_model::r4::generated::resources::ParametersParameterValueTypeChoice::#parameter_value_type(value)
443            ) = #current_parameter.value {
444                Ok(Some(*value))
445            } else {
446                Err(OperationOutcomeError::error(
447                    haste_fhir_model::r4::generated::terminology::IssueType::invalid(),
448                    format!(
449                        "Parameter '{}' does not contain correct value type.",
450                        #expected_parameter_name
451                    )
452                ))
453            }
454        }
455    }
456}