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 {
29 match ty.name.as_str() {
30 "()" => "void".to_string(),
32
33 "String" | "str" | "char" | "&str" => "string".to_string(),
35
36 "bool" => "boolean".to_string(),
38
39 "i8" | "i16" | "i32" | "i64" | "i128" | "u8" | "u16" | "u32" | "u64" | "u128" | "f32"
41 | "f64" | "isize" | "usize" => "number".to_string(),
42
43 "Vec" | "Array" | "HashSet" | "BTreeSet" => {
45 let inner = ty
46 .generics
47 .first()
48 .map(rust_type_to_ts)
49 .unwrap_or_else(|| "unknown".to_string());
50 if inner.contains(" | ") {
52 format!("({inner})[]")
53 } else {
54 format!("{inner}[]")
55 }
56 }
57
58 "Option" => {
60 let inner = ty
61 .generics
62 .first()
63 .map(rust_type_to_ts)
64 .unwrap_or_else(|| "unknown".to_string());
65 format!("{inner} | null")
66 }
67
68 "HashMap" | "BTreeMap" => {
70 let key = ty
71 .generics
72 .first()
73 .map(rust_type_to_ts)
74 .unwrap_or_else(|| "string".to_string());
75 let value = ty
76 .generics
77 .get(1)
78 .map(rust_type_to_ts)
79 .unwrap_or_else(|| "unknown".to_string());
80 format!("Record<{key}, {value}>")
81 }
82
83 "Box" | "Arc" | "Rc" | "Cow" => ty
85 .generics
86 .first()
87 .map(rust_type_to_ts)
88 .unwrap_or_else(|| "unknown".to_string()),
89
90 "tuple" => {
92 let elems: Vec<String> = ty.generics.iter().map(rust_type_to_ts).collect();
93 format!("[{}]", elems.join(", "))
94 }
95
96 other => other.to_string(),
98 }
99}
100
101pub fn emit_jsdoc(doc: &str, indent: &str, out: &mut String) {
103 if !doc.contains('\n') {
104 let _ = writeln!(out, "{indent}/** {doc} */");
105 } else {
106 let _ = writeln!(out, "{indent}/**");
107 for line in doc.lines() {
108 let _ = writeln!(out, "{indent} * {line}");
109 }
110 let _ = writeln!(out, "{indent} */");
111 }
112}
113
114pub fn to_camel_case(s: &str) -> String {
116 let mut segments = s.split('_');
117 let mut result = segments.next().unwrap().to_lowercase();
119 for segment in segments {
120 let mut chars = segment.chars();
121 if let Some(first) = chars.next() {
122 result.extend(first.to_uppercase());
123 result.push_str(&chars.as_str().to_lowercase());
124 }
125 }
126 result
127}
128
129fn transform_field_name(name: &str, naming: FieldNaming) -> String {
131 match naming {
132 FieldNaming::Preserve => name.to_string(),
133 FieldNaming::CamelCase => to_camel_case(name),
134 }
135}
136
137fn resolve_field_name(
141 field: &FieldDef,
142 container_rename_all: Option<RenameRule>,
143 config_naming: FieldNaming,
144) -> String {
145 if let Some(rename) = &field.rename {
146 return rename.clone();
147 }
148 if let Some(rule) = container_rename_all {
149 return rule.apply(&field.name);
150 }
151 transform_field_name(&field.name, config_naming)
152}
153
154fn resolve_variant_name(variant: &EnumVariant, container_rename_all: Option<RenameRule>) -> String {
158 if let Some(rename) = &variant.rename {
159 return rename.clone();
160 }
161 if let Some(rule) = container_rename_all {
162 return rule.apply(&variant.name);
163 }
164 variant.name.clone()
165}
166
167fn option_inner_type(ty: &RustType) -> Option<&RustType> {
169 if ty.name == "Option" {
170 ty.generics.first()
171 } else {
172 None
173 }
174}
175
176fn generate_interface(
178 s: &StructDef,
179 preserve_docs: bool,
180 field_naming: FieldNaming,
181 out: &mut String,
182) {
183 if preserve_docs && let Some(doc) = &s.docs {
184 emit_jsdoc(doc, "", out);
185 }
186 let _ = writeln!(out, "export interface {} {{", s.name);
187 for field in &s.fields {
188 if field.skip {
189 continue;
190 }
191 let field_name = resolve_field_name(field, s.rename_all, field_naming);
192 if field.has_default
193 && let Some(inner) = option_inner_type(&field.ty)
194 {
195 let ts_type = rust_type_to_ts(inner);
196 let _ = writeln!(out, " {field_name}?: {ts_type} | null;");
197 continue;
198 }
199 let ts_type = rust_type_to_ts(&field.ty);
200 let _ = writeln!(out, " {field_name}: {ts_type};");
201 }
202 let _ = writeln!(out, "}}");
203}
204
205fn generate_enum_type(
215 e: &EnumDef,
216 preserve_docs: bool,
217 field_naming: FieldNaming,
218 out: &mut String,
219) {
220 if preserve_docs && let Some(doc) = &e.docs {
221 emit_jsdoc(doc, "", out);
222 }
223 let all_unit = e
224 .variants
225 .iter()
226 .all(|v| matches!(v.kind, VariantKind::Unit));
227
228 if all_unit {
229 let variants: Vec<String> = e
231 .variants
232 .iter()
233 .map(|v| {
234 let name = resolve_variant_name(v, e.rename_all);
235 format!("\"{name}\"")
236 })
237 .collect();
238 if variants.is_empty() {
239 let _ = writeln!(out, "export type {} = never;", e.name);
240 } else {
241 let _ = writeln!(out, "export type {} = {};", e.name, variants.join(" | "));
242 }
243 } else {
244 let mut variant_types: Vec<String> = Vec::new();
246
247 for v in &e.variants {
248 let variant_name = resolve_variant_name(v, e.rename_all);
249 match &v.kind {
250 VariantKind::Unit => {
251 variant_types.push(format!("\"{variant_name}\""));
252 }
253 VariantKind::Tuple(types) => {
254 let inner = if types.len() == 1 {
255 rust_type_to_ts(&types[0])
256 } else {
257 let elems: Vec<String> = types.iter().map(rust_type_to_ts).collect();
258 format!("[{}]", elems.join(", "))
259 };
260 variant_types.push(format!("{{ {variant_name}: {inner} }}"));
261 }
262 VariantKind::Struct(fields) => {
263 let field_strs: Vec<String> = fields
266 .iter()
267 .filter(|f| !f.skip)
268 .map(|field| {
269 let field_name = resolve_field_name(field, e.rename_all, field_naming);
270 format!("{}: {}", field_name, rust_type_to_ts(&field.ty))
271 })
272 .collect();
273 variant_types.push(format!(
274 "{{ {variant_name}: {{ {} }} }}",
275 field_strs.join("; ")
276 ));
277 }
278 }
279 }
280
281 let _ = writeln!(
282 out,
283 "export type {} = {};",
284 e.name,
285 variant_types.join(" | ")
286 );
287 }
288}
289
290fn generate_procedures_type(procedures: &[Procedure], preserve_docs: bool, out: &mut String) {
293 let (queries, mutations): (Vec<_>, Vec<_>) = procedures
294 .iter()
295 .partition(|p| p.kind == ProcedureKind::Query);
296
297 let _ = writeln!(out, "export type Procedures = {{");
298
299 let _ = writeln!(out, " queries: {{");
301 for proc in &queries {
302 if preserve_docs && let Some(doc) = &proc.docs {
303 emit_jsdoc(doc, " ", out);
304 }
305 let input = proc
306 .input
307 .as_ref()
308 .map(rust_type_to_ts)
309 .unwrap_or_else(|| "void".to_string());
310 let output = proc
311 .output
312 .as_ref()
313 .map(rust_type_to_ts)
314 .unwrap_or_else(|| "void".to_string());
315 let _ = writeln!(
316 out,
317 " {}: {{ input: {input}; output: {output} }};",
318 proc.name
319 );
320 }
321 let _ = writeln!(out, " }};");
322
323 let _ = writeln!(out, " mutations: {{");
325 for proc in &mutations {
326 if preserve_docs && let Some(doc) = &proc.docs {
327 emit_jsdoc(doc, " ", out);
328 }
329 let input = proc
330 .input
331 .as_ref()
332 .map(rust_type_to_ts)
333 .unwrap_or_else(|| "void".to_string());
334 let output = proc
335 .output
336 .as_ref()
337 .map(rust_type_to_ts)
338 .unwrap_or_else(|| "void".to_string());
339 let _ = writeln!(
340 out,
341 " {}: {{ input: {input}; output: {output} }};",
342 proc.name
343 );
344 }
345 let _ = writeln!(out, " }};");
346
347 let _ = writeln!(out, "}};");
348}
349
350pub fn generate_types_file(
357 manifest: &Manifest,
358 preserve_docs: bool,
359 field_naming: FieldNaming,
360) -> String {
361 let mut out = String::with_capacity(1024);
362
363 out.push_str(GENERATED_HEADER);
365 out.push('\n');
366
367 for s in &manifest.structs {
369 generate_interface(s, preserve_docs, field_naming, &mut out);
370 out.push('\n');
371 }
372
373 for e in &manifest.enums {
375 generate_enum_type(e, preserve_docs, field_naming, &mut out);
376 out.push('\n');
377 }
378
379 generate_procedures_type(&manifest.procedures, preserve_docs, &mut out);
381
382 out
383}