Skip to main content

nautilus_codegen/
writer.rs

1//! File writing utilities for generated code.
2
3use 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
14/// Write generated code to files in the output directory.
15///
16/// Creates:
17/// - `{output}/src/lib.rs`           — module declarations and re-exports
18/// - `{output}/src/{model_snake}.rs` — model code for each model
19/// - `{output}/src/enums.rs`         — all enum types (if any)
20/// - `{output}/Cargo.toml`           — **only** when `standalone == true`
21///
22/// When `standalone` is `false` (the default) the output is a plain directory
23/// of `.rs` source files ready to be included in an existing Cargo workspace
24/// without any generated `Cargo.toml`.
25pub 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        // Walk up from the output directory to find the workspace root, then
80        // compute how many `..` hops separate the output from it so generated
81        // path-dependency references are correct.
82        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                    // Fallback: legacy upward walk (shouldn't normally be reached).
110                    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
140/// Generate Cargo.toml for the generated Rust package.
141///
142/// `workspace_root_path` is the relative path from the output directory back
143/// to the Cargo workspace root, e.g. `"../../../.."` when the output sits
144/// four directory levels below the workspace root.
145fn 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
150/// Generate the lib.rs file content with module declarations and re-exports.
151fn 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
211/// Write generated Python code to files in the output directory with organized structure.
212///
213/// Creates a structure:
214/// - `{output}/__init__.py` - Package init with exports
215/// - `{output}/client.py` - Nautilus client with model delegates
216/// - `{output}/models/__init__.py` - Models package
217/// - `{output}/models/{model_snake}.py` - Model code for each model
218/// - `{output}/enums/__init__.py` - Enums package
219/// - `{output}/enums/enums.py` - All enum types (if any)
220/// - `{output}/errors/__init__.py` - Errors package
221/// - `{output}/errors/errors.py` - Error classes
222/// - `{output}/_internal/` - Internal runtime files
223/// - `{output}/py.typed` - Marker for mypy
224pub 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/// Write generated JavaScript + TypeScript declaration code to the output directory.
355///
356/// Creates:
357/// - `{output}/index.js`              — generated `Nautilus` class (runtime)
358/// - `{output}/index.d.ts`            — generated `Nautilus` class (declarations)
359/// - `{output}/models/index.js`       — barrel re-export for all models (runtime)
360/// - `{output}/models/index.d.ts`     — barrel re-export for all models (declarations)
361/// - `{output}/models/{snake}.js`     — per-model delegate + helpers (runtime)
362/// - `{output}/models/{snake}.d.ts`   — per-model interfaces + types (declarations)
363/// - `{output}/enums.js`              — JavaScript enums (if any)
364/// - `{output}/enums.d.ts`            — TypeScript enum declarations (if any)
365/// - `{output}/types.d.ts`            — composite type interfaces (if any, declarations only)
366/// - `{output}/_internal/_*.js`       — runtime files (client, engine, protocol, etc.)
367/// - `{output}/_internal/_*.d.ts`     — runtime declaration files
368#[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    // Write types.d.ts (composite types — declarations only, no runtime needed).
428    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}