haste_codegen/type_gen/
operation_definitions.rs1use 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 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
68fn is_resource_return(parameters: &[&OperationDefinitionParameter]) -> bool {
70 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 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 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
144fn 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
165fn 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
178fn 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 #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 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 });
304 }
305
306 Ok(generated)
307}
308
309pub 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}