Skip to main content

haste_health/commands/
codegen.rs

1use clap::{Subcommand, ValueEnum};
2use haste_codegen::{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 FHIR TestScript resources.
41    TestScripts {
42        /// Input file(s) or directories describing the TestScripts to generate. Repeatable.
43        #[arg(short, long)]
44        input: Vec<String>,
45        /// Output directory for the generated TestScript JSON files.
46        #[arg(short, long)]
47        output: String,
48    },
49}
50
51fn format_code(rust_code: String) -> String {
52    let mut format_command = std::process::Command::new("rustfmt")
53        .stdin(Stdio::piped())
54        .stdout(Stdio::piped())
55        .spawn()
56        .expect("Failed to spawn child process");
57
58    let mut stdin = format_command.stdin.take().expect("Failed to open stdin");
59    std::thread::spawn(move || {
60        stdin
61            .write_all(rust_code.as_bytes())
62            .expect("Failed to write to stdin");
63    });
64
65    let command_output = format_command
66        .wait_with_output()
67        .expect("Failed to read stdout");
68
69    let formatted_code = String::from_utf8_lossy(&command_output.stdout);
70
71    formatted_code.to_string()
72}
73
74/// Runs the `generate` command group.
75pub(crate) async fn run(command: &CodeGen) -> Result<(), OperationOutcomeError> {
76    match command {
77        CodeGen::Operations { input, output } => {
78            let generated_operation_definitions =
79                type_gen::operation_definitions::generate_operation_definitions_from_files(input)
80                    .map_err(|e| OperationOutcomeError::error(IssueType::exception(), e))?;
81
82            let formatted_code = format_code(generated_operation_definitions);
83
84            match output {
85                Some(output_path) => {
86                    tokio::fs::write(output_path, formatted_code.to_string())
87                        .await
88                        .map_err(|e| {
89                            OperationOutcomeError::error(IssueType::exception(), e.to_string())
90                        })?;
91                    println!("Generated FHIR types written to: {}", output_path);
92                }
93                None => {
94                    println!("{}", formatted_code);
95                }
96            }
97
98            Ok(())
99        }
100        CodeGen::Types {
101            input,
102            output,
103            level,
104        } => {
105            let level = {
106                match level {
107                    Some(GenerateLevel::Primitive) => Some("primitive-type"),
108                    Some(GenerateLevel::Complex) => Some("complex-type"),
109                    Some(GenerateLevel::Resource) => Some("resource"),
110                    None => None,
111                }
112            };
113
114            let rust_code = type_gen::rust_types::generate(input, level).await?;
115
116            let output_path = Path::new(output);
117            let resource_path = output_path.join("resources.rs");
118            tokio::fs::write(resource_path, format_code(rust_code.resources.to_string()))
119                .await
120                .map_err(|e| OperationOutcomeError::error(IssueType::exception(), e.to_string()))?;
121
122            let type_path = output_path.join("types.rs");
123            tokio::fs::write(type_path, format_code(rust_code.types.to_string()))
124                .await
125                .map_err(|e| OperationOutcomeError::error(IssueType::exception(), e.to_string()))?;
126
127            let terminology_path = output_path.join("terminology.rs");
128            tokio::fs::write(
129                terminology_path,
130                format_code(rust_code.terminology.to_string()),
131            )
132            .await
133            .map_err(|e| OperationOutcomeError::error(IssueType::exception(), e.to_string()))?;
134
135            let mod_path = output_path.join("mod.rs");
136            let module_code = quote! {
137                /// DO NOT EDIT THIS FILE. It is auto-generated by the FHIR Rust code generator.
138               pub mod resources;
139               pub mod types;
140               pub mod terminology;
141            };
142            tokio::fs::write(mod_path, format_code(module_code.to_string()))
143                .await
144                .map_err(|e| OperationOutcomeError::error(IssueType::exception(), e.to_string()))?;
145
146            println!("Generated FHIR types written to: {}", output_path.display());
147            Ok(())
148        }
149        CodeGen::TestScripts { input, output } => {
150            let output_path = Path::new(output);
151            let testscripts = testscript_gen::generate_testscripts(input)
152                .map_err(|e| OperationOutcomeError::error(IssueType::exception(), e))?;
153
154            for testscript in &testscripts {
155                let id = testscript.id.clone().unwrap();
156                let id = id.replace("/", "_").replace(" ", "").replace(".", "_") + ".json";
157                let testscript_path = output_path.join(id);
158
159                println!("Writing TestScript to: {}", testscript_path.display());
160
161                tokio::fs::write(
162                    testscript_path,
163                    serde_json::to_string_pretty(testscript)
164                        .expect("Failed to serialize TestScript to JSON"),
165                )
166                .await
167                .map_err(|e| OperationOutcomeError::error(IssueType::exception(), e.to_string()))?;
168            }
169
170            println!(
171                "Generated TestScripts written to: {}",
172                output_path.display()
173            );
174
175            Ok(())
176        }
177    }
178}