1use std::fmt::Write;
2
3use crate::config::FieldNaming;
4use crate::model::{
5 EnumDef, EnumVariant, FieldDef, Manifest, Procedure, ProcedureKind, RenameRule, RustType,
6 StructDef, VariantKind,
7};
8
9const GENERATED_HEADER: &str = "\
11// This file is auto-generated by vercel-rpc-cli. Do not edit manually.
12// Re-run `rpc generate` or use `rpc watch` to regenerate.
13";
14
15pub fn rust_type_to_ts(ty: &RustType) -> String {
28 match ty.name.as_str() {
29 "()" => "void".to_string(),
31
32 "String" | "str" | "char" | "&str" => "string".to_string(),
34
35 "bool" => "boolean".to_string(),
37
38 "i8" | "i16" | "i32" | "i64" | "i128" | "u8" | "u16" | "u32" | "u64" | "u128" | "f32"
40 | "f64" | "isize" | "usize" => "number".to_string(),
41
42 "Vec" | "Array" => {
44 let inner = ty
45 .generics
46 .first()
47 .map(rust_type_to_ts)
48 .unwrap_or_else(|| "unknown".to_string());
49 if inner.contains(" | ") {
51 format!("({inner})[]")
52 } else {
53 format!("{inner}[]")
54 }
55 }
56
57 "Option" => {
59 let inner = ty
60 .generics
61 .first()
62 .map(rust_type_to_ts)
63 .unwrap_or_else(|| "unknown".to_string());
64 format!("{inner} | null")
65 }
66
67 "HashMap" | "BTreeMap" => {
69 let key = ty
70 .generics
71 .first()
72 .map(rust_type_to_ts)
73 .unwrap_or_else(|| "string".to_string());
74 let value = ty
75 .generics
76 .get(1)
77 .map(rust_type_to_ts)
78 .unwrap_or_else(|| "unknown".to_string());
79 format!("Record<{key}, {value}>")
80 }
81
82 "tuple" => {
84 let elems: Vec<String> = ty.generics.iter().map(rust_type_to_ts).collect();
85 format!("[{}]", elems.join(", "))
86 }
87
88 other => other.to_string(),
90 }
91}
92
93pub fn emit_jsdoc(doc: &str, indent: &str, out: &mut String) {
95 if !doc.contains('\n') {
96 let _ = writeln!(out, "{indent}/** {doc} */");
97 } else {
98 let _ = writeln!(out, "{indent}/**");
99 for line in doc.lines() {
100 let _ = writeln!(out, "{indent} * {line}");
101 }
102 let _ = writeln!(out, "{indent} */");
103 }
104}
105
106pub fn to_camel_case(s: &str) -> String {
108 let mut segments = s.split('_');
109 let mut result = segments.next().unwrap().to_lowercase();
111 for segment in segments {
112 let mut chars = segment.chars();
113 if let Some(first) = chars.next() {
114 result.extend(first.to_uppercase());
115 result.push_str(&chars.as_str().to_lowercase());
116 }
117 }
118 result
119}
120
121fn transform_field_name(name: &str, naming: FieldNaming) -> String {
123 match naming {
124 FieldNaming::Preserve => name.to_string(),
125 FieldNaming::CamelCase => to_camel_case(name),
126 }
127}
128
129fn resolve_field_name(
133 field: &FieldDef,
134 container_rename_all: Option<RenameRule>,
135 config_naming: FieldNaming,
136) -> String {
137 if let Some(rename) = &field.rename {
138 return rename.clone();
139 }
140 if let Some(rule) = container_rename_all {
141 return rule.apply(&field.name);
142 }
143 transform_field_name(&field.name, config_naming)
144}
145
146fn resolve_variant_name(variant: &EnumVariant, container_rename_all: Option<RenameRule>) -> String {
150 if let Some(rename) = &variant.rename {
151 return rename.clone();
152 }
153 if let Some(rule) = container_rename_all {
154 return rule.apply(&variant.name);
155 }
156 variant.name.clone()
157}
158
159fn option_inner_type(ty: &RustType) -> Option<&RustType> {
161 if ty.name == "Option" {
162 ty.generics.first()
163 } else {
164 None
165 }
166}
167
168fn generate_interface(
170 s: &StructDef,
171 preserve_docs: bool,
172 field_naming: FieldNaming,
173 out: &mut String,
174) {
175 if preserve_docs && let Some(doc) = &s.docs {
176 emit_jsdoc(doc, "", out);
177 }
178 let _ = writeln!(out, "export interface {} {{", s.name);
179 for field in &s.fields {
180 if field.skip {
181 continue;
182 }
183 let field_name = resolve_field_name(field, s.rename_all, field_naming);
184 if field.has_default
185 && let Some(inner) = option_inner_type(&field.ty)
186 {
187 let ts_type = rust_type_to_ts(inner);
188 let _ = writeln!(out, " {field_name}?: {ts_type} | null;");
189 continue;
190 }
191 let ts_type = rust_type_to_ts(&field.ty);
192 let _ = writeln!(out, " {field_name}: {ts_type};");
193 }
194 let _ = writeln!(out, "}}");
195}
196
197fn generate_enum_type(
207 e: &EnumDef,
208 preserve_docs: bool,
209 field_naming: FieldNaming,
210 out: &mut String,
211) {
212 if preserve_docs && let Some(doc) = &e.docs {
213 emit_jsdoc(doc, "", out);
214 }
215 let all_unit = e
216 .variants
217 .iter()
218 .all(|v| matches!(v.kind, VariantKind::Unit));
219
220 if all_unit {
221 let variants: Vec<String> = e
223 .variants
224 .iter()
225 .map(|v| {
226 let name = resolve_variant_name(v, e.rename_all);
227 format!("\"{name}\"")
228 })
229 .collect();
230 if variants.is_empty() {
231 let _ = writeln!(out, "export type {} = never;", e.name);
232 } else {
233 let _ = writeln!(out, "export type {} = {};", e.name, variants.join(" | "));
234 }
235 } else {
236 let mut variant_types: Vec<String> = Vec::new();
238
239 for v in &e.variants {
240 let variant_name = resolve_variant_name(v, e.rename_all);
241 match &v.kind {
242 VariantKind::Unit => {
243 variant_types.push(format!("\"{variant_name}\""));
244 }
245 VariantKind::Tuple(types) => {
246 let inner = if types.len() == 1 {
247 rust_type_to_ts(&types[0])
248 } else {
249 let elems: Vec<String> = types.iter().map(rust_type_to_ts).collect();
250 format!("[{}]", elems.join(", "))
251 };
252 variant_types.push(format!("{{ {variant_name}: {inner} }}"));
253 }
254 VariantKind::Struct(fields) => {
255 let field_strs: Vec<String> = fields
258 .iter()
259 .filter(|f| !f.skip)
260 .map(|field| {
261 let field_name = resolve_field_name(field, e.rename_all, field_naming);
262 format!("{}: {}", field_name, rust_type_to_ts(&field.ty))
263 })
264 .collect();
265 variant_types.push(format!(
266 "{{ {variant_name}: {{ {} }} }}",
267 field_strs.join("; ")
268 ));
269 }
270 }
271 }
272
273 let _ = writeln!(
274 out,
275 "export type {} = {};",
276 e.name,
277 variant_types.join(" | ")
278 );
279 }
280}
281
282fn generate_procedures_type(procedures: &[Procedure], preserve_docs: bool, out: &mut String) {
285 let (queries, mutations): (Vec<_>, Vec<_>) = procedures
286 .iter()
287 .partition(|p| p.kind == ProcedureKind::Query);
288
289 let _ = writeln!(out, "export type Procedures = {{");
290
291 let _ = writeln!(out, " queries: {{");
293 for proc in &queries {
294 if preserve_docs && let Some(doc) = &proc.docs {
295 emit_jsdoc(doc, " ", out);
296 }
297 let input = proc
298 .input
299 .as_ref()
300 .map(rust_type_to_ts)
301 .unwrap_or_else(|| "void".to_string());
302 let output = proc
303 .output
304 .as_ref()
305 .map(rust_type_to_ts)
306 .unwrap_or_else(|| "void".to_string());
307 let _ = writeln!(
308 out,
309 " {}: {{ input: {input}; output: {output} }};",
310 proc.name
311 );
312 }
313 let _ = writeln!(out, " }};");
314
315 let _ = writeln!(out, " mutations: {{");
317 for proc in &mutations {
318 if preserve_docs && let Some(doc) = &proc.docs {
319 emit_jsdoc(doc, " ", out);
320 }
321 let input = proc
322 .input
323 .as_ref()
324 .map(rust_type_to_ts)
325 .unwrap_or_else(|| "void".to_string());
326 let output = proc
327 .output
328 .as_ref()
329 .map(rust_type_to_ts)
330 .unwrap_or_else(|| "void".to_string());
331 let _ = writeln!(
332 out,
333 " {}: {{ input: {input}; output: {output} }};",
334 proc.name
335 );
336 }
337 let _ = writeln!(out, " }};");
338
339 let _ = writeln!(out, "}};");
340}
341
342pub fn generate_types_file(
349 manifest: &Manifest,
350 preserve_docs: bool,
351 field_naming: FieldNaming,
352) -> String {
353 let mut out = String::with_capacity(1024);
354
355 out.push_str(GENERATED_HEADER);
357 out.push('\n');
358
359 for s in &manifest.structs {
361 generate_interface(s, preserve_docs, field_naming, &mut out);
362 out.push('\n');
363 }
364
365 for e in &manifest.enums {
367 generate_enum_type(e, preserve_docs, field_naming, &mut out);
368 out.push('\n');
369 }
370
371 generate_procedures_type(&manifest.procedures, preserve_docs, &mut out);
373
374 out
375}