1use crate::config::FieldNaming;
2use crate::model::{
3 EnumDef, EnumVariant, FieldDef, Manifest, Procedure, ProcedureKind, RenameRule, RustType,
4 StructDef, VariantKind,
5};
6
7const GENERATED_HEADER: &str = "\
9// This file is auto-generated by vercel-rpc-cli. Do not edit manually.
10// Re-run `rpc generate` or use `rpc watch` to regenerate.
11";
12
13pub fn rust_type_to_ts(ty: &RustType) -> String {
27 match ty.name.as_str() {
28 "()" => "void".to_string(),
30
31 "String" | "str" | "char" | "&str" => "string".to_string(),
33
34 "bool" => "boolean".to_string(),
36
37 "i8" | "i16" | "i32" | "i64" | "i128" | "u8" | "u16" | "u32" | "u64" | "u128" | "f32"
39 | "f64" | "isize" | "usize" => "number".to_string(),
40
41 "Vec" | "Array" | "HashSet" | "BTreeSet" => {
43 let inner = ty
44 .generics
45 .first()
46 .map(rust_type_to_ts)
47 .unwrap_or_else(|| "unknown".to_string());
48 if inner.contains(" | ") {
50 format!("({inner})[]")
51 } else {
52 format!("{inner}[]")
53 }
54 }
55
56 "Option" => {
58 let inner = ty
59 .generics
60 .first()
61 .map(rust_type_to_ts)
62 .unwrap_or_else(|| "unknown".to_string());
63 format!("{inner} | null")
64 }
65
66 "HashMap" | "BTreeMap" => {
68 let key = ty
69 .generics
70 .first()
71 .map(rust_type_to_ts)
72 .unwrap_or_else(|| "string".to_string());
73 let value = ty
74 .generics
75 .get(1)
76 .map(rust_type_to_ts)
77 .unwrap_or_else(|| "unknown".to_string());
78 format!("Record<{key}, {value}>")
79 }
80
81 "Box" | "Arc" | "Rc" | "Cow" => ty
83 .generics
84 .first()
85 .map(rust_type_to_ts)
86 .unwrap_or_else(|| "unknown".to_string()),
87
88 "tuple" => {
90 let elems: Vec<String> = ty.generics.iter().map(rust_type_to_ts).collect();
91 format!("[{}]", elems.join(", "))
92 }
93
94 other => other.to_string(),
96 }
97}
98
99pub fn emit_jsdoc(doc: &str, indent: &str, out: &mut String) {
101 if !doc.contains('\n') {
102 emit!(out, "{indent}/** {doc} */");
103 } else {
104 emit!(out, "{indent}/**");
105 for line in doc.lines() {
106 emit!(out, "{indent} * {line}");
107 }
108 emit!(out, "{indent} */");
109 }
110}
111
112pub fn to_camel_case(s: &str) -> String {
114 let mut segments = s.split('_');
115 let mut result = segments
116 .next()
117 .expect("split always yields at least one element")
118 .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 emit!(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 emit!(out, " {field_name}?: {ts_type} | null;");
197 continue;
198 }
199 let ts_type = rust_type_to_ts(&field.ty);
200 emit!(out, " {field_name}: {ts_type};");
201 }
202 emit!(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 emit!(out, "export type {} = never;", e.name);
240 } else {
241 emit!(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 emit!(
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 emit!(out, "export type Procedures = {{");
298
299 emit!(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 emit!(
316 out,
317 " {}: {{ input: {input}; output: {output} }};",
318 proc.name
319 );
320 }
321 emit!(out, " }};");
322
323 emit!(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 emit!(
340 out,
341 " {}: {{ input: {input}; output: {output} }};",
342 proc.name
343 );
344 }
345 emit!(out, " }};");
346
347 emit!(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}