1use std::fmt::Write;
2
3use crate::config::FieldNaming;
4use crate::model::{EnumDef, Manifest, Procedure, ProcedureKind, RustType, StructDef, VariantKind};
5
6const GENERATED_HEADER: &str = "\
8// This file is auto-generated by vercel-rpc-cli. Do not edit manually.
9// Re-run `rpc generate` or use `rpc watch` to regenerate.
10";
11
12pub fn rust_type_to_ts(ty: &RustType) -> String {
25 match ty.name.as_str() {
26 "()" => "void".to_string(),
28
29 "String" | "str" | "char" | "&str" => "string".to_string(),
31
32 "bool" => "boolean".to_string(),
34
35 "i8" | "i16" | "i32" | "i64" | "i128" | "u8" | "u16" | "u32" | "u64" | "u128" | "f32"
37 | "f64" | "isize" | "usize" => "number".to_string(),
38
39 "Vec" | "Array" => {
41 let inner = ty
42 .generics
43 .first()
44 .map(rust_type_to_ts)
45 .unwrap_or_else(|| "unknown".to_string());
46 if inner.contains(" | ") {
48 format!("({inner})[]")
49 } else {
50 format!("{inner}[]")
51 }
52 }
53
54 "Option" => {
56 let inner = ty
57 .generics
58 .first()
59 .map(rust_type_to_ts)
60 .unwrap_or_else(|| "unknown".to_string());
61 format!("{inner} | null")
62 }
63
64 "HashMap" | "BTreeMap" => {
66 let key = ty
67 .generics
68 .first()
69 .map(rust_type_to_ts)
70 .unwrap_or_else(|| "string".to_string());
71 let value = ty
72 .generics
73 .get(1)
74 .map(rust_type_to_ts)
75 .unwrap_or_else(|| "unknown".to_string());
76 format!("Record<{key}, {value}>")
77 }
78
79 "tuple" => {
81 let elems: Vec<String> = ty.generics.iter().map(rust_type_to_ts).collect();
82 format!("[{}]", elems.join(", "))
83 }
84
85 other => other.to_string(),
87 }
88}
89
90pub fn emit_jsdoc(doc: &str, indent: &str, out: &mut String) {
92 if !doc.contains('\n') {
93 let _ = writeln!(out, "{indent}/** {doc} */");
94 } else {
95 let _ = writeln!(out, "{indent}/**");
96 for line in doc.lines() {
97 let _ = writeln!(out, "{indent} * {line}");
98 }
99 let _ = writeln!(out, "{indent} */");
100 }
101}
102
103pub fn to_camel_case(s: &str) -> String {
105 let mut segments = s.split('_');
106 let mut result = segments.next().unwrap().to_lowercase();
108 for segment in segments {
109 let mut chars = segment.chars();
110 if let Some(first) = chars.next() {
111 result.extend(first.to_uppercase());
112 result.push_str(&chars.as_str().to_lowercase());
113 }
114 }
115 result
116}
117
118fn transform_field_name(name: &str, naming: FieldNaming) -> String {
120 match naming {
121 FieldNaming::Preserve => name.to_string(),
122 FieldNaming::CamelCase => to_camel_case(name),
123 }
124}
125
126fn generate_interface(
128 s: &StructDef,
129 preserve_docs: bool,
130 field_naming: FieldNaming,
131 out: &mut String,
132) {
133 if preserve_docs && let Some(doc) = &s.docs {
134 emit_jsdoc(doc, "", out);
135 }
136 let _ = writeln!(out, "export interface {} {{", s.name);
137 for (name, ty) in &s.fields {
138 let ts_type = rust_type_to_ts(ty);
139 let field_name = transform_field_name(name, field_naming);
140 let _ = writeln!(out, " {field_name}: {ts_type};");
141 }
142 let _ = writeln!(out, "}}");
143}
144
145fn generate_enum_type(
155 e: &EnumDef,
156 preserve_docs: bool,
157 field_naming: FieldNaming,
158 out: &mut String,
159) {
160 if preserve_docs && let Some(doc) = &e.docs {
161 emit_jsdoc(doc, "", out);
162 }
163 let all_unit = e
164 .variants
165 .iter()
166 .all(|v| matches!(v.kind, VariantKind::Unit));
167
168 if all_unit {
169 let variants: Vec<String> = e
171 .variants
172 .iter()
173 .map(|v| format!("\"{}\"", v.name))
174 .collect();
175 if variants.is_empty() {
176 let _ = writeln!(out, "export type {} = never;", e.name);
177 } else {
178 let _ = writeln!(out, "export type {} = {};", e.name, variants.join(" | "));
179 }
180 } else {
181 let mut variant_types: Vec<String> = Vec::new();
183
184 for v in &e.variants {
185 match &v.kind {
186 VariantKind::Unit => {
187 variant_types.push(format!("\"{}\"", v.name));
188 }
189 VariantKind::Tuple(types) => {
190 let inner = if types.len() == 1 {
191 rust_type_to_ts(&types[0])
192 } else {
193 let elems: Vec<String> = types.iter().map(rust_type_to_ts).collect();
194 format!("[{}]", elems.join(", "))
195 };
196 variant_types.push(format!("{{ {}: {} }}", v.name, inner));
197 }
198 VariantKind::Struct(fields) => {
199 let field_strs: Vec<String> = fields
200 .iter()
201 .map(|(name, ty)| {
202 let field_name = transform_field_name(name, field_naming);
203 format!("{}: {}", field_name, rust_type_to_ts(ty))
204 })
205 .collect();
206 variant_types.push(format!(
207 "{{ {}: {{ {} }} }}",
208 v.name,
209 field_strs.join("; ")
210 ));
211 }
212 }
213 }
214
215 let _ = writeln!(
216 out,
217 "export type {} = {};",
218 e.name,
219 variant_types.join(" | ")
220 );
221 }
222}
223
224fn generate_procedures_type(procedures: &[Procedure], preserve_docs: bool, out: &mut String) {
227 let (queries, mutations): (Vec<_>, Vec<_>) = procedures
228 .iter()
229 .partition(|p| p.kind == ProcedureKind::Query);
230
231 let _ = writeln!(out, "export type Procedures = {{");
232
233 let _ = writeln!(out, " queries: {{");
235 for proc in &queries {
236 if preserve_docs && let Some(doc) = &proc.docs {
237 emit_jsdoc(doc, " ", out);
238 }
239 let input = proc
240 .input
241 .as_ref()
242 .map(rust_type_to_ts)
243 .unwrap_or_else(|| "void".to_string());
244 let output = proc
245 .output
246 .as_ref()
247 .map(rust_type_to_ts)
248 .unwrap_or_else(|| "void".to_string());
249 let _ = writeln!(
250 out,
251 " {}: {{ input: {input}; output: {output} }};",
252 proc.name
253 );
254 }
255 let _ = writeln!(out, " }};");
256
257 let _ = writeln!(out, " mutations: {{");
259 for proc in &mutations {
260 if preserve_docs && let Some(doc) = &proc.docs {
261 emit_jsdoc(doc, " ", out);
262 }
263 let input = proc
264 .input
265 .as_ref()
266 .map(rust_type_to_ts)
267 .unwrap_or_else(|| "void".to_string());
268 let output = proc
269 .output
270 .as_ref()
271 .map(rust_type_to_ts)
272 .unwrap_or_else(|| "void".to_string());
273 let _ = writeln!(
274 out,
275 " {}: {{ input: {input}; output: {output} }};",
276 proc.name
277 );
278 }
279 let _ = writeln!(out, " }};");
280
281 let _ = writeln!(out, "}};");
282}
283
284pub fn generate_types_file(
291 manifest: &Manifest,
292 preserve_docs: bool,
293 field_naming: FieldNaming,
294) -> String {
295 let mut out = String::with_capacity(1024);
296
297 out.push_str(GENERATED_HEADER);
299 out.push('\n');
300
301 for s in &manifest.structs {
303 generate_interface(s, preserve_docs, field_naming, &mut out);
304 out.push('\n');
305 }
306
307 for e in &manifest.enums {
309 generate_enum_type(e, preserve_docs, field_naming, &mut out);
310 out.push('\n');
311 }
312
313 generate_procedures_type(&manifest.procedures, preserve_docs, &mut out);
315
316 out
317}