Skip to main content

haste_codegen/type_gen/
operation_definitions.rs

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