Skip to main content

haste_codegen/type_gen/
operation_definitions.rs

1use std::{borrow::Cow, path::Path};
2
3use crate::utilities::{FHIR_PRIMITIVES, RUST_KEYWORDS, generate::capitalize, load};
4use haste_fhir_model::r4::generated::{
5    resources::{OperationDefinition, OperationDefinitionParameter, Resource, ResourceType},
6    terminology::{AllTypes, OperationParameterUse},
7};
8use proc_macro2::TokenStream;
9use quote::{format_ident, quote};
10use walkdir::WalkDir;
11
12fn get_operation_definitions<'a>(
13    resource: &'a Resource,
14) -> Result<Vec<&'a OperationDefinition>, String> {
15    match resource {
16        Resource::Bundle(bundle) => {
17            if let Some(entries) = bundle.entry.as_ref() {
18                let op_defs = entries
19                    .iter()
20                    .filter_map(|e| e.resource.as_ref())
21                    .filter_map(|sd| match sd.as_ref() {
22                        Resource::OperationDefinition(op_def) => Some(op_def),
23                        _ => None,
24                    });
25                Ok(op_defs.collect())
26            } else {
27                Ok(vec![])
28            }
29        }
30        Resource::OperationDefinition(op_def) => {
31            let op_def = op_def;
32
33            Ok(vec![op_def])
34        }
35        _ => Err("Resource is not a Bundle or OperationDefinition".to_string()),
36    }
37}
38
39fn get_name(op_def: &OperationDefinition) -> String {
40    let id = op_def
41        .id
42        .clone()
43        .expect("Operation definition must have an id.");
44    let interface_name = id
45        .split("-")
46        .into_iter()
47        .map(|s| capitalize(s))
48        .collect::<Vec<String>>()
49        .join("");
50    interface_name
51}
52
53fn create_field_value(type_: &str, is_array: bool, required: bool) -> TokenStream {
54    let base_type = if let Some(primitive) = FHIR_PRIMITIVES.get(type_) {
55        primitive.as_str()
56    }
57    // For element move to ParametersParameterValueTypeChoice
58    // This sets it as parameter.parameter.value where it would be pulled from.
59    else if type_ == "Element" {
60        "ParametersParameterValueTypeChoice"
61    } else {
62        type_
63    };
64
65    let type_ = format_ident!("{}", base_type);
66
67    let type_ = if is_array {
68        quote! {Vec<#type_>}
69    } else {
70        quote! {#type_}
71    };
72
73    let type_ = if required {
74        quote! { #type_ }
75    } else {
76        quote! {Option<#type_>}
77    };
78
79    type_
80}
81
82/// If param is return and type is a resource, you can return resource directly from field.
83fn is_resource_return(parameters: &Vec<&OperationDefinitionParameter>) -> bool {
84    // Need special handling for single "return" parameter of type Any or a Resource type
85    if parameters.len() == 1
86        && parameters[0].name.value.as_deref() == Some("return")
87        && let Some(parameter_type) = parameters[0].type_.as_ref()
88        && (std::mem::discriminant(&**parameter_type)
89            == std::mem::discriminant(&AllTypes::Any(None))
90            || ResourceType::try_from(
91                Into::<Option<String>>::into(&**parameter_type).unwrap_or_default(),
92            )
93            .is_ok())
94    {
95        true
96    } else {
97        false
98    }
99}
100
101fn generate_parameter_type(
102    name: &str,
103    parameters: &Vec<&OperationDefinitionParameter>,
104    direction: &Direction,
105    is_base: bool,
106) -> Vec<TokenStream> {
107    let mut types = vec![];
108    let mut fields = vec![];
109
110    for p in parameters.iter() {
111        let is_array = p.max.value != Some("1".to_string());
112        let required = p.min.value.unwrap_or(0) > 0;
113        let description = p
114            .documentation
115            .as_ref()
116            .and_then(|d| d.value.clone())
117            .unwrap_or_default();
118        let initial_field_name = p.name.value.as_ref().expect("Parameter must have a name");
119        let formatted_field_name = initial_field_name.replace("-", "_");
120
121        let field_ident = if RUST_KEYWORDS.contains(&formatted_field_name.as_str()) {
122            format_ident!("{}_", formatted_field_name)
123        } else {
124            format_ident!("{}", formatted_field_name)
125        };
126
127        let attribute_rename = if RUST_KEYWORDS.contains(&formatted_field_name.as_str())
128            || formatted_field_name != *initial_field_name
129        {
130            quote! {  #[parameter_rename=#initial_field_name] }
131        } else {
132            quote! {}
133        };
134
135        if let Some(type_) = p.type_.as_ref() {
136            let type_ = if std::mem::discriminant(&**type_)
137                == std::mem::discriminant(&AllTypes::Any(None))
138            {
139                AllTypes::Resource(None)
140            } else {
141                *type_.clone()
142            };
143            let field = create_field_value(
144                Into::<Option<String>>::into(&type_)
145                    .unwrap_or_default()
146                    .as_str(),
147                is_array,
148                required,
149            );
150
151            fields.push(quote! {
152                #[doc = #description]
153                #attribute_rename
154                pub #field_ident: #field
155            })
156        } else {
157            let name = name.to_string()
158                + formatted_field_name
159                    .split("_")
160                    .map(|s| capitalize(s))
161                    .collect::<String>()
162                    .as_str();
163            let nested_types = generate_parameter_type(
164                &name,
165                &p.part
166                    .as_ref()
167                    .map(|v| v.iter().collect())
168                    .unwrap_or(vec![]),
169                direction,
170                false,
171            );
172            types.extend(nested_types);
173
174            let type_ = create_field_value(&name, is_array, required);
175            fields.push(quote! {
176                #[doc = #description]
177                #attribute_rename
178                #[parameter_nested]
179                pub #field_ident: #type_
180            })
181        }
182    }
183
184    let struct_name = format_ident!("{}", name);
185
186    let base_parameter_type = if is_base && is_resource_return(parameters) {
187        let required = parameters.get(0).and_then(|p| p.min.value).unwrap_or(0) > 0;
188        let type_ = parameters
189            .get(0)
190            .and_then(|p| {
191                p.type_
192                    .as_ref()
193                    .and_then(|v| Into::<Option<String>>::into(&**v))
194            })
195            .unwrap_or_default();
196
197        let return_type = if type_ == "Any" {
198            "Resource"
199        } else {
200            type_.as_str()
201        };
202        let return_type_ident = format_ident!("{}", return_type);
203
204        let return_v = if required {
205            quote! {
206                value.return_
207            }
208        } else {
209            quote! {
210               value.return_.unwrap_or_default()
211            }
212        };
213
214        let returned_value = if return_type == "Resource" {
215            quote! {#return_v}
216        } else {
217            quote! { Resource::#return_type_ident(#return_v) }
218        };
219
220        quote! {
221            #[derive(Debug, FromParameters)]
222            pub struct #struct_name {
223                #(#fields),*
224            }
225
226            impl From<#struct_name> for Resource {
227                fn from(value: #struct_name) -> Self {
228                    // Special handling for single "return" parameter of type Any or a Resource type
229                    #returned_value
230                }
231            }
232        }
233    } else {
234        quote! {
235            #[derive(Debug, FromParameters, ToParameters)]
236            pub struct #struct_name {
237                #(#fields),*
238            }
239
240            impl From<#struct_name> for Resource {
241                fn from(value: #struct_name) -> Self {
242                    let parameters: Vec<ParametersParameter> = value.into();
243                    Resource::Parameters(Parameters {
244                        parameter: Some(parameters),
245                        ..Default::default()
246                    })
247                }
248            }
249        }
250    };
251
252    types.push(base_parameter_type);
253
254    types
255}
256
257fn generate_output(parameters: &Cow<Vec<OperationDefinitionParameter>>) -> Vec<TokenStream> {
258    let input_parameters = parameters
259        .iter()
260        .filter(|p| match p.use_.as_ref() {
261            OperationParameterUse::Out(_) => true,
262            _ => false,
263        })
264        .collect::<Vec<_>>();
265
266    generate_parameter_type("Output", &input_parameters, &Direction::Output, true)
267}
268
269fn generate_input(parameters: &Cow<Vec<OperationDefinitionParameter>>) -> Vec<TokenStream> {
270    let input_parameters = parameters
271        .iter()
272        .filter(|p| match p.use_.as_ref() {
273            OperationParameterUse::In(_) => true,
274            _ => false,
275        })
276        .collect::<Vec<_>>();
277
278    generate_parameter_type("Input", &input_parameters, &Direction::Input, true)
279}
280
281enum Direction {
282    Input,
283    Output,
284}
285
286fn generate_operation_definition(file_path: &Path) -> Result<TokenStream, String> {
287    let resource = load::load_from_file(file_path)?;
288    let op_defs = get_operation_definitions(&resource)?;
289    // Generate code for each operation definition
290    let mut generated = quote! {};
291    for op_def in op_defs {
292        let name = format_ident!("{}", get_name(op_def));
293        let op_code = op_def
294            .code
295            .value
296            .as_ref()
297            .expect("Operation must have a code.");
298        let parameters = op_def
299            .parameter
300            .as_ref()
301            .map(Cow::Borrowed)
302            .unwrap_or(Cow::Owned(vec![]));
303
304        let operation_description = op_def
305            .description
306            .as_ref()
307            .and_then(|d| d.value.clone())
308            .unwrap_or("".to_string());
309
310        let generate_input = generate_input(&parameters);
311        let generate_output = generate_output(&parameters);
312
313        generated.extend(quote! {
314            #[doc = #operation_description]
315            pub mod #name {
316                use super::*;
317                pub const CODE: &str = #op_code;
318                #(#generate_input)*
319                #(#generate_output)*
320            }
321            // Code generation for each operation definition
322        });
323    }
324
325    Ok(generated)
326}
327
328pub fn generate_operation_definitions_from_files(
329    file_paths: &Vec<String>,
330) -> Result<String, String> {
331    let mut generated_code = quote! {
332        #![allow(non_snake_case)]
333        use haste_fhir_ops::derive::{FromParameters, ToParameters};
334        use haste_fhir_model::r4::generated::types::*;
335        use haste_fhir_model::r4::generated::resources::*;
336        use haste_fhir_operation_error::*;
337    };
338
339    for dir_path in file_paths {
340        let walker = WalkDir::new(dir_path).into_iter();
341
342        for entry in walker
343            .filter_map(|e| e.ok())
344            .filter(|e| e.metadata().unwrap().is_file())
345            .filter(|e| {
346                e.path()
347                    .extension()
348                    .map(|ext| ext == "json")
349                    .unwrap_or(false)
350            })
351        {
352            let generated_types = generate_operation_definition(entry.path())?;
353
354            generated_code = quote! {
355                #generated_code
356                #generated_types
357            }
358        }
359    }
360
361    Ok(generated_code.to_string())
362}