1use super::common::GENERATED_HEADER;
2use crate::config::FieldNaming;
3use crate::model::{
4 EnumDef, EnumTagging, EnumVariant, FieldDef, Manifest, Procedure, ProcedureKind, RenameRule,
5 RustType, StructDef, VariantKind,
6};
7
8pub fn rust_type_to_ts(ty: &RustType) -> String {
22 match ty.name.as_str() {
23 "()" => "void".to_string(),
25
26 "String" | "str" | "char" | "&str" => "string".to_string(),
28
29 "bool" => "boolean".to_string(),
31
32 "i8" | "i16" | "i32" | "i64" | "i128" | "u8" | "u16" | "u32" | "u64" | "u128" | "f32"
34 | "f64" | "isize" | "usize" => "number".to_string(),
35
36 "Vec" | "Array" | "HashSet" | "BTreeSet" => {
38 let inner = ty
39 .generics
40 .first()
41 .map(rust_type_to_ts)
42 .unwrap_or_else(|| "unknown".to_string());
43 if inner.contains(" | ") {
45 format!("({inner})[]")
46 } else {
47 format!("{inner}[]")
48 }
49 }
50
51 "Option" => {
53 let inner = ty
54 .generics
55 .first()
56 .map(rust_type_to_ts)
57 .unwrap_or_else(|| "unknown".to_string());
58 format!("{inner} | null")
59 }
60
61 "HashMap" | "BTreeMap" => {
63 let key = ty
64 .generics
65 .first()
66 .map(rust_type_to_ts)
67 .unwrap_or_else(|| "string".to_string());
68 let value = ty
69 .generics
70 .get(1)
71 .map(rust_type_to_ts)
72 .unwrap_or_else(|| "unknown".to_string());
73 format!("Record<{key}, {value}>")
74 }
75
76 "Box" | "Arc" | "Rc" | "Cow" => ty
78 .generics
79 .first()
80 .map(rust_type_to_ts)
81 .unwrap_or_else(|| "unknown".to_string()),
82
83 "tuple" => {
85 let elems: Vec<String> = ty.generics.iter().map(rust_type_to_ts).collect();
86 format!("[{}]", elems.join(", "))
87 }
88
89 other => other.to_string(),
91 }
92}
93
94pub fn emit_jsdoc(doc: &str, indent: &str, out: &mut String) {
96 if !doc.contains('\n') {
97 emit!(out, "{indent}/** {doc} */");
98 } else {
99 emit!(out, "{indent}/**");
100 for line in doc.lines() {
101 emit!(out, "{indent} * {line}");
102 }
103 emit!(out, "{indent} */");
104 }
105}
106
107pub fn to_camel_case(s: &str) -> String {
109 let mut segments = s.split('_');
110 let mut result = segments.next().unwrap_or_default().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 render_field_str(
173 field: &FieldDef,
174 container_rename_all: Option<RenameRule>,
175 config_naming: FieldNaming,
176) -> String {
177 let name = resolve_field_name(field, container_rename_all, config_naming);
178 if field.has_default
179 && let Some(inner) = option_inner_type(&field.ty)
180 {
181 format!("{}?: {} | null", name, rust_type_to_ts(inner))
182 } else {
183 format!("{}: {}", name, rust_type_to_ts(&field.ty))
184 }
185}
186
187fn generate_interface(
189 s: &StructDef,
190 preserve_docs: bool,
191 field_naming: FieldNaming,
192 out: &mut String,
193) {
194 if preserve_docs && let Some(doc) = &s.docs {
195 emit_jsdoc(doc, "", out);
196 }
197 emit!(out, "export interface {} {{", s.name);
198 for field in &s.fields {
199 if field.skip {
200 continue;
201 }
202 let rendered = render_field_str(field, s.rename_all, field_naming);
203 emit!(out, " {rendered};");
204 }
205 emit!(out, "}}");
206}
207
208fn generate_enum_type(
216 e: &EnumDef,
217 preserve_docs: bool,
218 field_naming: FieldNaming,
219 out: &mut String,
220) {
221 if preserve_docs && let Some(doc) = &e.docs {
222 emit_jsdoc(doc, "", out);
223 }
224
225 match &e.tagging {
226 EnumTagging::External => generate_enum_external(e, field_naming, out),
227 EnumTagging::Internal { tag } => generate_enum_internal(e, tag, field_naming, out),
228 EnumTagging::Adjacent { tag, content } => {
229 generate_enum_adjacent(e, tag, content, field_naming, out);
230 }
231 EnumTagging::Untagged => generate_enum_untagged(e, field_naming, out),
232 }
233}
234
235fn generate_enum_external(e: &EnumDef, field_naming: FieldNaming, out: &mut String) {
237 let all_unit = e
238 .variants
239 .iter()
240 .all(|v| matches!(v.kind, VariantKind::Unit));
241
242 if all_unit {
243 let variants: Vec<String> = e
244 .variants
245 .iter()
246 .map(|v| {
247 let name = resolve_variant_name(v, e.rename_all);
248 format!("\"{name}\"")
249 })
250 .collect();
251 if variants.is_empty() {
252 emit!(out, "export type {} = never;", e.name);
253 } else {
254 emit!(out, "export type {} = {};", e.name, variants.join(" | "));
255 }
256 } else {
257 let mut variant_types: Vec<String> = Vec::new();
258
259 for v in &e.variants {
260 let variant_name = resolve_variant_name(v, e.rename_all);
261 match &v.kind {
262 VariantKind::Unit => {
263 variant_types.push(format!("\"{variant_name}\""));
264 }
265 VariantKind::Tuple(types) => {
266 let inner = if types.len() == 1 {
267 rust_type_to_ts(&types[0])
268 } else {
269 let elems: Vec<String> = types.iter().map(rust_type_to_ts).collect();
270 format!("[{}]", elems.join(", "))
271 };
272 variant_types.push(format!("{{ {variant_name}: {inner} }}"));
273 }
274 VariantKind::Struct(fields) => {
275 let field_strs: Vec<String> = fields
276 .iter()
277 .filter(|f| !f.skip)
278 .map(|field| render_field_str(field, e.rename_all, field_naming))
279 .collect();
280 variant_types.push(format!(
281 "{{ {variant_name}: {{ {} }} }}",
282 field_strs.join("; ")
283 ));
284 }
285 }
286 }
287
288 emit!(
289 out,
290 "export type {} = {};",
291 e.name,
292 variant_types.join(" | ")
293 );
294 }
295}
296
297fn generate_enum_internal(e: &EnumDef, tag: &str, field_naming: FieldNaming, out: &mut String) {
304 if e.variants.is_empty() {
305 emit!(out, "export type {} = never;", e.name);
306 return;
307 }
308
309 let mut variant_types: Vec<String> = Vec::new();
310
311 for v in &e.variants {
312 let variant_name = resolve_variant_name(v, e.rename_all);
313 match &v.kind {
314 VariantKind::Unit => {
315 variant_types.push(format!("{{ {tag}: \"{variant_name}\" }}"));
316 }
317 VariantKind::Struct(fields) => {
318 let field_strs: Vec<String> = fields
319 .iter()
320 .filter(|f| !f.skip)
321 .map(|field| render_field_str(field, e.rename_all, field_naming))
322 .collect();
323 let mut parts = vec![format!("{tag}: \"{variant_name}\"")];
324 parts.extend(field_strs);
325 variant_types.push(format!("{{ {} }}", parts.join("; ")));
326 }
327 VariantKind::Tuple(types) => {
328 if types.len() == 1 {
329 let inner = rust_type_to_ts(&types[0]);
330 variant_types.push(format!("{{ {tag}: \"{variant_name}\" }} & {inner}"));
331 }
332 }
334 }
335 }
336
337 if variant_types.is_empty() {
338 emit!(out, "export type {} = never;", e.name);
339 } else {
340 emit!(
341 out,
342 "export type {} = {};",
343 e.name,
344 variant_types.join(" | ")
345 );
346 }
347}
348
349fn generate_enum_adjacent(
356 e: &EnumDef,
357 tag: &str,
358 content: &str,
359 field_naming: FieldNaming,
360 out: &mut String,
361) {
362 if e.variants.is_empty() {
363 emit!(out, "export type {} = never;", e.name);
364 return;
365 }
366
367 let mut variant_types: Vec<String> = Vec::new();
368
369 for v in &e.variants {
370 let variant_name = resolve_variant_name(v, e.rename_all);
371 match &v.kind {
372 VariantKind::Unit => {
373 variant_types.push(format!("{{ {tag}: \"{variant_name}\" }}"));
374 }
375 VariantKind::Tuple(types) => {
376 let inner = if types.len() == 1 {
377 rust_type_to_ts(&types[0])
378 } else {
379 let elems: Vec<String> = types.iter().map(rust_type_to_ts).collect();
380 format!("[{}]", elems.join(", "))
381 };
382 variant_types.push(format!(
383 "{{ {tag}: \"{variant_name}\"; {content}: {inner} }}"
384 ));
385 }
386 VariantKind::Struct(fields) => {
387 let field_strs: Vec<String> = fields
388 .iter()
389 .filter(|f| !f.skip)
390 .map(|field| render_field_str(field, e.rename_all, field_naming))
391 .collect();
392 variant_types.push(format!(
393 "{{ {tag}: \"{variant_name}\"; {content}: {{ {} }} }}",
394 field_strs.join("; ")
395 ));
396 }
397 }
398 }
399
400 emit!(
401 out,
402 "export type {} = {};",
403 e.name,
404 variant_types.join(" | ")
405 );
406}
407
408fn generate_enum_untagged(e: &EnumDef, field_naming: FieldNaming, out: &mut String) {
416 if e.variants.is_empty() {
417 emit!(out, "export type {} = never;", e.name);
418 return;
419 }
420
421 let mut variant_types: Vec<String> = Vec::new();
422
423 for v in &e.variants {
424 match &v.kind {
425 VariantKind::Unit => {
426 variant_types.push("null".to_string());
427 }
428 VariantKind::Tuple(types) => {
429 if types.len() == 1 {
430 variant_types.push(rust_type_to_ts(&types[0]));
431 } else {
432 let elems: Vec<String> = types.iter().map(rust_type_to_ts).collect();
433 variant_types.push(format!("[{}]", elems.join(", ")));
434 }
435 }
436 VariantKind::Struct(fields) => {
437 let field_strs: Vec<String> = fields
438 .iter()
439 .filter(|f| !f.skip)
440 .map(|field| render_field_str(field, e.rename_all, field_naming))
441 .collect();
442 variant_types.push(format!("{{ {} }}", field_strs.join("; ")));
443 }
444 }
445 }
446
447 emit!(
448 out,
449 "export type {} = {};",
450 e.name,
451 variant_types.join(" | ")
452 );
453}
454
455fn generate_procedures_type(procedures: &[Procedure], preserve_docs: bool, out: &mut String) {
458 let (queries, mutations): (Vec<_>, Vec<_>) = procedures
459 .iter()
460 .partition(|p| p.kind == ProcedureKind::Query);
461
462 emit!(out, "export type Procedures = {{");
463
464 emit!(out, " queries: {{");
466 for proc in &queries {
467 if preserve_docs && let Some(doc) = &proc.docs {
468 emit_jsdoc(doc, " ", out);
469 }
470 let input = proc
471 .input
472 .as_ref()
473 .map(rust_type_to_ts)
474 .unwrap_or_else(|| "void".to_string());
475 let output = proc
476 .output
477 .as_ref()
478 .map(rust_type_to_ts)
479 .unwrap_or_else(|| "void".to_string());
480 emit!(
481 out,
482 " {}: {{ input: {input}; output: {output} }};",
483 proc.name
484 );
485 }
486 emit!(out, " }};");
487
488 emit!(out, " mutations: {{");
490 for proc in &mutations {
491 if preserve_docs && let Some(doc) = &proc.docs {
492 emit_jsdoc(doc, " ", out);
493 }
494 let input = proc
495 .input
496 .as_ref()
497 .map(rust_type_to_ts)
498 .unwrap_or_else(|| "void".to_string());
499 let output = proc
500 .output
501 .as_ref()
502 .map(rust_type_to_ts)
503 .unwrap_or_else(|| "void".to_string());
504 emit!(
505 out,
506 " {}: {{ input: {input}; output: {output} }};",
507 proc.name
508 );
509 }
510 emit!(out, " }};");
511
512 emit!(out, "}};");
513}
514
515pub fn generate_types_file(
522 manifest: &Manifest,
523 preserve_docs: bool,
524 field_naming: FieldNaming,
525) -> String {
526 let mut out = String::with_capacity(1024);
527
528 out.push_str(GENERATED_HEADER);
530 out.push('\n');
531
532 for s in &manifest.structs {
534 generate_interface(s, preserve_docs, field_naming, &mut out);
535 out.push('\n');
536 }
537
538 for e in &manifest.enums {
540 generate_enum_type(e, preserve_docs, field_naming, &mut out);
541 out.push('\n');
542 }
543
544 generate_procedures_type(&manifest.procedures, preserve_docs, &mut out);
546
547 out
548}