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 schema_source: &str,
31 standalone: bool,
32) -> Result<()> {
33 let output_dir = Path::new(output_path);
34
35 clear_output_dir(output_path)?;
36
37 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");
41 fs::create_dir_all(&src_dir)
42 .with_context(|| format!("Failed to create src directory: {}", src_dir.display()))?;
43
44 for (model_name, code) in models {
45 let file_name = format!("{}.rs", model_name.to_snake_case());
46 let file_path = src_dir.join(&file_name);
47
48 fs::write(&file_path, code)
49 .with_context(|| format!("Failed to write file: {}", file_path.display()))?;
50 }
51
52 let has_enums = enums_code.is_some();
53 if let Some(enums_code) = enums_code {
54 let enums_path = src_dir.join("enums.rs");
55 fs::write(&enums_path, enums_code)
56 .with_context(|| format!("Failed to write enums file: {}", enums_path.display()))?;
57 }
58
59 let has_composite_types = composite_types_code.is_some();
60 if let Some(types_code) = composite_types_code {
61 let types_path = src_dir.join("types.rs");
62 fs::write(&types_path, types_code)
63 .with_context(|| format!("Failed to write types file: {}", types_path.display()))?;
64 }
65
66 let lib_content = generate_lib_rs(models, has_enums, has_composite_types, schema_source);
67 let lib_path = src_dir.join("lib.rs");
68 fs::write(&lib_path, lib_content)
69 .with_context(|| format!("Failed to write lib.rs: {}", lib_path.display()))?;
70
71 let runtime_path = src_dir.join("runtime.rs");
72 fs::write(
73 &runtime_path,
74 include_str!("../templates/rust/runtime.rs.tpl"),
75 )
76 .with_context(|| format!("Failed to write runtime.rs: {}", runtime_path.display()))?;
77
78 if standalone {
79 let abs_output = if output_dir.is_absolute() {
83 output_dir.to_path_buf()
84 } else {
85 std::env::current_dir()
86 .context("Failed to get current directory")?
87 .join(output_dir)
88 };
89 let workspace_root_path = {
90 let workspace_toml = crate::find_workspace_cargo_toml(&abs_output);
91 match workspace_toml {
92 Some(toml_path) => {
93 let workspace_dir = toml_path.parent().unwrap();
94 let mut up = std::path::PathBuf::new();
95 let mut candidate = abs_output.clone();
96 loop {
97 if candidate == workspace_dir {
98 break;
99 }
100 up.push("..");
101 match candidate.parent() {
102 Some(p) => candidate = p.to_path_buf(),
103 None => break,
104 }
105 }
106 up.to_string_lossy().replace('\\', "/")
107 }
108 None => {
109 let mut up = std::path::PathBuf::new();
111 let mut candidate = abs_output.clone();
112 loop {
113 candidate = match candidate.parent() {
114 Some(p) => p.to_path_buf(),
115 None => break,
116 };
117 up.push("..");
118 if candidate.join("Cargo.toml").exists() {
119 if let Ok(txt) = fs::read_to_string(candidate.join("Cargo.toml")) {
120 if txt.contains("[workspace]") {
121 break;
122 }
123 }
124 }
125 }
126 up.to_string_lossy().replace('\\', "/")
127 }
128 }
129 };
130 let cargo_toml_content = generate_rust_cargo_toml(&workspace_root_path);
131 let cargo_toml_path = output_dir.join("Cargo.toml");
132 fs::write(&cargo_toml_path, cargo_toml_content).with_context(|| {
133 format!("Failed to write Cargo.toml: {}", cargo_toml_path.display())
134 })?;
135 }
136
137 Ok(())
138}
139
140fn generate_rust_cargo_toml(workspace_root_path: &str) -> String {
146 include_str!("../templates/rust/Cargo.toml.tpl")
147 .replace("{{ workspace_root_path }}", workspace_root_path)
148}
149
150fn generate_lib_rs(
152 models: &HashMap<String, String>,
153 has_enums: bool,
154 has_composite_types: bool,
155 schema_source: &str,
156) -> String {
157 let mut output = String::new();
158
159 output.push_str("//! Generated Nautilus client library.\n");
160 output.push_str("//!\n");
161 output.push_str("//! This library is auto-generated by nautilus-codegen.\n");
162 output.push_str("//! Do not edit manually.\n\n");
163
164 output.push_str("pub use nautilus_connector::{Executor, FromRow, Row};\n");
165 output.push_str(
166 "pub use nautilus_connector::{ValueHint, decode_row_with_hints, normalize_row_with_hints, normalize_rows_with_hints};\n",
167 );
168 output.push_str("pub use nautilus_connector::{IsolationLevel, TransactionOptions};\n");
169 output.push_str("pub use nautilus_connector::TransactionExecutor;\n\n");
170 output.push_str(&format!(
171 "pub(crate) const SCHEMA_SOURCE: &str = {:?};\n\n",
172 schema_source
173 ));
174 output.push_str("mod runtime;\n");
175 output.push_str("pub use runtime::Client;\n\n");
176
177 if has_composite_types {
178 output.push_str("pub mod types;\n");
179 }
180
181 if has_enums {
182 output.push_str("pub mod enums;\n");
183 }
184
185 let mut model_names: Vec<_> = models.keys().collect();
186 model_names.sort();
187
188 for model_name in &model_names {
189 let module_name = model_name.to_snake_case();
190 output.push_str(&format!("pub mod {};\n", module_name));
191 }
192
193 output.push('\n');
194
195 if has_composite_types {
196 output.push_str("pub use types::*;\n");
197 }
198
199 if has_enums {
200 output.push_str("pub use enums::*;\n");
201 }
202
203 for model_name in model_names {
204 let module_name = model_name.to_snake_case();
205 output.push_str(&format!("pub use {}::*;\n", module_name));
206 }
207
208 output
209}
210
211pub fn write_python_code(
225 output_path: &str,
226 models: &[(String, String)],
227 enums_code: Option<String>,
228 composite_types_code: Option<String>,
229 client_code: Option<String>,
230 runtime_files: &[(&str, &str)],
231) -> Result<()> {
232 let output_dir = Path::new(output_path);
233
234 clear_output_dir(output_path)?;
235
236 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 {
268 let file_path = models_dir.join(file_name);
269
270 fs::write(&file_path, code)
271 .with_context(|| format!("Failed to write file: {}", file_path.display()))?;
272 }
273
274 let models_init = generate_models_init(models);
275 let models_init_path = models_dir.join("__init__.py");
276 fs::write(&models_init_path, models_init)
277 .with_context(|| "Failed to write models/__init__.py")?;
278
279 if let Some(types_code) = composite_types_code {
280 let types_dir = output_dir.join("types");
281 fs::create_dir_all(&types_dir).with_context(|| {
282 format!("Failed to create types directory: {}", types_dir.display())
283 })?;
284
285 let types_path = types_dir.join("types.py");
286 fs::write(&types_path, types_code)
287 .with_context(|| format!("Failed to write types file: {}", types_path.display()))?;
288
289 let types_init = "from .types import * # noqa: F401, F403\n";
290 let types_init_path = types_dir.join("__init__.py");
291 fs::write(&types_init_path, types_init)
292 .with_context(|| "Failed to write types/__init__.py")?;
293 }
294
295 let has_enums = enums_code.is_some();
296 if let Some(enums_code) = enums_code {
297 let enums_path = enums_dir.join("enums.py");
298 fs::write(&enums_path, enums_code)
299 .with_context(|| format!("Failed to write enums file: {}", enums_path.display()))?;
300 }
301
302 let enums_init = generate_enums_init(has_enums);
303 let enums_init_path = enums_dir.join("__init__.py");
304 fs::write(&enums_init_path, enums_init).with_context(|| "Failed to write enums/__init__.py")?;
305
306 for (file_name, content) in runtime_files {
307 let (target_dir, new_name) = match *file_name {
308 "_errors.py" => (&errors_dir, "errors.py"),
309 _ => (&internal_dir, file_name.trim_start_matches('_')),
310 };
311
312 let file_path = target_dir.join(new_name);
313 fs::write(&file_path, content)
314 .with_context(|| format!("Failed to write runtime file: {}", file_path.display()))?;
315 }
316
317 let errors_init = generate_errors_init();
318 let errors_init_path = errors_dir.join("__init__.py");
319 fs::write(&errors_init_path, errors_init)
320 .with_context(|| "Failed to write errors/__init__.py")?;
321
322 let internal_init = generate_internal_init();
323 let internal_init_path = internal_dir.join("__init__.py");
324 fs::write(&internal_init_path, internal_init)
325 .with_context(|| "Failed to write _internal/__init__.py")?;
326
327 if let Some(client_code) = client_code {
328 let client_path = output_dir.join("client.py");
329 fs::write(&client_path, client_code)
330 .with_context(|| format!("Failed to write client.py: {}", client_path.display()))?;
331 }
332
333 let transaction_content = generate_transaction_init();
334 let transaction_path = output_dir.join("transaction.py");
335 fs::write(&transaction_path, transaction_content).with_context(|| {
336 format!(
337 "Failed to write transaction.py: {}",
338 transaction_path.display()
339 )
340 })?;
341
342 let init_content = generate_package_init(has_enums);
343 let init_path = output_dir.join("__init__.py");
344 fs::write(&init_path, init_content)
345 .with_context(|| format!("Failed to write __init__.py: {}", init_path.display()))?;
346
347 let py_typed_path = output_dir.join("py.typed");
348 fs::write(&py_typed_path, "")
349 .with_context(|| format!("Failed to write py.typed: {}", py_typed_path.display()))?;
350
351 Ok(())
352}
353
354#[allow(clippy::too_many_arguments)]
369pub fn write_js_code(
370 output_path: &str,
371 js_models: &[(String, String)],
372 dts_models: &[(String, String)],
373 js_enums: Option<String>,
374 dts_enums: Option<String>,
375 dts_composite_types: Option<String>,
376 js_client: Option<String>,
377 dts_client: Option<String>,
378 js_models_index: Option<String>,
379 dts_models_index: Option<String>,
380 runtime_files: &[(&str, &str)],
381) -> Result<()> {
382 let output_dir = Path::new(output_path);
383
384 clear_output_dir(output_path)?;
385
386 fs::create_dir_all(output_dir)
387 .with_context(|| format!("Failed to create directory: {}", output_dir.display()))?;
388
389 let models_dir = output_dir.join("models");
390 fs::create_dir_all(&models_dir)?;
391
392 let internal_dir = output_dir.join("_internal");
393 fs::create_dir_all(&internal_dir)?;
394
395 for (file_name, code) in js_models {
396 let file_path = models_dir.join(file_name);
397 fs::write(&file_path, code)
398 .with_context(|| format!("Failed to write file: {}", file_path.display()))?;
399 }
400
401 for (file_name, code) in dts_models {
402 let file_path = models_dir.join(file_name);
403 fs::write(&file_path, code)
404 .with_context(|| format!("Failed to write file: {}", file_path.display()))?;
405 }
406
407 if let Some(index_js) = js_models_index {
408 let path = models_dir.join("index.js");
409 fs::write(&path, index_js).with_context(|| "Failed to write models/index.js")?;
410 }
411 if let Some(index_dts) = dts_models_index {
412 let path = models_dir.join("index.d.ts");
413 fs::write(&path, index_dts).with_context(|| "Failed to write models/index.d.ts")?;
414 }
415
416 if let Some(enums_js) = js_enums {
417 let path = output_dir.join("enums.js");
418 fs::write(&path, enums_js)
419 .with_context(|| format!("Failed to write enums.js: {}", output_dir.display()))?;
420 }
421 if let Some(enums_dts) = dts_enums {
422 let path = output_dir.join("enums.d.ts");
423 fs::write(&path, enums_dts)
424 .with_context(|| format!("Failed to write enums.d.ts: {}", output_dir.display()))?;
425 }
426
427 if let Some(types_dts) = dts_composite_types {
429 let path = output_dir.join("types.d.ts");
430 fs::write(&path, types_dts)
431 .with_context(|| format!("Failed to write types.d.ts: {}", output_dir.display()))?;
432 }
433
434 for (file_name, content) in runtime_files {
435 let file_path = internal_dir.join(file_name);
436 fs::write(&file_path, content)
437 .with_context(|| format!("Failed to write runtime file: {}", file_path.display()))?;
438 }
439
440 if let Some(client_js) = js_client {
441 let path = output_dir.join("index.js");
442 fs::write(&path, client_js)
443 .with_context(|| format!("Failed to write index.js: {}", output_dir.display()))?;
444 }
445 if let Some(client_dts) = dts_client {
446 let path = output_dir.join("index.d.ts");
447 fs::write(&path, client_dts)
448 .with_context(|| format!("Failed to write index.d.ts: {}", output_dir.display()))?;
449 }
450
451 Ok(())
452}
453
454fn clear_output_dir(output_path: &str) -> Result<()> {
455 let output_dir = Path::new(output_path);
456 if output_dir.exists() {
457 fs::remove_dir_all(output_dir).with_context(|| {
458 format!("Failed to clean output directory: {}", output_dir.display())
459 })?;
460 }
461 Ok(())
462}