1use anyhow::{Context, Result};
4use heck::ToSnakeCase;
5use std::collections::HashMap;
6use std::fs;
7use std::path::Path;
8
9use crate::python::generator::{
10 generate_enums_init, generate_errors_init, generate_internal_init, generate_models_init,
11 generate_package_init, generate_transaction_init,
12};
13
14pub fn write_rust_code(
26 output_path: &str,
27 models: &HashMap<String, String>,
28 enums_code: Option<String>,
29 composite_types_code: Option<String>,
30 standalone: bool,
31) -> Result<()> {
32 let output_dir = Path::new(output_path);
33
34 clear_output_dir(output_path)?;
35
36 fs::create_dir_all(output_dir)
38 .with_context(|| format!("Failed to create directory: {}", output_dir.display()))?;
39
40 let src_dir = output_dir.join("src");
42 fs::create_dir_all(&src_dir)
43 .with_context(|| format!("Failed to create src directory: {}", src_dir.display()))?;
44
45 for (model_name, code) in models {
47 let file_name = format!("{}.rs", model_name.to_snake_case());
48 let file_path = src_dir.join(&file_name);
49
50 fs::write(&file_path, code)
51 .with_context(|| format!("Failed to write file: {}", file_path.display()))?;
52 }
53
54 let has_enums = enums_code.is_some();
56 if let Some(enums_code) = enums_code {
57 let enums_path = src_dir.join("enums.rs");
58 fs::write(&enums_path, enums_code)
59 .with_context(|| format!("Failed to write enums file: {}", enums_path.display()))?;
60 }
61
62 let has_composite_types = composite_types_code.is_some();
64 if let Some(types_code) = composite_types_code {
65 let types_path = src_dir.join("types.rs");
66 fs::write(&types_path, types_code)
67 .with_context(|| format!("Failed to write types file: {}", types_path.display()))?;
68 }
69
70 let lib_content = generate_lib_rs(models, has_enums, has_composite_types);
72 let lib_path = src_dir.join("lib.rs");
73 fs::write(&lib_path, lib_content)
74 .with_context(|| format!("Failed to write lib.rs: {}", lib_path.display()))?;
75
76 if standalone {
78 let abs_output = if output_dir.is_absolute() {
82 output_dir.to_path_buf()
83 } else {
84 std::env::current_dir()
85 .context("Failed to get current directory")?
86 .join(output_dir)
87 };
88 let workspace_root_path = {
89 let workspace_toml = crate::find_workspace_cargo_toml(&abs_output);
90 match workspace_toml {
91 Some(toml_path) => {
92 let workspace_dir = toml_path.parent().unwrap();
93 let mut up = std::path::PathBuf::new();
94 let mut candidate = abs_output.clone();
95 loop {
96 if candidate == workspace_dir {
97 break;
98 }
99 up.push("..");
100 match candidate.parent() {
101 Some(p) => candidate = p.to_path_buf(),
102 None => break,
103 }
104 }
105 up.to_string_lossy().replace('\\', "/")
106 }
107 None => {
108 let mut up = std::path::PathBuf::new();
110 let mut candidate = abs_output.clone();
111 loop {
112 candidate = match candidate.parent() {
113 Some(p) => p.to_path_buf(),
114 None => break,
115 };
116 up.push("..");
117 if candidate.join("Cargo.toml").exists() {
118 if let Ok(txt) = fs::read_to_string(candidate.join("Cargo.toml")) {
119 if txt.contains("[workspace]") {
120 break;
121 }
122 }
123 }
124 }
125 up.to_string_lossy().replace('\\', "/")
126 }
127 }
128 };
129 let cargo_toml_content = generate_rust_cargo_toml(&workspace_root_path);
130 let cargo_toml_path = output_dir.join("Cargo.toml");
131 fs::write(&cargo_toml_path, cargo_toml_content).with_context(|| {
132 format!("Failed to write Cargo.toml: {}", cargo_toml_path.display())
133 })?;
134 }
135
136 Ok(())
137}
138
139fn generate_rust_cargo_toml(workspace_root_path: &str) -> String {
145 include_str!("../templates/rust/Cargo.toml.tpl")
146 .replace("{{ workspace_root_path }}", workspace_root_path)
147}
148
149fn generate_lib_rs(
151 models: &HashMap<String, String>,
152 has_enums: bool,
153 has_composite_types: bool,
154) -> String {
155 let mut output = String::new();
156
157 output.push_str("//! Generated Nautilus client library.\n");
158 output.push_str("//!\n");
159 output.push_str("//! This library is auto-generated by nautilus-codegen.\n");
160 output.push_str("//! Do not edit manually.\n\n");
161
162 output.push_str("// Re-export types from nautilus-connector\n");
164 output.push_str("pub use nautilus_connector::{Client, Executor, FromRow, Row};\n");
165 output.push_str("pub use nautilus_connector::{IsolationLevel, TransactionOptions};\n");
166 output.push_str(
167 "pub use nautilus_connector::{TxPgExecutor, TxMysqlExecutor, TxSqliteExecutor};\n\n",
168 );
169
170 if has_composite_types {
172 output.push_str("pub mod types;\n");
173 }
174
175 if has_enums {
177 output.push_str("pub mod enums;\n");
178 }
179
180 let mut model_names: Vec<_> = models.keys().collect();
182 model_names.sort();
183
184 for model_name in &model_names {
185 let module_name = model_name.to_snake_case();
186 output.push_str(&format!("pub mod {};\n", module_name));
187 }
188
189 output.push('\n');
190
191 if has_composite_types {
193 output.push_str("pub use types::*;\n");
194 }
195
196 if has_enums {
198 output.push_str("pub use enums::*;\n");
199 }
200
201 for model_name in model_names {
203 let module_name = model_name.to_snake_case();
204 output.push_str(&format!("pub use {}::*;\n", module_name));
205 }
206
207 output
208}
209
210pub fn write_python_code(
224 output_path: &str,
225 models: &[(String, String)],
226 enums_code: Option<String>,
227 composite_types_code: Option<String>,
228 client_code: Option<String>,
229 runtime_files: &[(&str, &str)],
230) -> Result<()> {
231 let output_dir = Path::new(output_path);
232
233 clear_output_dir(output_path)?;
234
235 fs::create_dir_all(output_dir)
237 .with_context(|| format!("Failed to create directory: {}", output_dir.display()))?;
238
239 let models_dir = output_dir.join("models");
240 fs::create_dir_all(&models_dir).with_context(|| {
241 format!(
242 "Failed to create models directory: {}",
243 models_dir.display()
244 )
245 })?;
246
247 let enums_dir = output_dir.join("enums");
248 fs::create_dir_all(&enums_dir)
249 .with_context(|| format!("Failed to create enums directory: {}", enums_dir.display()))?;
250
251 let errors_dir = output_dir.join("errors");
252 fs::create_dir_all(&errors_dir).with_context(|| {
253 format!(
254 "Failed to create errors directory: {}",
255 errors_dir.display()
256 )
257 })?;
258
259 let internal_dir = output_dir.join("_internal");
260 fs::create_dir_all(&internal_dir).with_context(|| {
261 format!(
262 "Failed to create _internal directory: {}",
263 internal_dir.display()
264 )
265 })?;
266
267 for (file_name, code) in models {
269 let file_path = models_dir.join(file_name);
270
271 fs::write(&file_path, code)
272 .with_context(|| format!("Failed to write file: {}", file_path.display()))?;
273 }
274
275 let models_init = generate_models_init(models);
277 let models_init_path = models_dir.join("__init__.py");
278 fs::write(&models_init_path, models_init)
279 .with_context(|| "Failed to write models/__init__.py")?;
280
281 if let Some(types_code) = composite_types_code {
283 let types_dir = output_dir.join("types");
284 fs::create_dir_all(&types_dir).with_context(|| {
285 format!("Failed to create types directory: {}", types_dir.display())
286 })?;
287
288 let types_path = types_dir.join("types.py");
289 fs::write(&types_path, types_code)
290 .with_context(|| format!("Failed to write types file: {}", types_path.display()))?;
291
292 let types_init = "from .types import * # noqa: F401, F403\n";
294 let types_init_path = types_dir.join("__init__.py");
295 fs::write(&types_init_path, types_init)
296 .with_context(|| "Failed to write types/__init__.py")?;
297 }
298
299 let has_enums = enums_code.is_some();
301 if let Some(enums_code) = enums_code {
302 let enums_path = enums_dir.join("enums.py");
303 fs::write(&enums_path, enums_code)
304 .with_context(|| format!("Failed to write enums file: {}", enums_path.display()))?;
305 }
306
307 let enums_init = generate_enums_init(has_enums);
309 let enums_init_path = enums_dir.join("__init__.py");
310 fs::write(&enums_init_path, enums_init).with_context(|| "Failed to write enums/__init__.py")?;
311
312 for (file_name, content) in runtime_files {
314 let (target_dir, new_name) = match *file_name {
315 "_errors.py" => (&errors_dir, "errors.py"),
316 _ => (&internal_dir, file_name.trim_start_matches('_')),
317 };
318
319 let file_path = target_dir.join(new_name);
320 fs::write(&file_path, content)
321 .with_context(|| format!("Failed to write runtime file: {}", file_path.display()))?;
322 }
323
324 let errors_init = generate_errors_init();
326 let errors_init_path = errors_dir.join("__init__.py");
327 fs::write(&errors_init_path, errors_init)
328 .with_context(|| "Failed to write errors/__init__.py")?;
329
330 let internal_init = generate_internal_init();
332 let internal_init_path = internal_dir.join("__init__.py");
333 fs::write(&internal_init_path, internal_init)
334 .with_context(|| "Failed to write _internal/__init__.py")?;
335
336 if let Some(client_code) = client_code {
338 let client_path = output_dir.join("client.py");
339 fs::write(&client_path, client_code)
340 .with_context(|| format!("Failed to write client.py: {}", client_path.display()))?;
341 }
342
343 let transaction_content = generate_transaction_init();
345 let transaction_path = output_dir.join("transaction.py");
346 fs::write(&transaction_path, transaction_content).with_context(|| {
347 format!(
348 "Failed to write transaction.py: {}",
349 transaction_path.display()
350 )
351 })?;
352
353 let init_content = generate_package_init(has_enums);
355 let init_path = output_dir.join("__init__.py");
356 fs::write(&init_path, init_content)
357 .with_context(|| format!("Failed to write __init__.py: {}", init_path.display()))?;
358
359 let py_typed_path = output_dir.join("py.typed");
361 fs::write(&py_typed_path, "")
362 .with_context(|| format!("Failed to write py.typed: {}", py_typed_path.display()))?;
363
364 Ok(())
365}
366
367#[allow(clippy::too_many_arguments)]
382pub fn write_js_code(
383 output_path: &str,
384 js_models: &[(String, String)],
385 dts_models: &[(String, String)],
386 js_enums: Option<String>,
387 dts_enums: Option<String>,
388 dts_composite_types: Option<String>,
389 js_client: Option<String>,
390 dts_client: Option<String>,
391 js_models_index: Option<String>,
392 dts_models_index: Option<String>,
393 runtime_files: &[(&str, &str)],
394) -> Result<()> {
395 let output_dir = Path::new(output_path);
396
397 clear_output_dir(output_path)?;
398
399 fs::create_dir_all(output_dir)
400 .with_context(|| format!("Failed to create directory: {}", output_dir.display()))?;
401
402 let models_dir = output_dir.join("models");
403 fs::create_dir_all(&models_dir)?;
404
405 let internal_dir = output_dir.join("_internal");
406 fs::create_dir_all(&internal_dir)?;
407
408 for (file_name, code) in js_models {
410 let file_path = models_dir.join(file_name);
411 fs::write(&file_path, code)
412 .with_context(|| format!("Failed to write file: {}", file_path.display()))?;
413 }
414
415 for (file_name, code) in dts_models {
417 let file_path = models_dir.join(file_name);
418 fs::write(&file_path, code)
419 .with_context(|| format!("Failed to write file: {}", file_path.display()))?;
420 }
421
422 if let Some(index_js) = js_models_index {
424 let path = models_dir.join("index.js");
425 fs::write(&path, index_js).with_context(|| "Failed to write models/index.js")?;
426 }
427 if let Some(index_dts) = dts_models_index {
428 let path = models_dir.join("index.d.ts");
429 fs::write(&path, index_dts).with_context(|| "Failed to write models/index.d.ts")?;
430 }
431
432 if let Some(enums_js) = js_enums {
434 let path = output_dir.join("enums.js");
435 fs::write(&path, enums_js)
436 .with_context(|| format!("Failed to write enums.js: {}", output_dir.display()))?;
437 }
438 if let Some(enums_dts) = dts_enums {
439 let path = output_dir.join("enums.d.ts");
440 fs::write(&path, enums_dts)
441 .with_context(|| format!("Failed to write enums.d.ts: {}", output_dir.display()))?;
442 }
443
444 if let Some(types_dts) = dts_composite_types {
446 let path = output_dir.join("types.d.ts");
447 fs::write(&path, types_dts)
448 .with_context(|| format!("Failed to write types.d.ts: {}", output_dir.display()))?;
449 }
450
451 for (file_name, content) in runtime_files {
453 let file_path = internal_dir.join(file_name);
454 fs::write(&file_path, content)
455 .with_context(|| format!("Failed to write runtime file: {}", file_path.display()))?;
456 }
457
458 if let Some(client_js) = js_client {
460 let path = output_dir.join("index.js");
461 fs::write(&path, client_js)
462 .with_context(|| format!("Failed to write index.js: {}", output_dir.display()))?;
463 }
464 if let Some(client_dts) = dts_client {
465 let path = output_dir.join("index.d.ts");
466 fs::write(&path, client_dts)
467 .with_context(|| format!("Failed to write index.d.ts: {}", output_dir.display()))?;
468 }
469
470 Ok(())
471}
472
473fn clear_output_dir(output_path: &str) -> Result<()> {
474 let output_dir = Path::new(output_path);
475 if output_dir.exists() {
476 fs::remove_dir_all(output_dir).with_context(|| {
477 format!("Failed to clean output directory: {}", output_dir.display())
478 })?;
479 }
480 Ok(())
481}