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
10pub 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 println!("\n--- JSON manifest ---");
70 println!("{}", serde_json::to_string_pretty(&manifest)?);
71
72 Ok(())
73}
74
75pub 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
104pub fn generate_all(config: &RpcConfig) -> Result<Manifest> {
108 let mut manifest = parser::scan_directory(&config.input)?;
109
110 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 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
142fn 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
153type FrameworkEntry<'a> = (
155 Option<&'a PathBuf>,
156 fn(&Manifest, &str, &str, bool) -> String,
157);
158
159fn 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
197pub 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
207fn format_generic_params(generics: &[String]) -> String {
209 if generics.is_empty() {
210 String::new()
211 } else {
212 format!("<{}>", generics.join(", "))
213 }
214}
215
216pub 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}