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.base_name() {
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 => {
91 if ty.generics.is_empty() {
92 other.to_string()
93 } else {
94 let params: Vec<String> = ty.generics.iter().map(rust_type_to_ts).collect();
95 format!("{other}<{}>", params.join(", "))
96 }
97 }
98 }
99}
100
101pub fn emit_jsdoc(doc: &str, indent: &str, out: &mut String) {
103 if !doc.contains('\n') {
104 emit!(out, "{indent}/** {doc} */");
105 } else {
106 emit!(out, "{indent}/**");
107 for line in doc.lines() {
108 emit!(out, "{indent} * {line}");
109 }
110 emit!(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_or_default().to_lowercase();
118 for segment in segments {
119 let mut chars = segment.chars();
120 if let Some(first) = chars.next() {
121 result.extend(first.to_uppercase());
122 result.push_str(&chars.as_str().to_lowercase());
123 }
124 }
125 result
126}
127
128fn transform_field_name(name: &str, naming: FieldNaming) -> String {
130 match naming {
131 FieldNaming::Preserve => name.to_string(),
132 FieldNaming::CamelCase => to_camel_case(name),
133 }
134}
135
136fn resolve_field_name(
140 field: &FieldDef,
141 container_rename_all: Option<RenameRule>,
142 config_naming: FieldNaming,
143) -> String {
144 if let Some(rename) = &field.rename {
145 return rename.clone();
146 }
147 if let Some(rule) = container_rename_all {
148 return rule.apply(&field.name);
149 }
150 transform_field_name(&field.name, config_naming)
151}
152
153fn resolve_variant_name(variant: &EnumVariant, container_rename_all: Option<RenameRule>) -> String {
157 if let Some(rename) = &variant.rename {
158 return rename.clone();
159 }
160 if let Some(rule) = container_rename_all {
161 return rule.apply(&variant.name);
162 }
163 variant.name.clone()
164}
165
166fn option_inner_type(ty: &RustType) -> Option<&RustType> {
168 if ty.name == "Option" {
169 ty.generics.first()
170 } else {
171 None
172 }
173}
174
175fn render_field_str(
180 field: &FieldDef,
181 container_rename_all: Option<RenameRule>,
182 config_naming: FieldNaming,
183) -> String {
184 let name = resolve_field_name(field, container_rename_all, config_naming);
185 if field.has_default
186 && let Some(inner) = option_inner_type(&field.ty)
187 {
188 format!("{}?: {} | null", name, rust_type_to_ts(inner))
189 } else {
190 format!("{}: {}", name, rust_type_to_ts(&field.ty))
191 }
192}
193
194fn render_struct_body(
200 fields: &[FieldDef],
201 rename_all: Option<RenameRule>,
202 field_naming: FieldNaming,
203) -> (Vec<String>, Vec<String>) {
204 let mut regular = Vec::new();
205 let mut flattened = Vec::new();
206 for f in fields {
207 if f.skip {
208 continue;
209 }
210 if f.flatten {
211 flattened.push(rust_type_to_ts(&f.ty));
212 } else {
213 regular.push(render_field_str(f, rename_all, field_naming));
214 }
215 }
216 (regular, flattened)
217}
218
219fn build_object_with_flatten(regular: &[String], flattened: &[String]) -> String {
222 let mut parts = Vec::new();
223 if !regular.is_empty() {
224 parts.push(format!("{{ {} }}", regular.join("; ")));
225 }
226 parts.extend(flattened.iter().cloned());
227 parts.join(" & ")
228}
229
230fn generate_interface(
238 s: &StructDef,
239 preserve_docs: bool,
240 field_naming: FieldNaming,
241 branded_newtypes: bool,
242 out: &mut String,
243) {
244 if preserve_docs && let Some(doc) = &s.docs {
245 emit_jsdoc(doc, "", out);
246 }
247 let generic_params = format_generic_params(&s.generics);
248
249 if !s.tuple_fields.is_empty() {
251 if s.tuple_fields.len() == 1 {
252 let inner = rust_type_to_ts(&s.tuple_fields[0]);
254 if branded_newtypes {
255 emit!(
256 out,
257 "export type {}{generic_params} = {inner} & {{ readonly __brand: \"{}\" }};",
258 s.name,
259 s.name
260 );
261 } else {
262 emit!(out, "export type {}{generic_params} = {inner};", s.name);
263 }
264 } else {
265 let elems: Vec<String> = s.tuple_fields.iter().map(rust_type_to_ts).collect();
267 emit!(
268 out,
269 "export type {}{generic_params} = [{}];",
270 s.name,
271 elems.join(", ")
272 );
273 }
274 return;
275 }
276
277 let (regular, flattened) = render_struct_body(&s.fields, s.rename_all, field_naming);
278
279 if flattened.is_empty() {
280 emit!(out, "export interface {}{generic_params} {{", s.name);
282 for r in ®ular {
283 emit!(out, " {r};");
284 }
285 emit!(out, "}}");
286 } else {
287 let body = build_object_with_flatten(®ular, &flattened);
289 emit!(out, "export type {}{generic_params} = {body};", s.name);
290 }
291}
292
293fn generate_enum_type(
301 e: &EnumDef,
302 preserve_docs: bool,
303 field_naming: FieldNaming,
304 out: &mut String,
305) {
306 if preserve_docs && let Some(doc) = &e.docs {
307 emit_jsdoc(doc, "", out);
308 }
309
310 match &e.tagging {
311 EnumTagging::External => generate_enum_external(e, field_naming, out),
312 EnumTagging::Internal { tag } => generate_enum_internal(e, tag, field_naming, out),
313 EnumTagging::Adjacent { tag, content } => {
314 generate_enum_adjacent(e, tag, content, field_naming, out);
315 }
316 EnumTagging::Untagged => generate_enum_untagged(e, field_naming, out),
317 }
318}
319
320fn generate_enum_external(e: &EnumDef, field_naming: FieldNaming, out: &mut String) {
322 let generic_params = format_generic_params(&e.generics);
323 let all_unit = e
324 .variants
325 .iter()
326 .all(|v| matches!(v.kind, VariantKind::Unit));
327
328 if all_unit {
329 let variants: Vec<String> = e
330 .variants
331 .iter()
332 .map(|v| {
333 let name = resolve_variant_name(v, e.rename_all);
334 format!("\"{name}\"")
335 })
336 .collect();
337 if variants.is_empty() {
338 emit!(out, "export type {}{generic_params} = never;", e.name);
339 } else {
340 emit!(
341 out,
342 "export type {}{generic_params} = {};",
343 e.name,
344 variants.join(" | ")
345 );
346 }
347 } else {
348 let mut variant_types: Vec<String> = Vec::new();
349
350 for v in &e.variants {
351 let variant_name = resolve_variant_name(v, e.rename_all);
352 match &v.kind {
353 VariantKind::Unit => {
354 variant_types.push(format!("\"{variant_name}\""));
355 }
356 VariantKind::Tuple(types) => {
357 let inner = if types.len() == 1 {
358 rust_type_to_ts(&types[0])
359 } else {
360 let elems: Vec<String> = types.iter().map(rust_type_to_ts).collect();
361 format!("[{}]", elems.join(", "))
362 };
363 variant_types.push(format!("{{ {variant_name}: {inner} }}"));
364 }
365 VariantKind::Struct(fields) => {
366 let (regular, flattened) =
367 render_struct_body(fields, e.rename_all, field_naming);
368 let inner = build_object_with_flatten(®ular, &flattened);
369 variant_types.push(format!("{{ {variant_name}: {inner} }}"));
370 }
371 }
372 }
373
374 emit!(
375 out,
376 "export type {}{generic_params} = {};",
377 e.name,
378 variant_types.join(" | ")
379 );
380 }
381}
382
383fn generate_enum_internal(e: &EnumDef, tag: &str, field_naming: FieldNaming, out: &mut String) {
390 let generic_params = format_generic_params(&e.generics);
391 if e.variants.is_empty() {
392 emit!(out, "export type {}{generic_params} = never;", e.name);
393 return;
394 }
395
396 let mut variant_types: Vec<String> = Vec::new();
397
398 for v in &e.variants {
399 let variant_name = resolve_variant_name(v, e.rename_all);
400 match &v.kind {
401 VariantKind::Unit => {
402 variant_types.push(format!("{{ {tag}: \"{variant_name}\" }}"));
403 }
404 VariantKind::Struct(fields) => {
405 let (regular, flattened) = render_struct_body(fields, e.rename_all, field_naming);
406 let mut parts = vec![format!("{tag}: \"{variant_name}\"")];
407 parts.extend(regular);
408 let obj = format!("{{ {} }}", parts.join("; "));
409 if flattened.is_empty() {
410 variant_types.push(obj);
411 } else {
412 let all: Vec<String> = std::iter::once(obj).chain(flattened).collect();
413 variant_types.push(all.join(" & "));
414 }
415 }
416 VariantKind::Tuple(types) => {
417 if types.len() == 1 {
418 let inner = rust_type_to_ts(&types[0]);
419 variant_types.push(format!("{{ {tag}: \"{variant_name}\" }} & {inner}"));
420 }
421 }
423 }
424 }
425
426 if variant_types.is_empty() {
427 emit!(out, "export type {}{generic_params} = never;", e.name);
428 } else {
429 emit!(
430 out,
431 "export type {}{generic_params} = {};",
432 e.name,
433 variant_types.join(" | ")
434 );
435 }
436}
437
438fn generate_enum_adjacent(
445 e: &EnumDef,
446 tag: &str,
447 content: &str,
448 field_naming: FieldNaming,
449 out: &mut String,
450) {
451 let generic_params = format_generic_params(&e.generics);
452 if e.variants.is_empty() {
453 emit!(out, "export type {}{generic_params} = never;", e.name);
454 return;
455 }
456
457 let mut variant_types: Vec<String> = Vec::new();
458
459 for v in &e.variants {
460 let variant_name = resolve_variant_name(v, e.rename_all);
461 match &v.kind {
462 VariantKind::Unit => {
463 variant_types.push(format!("{{ {tag}: \"{variant_name}\" }}"));
464 }
465 VariantKind::Tuple(types) => {
466 let inner = if types.len() == 1 {
467 rust_type_to_ts(&types[0])
468 } else {
469 let elems: Vec<String> = types.iter().map(rust_type_to_ts).collect();
470 format!("[{}]", elems.join(", "))
471 };
472 variant_types.push(format!(
473 "{{ {tag}: \"{variant_name}\"; {content}: {inner} }}"
474 ));
475 }
476 VariantKind::Struct(fields) => {
477 let (regular, flattened) = render_struct_body(fields, e.rename_all, field_naming);
478 let inner = build_object_with_flatten(®ular, &flattened);
479 variant_types.push(format!(
480 "{{ {tag}: \"{variant_name}\"; {content}: {inner} }}"
481 ));
482 }
483 }
484 }
485
486 emit!(
487 out,
488 "export type {}{generic_params} = {};",
489 e.name,
490 variant_types.join(" | ")
491 );
492}
493
494fn generate_enum_untagged(e: &EnumDef, field_naming: FieldNaming, out: &mut String) {
502 let generic_params = format_generic_params(&e.generics);
503 if e.variants.is_empty() {
504 emit!(out, "export type {}{generic_params} = never;", e.name);
505 return;
506 }
507
508 let mut variant_types: Vec<String> = Vec::new();
509
510 for v in &e.variants {
511 match &v.kind {
512 VariantKind::Unit => {
513 variant_types.push("null".to_string());
514 }
515 VariantKind::Tuple(types) => {
516 if types.len() == 1 {
517 variant_types.push(rust_type_to_ts(&types[0]));
518 } else {
519 let elems: Vec<String> = types.iter().map(rust_type_to_ts).collect();
520 variant_types.push(format!("[{}]", elems.join(", ")));
521 }
522 }
523 VariantKind::Struct(fields) => {
524 let (regular, flattened) = render_struct_body(fields, e.rename_all, field_naming);
525 let inner = build_object_with_flatten(®ular, &flattened);
526 variant_types.push(inner);
527 }
528 }
529 }
530
531 emit!(
532 out,
533 "export type {}{generic_params} = {};",
534 e.name,
535 variant_types.join(" | ")
536 );
537}
538
539fn format_generic_params(generics: &[String]) -> String {
543 if generics.is_empty() {
544 String::new()
545 } else {
546 format!("<{}>", generics.join(", "))
547 }
548}
549
550fn generate_procedures_type(procedures: &[Procedure], preserve_docs: bool, out: &mut String) {
553 let (queries, mutations): (Vec<_>, Vec<_>) = procedures
554 .iter()
555 .partition(|p| p.kind == ProcedureKind::Query);
556
557 emit!(out, "export type Procedures = {{");
558
559 emit!(out, " queries: {{");
561 for proc in &queries {
562 if preserve_docs && let Some(doc) = &proc.docs {
563 emit_jsdoc(doc, " ", out);
564 }
565 let input = proc
566 .input
567 .as_ref()
568 .map(rust_type_to_ts)
569 .unwrap_or_else(|| "void".to_string());
570 let output = proc
571 .output
572 .as_ref()
573 .map(rust_type_to_ts)
574 .unwrap_or_else(|| "void".to_string());
575 emit!(
576 out,
577 " {}: {{ input: {input}; output: {output} }};",
578 proc.name
579 );
580 }
581 emit!(out, " }};");
582
583 emit!(out, " mutations: {{");
585 for proc in &mutations {
586 if preserve_docs && let Some(doc) = &proc.docs {
587 emit_jsdoc(doc, " ", out);
588 }
589 let input = proc
590 .input
591 .as_ref()
592 .map(rust_type_to_ts)
593 .unwrap_or_else(|| "void".to_string());
594 let output = proc
595 .output
596 .as_ref()
597 .map(rust_type_to_ts)
598 .unwrap_or_else(|| "void".to_string());
599 emit!(
600 out,
601 " {}: {{ input: {input}; output: {output} }};",
602 proc.name
603 );
604 }
605 emit!(out, " }};");
606
607 emit!(out, "}};");
608}
609
610pub fn generate_types_file(
617 manifest: &Manifest,
618 preserve_docs: bool,
619 field_naming: FieldNaming,
620 branded_newtypes: bool,
621) -> String {
622 let mut out = String::with_capacity(1024);
623
624 out.push_str(GENERATED_HEADER);
626 out.push('\n');
627
628 for s in &manifest.structs {
630 generate_interface(s, preserve_docs, field_naming, branded_newtypes, &mut out);
631 out.push('\n');
632 }
633
634 for e in &manifest.enums {
636 generate_enum_type(e, preserve_docs, field_naming, &mut out);
637 out.push('\n');
638 }
639
640 generate_procedures_type(&manifest.procedures, preserve_docs, &mut out);
642
643 out
644}