Skip to main content

haste_codegen/type_gen/
operation_definitions.rs

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    // A leading `_` is a FHIR naming convention and is not meaningful to the
156    // generated Rust field name. Preserve the original FHIR name through
157    // #[parameter_rename = "..."] instead.
158    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
235/// Constructs the final `TokenStream` (struct definition and `From`
236/// implementation) by differentiating between base resource returns and
237/// standard parameter wraps.
238fn 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, &parameters, 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
375/// The definition as compact JSON in a raw string literal, so the generated
376/// module carries what it was generated from.
377fn 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    // Escape non-ASCII (JSON only has it inside strings) so HL7's zero-width
382    // spaces don't sit invisibly in the generated source.
383    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    // A raw string ends at `"` followed by its hashes, so use one more than
395    // any run of them after a `"` in the JSON.
396    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
406/* ------------------------------------------------------------------------- */
407/* Operation generation                                                       */
408/* ------------------------------------------------------------------------- */
409
410fn 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                // Its own const, so rustfmt can format around the long line.
479                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
497/// Generates operation definitions from JSON files in the provided directories.
498///
499/// # Errors
500///
501/// Returns an error if an operation definition cannot be generated from one of
502/// the input files.
503pub 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}