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;
8use tera::Context as TeraContext;
9
10use crate::generator::TEMPLATES;
11use crate::python::generator::{
12    generate_enums_init, generate_errors_init, generate_internal_init, generate_models_init,
13    generate_package_init, generate_transaction_init,
14};
15
16/// Write generated code to files in the output directory.
17///
18/// Creates:
19/// - `{output}/src/lib.rs`           — module declarations and re-exports
20/// - `{output}/src/{model_snake}.rs` — model code for each model
21/// - `{output}/src/enums.rs`         — all enum types (if any)
22/// - `{output}/Cargo.toml`           — **only** when `standalone == true`
23///
24/// When `standalone` is `false` (the default) the output is a plain directory
25/// of `.rs` source files ready to be included in an existing Cargo workspace
26/// without any generated `Cargo.toml`.
27pub fn write_rust_code(
28    output_path: &str,
29    models: &HashMap<String, String>,
30    enums_code: Option<String>,
31    composite_types_code: Option<String>,
32    schema_source: &str,
33    standalone: bool,
34) -> Result<()> {
35    let output_dir = Path::new(output_path);
36
37    clear_output_dir(output_path)?;
38
39    fs::create_dir_all(output_dir)
40        .with_context(|| format!("Failed to create directory: {}", output_dir.display()))?;
41
42    let src_dir = output_dir.join("src");
43    fs::create_dir_all(&src_dir)
44        .with_context(|| format!("Failed to create src directory: {}", src_dir.display()))?;
45
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    let has_enums = enums_code.is_some();
55    if let Some(enums_code) = enums_code {
56        let enums_path = src_dir.join("enums.rs");
57        fs::write(&enums_path, enums_code)
58            .with_context(|| format!("Failed to write enums file: {}", enums_path.display()))?;
59    }
60
61    let has_composite_types = composite_types_code.is_some();
62    if let Some(types_code) = composite_types_code {
63        let types_path = src_dir.join("types.rs");
64        fs::write(&types_path, types_code)
65            .with_context(|| format!("Failed to write types file: {}", types_path.display()))?;
66    }
67
68    let lib_content = generate_lib_rs(models, has_enums, has_composite_types, schema_source)?;
69    let lib_path = src_dir.join("lib.rs");
70    fs::write(&lib_path, lib_content)
71        .with_context(|| format!("Failed to write lib.rs: {}", lib_path.display()))?;
72
73    let runtime_path = src_dir.join("runtime.rs");
74    fs::write(
75        &runtime_path,
76        include_str!("../templates/rust/runtime.rs.tpl"),
77    )
78    .with_context(|| format!("Failed to write runtime.rs: {}", runtime_path.display()))?;
79
80    if standalone {
81        // Walk up from the output directory to find the workspace root, then
82        // compute how many `..` hops separate the output from it so generated
83        // path-dependency references are correct.
84        let abs_output = if output_dir.is_absolute() {
85            output_dir.to_path_buf()
86        } else {
87            std::env::current_dir()
88                .context("Failed to get current directory")?
89                .join(output_dir)
90        };
91        let workspace_root_path = {
92            let workspace_toml = crate::find_workspace_cargo_toml(&abs_output);
93            match workspace_toml {
94                Some(toml_path) => {
95                    let workspace_dir = toml_path.parent().unwrap();
96                    let mut up = std::path::PathBuf::new();
97                    let mut candidate = abs_output.clone();
98                    loop {
99                        if candidate == workspace_dir {
100                            break;
101                        }
102                        up.push("..");
103                        match candidate.parent() {
104                            Some(p) => candidate = p.to_path_buf(),
105                            None => break,
106                        }
107                    }
108                    up.to_string_lossy().replace('\\', "/")
109                }
110                None => {
111                    // Fallback: legacy upward walk (shouldn't normally be reached).
112                    let mut up = std::path::PathBuf::new();
113                    let mut candidate = abs_output.clone();
114                    loop {
115                        candidate = match candidate.parent() {
116                            Some(p) => p.to_path_buf(),
117                            None => break,
118                        };
119                        up.push("..");
120                        let cargo_toml = candidate.join("Cargo.toml");
121                        let Ok(txt) = fs::read_to_string(&cargo_toml) else {
122                            continue;
123                        };
124                        if txt.contains("[workspace]") {
125                            break;
126                        }
127                    }
128                    up.to_string_lossy().replace('\\', "/")
129                }
130            }
131        };
132        let cargo_toml_content = generate_rust_cargo_toml(&workspace_root_path);
133        let cargo_toml_path = output_dir.join("Cargo.toml");
134        fs::write(&cargo_toml_path, cargo_toml_content).with_context(|| {
135            format!("Failed to write Cargo.toml: {}", cargo_toml_path.display())
136        })?;
137    }
138
139    Ok(())
140}
141
142/// Generate Cargo.toml for the generated Rust package.
143///
144/// `workspace_root_path` is the relative path from the output directory back
145/// to the Cargo workspace root, e.g. `"../../../.."` when the output sits
146/// four directory levels below the workspace root.
147fn generate_rust_cargo_toml(workspace_root_path: &str) -> String {
148    include_str!("../templates/rust/Cargo.toml.tpl")
149        .replace("{{ workspace_root_path }}", workspace_root_path)
150}
151
152/// Generate the lib.rs file content with module declarations and re-exports.
153fn generate_lib_rs(
154    models: &HashMap<String, String>,
155    has_enums: bool,
156    has_composite_types: bool,
157    schema_source: &str,
158) -> Result<String> {
159    let mut model_names: Vec<_> = models.keys().cloned().collect();
160    model_names.sort();
161
162    let model_modules: Vec<String> = model_names
163        .iter()
164        .map(|model_name| model_name.to_snake_case())
165        .collect();
166
167    let mut context = TeraContext::new();
168    context.insert("has_enums", &has_enums);
169    context.insert("has_composite_types", &has_composite_types);
170    context.insert("model_modules", &model_modules);
171    context.insert("schema_source_literal", &format!("{:?}", schema_source));
172
173    TEMPLATES
174        .render("lib_rs.tera", &context)
175        .context("Failed to render lib.rs template")
176}
177
178/// Write generated Python code to files in the output directory with organized structure.
179///
180/// Creates a structure:
181/// - `{output}/__init__.py` - Package init with exports
182/// - `{output}/client.py` - Nautilus client with model delegates
183/// - `{output}/models/__init__.py` - Models package
184/// - `{output}/models/{model_snake}.py` - Model code for each model
185/// - `{output}/enums/__init__.py` - Enums package
186/// - `{output}/enums/enums.py` - All enum types (if any)
187/// - `{output}/errors/__init__.py` - Errors package
188/// - `{output}/errors/errors.py` - Error classes
189/// - `{output}/_internal/` - Internal runtime files
190/// - `{output}/py.typed` - Marker for mypy
191pub fn write_python_code(
192    output_path: &str,
193    models: &[(String, String)],
194    enums_code: Option<String>,
195    composite_types_code: Option<String>,
196    client_code: Option<String>,
197    runtime_files: &[(&str, &str)],
198) -> Result<()> {
199    let output_dir = Path::new(output_path);
200
201    clear_output_dir(output_path)?;
202
203    fs::create_dir_all(output_dir)
204        .with_context(|| format!("Failed to create directory: {}", output_dir.display()))?;
205
206    let models_dir = output_dir.join("models");
207    fs::create_dir_all(&models_dir).with_context(|| {
208        format!(
209            "Failed to create models directory: {}",
210            models_dir.display()
211        )
212    })?;
213
214    let enums_dir = output_dir.join("enums");
215    fs::create_dir_all(&enums_dir)
216        .with_context(|| format!("Failed to create enums directory: {}", enums_dir.display()))?;
217
218    let errors_dir = output_dir.join("errors");
219    fs::create_dir_all(&errors_dir).with_context(|| {
220        format!(
221            "Failed to create errors directory: {}",
222            errors_dir.display()
223        )
224    })?;
225
226    let internal_dir = output_dir.join("_internal");
227    fs::create_dir_all(&internal_dir).with_context(|| {
228        format!(
229            "Failed to create _internal directory: {}",
230            internal_dir.display()
231        )
232    })?;
233
234    for (file_name, code) in models {
235        let file_path = models_dir.join(file_name);
236
237        fs::write(&file_path, code)
238            .with_context(|| format!("Failed to write file: {}", file_path.display()))?;
239    }
240
241    let models_init = generate_models_init(models);
242    let models_init_path = models_dir.join("__init__.py");
243    fs::write(&models_init_path, models_init)
244        .with_context(|| "Failed to write models/__init__.py")?;
245
246    if let Some(types_code) = composite_types_code {
247        let types_dir = output_dir.join("types");
248        fs::create_dir_all(&types_dir).with_context(|| {
249            format!("Failed to create types directory: {}", types_dir.display())
250        })?;
251
252        let types_path = types_dir.join("types.py");
253        fs::write(&types_path, types_code)
254            .with_context(|| format!("Failed to write types file: {}", types_path.display()))?;
255
256        let types_init = "from .types import *  # noqa: F401, F403\n";
257        let types_init_path = types_dir.join("__init__.py");
258        fs::write(&types_init_path, types_init)
259            .with_context(|| "Failed to write types/__init__.py")?;
260    }
261
262    let has_enums = enums_code.is_some();
263    if let Some(enums_code) = enums_code {
264        let enums_path = enums_dir.join("enums.py");
265        fs::write(&enums_path, enums_code)
266            .with_context(|| format!("Failed to write enums file: {}", enums_path.display()))?;
267    }
268
269    let enums_init = generate_enums_init(has_enums);
270    let enums_init_path = enums_dir.join("__init__.py");
271    fs::write(&enums_init_path, enums_init).with_context(|| "Failed to write enums/__init__.py")?;
272
273    for (file_name, content) in runtime_files {
274        let (target_dir, new_name) = match *file_name {
275            "_errors.py" => (&errors_dir, "errors.py"),
276            _ => (&internal_dir, file_name.trim_start_matches('_')),
277        };
278
279        let file_path = target_dir.join(new_name);
280        fs::write(&file_path, content)
281            .with_context(|| format!("Failed to write runtime file: {}", file_path.display()))?;
282    }
283
284    let errors_init = generate_errors_init();
285    let errors_init_path = errors_dir.join("__init__.py");
286    fs::write(&errors_init_path, errors_init)
287        .with_context(|| "Failed to write errors/__init__.py")?;
288
289    let internal_init = generate_internal_init();
290    let internal_init_path = internal_dir.join("__init__.py");
291    fs::write(&internal_init_path, internal_init)
292        .with_context(|| "Failed to write _internal/__init__.py")?;
293
294    if let Some(client_code) = client_code {
295        let client_path = output_dir.join("client.py");
296        fs::write(&client_path, client_code)
297            .with_context(|| format!("Failed to write client.py: {}", client_path.display()))?;
298    }
299
300    let transaction_content = generate_transaction_init();
301    let transaction_path = output_dir.join("transaction.py");
302    fs::write(&transaction_path, transaction_content).with_context(|| {
303        format!(
304            "Failed to write transaction.py: {}",
305            transaction_path.display()
306        )
307    })?;
308
309    let init_content = generate_package_init(has_enums);
310    let init_path = output_dir.join("__init__.py");
311    fs::write(&init_path, init_content)
312        .with_context(|| format!("Failed to write __init__.py: {}", init_path.display()))?;
313
314    let py_typed_path = output_dir.join("py.typed");
315    fs::write(&py_typed_path, "")
316        .with_context(|| format!("Failed to write py.typed: {}", py_typed_path.display()))?;
317
318    Ok(())
319}
320
321/// Write generated JavaScript + TypeScript declaration code to the output directory.
322///
323/// Creates:
324/// - `{output}/index.js`              — generated `Nautilus` class (runtime)
325/// - `{output}/index.d.ts`            — generated `Nautilus` class (declarations)
326/// - `{output}/models/index.js`       — barrel re-export for all models (runtime)
327/// - `{output}/models/index.d.ts`     — barrel re-export for all models (declarations)
328/// - `{output}/models/{snake}.js`     — per-model delegate + helpers (runtime)
329/// - `{output}/models/{snake}.d.ts`   — per-model interfaces + types (declarations)
330/// - `{output}/enums.js`              — JavaScript enums (if any)
331/// - `{output}/enums.d.ts`            — TypeScript enum declarations (if any)
332/// - `{output}/types.d.ts`            — composite type interfaces (if any, declarations only)
333/// - `{output}/_internal/_*.js`       — runtime files (client, engine, protocol, etc.)
334/// - `{output}/_internal/_*.d.ts`     — runtime declaration files
335#[allow(clippy::too_many_arguments)]
336pub fn write_js_code(
337    output_path: &str,
338    js_models: &[(String, String)],
339    dts_models: &[(String, String)],
340    js_enums: Option<String>,
341    dts_enums: Option<String>,
342    dts_composite_types: Option<String>,
343    js_client: Option<String>,
344    dts_client: Option<String>,
345    js_models_index: Option<String>,
346    dts_models_index: Option<String>,
347    runtime_files: &[(&str, &str)],
348) -> Result<()> {
349    let output_dir = Path::new(output_path);
350
351    clear_output_dir(output_path)?;
352
353    fs::create_dir_all(output_dir)
354        .with_context(|| format!("Failed to create directory: {}", output_dir.display()))?;
355
356    let models_dir = output_dir.join("models");
357    fs::create_dir_all(&models_dir)?;
358
359    let internal_dir = output_dir.join("_internal");
360    fs::create_dir_all(&internal_dir)?;
361
362    for (file_name, code) in js_models {
363        let file_path = models_dir.join(file_name);
364        fs::write(&file_path, code)
365            .with_context(|| format!("Failed to write file: {}", file_path.display()))?;
366    }
367
368    for (file_name, code) in dts_models {
369        let file_path = models_dir.join(file_name);
370        fs::write(&file_path, code)
371            .with_context(|| format!("Failed to write file: {}", file_path.display()))?;
372    }
373
374    if let Some(index_js) = js_models_index {
375        let path = models_dir.join("index.js");
376        fs::write(&path, index_js).with_context(|| "Failed to write models/index.js")?;
377    }
378    if let Some(index_dts) = dts_models_index {
379        let path = models_dir.join("index.d.ts");
380        fs::write(&path, index_dts).with_context(|| "Failed to write models/index.d.ts")?;
381    }
382
383    if let Some(enums_js) = js_enums {
384        let path = output_dir.join("enums.js");
385        fs::write(&path, enums_js)
386            .with_context(|| format!("Failed to write enums.js: {}", output_dir.display()))?;
387    }
388    if let Some(enums_dts) = dts_enums {
389        let path = output_dir.join("enums.d.ts");
390        fs::write(&path, enums_dts)
391            .with_context(|| format!("Failed to write enums.d.ts: {}", output_dir.display()))?;
392    }
393
394    // Write types.d.ts (composite types — declarations only, no runtime needed).
395    if let Some(types_dts) = dts_composite_types {
396        let path = output_dir.join("types.d.ts");
397        fs::write(&path, types_dts)
398            .with_context(|| format!("Failed to write types.d.ts: {}", output_dir.display()))?;
399    }
400
401    for (file_name, content) in runtime_files {
402        let file_path = internal_dir.join(file_name);
403        fs::write(&file_path, content)
404            .with_context(|| format!("Failed to write runtime file: {}", file_path.display()))?;
405    }
406
407    if let Some(client_js) = js_client {
408        let path = output_dir.join("index.js");
409        fs::write(&path, client_js)
410            .with_context(|| format!("Failed to write index.js: {}", output_dir.display()))?;
411    }
412    if let Some(client_dts) = dts_client {
413        let path = output_dir.join("index.d.ts");
414        fs::write(&path, client_dts)
415            .with_context(|| format!("Failed to write index.d.ts: {}", output_dir.display()))?;
416    }
417
418    Ok(())
419}
420
421fn clear_output_dir(output_path: &str) -> Result<()> {
422    let output_dir = Path::new(output_path);
423    if output_dir.exists() {
424        fs::remove_dir_all(output_dir).with_context(|| {
425            format!("Failed to clean output directory: {}", output_dir.display())
426        })?;
427    }
428    Ok(())
429}