1use std::{fmt::Write, path::Path};
2
3use crate::documentation::{format_documentation, generate_doc_attributes};
4use crate::utilities::{FHIR_PRIMITIVES, RUST_KEYWORDS, generate::capitalize, load};
5use haste_fhir_model::r4::generated::{
6 resources::{OperationDefinition, OperationDefinitionParameter, Resource, ResourceType},
7 terminology::{AllTypes, BoundCode, OperationParameterUse},
8};
9use proc_macro2::{Ident, Literal, TokenStream};
10use quote::{format_ident, quote};
11use walkdir::WalkDir;
12
13fn get_operation_definitions(resource: &Resource) -> Result<Vec<&OperationDefinition>, String> {
14 match resource {
15 Resource::Bundle(bundle) => Ok(bundle
16 .entry
17 .as_ref()
18 .map(|entries| {
19 entries
20 .iter()
21 .filter_map(|entry| entry.resource.as_ref())
22 .filter_map(|resource| match resource.as_ref() {
23 Resource::OperationDefinition(op_def) => Some(op_def),
24 _ => None,
25 })
26 .collect()
27 })
28 .unwrap_or_default()),
29 Resource::OperationDefinition(op_def) => Ok(vec![op_def]),
30 _ => Err("Resource is not a Bundle or OperationDefinition".to_string()),
31 }
32}
33
34fn get_name(op_def: &OperationDefinition) -> String {
35 op_def
36 .id
37 .clone()
38 .expect("Operation definition must have an id.")
39 .split('-')
40 .map(capitalize)
41 .collect()
42}
43
44fn parameter_type_name(type_: &str) -> &str {
45 if let Some(primitive) = FHIR_PRIMITIVES.get(type_) {
46 primitive.as_str()
47 } else if type_ == "Element" {
48 "ParametersParameterValueTypeChoice"
49 } else {
50 type_
51 }
52}
53
54fn parameter_type_ident(type_: &str) -> Ident {
55 format_ident!("{}", parameter_type_name(type_))
56}
57
58fn create_field_value(type_: &str, is_array: bool, required: bool) -> TokenStream {
59 let type_ident = parameter_type_ident(type_);
60
61 let field_type = if is_array {
62 quote! { Vec<#type_ident> }
63 } else {
64 quote! { #type_ident }
65 };
66
67 if required {
68 quote! { #field_type }
69 } else {
70 quote! { Option<#field_type> }
71 }
72}
73
74fn is_resource_return(parameters: &[&OperationDefinitionParameter]) -> bool {
75 parameters.len() == 1
76 && parameters[0].name.value.as_deref() == Some("return")
77 && parameters[0].type_.as_ref().is_some_and(|parameter_type| {
78 parameter_type == &AllTypes::any()
79 || ResourceType::try_from(parameter_type.as_str().unwrap_or_default()).is_ok()
80 })
81}
82
83fn generate_parameter_type(
84 name: &str,
85 parameters: &[&OperationDefinitionParameter],
86 is_base: bool,
87) -> Vec<TokenStream> {
88 let mut generated_types = Vec::new();
89 let mut fields = Vec::with_capacity(parameters.len());
90
91 for parameter in parameters {
92 let (field_ident, attribute_rename) = process_field_names(parameter);
93
94 let description = parameter
95 .documentation
96 .as_ref()
97 .and_then(|documentation| documentation.value.as_deref())
98 .map_or_else(|| field_ident.to_string(), format_documentation);
99
100 let doc_attributes = generate_doc_attributes(&description);
101
102 let is_array = parameter.max.value.as_deref() != Some("1");
103 let required = parameter.min.value.unwrap_or_default() > 0;
104
105 let (field_type, nested_attribute) = if let Some(type_) = parameter.type_.as_ref() {
106 let type_name = if type_ == &AllTypes::any() {
107 "Resource"
108 } else {
109 type_.as_str().unwrap_or_default()
110 };
111
112 (create_field_value(type_name, is_array, required), quote! {})
113 } else {
114 let nested_struct_name = format_nested_name(name, parameter);
115
116 let nested_parameters = parameter
117 .part
118 .as_deref()
119 .unwrap_or_default()
120 .iter()
121 .collect::<Vec<_>>();
122
123 generated_types.extend(generate_parameter_type(
124 &nested_struct_name,
125 &nested_parameters,
126 false,
127 ));
128
129 (
130 create_field_value(&nested_struct_name, is_array, required),
131 quote! {
132 #[parameter_nested]
133 },
134 )
135 };
136
137 fields.push(quote! {
138 #doc_attributes
139 #attribute_rename
140 #nested_attribute
141 pub #field_ident: #field_type
142 });
143 }
144
145 generated_types.push(build_struct_tokens(name, parameters, &fields, is_base));
146
147 generated_types
148}
149
150fn process_field_names(p: &OperationDefinitionParameter) -> (Ident, TokenStream) {
151 let initial_name = p.name.value.as_deref().expect("Parameter must have a name");
152
153 let replaced_name = initial_name.replace('-', "_");
154
155 let formatted_name = replaced_name
159 .strip_prefix('_')
160 .unwrap_or(&replaced_name)
161 .to_string();
162
163 let is_rust_keyword = RUST_KEYWORDS.contains(&formatted_name.as_str());
164
165 let field_ident = if is_rust_keyword {
166 format_ident!("{}_", formatted_name)
167 } else {
168 format_ident!("{}", formatted_name)
169 };
170
171 let attribute_rename = if formatted_name != *initial_name || is_rust_keyword {
172 quote! {
173 #[parameter_rename = #initial_name]
174 }
175 } else {
176 quote! {}
177 };
178
179 (field_ident, attribute_rename)
180}
181
182fn format_nested_name(parent_name: &str, parameter: &OperationDefinitionParameter) -> String {
183 let initial_name = parameter
184 .name
185 .value
186 .as_deref()
187 .expect("Parameter must have a name");
188
189 let formatted_name = initial_name.replace('-', "_");
190
191 let capitalized_parts = formatted_name
192 .split('_')
193 .map(capitalize)
194 .collect::<String>();
195
196 format!("{parent_name}{capitalized_parts}")
197}
198
199fn resource_return_tokens(parameters: &[&OperationDefinitionParameter]) -> Option<TokenStream> {
200 if !is_resource_return(parameters) {
201 return None;
202 }
203
204 let parameter = parameters.first()?;
205
206 let required = parameter.min.value.unwrap_or_default() > 0;
207
208 let type_str = parameter
209 .type_
210 .as_ref()
211 .and_then(BoundCode::as_str)
212 .unwrap_or_default();
213
214 let return_type = if type_str == "Any" {
215 "Resource"
216 } else {
217 type_str
218 };
219
220 let return_type_ident = format_ident!("{}", return_type);
221
222 let return_value = if required {
223 quote! { value.return_ }
224 } else {
225 quote! { value.return_.unwrap_or_default() }
226 };
227
228 Some(if return_type == "Resource" {
229 quote! { #return_value }
230 } else {
231 quote! { Resource::#return_type_ident(#return_value) }
232 })
233}
234
235fn build_struct_tokens(
239 name: &str,
240 parameters: &[&OperationDefinitionParameter],
241 fields: &[TokenStream],
242 is_base: bool,
243) -> TokenStream {
244 let struct_name = format_ident!("{}", name);
245
246 if is_base && let Some(returned_value) = resource_return_tokens(parameters) {
247 return quote! {
248 #[derive(Debug, FromParameters)]
249 pub struct #struct_name {
250 #(#fields),*
251 }
252
253 impl From<#struct_name> for Resource {
254 fn from(value: #struct_name) -> Self {
255 #returned_value
256 }
257 }
258 };
259 }
260
261 quote! {
262 #[derive(Debug, FromParameters, ToParameters)]
263 pub struct #struct_name {
264 #(#fields),*
265 }
266
267 impl From<#struct_name> for Resource {
268 fn from(value: #struct_name) -> Self {
269 let parameters: Vec<ParametersParameter> = value.into();
270
271 Resource::Parameters(Parameters {
272 parameter: Some(parameters),
273 ..Default::default()
274 })
275 }
276 }
277 }
278}
279
280fn generate_parameters(
281 parameters: &[OperationDefinitionParameter],
282 parameter_use: &BoundCode<OperationParameterUse>,
283 name: &str,
284) -> Vec<TokenStream> {
285 let parameters = parameters
286 .iter()
287 .filter(|parameter| parameter.use_ == *parameter_use)
288 .collect::<Vec<_>>();
289
290 generate_parameter_type(name, ¶meters, true)
291}
292
293fn generate_output(parameters: &[OperationDefinitionParameter]) -> Vec<TokenStream> {
294 generate_parameters(parameters, &OperationParameterUse::out(), "Output")
295}
296
297fn generate_input(parameters: &[OperationDefinitionParameter]) -> Vec<TokenStream> {
298 generate_parameters(parameters, &OperationParameterUse::in_(), "Input")
299}
300
301struct OperationImports {
302 resources: Vec<Ident>,
303 types: Vec<Ident>,
304}
305
306impl OperationImports {
307 fn new() -> Self {
308 Self {
309 resources: Vec::new(),
310 types: Vec::new(),
311 }
312 }
313
314 fn add_resource(&mut self, name: &str) {
315 self.resources.push(format_ident!("{}", name));
316 }
317
318 fn add_type(&mut self, name: &str) {
319 self.types.push(format_ident!("{}", name));
320 }
321
322 fn sort_and_dedup(&mut self) {
323 Self::sort_and_dedup_idents(&mut self.resources);
324 Self::sort_and_dedup_idents(&mut self.types);
325 }
326
327 fn sort_and_dedup_idents(idents: &mut Vec<Ident>) {
328 idents.sort_by_key(std::string::ToString::to_string);
329 idents.dedup_by_key(|ident| ident.to_string());
330 }
331}
332
333fn add_parameter_type(imports: &mut OperationImports, parameter: &OperationDefinitionParameter) {
334 if let Some(type_) = parameter.type_.as_ref() {
335 let type_name = if type_ == &AllTypes::any() {
336 "Resource"
337 } else {
338 type_.as_str().unwrap_or_default()
339 };
340
341 if type_name == "Resource" {
342 imports.add_resource("Resource");
343 } else if type_name == "Element" {
344 imports.add_resource("ParametersParameterValueTypeChoice");
345 } else if let Some(primitive) = FHIR_PRIMITIVES.get(type_name) {
346 imports.add_type(primitive);
347 } else if ResourceType::try_from(type_name).is_ok() {
348 imports.add_resource(type_name);
349 } else {
350 imports.add_type(type_name);
351 }
352 }
353
354 for part in parameter.part.as_deref().unwrap_or_default() {
355 add_parameter_type(imports, part);
356 }
357}
358
359fn collect_imports(parameters: &[OperationDefinitionParameter]) -> OperationImports {
360 let mut imports = OperationImports::new();
361
362 imports.add_resource("OperationDefinition");
363 imports.add_resource("Parameters");
364 imports.add_resource("ParametersParameter");
365 imports.add_resource("Resource");
366
367 for parameter in parameters {
368 add_parameter_type(&mut imports, parameter);
369 }
370
371 imports.sort_and_dedup();
372 imports
373}
374
375fn definition_literal(op_def: &OperationDefinition) -> Result<Literal, String> {
378 let json = serde_json::to_string(op_def)
379 .map_err(|e| format!("Failed to serialize OperationDefinition: {e}"))?;
380
381 let mut ascii = String::with_capacity(json.len());
384 for c in json.chars() {
385 if c.is_ascii() {
386 ascii.push(c);
387 } else {
388 for unit in c.encode_utf16(&mut [0; 2]) {
389 let _ = write!(ascii, "\\u{unit:04x}");
390 }
391 }
392 }
393
394 let mut hashes = String::from("#");
397 while ascii.contains(&format!("\"{hashes}")) {
398 hashes.push('#');
399 }
400
401 format!("r{hashes}\"{ascii}\"{hashes}")
402 .parse()
403 .map_err(|e| format!("Failed to build OperationDefinition literal: {e}"))
404}
405
406fn generate_operation_definition(file_path: &Path) -> Result<TokenStream, String> {
411 let resource = load::load_from_file(file_path)?;
412 let op_defs = get_operation_definitions(&resource)?;
413
414 let mut generated = quote! {};
415
416 for op_def in op_defs {
417 let name = format_ident!("{}", get_name(op_def));
418
419 let op_code = op_def
420 .code
421 .value
422 .as_ref()
423 .expect("Operation must have a code.");
424
425 let parameters = op_def.parameter.as_deref().unwrap_or_default();
426
427 let operation_description = op_def
428 .description
429 .as_ref()
430 .and_then(|description| description.value.as_deref())
431 .map(format_documentation)
432 .unwrap_or_default();
433
434 let operation_doc_attributes = generate_doc_attributes(&operation_description);
435
436 let imports = collect_imports(parameters);
437
438 let definition_json = definition_literal(op_def)?;
439
440 let generated_input = generate_input(parameters);
441 let generated_output = generate_output(parameters);
442
443 let resource_imports = &imports.resources;
444 let type_imports = &imports.types;
445
446 let resource_use = if resource_imports.is_empty() {
447 quote! {}
448 } else {
449 quote! {
450 use haste_fhir_model::r4::generated::resources::{
451 #(#resource_imports),*
452 };
453 }
454 };
455
456 let type_use = if type_imports.is_empty() {
457 quote! {}
458 } else {
459 quote! {
460 use haste_fhir_model::r4::generated::types::{
461 #(#type_imports),*
462 };
463 }
464 };
465
466 generated.extend(quote! {
467 #operation_doc_attributes
468 pub mod #name {
469 #resource_use
470 #type_use
471
472 use haste_fhir_operation_error::OperationOutcomeError;
473 use haste_fhir_ops::derive::{FromParameters, ToParameters};
474 use std::sync::LazyLock;
475
476 pub const CODE: &str = #op_code;
477
478 const OPERATION_DEFINITION_JSON: &str = #definition_json;
480
481 #[doc = "The `OperationDefinition` this module was generated from."]
482 pub static OPERATION_DEFINITION: LazyLock<OperationDefinition> =
483 LazyLock::new(|| {
484 serde_json::from_str(OPERATION_DEFINITION_JSON)
485 .expect("Generated OperationDefinition JSON must deserialize.")
486 });
487
488 #(#generated_input)*
489 #(#generated_output)*
490 }
491 });
492 }
493
494 Ok(generated)
495}
496
497pub fn generate_operation_definitions_from_files(file_paths: &[String]) -> Result<String, String> {
504 let mut generated_code = quote! {
505 #![allow(non_snake_case)]
506 #![doc = " @generated by `bash scripts/operation_build.sh` - do not edit."]
507 };
508
509 for dir_path in file_paths {
510 let walker = WalkDir::new(dir_path).sort_by_file_name().into_iter();
511
512 for entry in walker
513 .filter_map(std::result::Result::ok)
514 .filter(|entry| entry.metadata().is_ok_and(|metadata| metadata.is_file()))
515 .filter(|entry| {
516 entry
517 .path()
518 .extension()
519 .is_some_and(|extension| extension == "json")
520 })
521 {
522 let generated_types = generate_operation_definition(entry.path())?;
523
524 generated_code = quote! {
525 #generated_code
526 #generated_types
527 };
528 }
529 }
530
531 Ok(generated_code.to_string())
532}