Skip to main content

haste_health/commands/
codegen.rs

1use clap::{Subcommand, ValueEnum};
2use haste_codegen::{search_param_cardinality, testscript_gen, type_gen};
3use haste_fhir_model::r4::generated::terminology::IssueType;
4use haste_fhir_operation_error::OperationOutcomeError;
5use quote::quote;
6use std::{io::Write, path::Path, process::Stdio};
7
8/// Which tier of FHIR types to generate.
9#[derive(Clone, Debug, ValueEnum)]
10pub(crate) enum GenerateLevel {
11    Primitive,
12    Complex,
13    Resource,
14}
15
16/// Code generators (Rust FHIR types, operations, TestScripts) used to build this crate.
17#[derive(Subcommand, Debug)]
18pub(crate) enum CodeGen {
19    /// Generate Rust structs for FHIR resources/types/terminology from StructureDefinitions.
20    Types {
21        /// Input FHIR StructureDefinition file(s) or directories (JSON). Repeatable.
22        #[arg(short, long)]
23        input: Vec<String>,
24        /// Output directory for the generated `resources.rs`, `types.rs`, `terminology.rs`, `mod.rs`.
25        #[arg(short, long)]
26        output: String,
27        /// Restrict generation to one tier of types. Defaults to generating all tiers.
28        #[arg(short, long)]
29        level: Option<GenerateLevel>,
30    },
31    /// Generate Rust bindings for FHIR OperationDefinitions.
32    Operations {
33        /// Input FHIR OperationDefinition file(s) or directories (JSON). Repeatable.
34        #[arg(short, long)]
35        input: Vec<String>,
36        /// Output Rust file path. Prints to stdout if omitted.
37        #[arg(short, long)]
38        output: Option<String>,
39    },
40    /// Generate the compiled table of single-valued search parameters.
41    SearchParamCardinality {
42        /// StructureDefinition file(s) or directories (JSON) to resolve paths
43        /// against. Repeatable.
44        #[arg(short, long)]
45        definitions: Vec<String>,
46        /// SearchParameter file(s) or directories (JSON) to classify.
47        /// Repeatable.
48        #[arg(short, long)]
49        parameters: Vec<String>,
50        /// Output Rust file path. Prints to stdout if omitted.
51        #[arg(short, long)]
52        output: Option<String>,
53    },
54    /// Generate FHIR TestScript resources.
55    TestScripts {
56        /// Input file(s) or directories describing the TestScripts to generate. Repeatable.
57        #[arg(short, long)]
58        input: Vec<String>,
59        /// Output directory for the generated TestScript JSON files.
60        #[arg(short, long)]
61        output: String,
62    },
63}
64
65fn format_code(rust_code: String) -> String {
66    let mut format_command = std::process::Command::new("rustfmt")
67        .stdin(Stdio::piped())
68        .stdout(Stdio::piped())
69        .spawn()
70        .expect("Failed to spawn child process");
71
72    let mut stdin = format_command.stdin.take().expect("Failed to open stdin");
73    std::thread::spawn(move || {
74        stdin
75            .write_all(rust_code.as_bytes())
76            .expect("Failed to write to stdin");
77    });
78
79    let command_output = format_command
80        .wait_with_output()
81        .expect("Failed to read stdout");
82
83    let formatted_code = String::from_utf8_lossy(&command_output.stdout);
84
85    formatted_code.to_string()
86}
87
88/// Runs the `generate` command group.
89pub(crate) async fn run(command: &CodeGen) -> Result<(), OperationOutcomeError> {
90    match command {
91        CodeGen::Operations { input, output } => {
92            let generated_operation_definitions =
93                type_gen::operation_definitions::generate_operation_definitions_from_files(input)
94                    .map_err(|e| OperationOutcomeError::error(IssueType::exception(), e))?;
95
96            let formatted_code = format_code(generated_operation_definitions);
97
98            match output {
99                Some(output_path) => {
100                    tokio::fs::write(output_path, formatted_code.to_string())
101                        .await
102                        .map_err(|e| {
103                            OperationOutcomeError::error(IssueType::exception(), e.to_string())
104                        })?;
105                    println!("Generated FHIR types written to: {}", output_path);
106                }
107                None => {
108                    println!("{}", formatted_code);
109                }
110            }
111
112            Ok(())
113        }
114        CodeGen::Types {
115            input,
116            output,
117            level,
118        } => {
119            let level = {
120                match level {
121                    Some(GenerateLevel::Primitive) => Some("primitive-type"),
122                    Some(GenerateLevel::Complex) => Some("complex-type"),
123                    Some(GenerateLevel::Resource) => Some("resource"),
124                    None => None,
125                }
126            };
127
128            let rust_code = type_gen::rust_types::generate(input, level).await?;
129
130            let output_path = Path::new(output);
131            let resource_path = output_path.join("resources.rs");
132            tokio::fs::write(resource_path, format_code(rust_code.resources.to_string()))
133                .await
134                .map_err(|e| OperationOutcomeError::error(IssueType::exception(), e.to_string()))?;
135
136            let type_path = output_path.join("types.rs");
137            tokio::fs::write(type_path, format_code(rust_code.types.to_string()))
138                .await
139                .map_err(|e| OperationOutcomeError::error(IssueType::exception(), e.to_string()))?;
140
141            let terminology_path = output_path.join("terminology.rs");
142            tokio::fs::write(
143                terminology_path,
144                format_code(rust_code.terminology.to_string()),
145            )
146            .await
147            .map_err(|e| OperationOutcomeError::error(IssueType::exception(), e.to_string()))?;
148
149            let mod_path = output_path.join("mod.rs");
150            let module_code = quote! {
151                #![doc = " @generated by `bash scripts/types_build.sh` - do not edit."]
152               pub mod resources;
153               pub mod types;
154               pub mod terminology;
155            };
156            tokio::fs::write(mod_path, format_code(module_code.to_string()))
157                .await
158                .map_err(|e| OperationOutcomeError::error(IssueType::exception(), e.to_string()))?;
159
160            println!("Generated FHIR types written to: {}", output_path.display());
161            Ok(())
162        }
163        CodeGen::SearchParamCardinality {
164            definitions,
165            parameters,
166            output,
167        } => {
168            let definitions = search_param_cardinality::load_definitions(definitions)
169                .map_err(|e| OperationOutcomeError::error(IssueType::exception(), e))?;
170            let parameters = search_param_cardinality::load_search_parameters(parameters)
171                .map_err(|e| OperationOutcomeError::error(IssueType::exception(), e))?;
172
173            let generated = format_code(search_param_cardinality::generate_lookup(
174                &definitions,
175                &parameters,
176            ));
177
178            match output {
179                Some(output_path) => {
180                    tokio::fs::write(output_path, &generated)
181                        .await
182                        .map_err(|e| {
183                            OperationOutcomeError::error(IssueType::exception(), e.to_string())
184                        })?;
185                    println!("Search parameter cardinality table written to: {output_path}");
186                }
187                None => println!("{generated}"),
188            }
189
190            Ok(())
191        }
192        CodeGen::TestScripts { input, output } => {
193            let output_path = Path::new(output);
194            let testscripts = testscript_gen::generate_testscripts(input)
195                .map_err(|e| OperationOutcomeError::error(IssueType::exception(), e))?;
196
197            for testscript in &testscripts {
198                let id = testscript.id.clone().unwrap();
199                let id = id.replace("/", "_").replace(" ", "").replace(".", "_") + ".json";
200                let testscript_path = output_path.join(id);
201
202                println!("Writing TestScript to: {}", testscript_path.display());
203
204                tokio::fs::write(
205                    testscript_path,
206                    serde_json::to_string_pretty(testscript)
207                        .expect("Failed to serialize TestScript to JSON"),
208                )
209                .await
210                .map_err(|e| OperationOutcomeError::error(IssueType::exception(), e.to_string()))?;
211            }
212
213            println!(
214                "Generated TestScripts written to: {}",
215                output_path.display()
216            );
217
218            Ok(())
219        }
220    }
221}