1use std::{borrow::Cow, collections::BTreeSet};
7
8use specta::{
9 Format, Types,
10 datatype::{
11 DataType, Deprecated, Enum, Field, Fields, GenericDefinition, GenericReference, List, Map,
12 NamedDataType, NamedReference, NamedReferenceType, OpaqueReference, Primitive, Reference,
13 Struct, Tuple, Variant,
14 },
15};
16
17use crate::{
18 Branded, BrandedTypeExporter, Error, Exporter, Layout, map_keys, opaque,
19 reserved_names::RESERVED_TYPE_NAMES,
20};
21
22const STRING: &str = "string";
23const NULL: &str = "null";
24const NEVER: &str = "never";
25
26fn path_string(location: &[Cow<'static, str>]) -> String {
27 location.join(".")
28}
29
30fn rust_type_path(ndt: &NamedDataType) -> Cow<'static, str> {
31 if ndt.module_path.is_empty() {
32 ndt.name.clone()
33 } else {
34 Cow::Owned(format!("{}::{}", ndt.module_path, ndt.name))
35 }
36}
37
38fn module_prefixed_type_name(ndt: &NamedDataType) -> String {
39 let mut name = ndt.module_path.split("::").collect::<Vec<_>>().join("_");
40 name.push('_');
41 name.push_str(&ndt.name);
42 name
43}
44
45fn exported_type_name<'a>(exporter: &Exporter, ndt: &'a NamedDataType) -> Cow<'a, str> {
46 match exporter.layout {
47 Layout::ModulePrefixedName => Cow::Owned(module_prefixed_type_name(ndt)),
48 _ => ndt.name.clone(),
49 }
50}
51
52fn referenced_type_name<'a>(exporter: &Exporter, ndt: &'a NamedDataType) -> Cow<'a, str> {
53 match exporter.layout {
54 Layout::ModulePrefixedName => Cow::Owned(module_prefixed_type_name(ndt)),
55 Layout::Namespaces => {
56 if ndt.module_path.is_empty() {
57 ndt.name.clone()
58 } else {
59 let mut path =
60 ndt.module_path
61 .split("::")
62 .fold("$s$.".to_string(), |mut s, segment| {
63 s.push_str(segment);
64 s.push('.');
65 s
66 });
67 path.push_str(&ndt.name);
68 Cow::Owned(path)
69 }
70 }
71 Layout::Files => {
72 let current_module_path = crate::references::current_module_path().unwrap_or_default();
73
74 if ndt.module_path == current_module_path {
75 ndt.name.clone()
76 } else {
77 let mut path = crate::exporter::module_alias(&ndt.module_path);
78 path.push('.');
79 path.push_str(&ndt.name);
80 Cow::Owned(path)
81 }
82 }
83 _ => ndt.name.clone(),
84 }
85}
86
87fn inner_comments(
88 deprecated: Option<&Deprecated>,
89 docs: &str,
90 other: String,
91 start_with_newline: bool,
92 prefix: &str,
93) -> String {
94 let mut comments = String::new();
95 js_doc(&mut comments, docs, deprecated);
96 if comments.is_empty() {
97 return other;
98 }
99
100 let mut out = String::new();
101 if start_with_newline {
102 out.push('\n');
103 }
104
105 for line in comments.lines() {
106 out.push_str(prefix);
107 out.push_str(line);
108 out.push('\n');
109 }
110
111 out.push_str(&other);
112 out
113}
114
115pub(crate) fn is_identifier(name: &str) -> bool {
116 let mut chars = name.chars();
117 let Some(first) = chars.next() else {
118 return false;
119 };
120
121 (first.is_ascii_alphabetic() || first == '_' || first == '$')
122 && chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '$')
123}
124
125pub(crate) fn escape_typescript_string_literal(value: &str) -> Cow<'_, str> {
126 if !value.chars().any(|ch| {
127 ch == '"' || ch == '\\' || ch == '\u{2028}' || ch == '\u{2029}' || ch.is_control()
128 }) {
129 return Cow::Borrowed(value);
130 }
131
132 let mut escaped = String::with_capacity(value.len());
133 for ch in value.chars() {
134 match ch {
135 '"' => escaped.push_str(r#"\""#),
136 '\\' => escaped.push_str(r#"\\"#),
137 '\n' => escaped.push_str(r#"\n"#),
138 '\r' => escaped.push_str(r#"\r"#),
139 '\t' => escaped.push_str(r#"\t"#),
140 '\u{2028}' => escaped.push_str(r#"\u2028"#),
141 '\u{2029}' => escaped.push_str(r#"\u2029"#),
142 ch if ch.is_control() => push_unicode_escape(&mut escaped, ch),
143 _ => escaped.push(ch),
144 }
145 }
146
147 Cow::Owned(escaped)
148}
149
150fn push_unicode_escape(s: &mut String, ch: char) {
151 const HEX: &[u8; 16] = b"0123456789ABCDEF";
152 let value = ch as u32;
153
154 s.push_str(r#"\u"#);
155 s.push(HEX[((value >> 12) & 0xF) as usize] as char);
156 s.push(HEX[((value >> 8) & 0xF) as usize] as char);
157 s.push(HEX[((value >> 4) & 0xF) as usize] as char);
158 s.push(HEX[(value & 0xF) as usize] as char);
159}
160
161fn sanitise_key<'a>(field_name: Cow<'static, str>, force_string: bool) -> Cow<'a, str> {
162 if force_string || !is_identifier(&field_name) {
163 format!(r#""{}""#, escape_typescript_string_literal(&field_name)).into()
164 } else {
165 field_name
166 }
167}
168
169fn sanitise_type_name(location: &[Cow<'static, str>], ident: &str) -> Result<String, Error> {
170 let path = path_string(location);
171
172 if ident.is_empty() {
173 return Err(Error::empty_name(path));
174 }
175
176 if let Some(name) = RESERVED_TYPE_NAMES.iter().find(|v| **v == ident) {
177 return Err(Error::forbidden_name(path, name));
178 }
179
180 if let Some(first_char) = ident.chars().next()
181 && !first_char.is_alphabetic()
182 && first_char != '_'
183 {
184 return Err(Error::invalid_name(path, ident.to_string()));
185 }
186
187 if ident
188 .find(|c: char| !c.is_alphanumeric() && c != '_')
189 .is_some()
190 {
191 return Err(Error::invalid_name(path, ident.to_string()));
192 }
193
194 Ok(ident.to_string())
195}
196
197pub(crate) fn js_doc(s: &mut String, docs: &str, deprecated: Option<&Deprecated>) {
198 if docs.is_empty() && deprecated.is_none() {
199 return;
200 }
201
202 if deprecated.is_none() {
203 let mut lines = docs.lines();
204 if let (Some(line), None) = (lines.next(), lines.next()) {
205 s.push_str("/** ");
206 s.push_str(&escape_jsdoc_text(line));
207 s.push_str(" */\n");
208 return;
209 }
210 }
211
212 s.push_str("/**\n");
213 if !docs.is_empty() {
214 for line in docs.lines() {
215 s.push_str(" * ");
216 s.push_str(&escape_jsdoc_text(line));
217 s.push('\n');
218 }
219 }
220
221 if let Some(typ) = deprecated {
222 s.push_str(" * @deprecated");
223 if let Some(details) = deprecated_details(typ) {
224 s.push(' ');
225 s.push_str(&details);
226 }
227 s.push('\n');
228 }
229
230 s.push_str(" */\n");
231}
232
233pub(crate) fn escape_jsdoc_text(text: &str) -> Cow<'_, str> {
234 if text.contains("*/") {
235 Cow::Owned(text.replace("*/", "*\\/"))
236 } else {
237 Cow::Borrowed(text)
238 }
239}
240
241pub(crate) fn deprecated_details(typ: &Deprecated) -> Option<String> {
242 typ.note
243 .as_deref()
244 .map(str::trim)
245 .filter(|note| !note.is_empty())
246 .map(str::to_string)
247}
248
249pub fn export<'a>(
264 exporter: &dyn AsRef<Exporter>,
265 types: &Types,
266 ndts: impl Iterator<Item = &'a NamedDataType>,
267 indent: &str,
268) -> Result<String, Error> {
269 let mut s = String::new();
270 export_internal(&mut s, exporter.as_ref(), None, types, ndts, indent)?;
271 Ok(s)
272}
273
274pub(crate) fn export_internal<'a>(
275 s: &mut String,
276 exporter: &Exporter,
277 format: Option<&dyn Format>,
278 types: &Types,
279 ndts: impl Iterator<Item = &'a NamedDataType>,
280 indent: &str,
281) -> Result<(), Error> {
282 let ndts = ndts.filter(|ndt| ndt.ty.is_some());
283
284 if exporter.jsdoc {
285 let mut ndts = ndts.peekable();
286 if ndts.peek().is_none() {
287 return Ok(());
288 }
289
290 s.push_str(indent);
291 s.push_str("/**\n");
292
293 for (index, ndt) in ndts.enumerate() {
294 if index != 0 {
295 s.push_str(indent);
296 s.push_str("\t*\n");
297 }
298
299 append_typedef_body(s, exporter, format, types, ndt, indent)?;
300 }
301
302 s.push_str(indent);
303 s.push_str("\t*/\n");
304 return Ok(());
305 }
306
307 for (index, ndt) in ndts.enumerate() {
308 if index != 0 {
309 s.push('\n');
310 }
311
312 export_single_internal(s, exporter, format, types, ndt, indent)?;
313 }
314
315 Ok(())
316}
317
318fn export_single_internal(
319 s: &mut String,
320 exporter: &Exporter,
321 format: Option<&dyn Format>,
322 types: &Types,
323 ndt: &NamedDataType,
324 indent: &str,
325) -> Result<(), Error> {
326 if exporter.jsdoc {
327 let mut typedef = String::new();
328 typedef_internal(&mut typedef, exporter, format, types, ndt)?;
329 for line in typedef.lines() {
330 s.push_str(indent);
331 s.push_str(line);
332 s.push('\n');
333 }
334 return Ok(());
335 }
336
337 let raw_name = exported_type_name(exporter, ndt);
338 let name = sanitise_type_name(&[rust_type_path(ndt)], &raw_name)
339 .map_err(|err| err.with_named_datatype(ndt))?;
340
341 let mut comments = String::new();
342 js_doc(&mut comments, &ndt.docs, ndt.deprecated.as_ref());
343 if !comments.is_empty() {
344 for line in comments.lines() {
345 s.push_str(indent);
346 s.push_str(line);
347 s.push('\n');
348 }
349 }
350
351 s.push_str(indent);
352 s.push_str("export type ");
353 s.push_str(&name);
354 write_generic_parameters(s, exporter, types, &[rust_type_path(ndt)], &ndt.generics)?;
355 s.push_str(" = ");
356
357 datatype(
358 s,
359 exporter,
360 format,
361 types,
362 ndt.ty.as_ref().expect("named datatype must have a body"),
363 vec![rust_type_path(ndt)],
364 Some(ndt.name.as_ref()),
365 indent,
366 Default::default(),
367 )
368 .map_err(|err| err.with_named_datatype(ndt))?;
369 s.push_str(";\n");
370
371 Ok(())
372}
373
374pub fn inline(
385 exporter: &dyn AsRef<Exporter>,
386 types: &Types,
387 dt: &DataType,
388) -> Result<String, Error> {
389 let mut s = String::new();
390 inline_datatype(
391 &mut s,
392 exporter.as_ref(),
393 None,
394 types,
395 dt,
396 vec![],
397 None,
398 "",
399 0,
400 &[],
401 )?;
402 Ok(s)
403}
404
405pub(crate) fn typedef_internal(
408 s: &mut String,
409 exporter: &Exporter,
410 format: Option<&dyn Format>,
411 types: &Types,
412 dt: &NamedDataType,
413) -> Result<(), Error> {
414 s.push_str("/**\n");
415 append_typedef_body(s, exporter, format, types, dt, "")?;
416
417 s.push_str("\t*/");
418
419 Ok(())
420}
421
422fn append_jsdoc_properties(
423 s: &mut String,
424 exporter: &Exporter,
425 format: Option<&dyn Format>,
426 types: &Types,
427 dt_name: &str,
428 dt: &DataType,
429 indent: &str,
430) -> Result<(), Error> {
431 match dt {
432 DataType::Struct(strct) => match &strct.fields {
433 Fields::Unit => {}
434 Fields::Unnamed(unnamed) => {
435 for (idx, field) in unnamed.fields.iter().enumerate() {
436 let Some(ty) = field.ty.as_ref() else {
437 continue;
438 };
439
440 let mut ty_str = String::new();
441 let datatype_prefix = format!("{indent}\t*\t");
442 datatype(
443 &mut ty_str,
444 exporter,
445 format,
446 types,
447 ty,
448 vec![Cow::Owned(dt_name.to_owned()), idx.to_string().into()],
449 Some(dt_name),
450 &datatype_prefix,
451 Default::default(),
452 )?;
453
454 push_jsdoc_property(
455 s,
456 &ty_str,
457 &idx.to_string(),
458 field.optional,
459 &field.docs,
460 field.deprecated.as_ref(),
461 indent,
462 );
463 }
464 }
465 Fields::Named(named) => {
466 for (name, field) in &named.fields {
467 let Some(ty) = field.ty.as_ref() else {
468 continue;
469 };
470
471 let mut ty_str = String::new();
472 let datatype_prefix = format!("{indent}\t*\t");
473 datatype(
474 &mut ty_str,
475 exporter,
476 format,
477 types,
478 ty,
479 vec![Cow::Owned(dt_name.to_owned()), name.clone()],
480 Some(dt_name),
481 &datatype_prefix,
482 Default::default(),
483 )?;
484
485 push_jsdoc_property(
486 s,
487 &ty_str,
488 name,
489 field.optional,
490 &field.docs,
491 field.deprecated.as_ref(),
492 indent,
493 );
494 }
495 }
496 },
497 DataType::Enum(enm) => {
498 for (variant_name, variant) in enm.variants.iter().filter(|(_, v)| !v.skip) {
499 let mut one_variant_enum = enm.clone();
500 one_variant_enum
501 .variants
502 .retain(|(name, _)| name == variant_name);
503
504 let mut variant_ty = String::new();
505 enum_dt(
506 &mut variant_ty,
507 exporter,
508 types,
509 &one_variant_enum,
510 vec![Cow::Owned(dt_name.to_owned())],
511 "",
512 &[],
513 )?;
514
515 push_jsdoc_property(
516 s,
517 &variant_ty,
518 variant_name,
519 false,
520 &variant.docs,
521 variant.deprecated.as_ref(),
522 indent,
523 );
524 }
525 }
526 DataType::Intersection(types_) => {
527 for ty in types_ {
528 append_jsdoc_properties(s, exporter, format, types, dt_name, ty, indent)?;
529 }
530 }
531 _ => {}
532 }
533
534 Ok(())
535}
536
537fn push_jsdoc_property(
538 s: &mut String,
539 ty: &str,
540 name: &str,
541 optional: bool,
542 docs: &str,
543 deprecated: Option<&Deprecated>,
544 indent: &str,
545) {
546 s.push_str(indent);
547 s.push_str("\t* @property {");
548 push_jsdoc_type(s, ty, indent);
549 s.push_str("} ");
550 s.push_str(&jsdoc_property_name(name, optional));
551
552 if let Some(description) = jsdoc_description(docs, deprecated) {
553 s.push_str(" - ");
554 s.push_str(&description);
555 }
556
557 s.push('\n');
558}
559
560fn push_jsdoc_type(s: &mut String, ty: &str, indent: &str) {
561 let mut lines = ty.lines();
562 if let Some(first_line) = lines.next() {
563 s.push_str(first_line);
564 }
565
566 for line in lines {
567 s.push('\n');
568
569 if line
570 .strip_prefix(indent)
571 .is_some_and(|rest| rest.starts_with("\t*"))
572 {
573 s.push_str(line);
574 } else {
575 s.push_str(indent);
576 s.push_str("\t* ");
577 s.push_str(line);
578 }
579 }
580}
581
582fn jsdoc_property_name(name: &str, optional: bool) -> String {
583 let name = if is_identifier(name) {
584 name.to_string()
585 } else {
586 format!("\"{}\"", escape_typescript_string_literal(name))
587 };
588
589 if optional { format!("[{name}]") } else { name }
590}
591
592fn append_typedef_body(
593 s: &mut String,
594 exporter: &Exporter,
595 format: Option<&dyn Format>,
596 types: &Types,
597 dt: &NamedDataType,
598 indent: &str,
599) -> Result<(), Error> {
600 let name = &dt.name;
601 let mut type_name = String::from(name.as_ref());
602 write_generic_parameters(
603 &mut type_name,
604 exporter,
605 types,
606 &[rust_type_path(dt)],
607 &dt.generics,
608 )?;
609
610 let mut typedef_ty = String::new();
611 let datatype_prefix = format!("{indent}\t*\t");
612 datatype(
613 &mut typedef_ty,
614 exporter,
615 format,
616 types,
617 dt.ty.as_ref().expect("named datatype must have a body"),
618 vec![rust_type_path(dt)],
619 Some(dt.name.as_ref()),
620 &datatype_prefix,
621 Default::default(),
622 )
623 .map_err(|err| err.with_named_datatype(dt))?;
624
625 if !dt.docs.is_empty() {
626 for line in dt.docs.lines() {
627 s.push_str(indent);
628 s.push_str("\t* ");
629 s.push_str(&escape_jsdoc_text(line));
630 s.push('\n');
631 }
632 s.push_str(indent);
633 s.push_str("\t*\n");
634 }
635
636 if let Some(deprecated) = dt.deprecated.as_ref() {
637 s.push_str(indent);
638 s.push_str("\t* @deprecated");
639 if let Some(details) = deprecated_details(deprecated) {
640 s.push(' ');
641 s.push_str(&details);
642 }
643 s.push('\n');
644 }
645
646 s.push_str(indent);
647 s.push_str("\t* @typedef {");
648 push_jsdoc_type(s, &typedef_ty, indent);
649 s.push_str("} ");
650 s.push_str(&type_name);
651 s.push('\n');
652
653 if let Some(ty) = &dt.ty {
654 let dt_path = rust_type_path(dt);
655 append_jsdoc_properties(s, exporter, format, types, dt_path.as_ref(), ty, indent)?;
656 }
657
658 Ok(())
659}
660
661fn write_generic_parameters(
662 s: &mut String,
663 exporter: &Exporter,
664 types: &Types,
665 parent_location: &[Cow<'static, str>],
666 generics: &[GenericDefinition],
667) -> Result<(), Error> {
668 if generics.is_empty() {
669 return Ok(());
670 }
671
672 s.push('<');
673 for (index, generic) in generics.iter().enumerate() {
674 if index != 0 {
675 s.push_str(", ");
676 }
677
678 s.push_str(generic.name.as_ref());
679
680 if let Some(default) = &generic.default {
681 let mut rendered_default = String::new();
682 let mut default_location = parent_location.to_vec();
683 default_location.push(format!("<generic {} default>", generic.name).into());
684 shallow_inline_datatype(
685 &mut rendered_default,
686 exporter,
687 None,
688 types,
689 default,
690 default_location,
691 None,
692 "",
693 Default::default(),
694 )?;
695 s.push_str(" = ");
696 s.push_str(&rendered_default);
697 }
698 }
699 s.push('>');
700
701 Ok(())
702}
703
704fn jsdoc_description(docs: &str, deprecated: Option<&Deprecated>) -> Option<String> {
705 let docs = docs
706 .lines()
707 .map(str::trim)
708 .filter(|line| !line.is_empty())
709 .map(|line| escape_jsdoc_text(line).into_owned())
710 .collect::<Vec<_>>()
711 .join(" ");
712
713 let deprecated = deprecated.map(|deprecated| {
714 let mut value = String::from("@deprecated");
715 if let Some(details) = deprecated_details(deprecated) {
716 value.push(' ');
717 value.push_str(&escape_jsdoc_text(&details));
718 }
719 value
720 });
721
722 match (docs.is_empty(), deprecated) {
723 (true, None) => None,
724 (true, Some(deprecated)) => Some(deprecated),
725 (false, None) => Some(docs),
726 (false, Some(deprecated)) => Some(format!("{docs} {deprecated}")),
727 }
728}
729
730pub fn reference(
736 exporter: &dyn AsRef<Exporter>,
737 types: &Types,
738 r: &Reference,
739) -> Result<String, Error> {
740 let mut s = String::new();
741 datatype(
742 &mut s,
743 exporter.as_ref(),
744 None,
745 types,
746 &DataType::Reference(r.clone()),
747 vec![],
748 None,
749 "",
750 &[],
751 )?;
752 Ok(s)
753}
754
755pub(crate) fn datatype_with_inline_attr(
756 s: &mut String,
757 exporter: &Exporter,
758 format: Option<&dyn Format>,
759 types: &Types,
760 dt: &DataType,
761 location: Vec<Cow<'static, str>>,
762 parent_name: Option<&str>,
763 prefix: &str,
764 generics: &[(GenericReference, DataType)],
765 shallow_inline: bool,
766) -> Result<(), Error> {
767 if shallow_inline {
768 let inline_path = path_string(&location);
769 return shallow_inline_datatype(
770 s,
771 exporter,
772 format,
773 types,
774 dt,
775 location,
776 parent_name,
777 prefix,
778 generics,
779 )
780 .map_err(|err| err.with_inline_trace(inline_named_datatype(types, dt), inline_path));
781 }
782
783 datatype(
784 s,
785 exporter,
786 format,
787 types,
788 dt,
789 location,
790 parent_name,
791 prefix,
792 generics,
793 )
794}
795
796fn write_generic_reference(s: &mut String, generic: &GenericReference) {
797 s.push_str(generic.name());
798}
799
800fn scoped_reference_generics(
801 parent_generics: &[(GenericReference, DataType)],
802 reference_generics: &[(GenericReference, DataType)],
803) -> Vec<(GenericReference, DataType)> {
804 parent_generics
805 .iter()
806 .filter(|(parent_generic, _)| {
807 !reference_generics
808 .iter()
809 .any(|(child_generic, _)| child_generic == parent_generic)
810 })
811 .cloned()
812 .collect()
813}
814
815fn named_reference_generics(r: &NamedReference) -> Result<&[(GenericReference, DataType)], Error> {
816 match &r.inner {
817 NamedReferenceType::Reference { generics, .. } => Ok(generics),
818 NamedReferenceType::Inline { .. } => Ok(&[]),
819 NamedReferenceType::Recursive(_) => Ok(&[]),
820 }
821}
822
823fn named_reference_ty<'a>(
824 types: &'a Types,
825 r: &'a NamedReference,
826 location: &[Cow<'static, str>],
827) -> Result<&'a DataType, Error> {
828 let path = path_string(location);
829 match &r.inner {
830 NamedReferenceType::Reference { .. } => types
831 .get(r)
832 .and_then(|ndt| ndt.ty.as_ref())
833 .ok_or_else(|| Error::dangling_named_reference(path, format!("{r:?}"))),
834 NamedReferenceType::Inline { dt, .. } => Ok(dt),
835 NamedReferenceType::Recursive(cycle) => Err(Error::infinite_recursive_inline_type(
836 path,
837 format!("{r:?}"),
838 cycle.clone(),
839 )),
840 }
841}
842
843fn inline_named_datatype<'a>(types: &'a Types, dt: &DataType) -> Option<&'a NamedDataType> {
844 match dt {
845 DataType::Reference(Reference::Named(r)) => types.get(r),
846 _ => None,
847 }
848}
849
850fn resolve_scoped_generic_default(
851 default: &DataType,
852 scoped_generics: &[(GenericReference, DataType)],
853) -> DataType {
854 match default {
855 DataType::Generic(default) => scoped_generics
856 .iter()
857 .find_map(|(reference, dt)| (reference == default).then_some(dt.clone()))
858 .unwrap_or_else(|| DataType::Generic(default.clone())),
859 default => default.clone(),
860 }
861}
862
863fn resolved_reference_generics(
864 ndt: &specta::datatype::NamedDataType,
865 r: &NamedReference,
866 parent_generics: &[(GenericReference, DataType)],
867) -> Option<(Vec<DataType>, bool, Vec<(GenericReference, DataType)>)> {
868 let reference_generics = named_reference_generics(r).ok()?;
869 let mut scoped_generics = scoped_reference_generics(parent_generics, reference_generics);
870 let mut all_default = true;
871 let mut rendered_generics = Vec::with_capacity(ndt.generics.len());
872
873 for generic in ndt.generics.iter() {
874 let explicit = reference_generics
875 .iter()
876 .find(|(reference, _)| *reference == generic.reference())
877 .map(|(_, dt)| dt.clone());
878
879 let resolved_default = generic
880 .default
881 .as_ref()
882 .map(|default| resolve_scoped_generic_default(default, &scoped_generics));
883
884 let resolved = explicit.or_else(|| resolved_default.clone()).or_else(|| {
885 Some(DataType::Reference(Reference::opaque(
886 crate::opaque::Unknown,
887 )))
888 });
889
890 let resolved = resolved?;
891 all_default &= resolved_default
892 .as_ref()
893 .is_some_and(|default| default == &resolved);
894 scoped_generics.push((generic.reference(), resolved.clone()));
895 rendered_generics.push(resolved);
896 }
897
898 Some((rendered_generics, all_default, scoped_generics))
899}
900
901#[derive(Clone, Copy)]
902struct RenderCtx<'a> {
903 exporter: &'a Exporter,
904 format: Option<&'a dyn Format>,
905 types: &'a Types,
906 parent_name: Option<&'a str>,
907 prefix: &'a str,
908 generics: &'a [(GenericReference, DataType)],
909}
910
911#[derive(Clone, Copy)]
912enum RenderMode {
913 Normal,
914 ShallowInline,
915}
916
917impl RenderMode {
918 fn render(
919 self,
920 s: &mut String,
921 ctx: RenderCtx<'_>,
922 dt: &DataType,
923 location: Vec<Cow<'static, str>>,
924 ) -> Result<(), Error> {
925 match self {
926 Self::Normal => datatype(
927 s,
928 ctx.exporter,
929 ctx.format,
930 ctx.types,
931 dt,
932 location,
933 ctx.parent_name,
934 ctx.prefix,
935 ctx.generics,
936 ),
937 Self::ShallowInline => shallow_inline_datatype(
938 s,
939 ctx.exporter,
940 ctx.format,
941 ctx.types,
942 dt,
943 location,
944 ctx.parent_name,
945 ctx.prefix,
946 ctx.generics,
947 ),
948 }
949 }
950
951 fn render_intersection_part(
952 self,
953 s: &mut String,
954 ctx: RenderCtx<'_>,
955 dt: &DataType,
956 location: Vec<Cow<'static, str>>,
957 ) -> Result<(), Error> {
958 match (self, dt) {
959 (Self::ShallowInline, DataType::Reference(r)) => reference_dt(
960 s,
961 ctx.exporter,
962 ctx.format,
963 ctx.types,
964 r,
965 location,
966 ctx.prefix,
967 ctx.generics,
968 ),
969 _ => self.render(s, ctx, dt, location),
970 }
971 }
972}
973
974fn render_datatype(
975 s: &mut String,
976 ctx: RenderCtx<'_>,
977 dt: &DataType,
978 location: Vec<Cow<'static, str>>,
979 mode: RenderMode,
980) -> Result<(), Error> {
981 match (mode, dt) {
982 (_, DataType::Primitive(p)) => s.push_str(primitive_dt(p, location)?),
983 (_, DataType::Generic(g)) => write_generic_reference(s, g),
984 (RenderMode::Normal, DataType::List(list)) => {
985 list_dt(s, ctx.exporter, ctx.types, list, location, ctx.generics)?;
986 }
987 (RenderMode::ShallowInline, DataType::List(list)) => {
988 let mut inner = String::new();
989 render_datatype(&mut inner, ctx, &list.ty, location, mode)?;
990 push_list(s, &inner, list.length);
991 }
992 (RenderMode::Normal, DataType::Map(map)) => {
993 map_dt(
994 s,
995 ctx.exporter,
996 ctx.format,
997 ctx.types,
998 map,
999 location,
1000 ctx.generics,
1001 )?;
1002 }
1003 (RenderMode::ShallowInline, DataType::Map(map)) => render_map(
1004 s,
1005 ctx.exporter,
1006 ctx.format,
1007 ctx.types,
1008 map,
1009 location,
1010 ctx.parent_name,
1011 ctx.prefix,
1012 ctx.generics,
1013 mode,
1014 )?,
1015 (_, DataType::Nullable(inner)) => {
1016 let mut rendered = String::new();
1017 let child_ctx = RenderCtx { prefix: "", ..ctx };
1018 render_datatype(&mut rendered, child_ctx, inner, location, mode)?;
1019 push_nullable(s, &rendered);
1020 }
1021 (_, DataType::Struct(st)) => struct_dt(
1022 s,
1023 ctx.exporter,
1024 ctx.format,
1025 ctx.types,
1026 st,
1027 location,
1028 ctx.parent_name,
1029 ctx.prefix,
1030 ctx.generics,
1031 )?,
1032 (_, DataType::Enum(enm)) => enum_dt(
1033 s,
1034 ctx.exporter,
1035 ctx.types,
1036 enm,
1037 location,
1038 ctx.prefix,
1039 ctx.generics,
1040 )?,
1041 (RenderMode::Normal, DataType::Tuple(tuple)) => {
1042 tuple_dt(s, ctx.exporter, ctx.types, tuple, location, ctx.generics)?;
1043 }
1044 (RenderMode::ShallowInline, DataType::Tuple(tuple)) => match tuple.elements.as_slice() {
1045 [] => s.push_str(NULL),
1046 elements => {
1047 s.push('[');
1048 for (idx, dt) in elements.iter().enumerate() {
1049 if idx != 0 {
1050 s.push_str(", ");
1051 }
1052 render_datatype(s, ctx, dt, location.clone(), mode)?;
1053 }
1054 s.push(']');
1055 }
1056 },
1057 (RenderMode::Normal, DataType::Intersection(parts)) => {
1058 for (idx, ty) in parts.iter().enumerate() {
1059 if idx != 0 {
1060 s.push_str(" & ");
1061 }
1062 render_datatype(s, ctx, ty, location.clone(), mode)?;
1063 }
1064 }
1065 (RenderMode::ShallowInline, DataType::Intersection(parts)) => intersection_dt(
1066 s,
1067 ctx.exporter,
1068 ctx.format,
1069 ctx.types,
1070 parts,
1071 location,
1072 ctx.parent_name,
1073 ctx.prefix,
1074 ctx.generics,
1075 mode,
1076 )?,
1077 (RenderMode::Normal, DataType::Reference(r)) => reference_dt(
1078 s,
1079 ctx.exporter,
1080 ctx.format,
1081 ctx.types,
1082 r,
1083 location,
1084 ctx.prefix,
1085 ctx.generics,
1086 )?,
1087 (RenderMode::ShallowInline, DataType::Reference(r)) => match r {
1088 Reference::Named(r) => {
1089 let ty = named_reference_ty(ctx.types, r, &location)?;
1090 let reference_generics = named_reference_generics(r)?;
1091 let child_ctx = RenderCtx {
1092 generics: reference_generics,
1093 ..ctx
1094 };
1095 let inline_path = path_string(&location);
1096 render_datatype(s, child_ctx, ty, location, mode)
1097 .map_err(|err| err.with_inline_trace(ctx.types.get(r), inline_path))?;
1098 }
1099 Reference::Opaque(_) => reference_dt(
1100 s,
1101 ctx.exporter,
1102 ctx.format,
1103 ctx.types,
1104 r,
1105 location,
1106 ctx.prefix,
1107 ctx.generics,
1108 )?,
1109 },
1110 }
1111
1112 Ok(())
1113}
1114
1115fn needs_array_parens(ty: &str) -> bool {
1116 ty.contains(' ') && (!ty.ends_with('}') || ty.contains('&') || ty.contains('|'))
1117}
1118
1119fn push_list(s: &mut String, ty: &str, length: Option<usize>) {
1120 let ty = if needs_array_parens(ty) {
1121 Cow::Owned(format!("({ty})"))
1122 } else {
1123 Cow::Borrowed(ty)
1124 };
1125
1126 if let Some(length) = length {
1127 s.push('[');
1128 for i in 0..length {
1129 if i != 0 {
1130 s.push_str(", ");
1131 }
1132 s.push_str(&ty);
1133 }
1134 s.push(']');
1135 } else {
1136 s.push_str(&ty);
1137 s.push_str("[]");
1138 }
1139}
1140
1141fn push_nullable(s: &mut String, inner: &str) {
1142 s.push_str(inner);
1143 if inner != NULL && !inner.ends_with(" | null") {
1144 s.push_str(" | null");
1145 }
1146}
1147
1148fn is_exhaustive_map_key(dt: &DataType, types: &Types) -> bool {
1149 match dt {
1150 DataType::Enum(e) => e.variants.iter().filter(|(_, v)| !v.skip).count() == 0,
1151 DataType::Reference(Reference::Named(r)) => named_reference_ty(types, r, &[])
1152 .map(|ty| is_exhaustive_map_key(ty, types))
1153 .unwrap_or(false),
1154 DataType::Reference(Reference::Opaque(_)) => false,
1155 _ => true,
1156 }
1157}
1158
1159fn render_map(
1160 s: &mut String,
1161 exporter: &Exporter,
1162 format: Option<&dyn Format>,
1163 types: &Types,
1164 map: &Map,
1165 location: Vec<Cow<'static, str>>,
1166 parent_name: Option<&str>,
1167 prefix: &str,
1168 generics: &[(GenericReference, DataType)],
1169 value_mode: RenderMode,
1170) -> Result<(), Error> {
1171 let path = map_key_path(&location);
1172 map_keys::validate_map_key(map.key_ty(), types, format!("{path}.<map_key>"))?;
1173
1174 let rendered_key = map_key_render_type(map.key_ty().clone());
1175 let exhaustive = is_exhaustive_map_key(&rendered_key, types);
1176
1177 if !exhaustive {
1179 s.push_str("Partial<");
1180 }
1181
1182 s.push_str("{ [key in ");
1183 map_key_datatype(
1184 s,
1185 exporter,
1186 format,
1187 types,
1188 &rendered_key,
1189 location.clone(),
1190 parent_name,
1191 prefix,
1192 generics,
1193 )?;
1194 s.push_str("]: ");
1195 value_mode.render(
1196 s,
1197 RenderCtx {
1198 exporter,
1199 format,
1200 types,
1201 parent_name,
1202 prefix,
1203 generics,
1204 },
1205 map.value_ty(),
1206 location,
1207 )?;
1208 s.push_str(" }");
1209
1210 if !exhaustive {
1211 s.push('>');
1212 }
1213
1214 Ok(())
1215}
1216
1217fn shallow_inline_datatype(
1218 s: &mut String,
1219 exporter: &Exporter,
1220 format: Option<&dyn Format>,
1221 types: &Types,
1222 dt: &DataType,
1223 location: Vec<Cow<'static, str>>,
1224 parent_name: Option<&str>,
1225 prefix: &str,
1226 generics: &[(GenericReference, DataType)],
1227) -> Result<(), Error> {
1228 render_datatype(
1229 s,
1230 RenderCtx {
1231 exporter,
1232 format,
1233 types,
1234 parent_name,
1235 prefix,
1236 generics,
1237 },
1238 dt,
1239 location,
1240 RenderMode::ShallowInline,
1241 )
1242}
1243
1244fn intersection_dt(
1245 s: &mut String,
1246 exporter: &Exporter,
1247 format: Option<&dyn Format>,
1248 types: &Types,
1249 parts: &[DataType],
1250 location: Vec<Cow<'static, str>>,
1251 parent_name: Option<&str>,
1252 prefix: &str,
1253 generics: &[(GenericReference, DataType)],
1254 mode: RenderMode,
1255) -> Result<(), Error> {
1256 let mut rendered = Vec::with_capacity(parts.len());
1257 for part in parts {
1258 let mut out = String::new();
1259 mode.render_intersection_part(
1260 &mut out,
1261 RenderCtx {
1262 exporter,
1263 format,
1264 types,
1265 parent_name,
1266 prefix,
1267 generics,
1268 },
1269 part,
1270 location.clone(),
1271 )?;
1272 rendered.push(format!("({out})"));
1273 }
1274
1275 s.push_str(&rendered.join(" & "));
1276 Ok(())
1277}
1278
1279fn inline_datatype(
1281 s: &mut String,
1282 exporter: &Exporter,
1283 format: Option<&dyn Format>,
1284 types: &Types,
1285 dt: &DataType,
1286 location: Vec<Cow<'static, str>>,
1287 parent_name: Option<&str>,
1288 prefix: &str,
1289 depth: usize,
1290 generics: &[(GenericReference, DataType)],
1291) -> Result<(), Error> {
1292 if depth == 25 {
1294 return Err(Error::inline_recursion_limit_exceeded(path_string(
1295 &location,
1296 )));
1297 }
1298
1299 match dt {
1300 DataType::Primitive(p) => s.push_str(primitive_dt(p, location)?),
1301 DataType::Generic(g) => write_generic_reference(s, g),
1302 DataType::List(l) => {
1303 let mut dt_str = String::new();
1304 datatype(
1305 &mut dt_str,
1306 exporter,
1307 format,
1308 types,
1309 &l.ty,
1310 location.clone(),
1311 parent_name,
1312 prefix,
1313 generics,
1314 )?;
1315 push_list(s, &dt_str, l.length);
1316 }
1317 DataType::Map(m) => map_dt(s, exporter, format, types, m, location, generics)?,
1318 DataType::Nullable(def) => {
1319 let mut inner = String::new();
1320 inline_datatype(
1321 &mut inner,
1322 exporter,
1323 format,
1324 types,
1325 def,
1326 location,
1327 parent_name,
1328 "",
1329 depth + 1,
1330 generics,
1331 )?;
1332
1333 push_nullable(s, &inner);
1334 }
1335 DataType::Struct(st) => {
1336 if !generics.is_empty() {
1337 inline_struct_with_generics(
1338 s,
1339 exporter,
1340 format,
1341 types,
1342 st,
1343 location,
1344 parent_name,
1345 prefix,
1346 depth,
1347 generics,
1348 )?;
1349 } else {
1350 struct_dt(
1351 s,
1352 exporter,
1353 format,
1354 types,
1355 st,
1356 location,
1357 parent_name,
1358 prefix,
1359 generics,
1360 )?;
1361 }
1362 }
1363 DataType::Enum(e) => enum_dt(s, exporter, types, e, location, prefix, generics)?,
1364 DataType::Tuple(t) => tuple_dt(s, exporter, types, t, location, generics)?,
1365 DataType::Intersection(types_) => intersection_dt(
1366 s,
1367 exporter,
1368 format,
1369 types,
1370 types_,
1371 location,
1372 parent_name,
1373 prefix,
1374 generics,
1375 RenderMode::Normal,
1376 )?,
1377 DataType::Reference(r) => {
1378 if let Reference::Named(r) = r
1379 && let Ok(ty) = named_reference_ty(types, r, &location)
1380 {
1381 let reference_generics = named_reference_generics(r)?;
1382 let inline_path = path_string(&location);
1383 inline_datatype(
1384 s,
1385 exporter,
1386 format,
1387 types,
1388 ty,
1389 location,
1390 parent_name,
1391 prefix,
1392 depth + 1,
1393 reference_generics,
1394 )
1395 .map_err(|err| err.with_inline_trace(types.get(r), inline_path))?;
1396 } else {
1397 reference_dt(s, exporter, format, types, r, location, prefix, generics)?;
1398 }
1399 }
1400 }
1401
1402 Ok(())
1403}
1404
1405fn inline_struct_with_generics(
1406 s: &mut String,
1407 exporter: &Exporter,
1408 format: Option<&dyn Format>,
1409 types: &Types,
1410 st: &Struct,
1411 location: Vec<Cow<'static, str>>,
1412 parent_name: Option<&str>,
1413 prefix: &str,
1414 depth: usize,
1415 generics: &[(GenericReference, DataType)],
1416) -> Result<(), Error> {
1417 match &st.fields {
1418 Fields::Unit => s.push_str(NULL),
1419 Fields::Unnamed(_) => struct_dt(
1420 s,
1421 exporter,
1422 format,
1423 types,
1424 st,
1425 location,
1426 parent_name,
1427 prefix,
1428 generics,
1429 )?,
1430 Fields::Named(named) => {
1431 s.push('{');
1432 let mut has_field = false;
1433
1434 for (key, field) in &named.fields {
1435 let Some(field_ty) = field.ty.as_ref() else {
1436 continue;
1437 };
1438
1439 has_field = true;
1440 s.push('\n');
1441 s.push_str(prefix);
1442 s.push('\t');
1443 s.push_str(&sanitise_key(key.clone(), false));
1444 if field.optional {
1445 s.push('?');
1446 }
1447 s.push_str(": ");
1448 inline_datatype(
1449 s,
1450 exporter,
1451 format,
1452 types,
1453 field_ty,
1454 location.clone(),
1455 parent_name,
1456 prefix,
1457 depth + 1,
1458 generics,
1459 )?;
1460 s.push(',');
1461 }
1462
1463 if has_field {
1464 s.push('\n');
1465 s.push_str(prefix);
1466 }
1467
1468 s.push('}');
1469 }
1470 }
1471
1472 Ok(())
1473}
1474
1475pub(crate) fn datatype(
1476 s: &mut String,
1477 exporter: &Exporter,
1478 format: Option<&dyn Format>,
1479 types: &Types,
1480 dt: &DataType,
1481 location: Vec<Cow<'static, str>>,
1482 parent_name: Option<&str>,
1483 prefix: &str,
1484 generics: &[(GenericReference, DataType)],
1485) -> Result<(), Error> {
1486 render_datatype(
1487 s,
1488 RenderCtx {
1489 exporter,
1490 format,
1491 types,
1492 parent_name,
1493 prefix,
1494 generics,
1495 },
1496 dt,
1497 location,
1498 RenderMode::Normal,
1499 )
1500}
1501
1502fn primitive_dt(p: &Primitive, location: Vec<Cow<'static, str>>) -> Result<&'static str, Error> {
1503 use Primitive::*;
1504
1505 Ok(match p {
1506 i8 | i16 | i32 | u8 | u16 | u32 => "number",
1507 f16 | f32 | f64 => "number | null",
1509 usize | isize | i64 | u64 | i128 | u128 | f128 => {
1510 return Err(Error::bigint_forbidden(location.join(".")));
1511 }
1512 Primitive::bool => "boolean",
1513 str | char => "string",
1514 })
1515}
1516
1517fn list_dt(
1518 s: &mut String,
1519 exporter: &Exporter,
1520 types: &Types,
1521 l: &List,
1522 location: Vec<Cow<'static, str>>,
1523 generics: &[(GenericReference, DataType)],
1524) -> Result<(), Error> {
1525 let mut dt = String::new();
1526 datatype(
1527 &mut dt, exporter, None, types, &l.ty, location, None, "", generics,
1528 )?;
1529 push_list(s, &dt, l.length);
1530
1531 Ok(())
1532}
1533
1534fn map_key_datatype(
1535 s: &mut String,
1536 exporter: &Exporter,
1537 format: Option<&dyn Format>,
1538 types: &Types,
1539 key_ty: &DataType,
1540 location: Vec<Cow<'static, str>>,
1541 parent_name: Option<&str>,
1542 prefix: &str,
1543 generics: &[(GenericReference, DataType)],
1544) -> Result<(), Error> {
1545 match key_ty {
1546 DataType::Reference(r) => {
1547 reference_dt(s, exporter, format, types, r, location, prefix, generics)
1548 }
1549 key_ty => shallow_inline_datatype(
1550 s,
1551 exporter,
1552 format,
1553 types,
1554 key_ty,
1555 location,
1556 parent_name,
1557 prefix,
1558 generics,
1559 ),
1560 }
1561}
1562
1563fn map_dt(
1564 s: &mut String,
1565 exporter: &Exporter,
1566 format: Option<&dyn Format>,
1567 types: &Types,
1568 m: &Map,
1569 location: Vec<Cow<'static, str>>,
1570 generics: &[(GenericReference, DataType)],
1571) -> Result<(), Error> {
1572 render_map(
1573 s,
1574 exporter,
1575 format,
1576 types,
1577 m,
1578 location,
1579 None,
1580 "",
1581 generics,
1582 RenderMode::Normal,
1583 )
1584}
1585
1586fn map_key_path(location: &[Cow<'static, str>]) -> String {
1587 if location.is_empty() {
1588 return "HashMap".to_string();
1589 }
1590
1591 location.join(".")
1592}
1593
1594fn map_key_render_type(dt: DataType) -> DataType {
1595 if matches!(dt, DataType::Primitive(Primitive::bool)) {
1596 return bool_key_literal_datatype();
1597 }
1598
1599 dt
1600}
1601
1602fn bool_key_literal_datatype() -> DataType {
1603 let mut bool_enum = Enum::default();
1604 bool_enum
1605 .variants
1606 .push((Cow::Borrowed("true"), Variant::unit()));
1607 bool_enum
1608 .variants
1609 .push((Cow::Borrowed("false"), Variant::unit()));
1610 DataType::Enum(bool_enum)
1611}
1612
1613fn unnamed_fields_datatype(
1614 s: &mut String,
1615 exporter: &Exporter,
1616 format: Option<&dyn Format>,
1617 types: &Types,
1618 fields: &[(&Field, &DataType)],
1619 location: Vec<Cow<'static, str>>,
1620 parent_name: Option<&str>,
1621 prefix: &str,
1622 generics: &[(GenericReference, DataType)],
1623 force_inline: bool,
1624) -> Result<(), Error> {
1625 match fields {
1626 [(field, ty)] => {
1627 let mut v = String::new();
1628 datatype_with_inline_attr(
1629 &mut v,
1630 exporter,
1631 format,
1632 types,
1633 ty,
1634 location,
1635 parent_name,
1636 "",
1637 generics,
1638 force_inline,
1639 )?;
1640 s.push_str(&inner_comments(
1641 field.deprecated.as_ref(),
1642 &field.docs,
1643 v,
1644 true,
1645 prefix,
1646 ));
1647 }
1648 fields => {
1649 s.push('[');
1650 for (i, (field, ty)) in fields.iter().enumerate() {
1651 if i != 0 {
1652 s.push_str(", ");
1653 }
1654
1655 let mut v = String::new();
1656 let mut field_location = location.clone();
1657 field_location.push(i.to_string().into());
1658 datatype_with_inline_attr(
1659 &mut v,
1660 exporter,
1661 format,
1662 types,
1663 ty,
1664 field_location,
1665 parent_name,
1666 "",
1667 generics,
1668 force_inline,
1669 )?;
1670 s.push_str(&inner_comments(
1671 field.deprecated.as_ref(),
1672 &field.docs,
1673 v,
1674 true,
1675 prefix,
1676 ));
1677 }
1678 s.push(']');
1679 }
1680 }
1681
1682 Ok(())
1683}
1684
1685fn struct_dt(
1686 s: &mut String,
1687 exporter: &Exporter,
1688 format: Option<&dyn Format>,
1689 types: &Types,
1690 st: &Struct,
1691 location: Vec<Cow<'static, str>>,
1692 parent_name: Option<&str>,
1693 prefix: &str,
1694 generics: &[(GenericReference, DataType)],
1695) -> Result<(), Error> {
1696 match &st.fields {
1697 Fields::Unit => s.push_str(NULL),
1698 Fields::Unnamed(unnamed) => unnamed_fields_datatype(
1699 s,
1700 exporter,
1701 format,
1702 types,
1703 &unnamed
1704 .fields
1705 .iter()
1706 .filter_map(|field| field.ty.as_ref().map(|ty| (field, ty)))
1707 .collect::<Vec<_>>(),
1708 location,
1709 parent_name,
1710 prefix,
1711 generics,
1712 false,
1713 )?,
1714 Fields::Named(named) => {
1715 let fields = named
1716 .fields
1717 .iter()
1718 .filter_map(|(name, field)| field.ty.as_ref().map(|ty| (name, (field, ty))))
1719 .collect::<Vec<_>>();
1720
1721 if fields.is_empty() {
1722 s.push_str("Record<string, never>");
1723 return Ok(());
1724 }
1725
1726 let mut unflattened_fields = Vec::with_capacity(fields.len());
1727 for (key, (field, ty)) in fields {
1728 let field_prefix = format!("{prefix}\t");
1729 let mut other = String::new();
1730 let mut field_location = location.clone();
1731 field_location.push(key.clone());
1732 object_field_to_ts(
1733 &mut other,
1734 exporter,
1735 format,
1736 types,
1737 key.clone(),
1738 (field, ty),
1739 field_location,
1740 parent_name,
1741 generics,
1742 &field_prefix,
1743 false,
1744 None,
1745 )?;
1746
1747 unflattened_fields.push(inner_comments(
1748 field.deprecated.as_ref(),
1749 &field.docs,
1750 other,
1751 false,
1752 &field_prefix,
1753 ));
1754 }
1755
1756 s.push('{');
1757 for field in unflattened_fields {
1758 s.push('\n');
1759 s.push_str(&field);
1760 s.push(',');
1761 }
1762 s.push('\n');
1763 s.push_str(prefix);
1764 s.push('}');
1765 }
1766 }
1767
1768 Ok(())
1769}
1770
1771fn object_field_to_ts(
1772 s: &mut String,
1773 exporter: &Exporter,
1774 format: Option<&dyn Format>,
1775 types: &Types,
1776 key: Cow<'static, str>,
1777 (field, ty): (&Field, &DataType),
1778 location: Vec<Cow<'static, str>>,
1779 parent_name: Option<&str>,
1780 generics: &[(GenericReference, DataType)],
1781 prefix: &str,
1782 force_inline: bool,
1783 ty_override: Option<&str>,
1784) -> Result<(), Error> {
1785 let field_name_safe = sanitise_key(key, false);
1786 let key = if field.optional {
1787 format!("{field_name_safe}?").into()
1788 } else {
1789 field_name_safe
1790 };
1791
1792 let value = match ty_override {
1793 Some(ty_override) => ty_override.to_string(),
1794 None => {
1795 let mut value = String::new();
1796 datatype_with_inline_attr(
1797 &mut value,
1798 exporter,
1799 format,
1800 types,
1801 ty,
1802 location,
1803 parent_name,
1804 prefix,
1805 generics,
1806 force_inline,
1807 )?;
1808 value
1809 }
1810 };
1811
1812 s.push_str(prefix);
1813 s.push_str(&key);
1814 s.push_str(": ");
1815 s.push_str(&value);
1816
1817 Ok(())
1818}
1819
1820struct EnumVariantOutput {
1821 value: String,
1822 strict_keys: Option<BTreeSet<String>>,
1823}
1824
1825#[derive(Debug, Clone)]
1826struct DiscriminatorAnalysis {
1827 key: String,
1828 known_literals: Vec<String>,
1829 fallback_variant_idx: Option<usize>,
1830}
1831
1832#[derive(Debug, Clone, Copy)]
1833struct VariantTypeOverride<'a> {
1834 key: &'a str,
1835 ty: &'a str,
1836}
1837
1838#[derive(Debug, Clone)]
1839enum DiscriminatorValue {
1840 StringLiteral(String),
1841 String,
1842}
1843
1844fn analyze_discriminator(
1845 variants: &[&(Cow<'static, str>, Variant)],
1846) -> Option<DiscriminatorAnalysis> {
1847 if variants.iter().any(|(name, _)| name.is_empty()) {
1848 return None;
1849 }
1850
1851 let mut key = None::<String>;
1852 let mut known_literals = BTreeSet::new();
1853 let mut fallback_variant_idx = None;
1854
1855 for (idx, (_, variant)) in variants.iter().enumerate() {
1856 let (variant_key, value) = variant_discriminator(variant)?;
1857
1858 if let Some(expected) = &key {
1859 if expected != &variant_key {
1860 return None;
1861 }
1862 } else {
1863 key = Some(variant_key.clone());
1864 }
1865
1866 match value {
1867 DiscriminatorValue::StringLiteral(value) => {
1868 known_literals.insert(value);
1869 }
1870 DiscriminatorValue::String => {
1871 if fallback_variant_idx.replace(idx).is_some() {
1872 return None;
1873 }
1874 }
1875 }
1876 }
1877
1878 if known_literals.is_empty() {
1879 return None;
1880 }
1881
1882 Some(DiscriminatorAnalysis {
1883 key: key?,
1884 known_literals: known_literals.into_iter().collect(),
1885 fallback_variant_idx,
1886 })
1887}
1888
1889fn variant_discriminator(variant: &Variant) -> Option<(String, DiscriminatorValue)> {
1890 let Fields::Named(named) = &variant.fields else {
1891 return None;
1892 };
1893
1894 let (name, field) = named.fields.iter().find(|(_, field)| !field.optional)?;
1895 let ty = field.ty.as_ref()?;
1896
1897 if matches!(ty, DataType::Primitive(Primitive::str)) {
1898 return Some((name.to_string(), DiscriminatorValue::String));
1899 }
1900
1901 string_literal_datatype_value(ty)
1902 .map(|value| (name.to_string(), DiscriminatorValue::StringLiteral(value)))
1903}
1904
1905fn string_literal_datatype_value(ty: &DataType) -> Option<String> {
1906 let DataType::Enum(enm) = ty else {
1907 return None;
1908 };
1909
1910 let mut variants = enm.variants.iter();
1911 let (name, variant) = variants.next()?;
1912 if variants.next().is_some() || !matches!(&variant.fields, Fields::Unit) {
1913 return None;
1914 }
1915
1916 Some(name.to_string())
1917}
1918
1919fn exclude_known_literals_type(literals: &[String]) -> Option<String> {
1920 if literals.is_empty() {
1921 return None;
1922 }
1923
1924 let known = literals
1925 .iter()
1926 .map(|value| format!("\"{}\"", escape_typescript_string_literal(value.as_str())))
1927 .collect::<Vec<_>>()
1928 .join(" | ");
1929
1930 Some(format!("Exclude<string, {known}>"))
1931}
1932
1933fn fallback_discriminator_override(
1934 discriminator: Option<&DiscriminatorAnalysis>,
1935) -> Option<(usize, &str, String)> {
1936 let discriminator = discriminator?;
1937 discriminator.fallback_variant_idx.and_then(|idx| {
1938 exclude_known_literals_type(&discriminator.known_literals)
1939 .map(|ty| (idx, discriminator.key.as_str(), ty))
1940 })
1941}
1942
1943fn untagged_strict_keys(variant: &Variant) -> Option<BTreeSet<String>> {
1944 match &variant.fields {
1945 Fields::Named(obj) => Some(
1946 obj.fields
1947 .iter()
1948 .filter_map(|(name, field)| {
1949 field
1950 .ty
1951 .as_ref()
1952 .map(|_| sanitise_key(name.clone(), false).to_string())
1953 })
1954 .collect(),
1955 ),
1956 _ => None,
1957 }
1958}
1959
1960fn has_anonymous_variant(variants: &[&(Cow<'static, str>, Variant)]) -> bool {
1961 variants.iter().any(|(name, _)| name.is_empty())
1962}
1963
1964fn active_variants(e: &Enum) -> Vec<&(Cow<'static, str>, Variant)> {
1965 e.variants
1966 .iter()
1967 .filter(|(_, variant)| !variant.skip)
1968 .collect()
1969}
1970
1971fn strictify_enum_variants(variants: &mut [EnumVariantOutput]) {
1972 let strict_key_universe = variants
1973 .iter()
1974 .filter_map(|variant| variant.strict_keys.as_ref())
1975 .flat_map(|keys| keys.iter().cloned())
1976 .collect::<BTreeSet<_>>();
1977
1978 if strict_key_universe.len() < 2 {
1979 return;
1980 }
1981
1982 for variant in variants {
1983 let Some(keys) = variant.strict_keys.as_ref() else {
1984 continue;
1985 };
1986
1987 let missing_keys = strict_key_universe
1988 .iter()
1989 .filter(|key| !keys.contains(*key))
1990 .map(|key| format!("{key}?: {NEVER}"))
1991 .collect::<Vec<_>>();
1992
1993 if !missing_keys.is_empty() {
1994 variant.value = format!("({}) & {{ {} }}", variant.value, missing_keys.join("; "));
1995 }
1996 }
1997}
1998
1999fn push_union(s: &mut String, variants: Vec<String>) {
2000 let mut seen = BTreeSet::new();
2001 let variants = variants
2002 .into_iter()
2003 .filter(|variant| seen.insert(variant.clone()))
2004 .collect::<Vec<_>>();
2005
2006 if variants.is_empty() {
2007 s.push_str(NEVER);
2008 } else {
2009 s.push_str(&variants.join(" | "));
2010 }
2011}
2012
2013fn enum_variant_datatype(
2014 exporter: &Exporter,
2015 format: Option<&dyn Format>,
2016 types: &Types,
2017 name: Cow<'static, str>,
2018 variant: &Variant,
2019 location: Vec<Cow<'static, str>>,
2020 prefix: &str,
2021 generics: &[(GenericReference, DataType)],
2022 ty_override: Option<VariantTypeOverride<'_>>,
2023) -> Result<Option<String>, Error> {
2024 match &variant.fields {
2025 Fields::Unit if name.is_empty() => Err(Error::unsupported_anonymous_enum_variant(
2026 path_string(&location),
2027 "unit",
2028 )),
2029 Fields::Unit => Ok(Some(sanitise_key(name, true).to_string())),
2030 Fields::Named(_) if name.is_empty() => Err(Error::unsupported_anonymous_enum_variant(
2031 path_string(&location),
2032 "named-field",
2033 )),
2034 Fields::Named(obj) => {
2035 let mut regular_fields = Vec::new();
2036 for (field_name, field) in &obj.fields {
2037 let Some(ty) = field.ty.as_ref() else {
2038 continue;
2039 };
2040
2041 let mut other = String::new();
2042 let mut field_location = location.clone();
2043 if field_location
2044 .last()
2045 .is_some_and(|location| location == field_name)
2046 {
2047 if !matches!(ty, DataType::Struct(_)) {
2048 field_location.push("0".into());
2049 }
2050 } else {
2051 field_location.push(field_name.clone());
2052 }
2053 object_field_to_ts(
2054 &mut other,
2055 exporter,
2056 format,
2057 types,
2058 field_name.clone(),
2059 (field, ty),
2060 field_location,
2061 None,
2062 generics,
2063 "",
2064 false,
2065 ty_override
2066 .as_ref()
2067 .filter(|override_ty| override_ty.key == field_name.as_ref())
2068 .map(|override_ty| override_ty.ty),
2069 )?;
2070
2071 regular_fields.push(inner_comments(
2072 field.deprecated.as_ref(),
2073 &field.docs,
2074 other,
2075 true,
2076 prefix,
2077 ));
2078 }
2079
2080 Ok(Some(if regular_fields.is_empty() {
2081 format!("Record<{STRING}, {NEVER}>")
2082 } else {
2083 format!("{{ {} }}", regular_fields.join("; "))
2084 }))
2085 }
2086 Fields::Unnamed(obj) => {
2087 let fields = obj
2088 .fields
2089 .iter()
2090 .filter_map(|field| field.ty.as_ref())
2091 .enumerate()
2092 .map(|(idx, ty)| {
2093 let mut out = String::new();
2094 let mut field_location = location.clone();
2095 field_location.push(idx.to_string().into());
2096 datatype_with_inline_attr(
2097 &mut out,
2098 exporter,
2099 format,
2100 types,
2101 ty,
2102 field_location,
2103 None,
2104 "",
2105 generics,
2106 false,
2107 )
2108 .map(|_| out)
2109 })
2110 .collect::<Result<Vec<_>, _>>()?;
2111
2112 Ok(match &fields[..] {
2113 [] if obj.fields.is_empty() => Some("[]".to_string()),
2114 [] => None,
2115 [field] if obj.fields.len() == 1 => Some(field.to_string()),
2116 fields => Some(format!("[{}]", fields.join(", "))),
2117 })
2118 }
2119 }
2120}
2121
2122fn enum_dt(
2123 s: &mut String,
2124 exporter: &Exporter,
2125 types: &Types,
2126 e: &Enum,
2127 location: Vec<Cow<'static, str>>,
2128 prefix: &str,
2129 generics: &[(GenericReference, DataType)],
2130) -> Result<(), Error> {
2131 if e.variants.is_empty() {
2132 s.push_str(NEVER);
2133 return Ok(());
2134 }
2135
2136 let filtered_variants = active_variants(e);
2137
2138 let discriminator = analyze_discriminator(&filtered_variants);
2139 let fallback_override = fallback_discriminator_override(discriminator.as_ref());
2140
2141 let mut rendered_variants = Vec::with_capacity(filtered_variants.len());
2142 for (idx, (variant_name, variant)) in filtered_variants.iter().enumerate() {
2143 let variant_override = fallback_override
2144 .as_ref()
2145 .and_then(|(fallback_idx, key, ty)| {
2146 (*fallback_idx == idx).then_some(VariantTypeOverride {
2147 key,
2148 ty: ty.as_str(),
2149 })
2150 });
2151
2152 let mut variant_location = location.clone();
2153 variant_location.push(variant_name.clone());
2154 let ts_values = enum_variant_datatype(
2155 exporter,
2156 None,
2157 types,
2158 variant_name.clone(),
2159 variant,
2160 variant_location,
2161 prefix,
2162 generics,
2163 variant_override,
2164 )?;
2165
2166 rendered_variants.push(EnumVariantOutput {
2167 value: ts_values.unwrap_or_else(|| NEVER.to_string()),
2168 strict_keys: untagged_strict_keys(variant),
2169 });
2170 }
2171
2172 if discriminator.is_none() && !has_anonymous_variant(&filtered_variants) {
2173 strictify_enum_variants(&mut rendered_variants);
2174 }
2175
2176 let variants = filtered_variants
2177 .into_iter()
2178 .zip(rendered_variants)
2179 .map(|((_, variant), rendered)| {
2180 inner_comments(
2181 variant.deprecated.as_ref(),
2182 &variant.docs,
2183 rendered.value,
2184 true,
2185 prefix,
2186 )
2187 })
2188 .collect::<Vec<_>>();
2189
2190 push_union(s, variants);
2191
2192 Ok(())
2193}
2194
2195fn tuple_dt(
2196 s: &mut String,
2197 exporter: &Exporter,
2198 types: &Types,
2199 t: &Tuple,
2200 location: Vec<Cow<'static, str>>,
2201 generics: &[(GenericReference, DataType)],
2202) -> Result<(), Error> {
2203 match t.elements.as_slice() {
2204 [] => s.push_str(NULL),
2205 elements => {
2206 s.push('[');
2207 for (idx, dt) in elements.iter().enumerate() {
2208 if idx != 0 {
2209 s.push_str(", ");
2210 }
2211 let mut element_location = location.clone();
2212 element_location.push(idx.to_string().into());
2213 datatype(
2214 s,
2215 exporter,
2216 None,
2217 types,
2218 dt,
2219 element_location,
2220 None,
2221 "",
2222 generics,
2223 )?;
2224 }
2225 s.push(']');
2226 }
2227 }
2228
2229 Ok(())
2230}
2231
2232fn reference_dt(
2233 s: &mut String,
2234 exporter: &Exporter,
2235 format: Option<&dyn Format>,
2236 types: &Types,
2237 r: &Reference,
2238 location: Vec<Cow<'static, str>>,
2239 prefix: &str,
2240 generics: &[(GenericReference, DataType)],
2241) -> Result<(), Error> {
2242 match r {
2243 Reference::Named(r) => match &r.inner {
2244 NamedReferenceType::Reference { .. } => {
2245 reference_named_dt(s, exporter, types, r, location, generics)
2246 }
2247 NamedReferenceType::Inline { dt, .. } => {
2248 let inline_path = path_string(&location);
2249 inline_datatype(
2250 s, exporter, format, types, dt, location, None, prefix, 0, generics,
2251 )
2252 .map_err(|err| err.with_inline_trace(types.get(r), inline_path))
2253 }
2254 NamedReferenceType::Recursive(cycle) => Err(Error::infinite_recursive_inline_type(
2255 path_string(&location),
2256 format!("{r:?}"),
2257 cycle.clone(),
2258 )),
2259 },
2260 Reference::Opaque(r) => reference_opaque_dt(s, exporter, format, types, r, location),
2261 }
2262}
2263
2264fn reference_opaque_dt(
2265 s: &mut String,
2266 exporter: &Exporter,
2267 format: Option<&dyn Format>,
2268 types: &Types,
2269 r: &OpaqueReference,
2270 location: Vec<Cow<'static, str>>,
2271) -> Result<(), Error> {
2272 if let Some(def) = r.downcast_ref::<opaque::Define>() {
2273 s.push_str(&def.0);
2274 return Ok(());
2275 }
2276
2277 if r.downcast_ref::<opaque::Any>().is_some() {
2278 s.push_str("any");
2279 return Ok(());
2280 }
2281
2282 if r.downcast_ref::<opaque::Unknown>().is_some() {
2283 s.push_str("unknown");
2284 return Ok(());
2285 }
2286
2287 if r.downcast_ref::<opaque::Never>().is_some() {
2288 s.push_str("never");
2289 return Ok(());
2290 }
2291
2292 if r.downcast_ref::<opaque::Number>().is_some() {
2293 s.push_str("number");
2294 return Ok(());
2295 }
2296
2297 if r.downcast_ref::<opaque::BigInt>().is_some() {
2298 s.push_str("bigint");
2299 return Ok(());
2300 }
2301
2302 if let Some(def) = r.downcast_ref::<Branded>() {
2303 if let Some(branded_type) = exporter
2304 .branded_type_impl
2305 .as_ref()
2306 .map(|builder| {
2307 (builder.0)(
2308 BrandedTypeExporter {
2309 exporter,
2310 format,
2311 types,
2312 },
2313 def,
2314 )
2315 })
2316 .transpose()?
2317 {
2318 s.push_str(branded_type.as_ref());
2319 return Ok(());
2320 }
2321
2322 match def.ty() {
2323 DataType::Reference(r) => {
2324 reference_dt(s, exporter, format, types, r, location.clone(), "", &[])?
2325 }
2326 ty => inline_datatype(
2327 s,
2328 exporter,
2329 format,
2330 types,
2331 ty,
2332 location.clone(),
2333 None,
2334 "",
2335 0,
2336 &[],
2337 )?,
2338 }
2339 s.push_str(r#" & { readonly __brand: ""#);
2340 s.push_str(&escape_typescript_string_literal(def.brand()));
2341 s.push_str("\" }");
2342 return Ok(());
2343 }
2344
2345 Err(Error::unsupported_opaque_reference(
2346 path_string(&location),
2347 r.clone(),
2348 ))
2349}
2350
2351fn reference_named_dt(
2352 s: &mut String,
2353 exporter: &Exporter,
2354 types: &Types,
2355 r: &NamedReference,
2356 location: Vec<Cow<'static, str>>,
2357 generics: &[(GenericReference, DataType)],
2358) -> Result<(), Error> {
2359 let path = path_string(&location);
2360 let ndt = types
2361 .get(r)
2362 .ok_or_else(|| Error::dangling_named_reference(path.clone(), format!("{r:?}")))?;
2363 crate::references::track_nr(r);
2365
2366 let name = referenced_type_name(exporter, ndt);
2367
2368 let (rendered_generics, omit_generics, scoped_generics) =
2369 resolved_reference_generics(ndt, r, generics)
2370 .ok_or_else(|| Error::dangling_named_reference(path, format!("{r:?}")))?;
2371
2372 s.push_str(&name);
2373 if !omit_generics && !rendered_generics.is_empty() {
2374 s.push('<');
2375
2376 for (i, dt) in rendered_generics.iter().enumerate() {
2377 if i != 0 {
2378 s.push_str(", ");
2379 }
2380
2381 datatype(
2382 s,
2383 exporter,
2384 None,
2385 types,
2386 dt,
2387 vec![],
2388 None,
2389 "",
2390 &scoped_generics,
2391 )?;
2392 }
2393
2394 s.push('>');
2395 }
2396
2397 Ok(())
2398}