haste_health/commands/
codegen.rs1use 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#[derive(Clone, Debug, ValueEnum)]
10pub(crate) enum GenerateLevel {
11 Primitive,
12 Complex,
13 Resource,
14}
15
16#[derive(Subcommand, Debug)]
18pub(crate) enum CodeGen {
19 Types {
21 #[arg(short, long)]
23 input: Vec<String>,
24 #[arg(short, long)]
26 output: String,
27 #[arg(short, long)]
29 level: Option<GenerateLevel>,
30 },
31 Operations {
33 #[arg(short, long)]
35 input: Vec<String>,
36 #[arg(short, long)]
38 output: Option<String>,
39 },
40 SearchParamCardinality {
42 #[arg(short, long)]
45 definitions: Vec<String>,
46 #[arg(short, long)]
49 parameters: Vec<String>,
50 #[arg(short, long)]
52 output: Option<String>,
53 },
54 TestScripts {
56 #[arg(short, long)]
58 input: Vec<String>,
59 #[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
88pub(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 ¶meters,
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}