Skip to main content

metaxy_cli/
commands.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use anyhow::{Context, Result};
5
6use crate::config::RpcConfig;
7use crate::model::Manifest;
8use crate::{codegen, parser};
9
10/// Scans the configured directory and prints discovered RPC procedures, structs,
11/// and enums to stdout, followed by a JSON manifest.
12pub fn cmd_scan(config: &RpcConfig) -> Result<()> {
13    let manifest = parser::scan_directory(&config.input)?;
14
15    println!(
16        "Discovered {} procedure(s), {} struct(s), {} enum(s):\n",
17        manifest.procedures.len(),
18        manifest.structs.len(),
19        manifest.enums.len(),
20    );
21
22    for proc in &manifest.procedures {
23        let input_str = proc
24            .input
25            .as_ref()
26            .map(|t| t.to_string())
27            .unwrap_or_else(|| "()".to_string());
28        let output_str = proc
29            .output
30            .as_ref()
31            .map(|t| t.to_string())
32            .unwrap_or_else(|| "()".to_string());
33
34        println!(
35            "  {:?} {} ({}) -> {}  [{}]",
36            proc.kind,
37            proc.name,
38            input_str,
39            output_str,
40            proc.source_file.display(),
41        );
42    }
43
44    for s in &manifest.structs {
45        let generics = format_generic_params(&s.generics);
46        if !s.tuple_fields.is_empty() {
47            let types: Vec<String> = s.tuple_fields.iter().map(|t| t.to_string()).collect();
48            println!("\n  struct {}{generics}({})", s.name, types.join(", "));
49        } else {
50            println!("\n  struct {}{generics} {{", s.name);
51            for field in &s.fields {
52                println!("    {}: {},", field.name, field.ty);
53            }
54            println!("  }}");
55        }
56    }
57
58    for e in &manifest.enums {
59        let generics = format_generic_params(&e.generics);
60        let variants: Vec<&str> = e.variants.iter().map(|v| v.name.as_str()).collect();
61        println!(
62            "\n  enum {}{generics} {{ {} }}",
63            e.name,
64            variants.join(", ")
65        );
66    }
67
68    // Also output raw JSON for tooling consumption
69    println!("\n--- JSON manifest ---");
70    println!("{}", serde_json::to_string_pretty(&manifest)?);
71
72    Ok(())
73}
74
75/// Generates TypeScript type definitions and a typed RPC client from the
76/// configured directory, writing the results to the configured output paths.
77pub fn cmd_generate(config: &RpcConfig) -> Result<()> {
78    let manifest = generate_all(config)?;
79
80    println!(
81        "Generated {} procedure(s), {} struct(s), {} enum(s)",
82        manifest.procedures.len(),
83        manifest.structs.len(),
84        manifest.enums.len(),
85    );
86    println!("  → {}", config.output.types.display());
87    println!("  → {}", config.output.client.display());
88    if let Some(path) = &config.output.svelte {
89        println!("  → {}", path.display());
90    }
91    if let Some(path) = &config.output.react {
92        println!("  → {}", path.display());
93    }
94    if let Some(path) = &config.output.vue {
95        println!("  → {}", path.display());
96    }
97    if let Some(path) = &config.output.solid {
98        println!("  → {}", path.display());
99    }
100
101    Ok(())
102}
103
104/// Scans the source directory and generates all configured TypeScript output files.
105///
106/// Returns the manifest so callers can use it for logging/reporting.
107pub fn generate_all(config: &RpcConfig) -> Result<Manifest> {
108    let mut manifest = parser::scan_directory(&config.input)?;
109
110    // Merge bigint_types into effective overrides (explicit type_overrides take priority)
111    let mut effective_overrides = config.codegen.type_overrides.clone();
112    for ty in &config.codegen.bigint_types {
113        effective_overrides
114            .entry(ty.clone())
115            .or_insert_with(|| "bigint".to_string());
116    }
117
118    // Apply type overrides before codegen
119    let base_index = codegen::overrides::build_base_index(&effective_overrides);
120    codegen::overrides::apply_type_overrides(&mut manifest, &effective_overrides, &base_index);
121
122    let types_content = codegen::typescript::generate_types_file(
123        &manifest,
124        config.codegen.preserve_docs,
125        config.codegen.naming.fields,
126        config.codegen.branded_newtypes,
127    );
128    write_file(&config.output.types, &types_content)?;
129
130    let client_content = codegen::client::generate_client_file(
131        &manifest,
132        &config.output.imports.types_specifier(),
133        config.codegen.preserve_docs,
134    );
135    write_file(&config.output.client, &client_content)?;
136
137    write_framework_files(config, &manifest)?;
138
139    Ok(manifest)
140}
141
142/// Computes the client import specifier from config (e.g. `"./rpc-client"`).
143fn client_import_path(config: &RpcConfig) -> String {
144    let client_stem = config
145        .output
146        .client
147        .file_stem()
148        .unwrap_or_default()
149        .to_string_lossy();
150    format!("./{client_stem}{}", config.output.imports.extension)
151}
152
153/// A framework codegen entry: optional output path paired with its generator function.
154type FrameworkEntry<'a> = (
155    Option<&'a PathBuf>,
156    fn(&Manifest, &str, &str, bool) -> String,
157);
158
159/// Generates and writes all optional framework wrapper files (Svelte, React, Vue, Solid).
160fn write_framework_files(config: &RpcConfig, manifest: &Manifest) -> Result<()> {
161    let client_import = client_import_path(config);
162    let types_specifier = config.output.imports.types_specifier();
163
164    let frameworks: [FrameworkEntry<'_>; 4] = [
165        (
166            config.output.svelte.as_ref(),
167            codegen::svelte::generate_svelte_file,
168        ),
169        (
170            config.output.react.as_ref(),
171            codegen::react::generate_react_file,
172        ),
173        (config.output.vue.as_ref(), codegen::vue::generate_vue_file),
174        (
175            config.output.solid.as_ref(),
176            codegen::solid::generate_solid_file,
177        ),
178    ];
179
180    for (path_opt, generator) in &frameworks {
181        if let Some(path) = path_opt {
182            let content = generator(
183                manifest,
184                &client_import,
185                &types_specifier,
186                config.codegen.preserve_docs,
187            );
188            if !content.is_empty() {
189                write_file(path, &content)?;
190            }
191        }
192    }
193
194    Ok(())
195}
196
197/// Writes content to a file, creating parent directories as needed.
198pub fn write_file(path: &Path, content: &str) -> Result<()> {
199    if let Some(parent) = path.parent() {
200        fs::create_dir_all(parent)
201            .with_context(|| format!("Failed to create directory {}", parent.display()))?;
202    }
203    fs::write(path, content).with_context(|| format!("Failed to write {}", path.display()))?;
204    Ok(())
205}
206
207/// Formats generic type parameters for display (e.g. `<T>`, `<A, B>`).
208fn format_generic_params(generics: &[String]) -> String {
209    if generics.is_empty() {
210        String::new()
211    } else {
212        format!("<{}>", generics.join(", "))
213    }
214}
215
216/// Formats byte count in a human-readable way.
217pub fn bytecount(s: &str) -> String {
218    let bytes = s.len();
219    if bytes < 1024 {
220        format!("{bytes} bytes")
221    } else {
222        format!("{:.1} KB", bytes as f64 / 1024.0)
223    }
224}