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    standalone: bool,
31) -> Result<()> {
32    let output_dir = Path::new(output_path);
33
34    clear_output_dir(output_path)?;
35
36    // Create output directory if it doesn't exist
37    fs::create_dir_all(output_dir)
38        .with_context(|| format!("Failed to create directory: {}", output_dir.display()))?;
39
40    // Create src/ directory for proper Rust package structure
41    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    // Write model files to src/
46    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    // Write enums file if there are any enums
55    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    // Write types file if there are any composite types
63    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    // Write lib.rs with module declarations and re-exports
71    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    // Write Cargo.toml only in standalone mode
77    if standalone {
78        // Walk up from the output directory to find the workspace root, then
79        // compute how many `..` hops separate the output from it so generated
80        // path-dependency references are correct.
81        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                    // Fallback: legacy upward walk (shouldn't normally be reached)
109                    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
139/// Generate Cargo.toml for the generated Rust package.
140///
141/// `workspace_root_path` is the relative path from the output directory back
142/// to the Cargo workspace root, e.g. `"../../../.."` when the output sits
143/// four directory levels below the workspace root.
144fn 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
149/// Generate the lib.rs file content with module declarations and re-exports.
150fn 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    // Re-export nautilus-connector types that models need
163    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    // Declare composite types module first (models depend on it)
171    if has_composite_types {
172        output.push_str("pub mod types;\n");
173    }
174
175    // Declare enum module if present
176    if has_enums {
177        output.push_str("pub mod enums;\n");
178    }
179
180    // Declare model modules
181    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    // Re-export composite types
192    if has_composite_types {
193        output.push_str("pub use types::*;\n");
194    }
195
196    // Re-export enums
197    if has_enums {
198        output.push_str("pub use enums::*;\n");
199    }
200
201    // Re-export all model types
202    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
210/// Write generated Python code to files in the output directory with organized structure.
211///
212/// Creates a structure:
213/// - `{output}/__init__.py` - Package init with exports
214/// - `{output}/client.py` - Nautilus client with model delegates
215/// - `{output}/models/__init__.py` - Models package
216/// - `{output}/models/{model_snake}.py` - Model code for each model
217/// - `{output}/enums/__init__.py` - Enums package
218/// - `{output}/enums/enums.py` - All enum types (if any)
219/// - `{output}/errors/__init__.py` - Errors package
220/// - `{output}/errors/errors.py` - Error classes
221/// - `{output}/_internal/` - Internal runtime files
222/// - `{output}/py.typed` - Marker for mypy
223pub 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    // Create output directory structure
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    // Write model files to models/
268    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    // Write models/__init__.py
276    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    // Write composite types to types/
282    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        // Write types/__init__.py re-exporting everything
293        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    // Write enums file if there are any enums
300    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    // Write enums/__init__.py
308    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    // Write runtime files to _internal/ and errors/
313    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    // Write errors/__init__.py
325    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    // Write _internal/__init__.py
331    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    // Write client.py with model delegates if provided
337    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    // Write transaction.py at the package root
344    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    // Write main __init__.py with imports and exports
354    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    // Write py.typed marker for mypy
360    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/// Write generated JavaScript + TypeScript declaration code to the output directory.
368///
369/// Creates:
370/// - `{output}/index.js`              — generated `Nautilus` class (runtime)
371/// - `{output}/index.d.ts`            — generated `Nautilus` class (declarations)
372/// - `{output}/models/index.js`       — barrel re-export for all models (runtime)
373/// - `{output}/models/index.d.ts`     — barrel re-export for all models (declarations)
374/// - `{output}/models/{snake}.js`     — per-model delegate + helpers (runtime)
375/// - `{output}/models/{snake}.d.ts`   — per-model interfaces + types (declarations)
376/// - `{output}/enums.js`              — JavaScript enums (if any)
377/// - `{output}/enums.d.ts`            — TypeScript enum declarations (if any)
378/// - `{output}/types.d.ts`            — composite type interfaces (if any, declarations only)
379/// - `{output}/_internal/_*.js`       — runtime files (client, engine, protocol, etc.)
380/// - `{output}/_internal/_*.d.ts`     — runtime declaration files
381#[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    // Write per-model .js files.
409    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    // Write per-model .d.ts files.
416    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    // Write models/index.js and models/index.d.ts (barrel re-exports).
423    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    // Write enums.js and enums.d.ts.
433    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    // Write types.d.ts (composite types — declarations only, no runtime needed).
445    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    // Write runtime files to _internal/ (includes both .js and .d.ts entries).
452    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    // Write main index.js and index.d.ts (generated Nautilus class).
459    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}