specta_typescript/primitives.rs
1//! Primitives provide building blocks for Specta-based libraries.
2//!
3//! These are for advanced usecases, you should generally use [crate::Typescript] or
4//! [crate::JSDoc] in end-user applications.
5
6use std::{borrow::Cow, cell::RefCell, fmt::Write as _, iter};
7
8use specta::{
9 ResolvedTypes, Types,
10 datatype::{
11 DataType, Deprecated, Enum, Fields, GenericReference, List, Map, NamedDataType,
12 NamedReference, OpaqueReference, Primitive, Reference, Tuple, Variant,
13 },
14};
15
16use crate::{
17 Branded, BrandedTypeExporter, Error, Exporter, Layout,
18 legacy::{
19 ExportContext, deprecated_details, escape_jsdoc_text, escape_typescript_string_literal,
20 is_identifier, js_doc,
21 },
22 map_keys, opaque,
23};
24
25/// Generate a group of `export Type = ...` Typescript string for a specific [`NamedDataType`].
26///
27/// This method leaves the following up to the implementer:
28/// - Ensuring all referenced types are exported
29/// - Handling multiple type with overlapping names
30/// - Transforming the type for your serialization format (Eg. Serde)
31///
32/// We recommend passing in your types in bulk instead of doing individual calls as it leaves formatting to us and also allows us to merge the JSDoc types into a single large comment.
33///
34pub fn export<'a>(
35 exporter: &dyn AsRef<Exporter>,
36 types: &ResolvedTypes,
37 ndts: impl Iterator<Item = &'a NamedDataType>,
38 indent: &str,
39) -> Result<String, Error> {
40 let mut s = String::new();
41 export_internal(&mut s, exporter.as_ref(), types.as_types(), ndts, indent)?;
42 Ok(s)
43}
44
45pub(crate) fn export_internal<'a>(
46 s: &mut String,
47 exporter: &Exporter,
48 types: &Types,
49 ndts: impl Iterator<Item = &'a NamedDataType>,
50 indent: &str,
51) -> Result<(), Error> {
52 if exporter.jsdoc {
53 let mut ndts = ndts.peekable();
54 if ndts.peek().is_none() {
55 return Ok(());
56 }
57
58 s.push_str(indent);
59 s.push_str("/**\n");
60
61 for (index, ndt) in ndts.enumerate() {
62 if index != 0 {
63 s.push_str(indent);
64 s.push_str("\t*\n");
65 }
66
67 append_typedef_body(s, exporter, types, ndt, indent)?;
68 }
69
70 s.push_str(indent);
71 s.push_str("\t*/\n");
72 return Ok(());
73 }
74
75 for (index, ndt) in ndts.enumerate() {
76 if index != 0 {
77 s.push('\n');
78 }
79
80 export_single_internal(s, exporter, types, ndt, indent)?;
81 }
82
83 Ok(())
84}
85
86fn export_single_internal(
87 s: &mut String,
88 exporter: &Exporter,
89 types: &Types,
90 ndt: &NamedDataType,
91 indent: &str,
92) -> Result<(), Error> {
93 if exporter.jsdoc {
94 let mut typedef = String::new();
95 typedef_internal(&mut typedef, exporter, types, ndt)?;
96 for line in typedef.lines() {
97 s.push_str(indent);
98 s.push_str(line);
99 s.push('\n');
100 }
101 return Ok(());
102 }
103
104 let generics = (!ndt.generics().is_empty())
105 .then(|| {
106 iter::once("<")
107 .chain(intersperse(
108 ndt.generics().iter().map(|(_, g)| g.as_ref()),
109 ", ",
110 ))
111 .chain(iter::once(">"))
112 })
113 .into_iter()
114 .flatten();
115
116 // TODO: Modernise this
117 let name = crate::legacy::sanitise_type_name(
118 crate::legacy::ExportContext {
119 cfg: exporter,
120 path: vec![],
121 },
122 &match exporter.layout {
123 Layout::ModulePrefixedName => {
124 let mut s = ndt.module_path().split("::").collect::<Vec<_>>().join("_");
125 s.push('_');
126 s.push_str(ndt.name());
127 Cow::Owned(s)
128 }
129 _ => ndt.name().clone(),
130 },
131 )?;
132
133 let mut comments = String::new();
134 js_doc(&mut comments, ndt.docs(), ndt.deprecated(), !exporter.jsdoc);
135 if !comments.is_empty() {
136 for line in comments.lines() {
137 s.push_str(indent);
138 s.push_str(line);
139 s.push('\n');
140 }
141 }
142
143 s.push_str(indent);
144 s.push_str("export type ");
145 s.push_str(&name);
146 for part in generics {
147 s.push_str(part);
148 }
149 s.push_str(" = ");
150
151 let _generic_scope = push_generic_scope(ndt.generics());
152 datatype(
153 s,
154 exporter,
155 types,
156 ndt.ty(),
157 vec![ndt.name().clone()],
158 Some(ndt.name()),
159 indent,
160 Default::default(),
161 )?;
162 s.push_str(";\n");
163
164 Ok(())
165}
166
167/// Generate an inlined Typescript string for a specific [`DataType`].
168///
169/// This methods leaves all the same things as the [`export`] method up to the user.
170///
171/// Note that calling this method with a tagged struct or enum may cause the tag to not be exported.
172/// The type should be wrapped in a [`NamedDataType`] to provide a proper name.
173///
174pub fn inline(
175 exporter: &dyn AsRef<Exporter>,
176 types: &ResolvedTypes,
177 dt: &DataType,
178) -> Result<String, Error> {
179 let mut s = String::new();
180 inline_datatype(
181 &mut s,
182 exporter.as_ref(),
183 types.as_types(),
184 dt,
185 vec![],
186 None,
187 "",
188 0,
189 &[],
190 )?;
191 Ok(s)
192}
193
194// This can be used internally to prevent cloning `Typescript` instances.
195// Externally this shouldn't be a concern so we don't expose it.
196pub(crate) fn typedef_internal(
197 s: &mut String,
198 exporter: &Exporter,
199 types: &Types,
200 dt: &NamedDataType,
201) -> Result<(), Error> {
202 s.push_str("/**\n");
203 append_typedef_body(s, exporter, types, dt, "")?;
204
205 s.push_str("\t*/");
206
207 Ok(())
208}
209
210fn append_jsdoc_properties(
211 s: &mut String,
212 exporter: &Exporter,
213 types: &Types,
214 dt: &NamedDataType,
215 indent: &str,
216) -> Result<(), Error> {
217 match dt.ty() {
218 DataType::Struct(strct) => match strct.fields() {
219 Fields::Unit => {}
220 Fields::Unnamed(unnamed) => {
221 for (idx, field) in unnamed.fields().iter().enumerate() {
222 let Some(ty) = field.ty() else {
223 continue;
224 };
225
226 let mut ty_str = String::new();
227 let datatype_prefix = format!("{indent}\t*\t");
228 datatype(
229 &mut ty_str,
230 exporter,
231 types,
232 ty,
233 vec![dt.name().clone(), idx.to_string().into()],
234 Some(dt.name()),
235 &datatype_prefix,
236 Default::default(),
237 )?;
238
239 push_jsdoc_property(
240 s,
241 &ty_str,
242 &idx.to_string(),
243 field.optional(),
244 field.docs(),
245 field.deprecated(),
246 indent,
247 );
248 }
249 }
250 Fields::Named(named) => {
251 for (name, field) in named.fields() {
252 let Some(ty) = field.ty() else {
253 continue;
254 };
255
256 let mut ty_str = String::new();
257 let datatype_prefix = format!("{indent}\t*\t");
258 datatype(
259 &mut ty_str,
260 exporter,
261 types,
262 ty,
263 vec![dt.name().clone(), name.clone()],
264 Some(dt.name()),
265 &datatype_prefix,
266 Default::default(),
267 )?;
268
269 push_jsdoc_property(
270 s,
271 &ty_str,
272 name,
273 field.optional(),
274 field.docs(),
275 field.deprecated(),
276 indent,
277 );
278 }
279 }
280 },
281 DataType::Enum(enm) => {
282 for (variant_name, variant) in enm.variants().iter().filter(|(_, v)| !v.skip()) {
283 let mut one_variant_enum = enm.clone();
284 one_variant_enum
285 .variants_mut()
286 .retain(|(name, _)| name == variant_name);
287
288 let mut variant_ty = String::new();
289 crate::legacy::enum_datatype(
290 ExportContext {
291 cfg: exporter,
292 path: vec![],
293 },
294 &one_variant_enum,
295 types,
296 &mut variant_ty,
297 "",
298 &[],
299 )?;
300
301 push_jsdoc_property(
302 s,
303 &variant_ty,
304 variant_name,
305 false,
306 variant.docs(),
307 variant.deprecated(),
308 indent,
309 );
310 }
311 }
312 _ => {}
313 }
314
315 Ok(())
316}
317
318fn push_jsdoc_property(
319 s: &mut String,
320 ty: &str,
321 name: &str,
322 optional: bool,
323 docs: &str,
324 deprecated: Option<&Deprecated>,
325 indent: &str,
326) {
327 s.push_str(indent);
328 s.push_str("\t* @property {");
329 push_jsdoc_type(s, ty, indent);
330 s.push_str("} ");
331 s.push_str(&jsdoc_property_name(name, optional));
332
333 if let Some(description) = jsdoc_description(docs, deprecated) {
334 s.push_str(" - ");
335 s.push_str(&description);
336 }
337
338 s.push('\n');
339}
340
341fn push_jsdoc_type(s: &mut String, ty: &str, indent: &str) {
342 let mut lines = ty.lines();
343 if let Some(first_line) = lines.next() {
344 s.push_str(first_line);
345 }
346
347 for line in lines {
348 s.push('\n');
349
350 if line
351 .strip_prefix(indent)
352 .is_some_and(|rest| rest.starts_with("\t*"))
353 {
354 s.push_str(line);
355 } else {
356 s.push_str(indent);
357 s.push_str("\t* ");
358 s.push_str(line);
359 }
360 }
361}
362
363fn jsdoc_property_name(name: &str, optional: bool) -> String {
364 let name = if is_identifier(name) {
365 name.to_string()
366 } else {
367 format!("\"{}\"", escape_typescript_string_literal(name))
368 };
369
370 if optional { format!("[{name}]") } else { name }
371}
372
373fn append_typedef_body(
374 s: &mut String,
375 exporter: &Exporter,
376 types: &Types,
377 dt: &NamedDataType,
378 indent: &str,
379) -> Result<(), Error> {
380 let generics = (!dt.generics().is_empty())
381 .then(|| {
382 iter::once("<")
383 .chain(intersperse(
384 dt.generics().iter().map(|(_, g)| g.as_ref()),
385 ", ",
386 ))
387 .chain(iter::once(">"))
388 })
389 .into_iter()
390 .flatten();
391
392 let name = dt.name();
393 let type_name = iter::empty()
394 .chain([name.as_ref()])
395 .chain(generics)
396 .collect::<String>();
397
398 let mut typedef_ty = String::new();
399 let datatype_prefix = format!("{indent}\t*\t");
400 let _generic_scope = push_generic_scope(dt.generics());
401 datatype(
402 &mut typedef_ty,
403 exporter,
404 types,
405 dt.ty(),
406 vec![dt.name().clone()],
407 Some(dt.name()),
408 &datatype_prefix,
409 Default::default(),
410 )?;
411
412 if !dt.docs().is_empty() {
413 for line in dt.docs().lines() {
414 s.push_str(indent);
415 s.push_str("\t* ");
416 s.push_str(&escape_jsdoc_text(line));
417 s.push('\n');
418 }
419 s.push_str(indent);
420 s.push_str("\t*\n");
421 }
422
423 if let Some(deprecated) = dt.deprecated() {
424 s.push_str(indent);
425 s.push_str("\t* @deprecated");
426 if let Some(details) = deprecated_details(deprecated) {
427 s.push(' ');
428 s.push_str(&details);
429 }
430 s.push('\n');
431 }
432
433 s.push_str(indent);
434 s.push_str("\t* @typedef {");
435 push_jsdoc_type(s, &typedef_ty, indent);
436 s.push_str("} ");
437 s.push_str(&type_name);
438 s.push('\n');
439
440 append_jsdoc_properties(s, exporter, types, dt, indent)?;
441
442 Ok(())
443}
444
445fn jsdoc_description(docs: &str, deprecated: Option<&Deprecated>) -> Option<String> {
446 let docs = docs
447 .lines()
448 .map(str::trim)
449 .filter(|line| !line.is_empty())
450 .map(|line| escape_jsdoc_text(line).into_owned())
451 .collect::<Vec<_>>()
452 .join(" ");
453
454 let deprecated = deprecated.map(|deprecated| {
455 let mut value = String::from("@deprecated");
456 if let Some(details) = deprecated_details(deprecated) {
457 value.push(' ');
458 value.push_str(&escape_jsdoc_text(&details));
459 }
460 value
461 });
462
463 match (docs.is_empty(), deprecated) {
464 (true, None) => None,
465 (true, Some(deprecated)) => Some(deprecated),
466 (false, None) => Some(docs),
467 (false, Some(deprecated)) => Some(format!("{docs} {deprecated}")),
468 }
469}
470
471/// Generate an Typescript string to refer to a specific [`DataType`].
472///
473/// For primitives this will include the literal type but for named type it will contain a reference.
474///
475/// See [`export`] for the list of things to consider when using this.
476pub fn reference(
477 exporter: &dyn AsRef<Exporter>,
478 types: &ResolvedTypes,
479 r: &Reference,
480) -> Result<String, Error> {
481 let mut s = String::new();
482 reference_dt(
483 &mut s,
484 exporter.as_ref(),
485 types.as_types(),
486 r,
487 vec![],
488 "",
489 &[],
490 )?;
491 Ok(s)
492}
493
494pub(crate) fn datatype_with_inline_attr(
495 s: &mut String,
496 exporter: &Exporter,
497 types: &Types,
498 dt: &DataType,
499 location: Vec<Cow<'static, str>>,
500 parent_name: Option<&str>,
501 prefix: &str,
502 generics: &[(GenericReference, DataType)],
503 inline: bool,
504) -> Result<(), Error> {
505 if inline {
506 return shallow_inline_datatype(
507 s,
508 exporter,
509 types,
510 dt,
511 location,
512 parent_name,
513 prefix,
514 generics,
515 );
516 }
517
518 datatype(
519 s,
520 exporter,
521 types,
522 dt,
523 location,
524 parent_name,
525 prefix,
526 generics,
527 )
528}
529
530fn merged_generics(
531 parent: &[(GenericReference, DataType)],
532 child: &[(GenericReference, DataType)],
533) -> Vec<(GenericReference, DataType)> {
534 let unshadowed_parent = parent
535 .iter()
536 .filter(|(parent_generic, _)| {
537 !child
538 .iter()
539 .any(|(child_generic, _)| child_generic == parent_generic)
540 })
541 .cloned();
542
543 child
544 .iter()
545 .map(|(generic, dt)| (generic.clone(), resolve_generics_in_datatype(dt, parent)))
546 .chain(unshadowed_parent)
547 .collect()
548}
549
550thread_local! {
551 static INLINE_REFERENCE_STACK: RefCell<Vec<(Cow<'static, str>, Cow<'static, str>, Vec<(GenericReference, DataType)>)>> = const { RefCell::new(Vec::new()) };
552 static RESOLVING_GENERICS: RefCell<Vec<GenericReference>> = const { RefCell::new(Vec::new()) };
553 static GENERIC_NAME_STACK: RefCell<Vec<Vec<(GenericReference, Cow<'static, str>)>>> = const { RefCell::new(Vec::new()) };
554}
555
556struct GenericScopeGuard;
557
558impl Drop for GenericScopeGuard {
559 fn drop(&mut self) {
560 GENERIC_NAME_STACK.with(|stack| {
561 stack.borrow_mut().pop();
562 });
563 }
564}
565
566fn push_generic_scope(generics: &[(GenericReference, Cow<'static, str>)]) -> GenericScopeGuard {
567 GENERIC_NAME_STACK.with(|stack| {
568 stack.borrow_mut().push(generics.to_vec());
569 });
570 GenericScopeGuard
571}
572
573fn resolve_generic_name(generic: &GenericReference) -> Option<Cow<'static, str>> {
574 GENERIC_NAME_STACK.with(|stack| {
575 stack.borrow().iter().rev().find_map(|scope| {
576 scope
577 .iter()
578 .find(|(candidate, _)| candidate == generic)
579 .map(|(_, name)| name.clone())
580 })
581 })
582}
583
584fn write_generic_reference(s: &mut String, generic: &GenericReference) -> Result<(), Error> {
585 let generic_name = resolve_generic_name(generic)
586 .ok_or_else(|| Error::unresolved_generic_reference(format!("{generic:?}")))?;
587 s.push_str(generic_name.as_ref());
588 Ok(())
589}
590
591fn shallow_inline_datatype(
592 s: &mut String,
593 exporter: &Exporter,
594 types: &Types,
595 dt: &DataType,
596 location: Vec<Cow<'static, str>>,
597 parent_name: Option<&str>,
598 prefix: &str,
599 generics: &[(GenericReference, DataType)],
600) -> Result<(), Error> {
601 match dt {
602 DataType::Primitive(p) => s.push_str(primitive_dt(p, location)?),
603 DataType::List(list) => {
604 let mut inner = String::new();
605 shallow_inline_datatype(
606 &mut inner,
607 exporter,
608 types,
609 list.ty(),
610 location,
611 parent_name,
612 prefix,
613 generics,
614 )?;
615
616 let inner = if (inner.contains(' ') && !inner.ends_with('}'))
617 || (inner.contains(' ') && (inner.contains('&') || inner.contains('|')))
618 {
619 format!("({inner})")
620 } else {
621 inner
622 };
623
624 if let Some(length) = list.length() {
625 s.push('[');
626 for i in 0..length {
627 if i != 0 {
628 s.push_str(", ");
629 }
630 s.push_str(&inner);
631 }
632 s.push(']');
633 } else {
634 write!(s, "{inner}[]")?;
635 }
636 }
637 DataType::Map(map) => {
638 let path = map_key_path(&location);
639 map_keys::validate_map_key(map.key_ty(), types, generics, format!("{path}.<map_key>"))?;
640 let rendered_key =
641 map_key_render_type(resolve_generics_in_datatype(map.key_ty(), generics));
642
643 fn is_exhaustive(dt: &DataType, types: &Types) -> bool {
644 match dt {
645 DataType::Enum(e) => {
646 e.variants().iter().filter(|(_, v)| !v.skip()).count() == 0
647 }
648 DataType::Reference(Reference::Named(r)) => r
649 .get(types)
650 .is_some_and(|ndt| is_exhaustive(ndt.ty(), types)),
651 DataType::Reference(Reference::Opaque(_)) => false,
652 _ => true,
653 }
654 }
655
656 let exhaustive = is_exhaustive(&rendered_key, types);
657 if !exhaustive {
658 s.push_str("Partial<");
659 }
660
661 s.push_str("{ [key in ");
662 shallow_inline_datatype(
663 s,
664 exporter,
665 types,
666 &rendered_key,
667 location.clone(),
668 parent_name,
669 prefix,
670 generics,
671 )?;
672 s.push_str("]: ");
673 shallow_inline_datatype(
674 s,
675 exporter,
676 types,
677 map.value_ty(),
678 location,
679 parent_name,
680 prefix,
681 generics,
682 )?;
683 s.push_str(" }");
684
685 if !exhaustive {
686 s.push('>');
687 }
688 }
689 DataType::Nullable(dt) => {
690 let mut inner = String::new();
691 shallow_inline_datatype(
692 &mut inner,
693 exporter,
694 types,
695 dt,
696 location,
697 parent_name,
698 prefix,
699 generics,
700 )?;
701
702 s.push_str(&inner);
703 if inner != "null" && !inner.ends_with(" | null") {
704 s.push_str(" | null");
705 }
706 }
707 DataType::Struct(st) => {
708 crate::legacy::struct_datatype(
709 crate::legacy::ExportContext {
710 cfg: exporter,
711 path: vec![],
712 },
713 parent_name,
714 st,
715 types,
716 s,
717 prefix,
718 generics,
719 )?;
720 }
721 DataType::Enum(enm) => {
722 crate::legacy::enum_datatype(
723 crate::legacy::ExportContext {
724 cfg: exporter,
725 path: vec![],
726 },
727 enm,
728 types,
729 s,
730 prefix,
731 generics,
732 )?;
733 }
734 DataType::Tuple(tuple) => match tuple.elements() {
735 [] => s.push_str("null"),
736 elements => {
737 s.push('[');
738 for (idx, dt) in elements.iter().enumerate() {
739 if idx != 0 {
740 s.push_str(", ");
741 }
742 shallow_inline_datatype(
743 s,
744 exporter,
745 types,
746 dt,
747 location.clone(),
748 parent_name,
749 prefix,
750 generics,
751 )?;
752 }
753 s.push(']');
754 }
755 },
756 DataType::Reference(r) => match r {
757 Reference::Named(r) => {
758 let ndt = r
759 .get(types)
760 .ok_or_else(|| Error::dangling_named_reference(format!("{r:?}")))?;
761 let inline_key = (
762 ndt.module_path().clone(),
763 ndt.name().clone(),
764 r.generics().to_vec(),
765 );
766 let already_inlining = INLINE_REFERENCE_STACK
767 .with(|stack| stack.borrow().iter().any(|key| key == &inline_key));
768
769 if already_inlining {
770 return reference_named_dt(s, exporter, types, r, location, prefix, generics);
771 }
772
773 INLINE_REFERENCE_STACK.with(|stack| stack.borrow_mut().push(inline_key));
774 let combined_generics = merged_generics(generics, r.generics());
775 let resolved = resolve_generics_in_datatype(ndt.ty(), &combined_generics);
776 let result = shallow_inline_datatype(
777 s,
778 exporter,
779 types,
780 &resolved,
781 location,
782 parent_name,
783 prefix,
784 &combined_generics,
785 );
786 INLINE_REFERENCE_STACK.with(|stack| {
787 stack.borrow_mut().pop();
788 });
789
790 result
791 }
792 Reference::Generic(g) => {
793 if let Some((_, resolved_dt)) = generics.iter().find(|(ge, _)| ge == g) {
794 if matches!(resolved_dt, DataType::Reference(Reference::Generic(inner)) if inner == g)
795 {
796 write_generic_reference(s, g)?;
797 } else {
798 let already_resolving = RESOLVING_GENERICS
799 .with(|stack| stack.borrow().iter().any(|seen| seen == g));
800 if already_resolving {
801 write_generic_reference(s, g)?;
802 } else {
803 RESOLVING_GENERICS.with(|stack| stack.borrow_mut().push(g.clone()));
804 let result = shallow_inline_datatype(
805 s,
806 exporter,
807 types,
808 resolved_dt,
809 location,
810 parent_name,
811 prefix,
812 generics,
813 );
814 RESOLVING_GENERICS.with(|stack| {
815 stack.borrow_mut().pop();
816 });
817 result?;
818 }
819 }
820 } else {
821 write_generic_reference(s, g)?;
822 }
823 Ok(())
824 }
825 Reference::Opaque(_) => reference_dt(s, exporter, types, r, location, prefix, generics),
826 }?,
827 }
828
829 Ok(())
830}
831
832fn resolve_generics_in_datatype(
833 dt: &DataType,
834 generics: &[(GenericReference, DataType)],
835) -> DataType {
836 fn resolve(
837 dt: &DataType,
838 generics: &[(GenericReference, DataType)],
839 visiting: &mut Vec<GenericReference>,
840 ) -> DataType {
841 match dt {
842 DataType::Primitive(_) => dt.clone(),
843 DataType::List(l) => {
844 let mut out = l.clone();
845 out.set_ty(resolve(l.ty(), generics, visiting));
846 DataType::List(out)
847 }
848 DataType::Map(m) => {
849 let mut out = m.clone();
850 out.set_key_ty(resolve(m.key_ty(), generics, visiting));
851 out.set_value_ty(resolve(m.value_ty(), generics, visiting));
852 DataType::Map(out)
853 }
854 DataType::Nullable(def) => {
855 DataType::Nullable(Box::new(resolve(def, generics, visiting)))
856 }
857 DataType::Struct(st) => {
858 let mut out = st.clone();
859 match out.fields_mut() {
860 specta::datatype::Fields::Unit => {}
861 specta::datatype::Fields::Unnamed(unnamed) => {
862 for field in unnamed.fields_mut() {
863 if let Some(ty) = field.ty_mut() {
864 *ty = resolve(ty, generics, visiting);
865 }
866 }
867 }
868 specta::datatype::Fields::Named(named) => {
869 for (_, field) in named.fields_mut() {
870 if let Some(ty) = field.ty_mut() {
871 *ty = resolve(ty, generics, visiting);
872 }
873 }
874 }
875 }
876 DataType::Struct(out)
877 }
878 DataType::Enum(en) => {
879 let mut out = en.clone();
880 for (_, variant) in out.variants_mut() {
881 match variant.fields_mut() {
882 specta::datatype::Fields::Unit => {}
883 specta::datatype::Fields::Unnamed(unnamed) => {
884 for field in unnamed.fields_mut() {
885 if let Some(ty) = field.ty_mut() {
886 *ty = resolve(ty, generics, visiting);
887 }
888 }
889 }
890 specta::datatype::Fields::Named(named) => {
891 for (_, field) in named.fields_mut() {
892 if let Some(ty) = field.ty_mut() {
893 *ty = resolve(ty, generics, visiting);
894 }
895 }
896 }
897 }
898 }
899 DataType::Enum(out)
900 }
901 DataType::Tuple(t) => {
902 let mut out = t.clone();
903 for element in out.elements_mut() {
904 *element = resolve(element, generics, visiting);
905 }
906 DataType::Tuple(out)
907 }
908 DataType::Reference(Reference::Generic(g)) => {
909 if visiting.iter().any(|seen| seen == g) {
910 return dt.clone();
911 }
912
913 if let Some((_, resolved_dt)) = generics.iter().find(|(ge, _)| ge == g) {
914 if matches!(resolved_dt, DataType::Reference(Reference::Generic(inner)) if inner == g)
915 {
916 dt.clone()
917 } else {
918 visiting.push(g.clone());
919 let out = resolve(resolved_dt, generics, visiting);
920 visiting.pop();
921 out
922 }
923 } else {
924 dt.clone()
925 }
926 }
927 DataType::Reference(_) => dt.clone(),
928 }
929 }
930
931 resolve(dt, generics, &mut Vec::new())
932}
933
934// Internal function to handle inlining without cloning DataType nodes
935fn inline_datatype(
936 s: &mut String,
937 exporter: &Exporter,
938 types: &Types,
939 dt: &DataType,
940 location: Vec<Cow<'static, str>>,
941 parent_name: Option<&str>,
942 prefix: &str,
943 depth: usize,
944 generics: &[(GenericReference, DataType)],
945) -> Result<(), Error> {
946 // Prevent infinite recursion
947 if depth == 25 {
948 return Err(Error::invalid_name(
949 location.join("."),
950 "Type recursion limit exceeded during inline expansion",
951 ));
952 }
953
954 match dt {
955 DataType::Primitive(p) => s.push_str(primitive_dt(p, location)?),
956 DataType::List(l) => {
957 // Inline the list element type
958 let mut dt_str = String::new();
959 crate::legacy::datatype_inner(
960 crate::legacy::ExportContext {
961 cfg: exporter,
962 path: vec![],
963 },
964 l.ty(),
965 types,
966 &mut dt_str,
967 generics,
968 )?;
969
970 let dt_str = if (dt_str.contains(' ') && !dt_str.ends_with('}'))
971 || (dt_str.contains(' ') && (dt_str.contains('&') || dt_str.contains('|')))
972 {
973 format!("({dt_str})")
974 } else {
975 dt_str
976 };
977
978 if let Some(length) = l.length() {
979 s.push('[');
980 for n in 0..length {
981 if n != 0 {
982 s.push_str(", ");
983 }
984 s.push_str(&dt_str);
985 }
986 s.push(']');
987 } else {
988 write!(s, "{dt_str}[]")?;
989 }
990 }
991 DataType::Map(m) => map_dt(s, exporter, types, m, location, generics)?,
992 DataType::Nullable(def) => {
993 let mut inner = String::new();
994 inline_datatype(
995 &mut inner,
996 exporter,
997 types,
998 def,
999 location,
1000 parent_name,
1001 prefix,
1002 depth + 1,
1003 generics,
1004 )?;
1005
1006 s.push_str(&inner);
1007 if inner != "null" && !inner.ends_with(" | null") {
1008 s.push_str(" | null");
1009 }
1010 }
1011 DataType::Struct(st) => {
1012 // If we have generics to resolve, handle the struct inline to preserve context
1013 if !generics.is_empty() {
1014 use specta::datatype::Fields;
1015 match st.fields() {
1016 Fields::Unit => s.push_str("null"),
1017 Fields::Named(named) => {
1018 s.push('{');
1019 let mut has_field = false;
1020 for (key, field) in named.fields() {
1021 // Skip fields without a type (e.g., flattened or skipped fields)
1022 let Some(field_ty) = field.ty() else {
1023 continue;
1024 };
1025
1026 has_field = true;
1027 s.push('\n');
1028 s.push_str(prefix);
1029 s.push('\t');
1030 s.push_str(key);
1031 s.push_str(": ");
1032 inline_datatype(
1033 s,
1034 exporter,
1035 types,
1036 field_ty,
1037 location.clone(),
1038 parent_name,
1039 prefix,
1040 depth + 1,
1041 generics,
1042 )?;
1043 s.push(',');
1044 }
1045
1046 if has_field {
1047 s.push('\n');
1048 s.push_str(prefix);
1049 }
1050
1051 s.push('}');
1052 }
1053 Fields::Unnamed(_) => {
1054 // For unnamed fields, fall back to legacy handling
1055 crate::legacy::struct_datatype(
1056 crate::legacy::ExportContext {
1057 cfg: exporter,
1058 path: vec![],
1059 },
1060 parent_name,
1061 st,
1062 types,
1063 s,
1064 prefix,
1065 generics,
1066 )?
1067 }
1068 }
1069 } else {
1070 // No generics, use legacy path
1071 crate::legacy::struct_datatype(
1072 crate::legacy::ExportContext {
1073 cfg: exporter,
1074 path: vec![],
1075 },
1076 parent_name,
1077 st,
1078 types,
1079 s,
1080 prefix,
1081 Default::default(),
1082 )?
1083 }
1084 }
1085 DataType::Enum(e) => enum_dt(s, exporter, types, e, location, prefix, generics)?,
1086 DataType::Tuple(t) => tuple_dt(s, exporter, types, t, location, generics)?,
1087 DataType::Reference(r) => {
1088 if let Reference::Named(r) = r
1089 && let Some(ndt) = r.get(types)
1090 {
1091 let combined_generics = merged_generics(generics, r.generics());
1092 inline_datatype(
1093 s,
1094 exporter,
1095 types,
1096 ndt.ty(),
1097 location,
1098 parent_name,
1099 prefix,
1100 depth + 1,
1101 &combined_generics,
1102 )?;
1103 } else {
1104 reference_dt(s, exporter, types, r, location, prefix, generics)?;
1105 }
1106 }
1107 }
1108
1109 Ok(())
1110}
1111
1112pub(crate) fn datatype(
1113 s: &mut String,
1114 exporter: &Exporter,
1115 types: &Types,
1116 dt: &DataType,
1117 location: Vec<Cow<'static, str>>,
1118 parent_name: Option<&str>,
1119 prefix: &str,
1120 generics: &[(GenericReference, DataType)],
1121) -> Result<(), Error> {
1122 // TODO: Validating the variant from `dt` can be flattened
1123
1124 match dt {
1125 DataType::Primitive(p) => s.push_str(primitive_dt(p, location)?),
1126 DataType::List(l) => list_dt(s, exporter, types, l, location, generics)?,
1127 DataType::Map(m) => map_dt(s, exporter, types, m, location, generics)?,
1128 DataType::Nullable(def) => {
1129 // TODO: Replace legacy stuff
1130 let mut inner = String::new();
1131 crate::legacy::datatype_inner(
1132 crate::legacy::ExportContext {
1133 cfg: exporter,
1134 path: vec![],
1135 },
1136 def,
1137 types,
1138 &mut inner,
1139 generics,
1140 )?;
1141
1142 s.push_str(&inner);
1143 if inner != "null" && !inner.ends_with(" | null") {
1144 s.push_str(" | null");
1145 }
1146
1147 // datatype(s, ts, types, &*t, location, state)?;
1148 // let or_null = " | null";
1149 // if !s.ends_with(or_null) {
1150 // s.push_str(or_null);
1151 // }
1152 }
1153 DataType::Struct(st) => {
1154 // location.push(st.name().clone());
1155 // fields_dt(s, ts, types, st.name(), &st.fields(), location, state)?
1156
1157 crate::legacy::struct_datatype(
1158 crate::legacy::ExportContext {
1159 cfg: exporter,
1160 path: vec![],
1161 },
1162 parent_name,
1163 st,
1164 types,
1165 s,
1166 prefix,
1167 generics,
1168 )?
1169 }
1170 DataType::Enum(e) => enum_dt(s, exporter, types, e, location, prefix, generics)?,
1171 DataType::Tuple(t) => tuple_dt(s, exporter, types, t, location, generics)?,
1172 DataType::Reference(r) => reference_dt(s, exporter, types, r, location, prefix, generics)?,
1173 };
1174
1175 Ok(())
1176}
1177
1178fn primitive_dt(p: &Primitive, location: Vec<Cow<'static, str>>) -> Result<&'static str, Error> {
1179 use Primitive::*;
1180
1181 Ok(match p {
1182 i8 | i16 | i32 | u8 | u16 | u32 | f16 | f32 | f64 /* this looks wrong but `f64` is the direct equivalent of `number` */ => "number",
1183 usize | isize | i64 | u64 | i128 | u128 | f128 => {
1184 return Err(Error::bigint_forbidden(location.join(".")));
1185 }
1186 Primitive::bool => "boolean",
1187 str | char => "string",
1188 })
1189}
1190
1191fn list_dt(
1192 s: &mut String,
1193 exporter: &Exporter,
1194 types: &Types,
1195 l: &List,
1196 _location: Vec<Cow<'static, str>>,
1197 generics: &[(GenericReference, DataType)],
1198) -> Result<(), Error> {
1199 // TODO: This is the legacy stuff
1200 {
1201 let mut dt = String::new();
1202 crate::legacy::datatype_inner(
1203 crate::legacy::ExportContext {
1204 cfg: exporter,
1205 path: vec![],
1206 },
1207 l.ty(),
1208 types,
1209 &mut dt,
1210 generics,
1211 )?;
1212
1213 let dt = if (dt.contains(' ') && !dt.ends_with('}'))
1214 // This is to do with maintaining order of operations.
1215 // Eg `{} | {}` must be wrapped in parens like `({} | {})[]` but `{}` doesn't cause `{}[]` is valid
1216 || (dt.contains(' ') && (dt.contains('&') || dt.contains('|')))
1217 {
1218 format!("({dt})")
1219 } else {
1220 dt
1221 };
1222
1223 if let Some(length) = l.length() {
1224 s.push('[');
1225
1226 for n in 0..length {
1227 if n != 0 {
1228 s.push_str(", ");
1229 }
1230
1231 s.push_str(&dt);
1232 }
1233
1234 s.push(']');
1235 } else {
1236 write!(s, "{dt}[]")?;
1237 }
1238 }
1239
1240 // // We use `T[]` instead of `Array<T>` to avoid issues with circular references.
1241
1242 // let mut result = String::new();
1243 // datatype(&mut result, ts, types, &l.ty(), location, state)?;
1244 // let result = if (result.contains(' ') && !result.ends_with('}'))
1245 // // This is to do with maintaining order of operations.
1246 // // Eg `{} | {}` must be wrapped in parens like `({} | {})[]` but `{}` doesn't cause `{}[]` is valid
1247 // || (result.contains(' ') && (result.contains('&') || result.contains('|')))
1248 // {
1249 // format!("({result})")
1250 // } else {
1251 // result
1252 // };
1253
1254 // match l.length() {
1255 // Some(len) => {
1256 // s.push_str("[");
1257 // iter_with_sep(
1258 // s,
1259 // 0..len,
1260 // |s, _| {
1261 // s.push_str(&result);
1262 // Ok(())
1263 // },
1264 // ", ",
1265 // )?;
1266 // s.push_str("]");
1267 // }
1268 // None => {
1269 // s.push_str(&result);
1270 // s.push_str("[]");
1271 // }
1272 // }
1273
1274 Ok(())
1275}
1276
1277fn map_dt(
1278 s: &mut String,
1279 exporter: &Exporter,
1280 types: &Types,
1281 m: &Map,
1282 location: Vec<Cow<'static, str>>,
1283 generics: &[(GenericReference, DataType)],
1284) -> Result<(), Error> {
1285 let path = map_key_path(&location);
1286 map_keys::validate_map_key(m.key_ty(), types, generics, format!("{path}.<map_key>"))?;
1287
1288 {
1289 fn is_exhaustive(dt: &DataType, types: &Types) -> bool {
1290 match dt {
1291 DataType::Enum(e) => e.variants().iter().filter(|(_, v)| !v.skip()).count() == 0,
1292 DataType::Reference(Reference::Named(r)) => {
1293 if let Some(ndt) = r.get(types) {
1294 is_exhaustive(ndt.ty(), types)
1295 } else {
1296 false
1297 }
1298 }
1299 DataType::Reference(Reference::Opaque(_)) => false,
1300 _ => true,
1301 }
1302 }
1303
1304 let resolved_key = map_key_render_type(resolve_generics_in_datatype(m.key_ty(), generics));
1305 let is_exhaustive = is_exhaustive(&resolved_key, types);
1306
1307 // We use `{ [key in K]: V }` instead of `Record<K, V>` to avoid issues with circular references.
1308 // Wrapped in Partial<> because otherwise TypeScript would enforce exhaustiveness.
1309 if !is_exhaustive {
1310 s.push_str("Partial<");
1311 }
1312 s.push_str("{ [key in ");
1313 crate::legacy::datatype_inner(
1314 crate::legacy::ExportContext {
1315 cfg: exporter,
1316 path: vec![],
1317 },
1318 &resolved_key,
1319 types,
1320 s,
1321 generics,
1322 )?;
1323 s.push_str("]: ");
1324 crate::legacy::datatype_inner(
1325 crate::legacy::ExportContext {
1326 cfg: exporter,
1327 path: vec![],
1328 },
1329 m.value_ty(),
1330 types,
1331 s,
1332 generics,
1333 )?;
1334 s.push_str(" }");
1335 if !is_exhaustive {
1336 s.push('>');
1337 }
1338 }
1339 // assert!(flattening, "todo: map flattening");
1340
1341 // // We use `{ [key in K]: V }` instead of `Record<K, V>` to avoid issues with circular references.
1342 // // Wrapped in Partial<> because otherwise TypeScript would enforce exhaustiveness.
1343 // s.push_str("Partial<{ [key in ");
1344 // datatype(s, ts, types, m.key_ty(), location.clone(), state)?;
1345 // s.push_str("]: ");
1346 // datatype(s, ts, types, m.value_ty(), location, state)?;
1347 // s.push_str(" }>");
1348 Ok(())
1349}
1350
1351fn map_key_path(location: &[Cow<'static, str>]) -> String {
1352 if location.is_empty() {
1353 return "HashMap".to_string();
1354 }
1355
1356 location.join(".")
1357}
1358
1359fn map_key_render_type(dt: DataType) -> DataType {
1360 if matches!(dt, DataType::Primitive(Primitive::bool)) {
1361 return bool_key_literal_datatype();
1362 }
1363
1364 dt
1365}
1366
1367fn bool_key_literal_datatype() -> DataType {
1368 let mut bool_enum = Enum::new();
1369 bool_enum
1370 .variants_mut()
1371 .push((Cow::Borrowed("true"), Variant::unit()));
1372 bool_enum
1373 .variants_mut()
1374 .push((Cow::Borrowed("false"), Variant::unit()));
1375 DataType::Enum(bool_enum)
1376}
1377
1378fn enum_dt(
1379 s: &mut String,
1380 exporter: &Exporter,
1381 types: &Types,
1382 e: &Enum,
1383 _location: Vec<Cow<'static, str>>,
1384 prefix: &str,
1385 generics: &[(GenericReference, DataType)],
1386) -> Result<(), Error> {
1387 // TODO: Drop legacy stuff
1388 {
1389 crate::legacy::enum_datatype(
1390 crate::legacy::ExportContext {
1391 cfg: exporter,
1392 path: vec![],
1393 },
1394 e,
1395 types,
1396 s,
1397 prefix,
1398 generics,
1399 )?
1400 }
1401
1402 // assert!(!state.flattening, "todo: support for flattening enums"); // TODO
1403
1404 // location.push(e.name().clone());
1405
1406 // let variants = e.variants().iter().filter(|(_, variant)| !variant.skip());
1407
1408 // if variants.clone().next().is_none()
1409 // /* is_empty */
1410 // {
1411 // s.push_str("never");
1412 // return Ok(());
1413 // }
1414
1415 // let mut variants = variants
1416 // .into_iter()
1417 // .map(|(variant_name, variant)| {
1418 // let mut s = String::new();
1419 // let mut location = location.clone();
1420 // location.push(variant_name.clone());
1421
1422 // // TODO
1423 // // variant.deprecated()
1424 // // variant.docs()
1425
1426 // match &e.repr() {
1427 // EnumRepr::Untagged => {
1428 // fields_dt(&mut s, ts, types, variant_name, variant.fields(), location, state)?;
1429 // },
1430 // EnumRepr::External => match variant.fields() {
1431 // Fields::Unit => {
1432 // s.push_str("\"");
1433 // s.push_str(variant_name);
1434 // s.push_str("\"");
1435 // },
1436 // Fields::Unnamed(n) if n.fields().into_iter().filter(|f| f.ty().is_some()).next().is_none() /* is_empty */ => {
1437 // // We detect `#[specta(skip)]` by checking if the unfiltered fields are also empty.
1438 // if n.fields().is_empty() {
1439 // s.push_str("{ ");
1440 // s.push_str(&escape_key(variant_name));
1441 // s.push_str(": [] }");
1442 // } else {
1443 // s.push_str("\"");
1444 // s.push_str(variant_name);
1445 // s.push_str("\"");
1446 // }
1447 // }
1448 // _ => {
1449 // s.push_str("{ ");
1450 // s.push_str(&escape_key(variant_name));
1451 // s.push_str(": ");
1452 // fields_dt(&mut s, ts, types, variant_name, variant.fields(), location, state)?;
1453 // s.push_str(" }");
1454 // }
1455 // }
1456 // EnumRepr::Internal { tag } => {
1457 // // TODO: Unconditionally wrapping in `(` kinda sucks.
1458 // write!(s, "({{ {}: \"{}\"", escape_key(tag), variant_name).expect("infallible");
1459
1460 // match variant.fields() {
1461 // Fields::Unit => {
1462 // s.push_str(" })");
1463 // },
1464 // // Fields::Unnamed(f) if f.fields.iter().filter(|f| f.ty().is_some()).count() == 1 => {
1465 // // // let mut fields = f.fields().into_iter().filter(|f| f.ty().is_some());
1466
1467 // // s.push_str("______"); // TODO
1468
1469 // // // // if fields.len
1470
1471 // // // // TODO: Having no fields are skipping is valid
1472 // // // // TODO: Having more than 1 field is invalid
1473
1474 // // // // TODO: Check if the field's type is object-like and can be merged.
1475
1476 // // // todo!();
1477 // // }
1478 // f => {
1479 // // TODO: Cleanup and explain this
1480 // let mut skip_join = false;
1481 // if let Fields::Unnamed(f) = &f {
1482 // let mut fields = f.fields.iter().filter(|f| f.ty().is_some());
1483 // if let (Some(v), None) = (fields.next(), fields.next()) {
1484 // if let Some(DataType::Tuple(tuple)) = &v.ty() {
1485 // skip_join = tuple.elements().len() == 0;
1486 // }
1487 // }
1488 // }
1489
1490 // if skip_join {
1491 // s.push_str(" })");
1492 // } else {
1493 // s.push_str(" } & ");
1494
1495 // // TODO: Can we be smart enough to omit the `{` and `}` if this is an object
1496 // fields_dt(&mut s, ts, types, variant_name, f, location, state)?;
1497 // s.push_str(")");
1498 // }
1499
1500 // // match f {
1501 // // // Checked above
1502 // // Fields::Unit => unreachable!(),
1503 // // Fields::Unnamed(unnamed_fields) => unnamed_fields,
1504 // // Fields::Named(named_fields) => todo!(),
1505 // // }
1506
1507 // // println!("{:?}", f); // TODO: If object we can join in fields like this, else `} & ...`
1508 // // flattened_fields_dt(&mut s, ts, types, variant_name, f, location, false)?; // TODO: Fix `flattening`
1509
1510 // }
1511 // }
1512
1513 // }
1514 // EnumRepr::Adjacent { tag, content } => {
1515 // write!(s, "{{ {}: \"{}\"", escape_key(tag), variant_name).expect("infallible");
1516
1517 // match variant.fields() {
1518 // Fields::Unit => {},
1519 // f => {
1520 // write!(s, "; {}: ", escape_key(content)).expect("infallible");
1521 // fields_dt(&mut s, ts, types, variant_name, f, location, state)?;
1522 // }
1523 // }
1524
1525 // s.push_str(" }");
1526 // }
1527 // }
1528
1529 // Ok(s)
1530 // })
1531 // .collect::<Result<Vec<String>, Error>>()?;
1532
1533 // // TODO: Instead of deduplicating on the string, we should do it in the AST.
1534 // // This would avoid the intermediate `String` allocations and be more reliable.
1535 // variants.dedup();
1536
1537 // iter_with_sep(
1538 // s,
1539 // variants,
1540 // |s, v| {
1541 // s.push_str(&v);
1542 // Ok(())
1543 // },
1544 // " | ",
1545 // )?;
1546
1547 Ok(())
1548}
1549
1550// fn fields_dt(
1551// s: &mut String,
1552// ts: &Typescript,
1553// types: &Types,
1554// name: &Cow<'static, str>,
1555// f: &Fields,
1556// location: Vec<Cow<'static, str>>,
1557// state: State,
1558// ) -> Result<(), Error> {
1559// match f {
1560// Fields::Unit => {
1561// assert!(!state.flattening, "todo: support for flattening enums"); // TODO
1562// s.push_str("null")
1563// }
1564// Fields::Unnamed(f) => {
1565// assert!(!state.flattening, "todo: support for flattening enums"); // TODO
1566// let mut fields = f.fields().into_iter().filter(|f| f.ty().is_some());
1567
1568// // A single field usually becomes `T`.
1569// // but when `#[serde(skip)]` is used it should be `[T]`.
1570// if fields.clone().count() == 1 && f.fields.len() == 1 {
1571// return field_dt(
1572// s,
1573// ts,
1574// types,
1575// None,
1576// fields.next().expect("checked above"),
1577// location,
1578// state,
1579// );
1580// }
1581
1582// s.push_str("[");
1583// iter_with_sep(
1584// s,
1585// fields.enumerate(),
1586// |s, (i, f)| {
1587// let mut location = location.clone();
1588// location.push(i.to_string().into());
1589
1590// field_dt(s, ts, types, None, f, location, state)
1591// },
1592// ", ",
1593// )?;
1594// s.push_str("]");
1595// }
1596// Fields::Named(f) => {
1597// let fields = f.fields().into_iter().filter(|(_, f)| f.ty().is_some());
1598// if fields.clone().next().is_none()
1599// /* is_empty */
1600// {
1601// assert!(!state.flattening, "todo: support for flattening enums"); // TODO
1602
1603// if let Some(tag) = f.tag() {
1604// if !state.flattening {}
1605
1606// write!(s, "{{ {}: \"{name}\" }}", escape_key(tag)).expect("infallible");
1607// } else {
1608// s.push_str("Record<string, never>");
1609// }
1610
1611// return Ok(());
1612// }
1613
1614// if !state.flattening {
1615// s.push_str("{ ");
1616// }
1617// if let Some(tag) = &f.tag() {
1618// write!(s, "{}: \"{name}\"; ", escape_key(tag)).expect("infallible");
1619// }
1620
1621// iter_with_sep(
1622// s,
1623// fields,
1624// |s, (key, f)| {
1625// let mut location = location.clone();
1626// location.push(key.clone());
1627
1628// field_dt(s, ts, types, Some(key), f, location, state)
1629// },
1630// "; ",
1631// )?;
1632// if !state.flattening {
1633// s.push_str(" }");
1634// }
1635// }
1636// }
1637// Ok(())
1638// }
1639
1640// // TODO: Remove this to avoid so much duplicate logic
1641// fn flattened_fields_dt(
1642// s: &mut String,
1643// ts: &Typescript,
1644// types: &Types,
1645// name: &Cow<'static, str>,
1646// f: &Fields,
1647// location: Vec<Cow<'static, str>>,
1648// state: State,
1649// ) -> Result<(), Error> {
1650// match f {
1651// Fields::Unit => todo!(), // s.push_str("null"),
1652// Fields::Unnamed(f) => {
1653// // TODO: Validate flattening?
1654
1655// let mut fields = f.fields().into_iter().filter(|f| f.ty().is_some());
1656
1657// // A single field usually becomes `T`.
1658// // but when `#[serde(skip)]` is used it should be `[T]`.
1659// if fields.clone().count() == 1 && f.fields.len() == 1 {
1660// return field_dt(
1661// s,
1662// ts,
1663// types,
1664// None,
1665// fields.next().expect("checked above"),
1666// location,
1667// state,
1668// );
1669// }
1670
1671// s.push_str("[");
1672// iter_with_sep(
1673// s,
1674// fields.enumerate(),
1675// |s, (i, f)| {
1676// let mut location = location.clone();
1677// location.push(i.to_string().into());
1678
1679// field_dt(s, ts, types, None, f, location, state)
1680// },
1681// ", ",
1682// )?;
1683// s.push_str("]");
1684// }
1685// Fields::Named(f) => {
1686// let fields = f.fields().into_iter().filter(|(_, f)| f.ty().is_some());
1687// if fields.clone().next().is_none()
1688// /* is_empty */
1689// {
1690// if let Some(tag) = f.tag() {
1691// write!(s, "{{ {}: \"{name}\" }}", escape_key(tag)).expect("infallible");
1692// } else {
1693// s.push_str("Record<string, never>");
1694// }
1695
1696// return Ok(());
1697// }
1698
1699// // s.push_str("{ "); // TODO
1700// if let Some(tag) = &f.tag() {
1701// write!(s, "{}: \"{name}\"; ", escape_key(tag)).expect("infallible");
1702// }
1703
1704// iter_with_sep(
1705// s,
1706// fields,
1707// |s, (key, f)| {
1708// let mut location = location.clone();
1709// location.push(key.clone());
1710
1711// field_dt(s, ts, types, Some(key), f, location, state)
1712// },
1713// "; ",
1714// )?;
1715// // s.push_str(" }"); // TODO
1716// }
1717// }
1718// Ok(())
1719// }
1720
1721// fn field_dt(
1722// s: &mut String,
1723// ts: &Typescript,
1724// types: &Types,
1725// key: Option<&Cow<'static, str>>,
1726// f: &Field,
1727// location: Vec<Cow<'static, str>>,
1728// state: State,
1729// ) -> Result<(), Error> {
1730// let Some(ty) = f.ty() else {
1731// // These should be filtered out before getting here.
1732// unreachable!()
1733// };
1734
1735// // TODO
1736// // field.deprecated(),
1737// // field.docs(),
1738
1739// let ty = if f.inline() {
1740// specta::datatype::inline_dt(types, ty.clone())
1741// } else {
1742// ty.clone()
1743// };
1744
1745// if !f.flatten() {
1746// if let Some(key) = key {
1747// s.push_str(&*escape_key(key));
1748// // https://github.com/specta-rs/rspc/issues/100#issuecomment-1373092211
1749// if f.optional() {
1750// s.push_str("?");
1751// }
1752// s.push_str(": ");
1753// }
1754// } else {
1755// // TODO: We need to validate the inner type can be flattened safely???
1756
1757// // data
1758
1759// // match ty {
1760// // DataType::Any => todo!(),
1761// // DataType::Unknown => todo!(),
1762// // DataType::Primitive(primitive_type) => todo!(),
1763// // DataType::Literal(literal_type) => todo!(),
1764// // DataType::List(list) => todo!(),
1765// // DataType::Map(map) => todo!(),
1766// // DataType::Nullable(data_type) => todo!(),
1767// // DataType::Struct(st) => {
1768// // // location.push(st.name().clone()); // TODO
1769// // flattened_fields_dt(s, ts, types, st.name(), &st.fields(), location)?
1770// // }
1771
1772// // // flattened_fields_dt(s, ts, types, &ty, location)?,
1773// // DataType::Enum(enum_type) => todo!(),
1774// // DataType::Tuple(tuple_type) => todo!(),
1775// // DataType::Reference(reference) => todo!(),
1776// // DataType::Generic(generic_type) => todo!(),
1777// // };
1778// }
1779
1780// // TODO: Only flatten when object is inline?
1781
1782// datatype(
1783// s,
1784// ts,
1785// types,
1786// &ty,
1787// location,
1788// State {
1789// flattening: state.flattening || f.flatten(),
1790// },
1791// )?;
1792
1793// // TODO: This is not always correct but is it ever correct?
1794// // If we can't use `?` (Eg. in a tuple) we manually join it.
1795// // if key.is_none() && f.optional() {
1796// // s.push_str(" | undefined");
1797// // }
1798
1799// Ok(())
1800// }
1801
1802fn tuple_dt(
1803 s: &mut String,
1804 exporter: &Exporter,
1805 types: &Types,
1806 t: &Tuple,
1807 _location: Vec<Cow<'static, str>>,
1808 generics: &[(GenericReference, DataType)],
1809) -> Result<(), Error> {
1810 {
1811 s.push_str(&crate::legacy::tuple_datatype(
1812 crate::legacy::ExportContext {
1813 cfg: exporter,
1814 path: vec![],
1815 },
1816 t,
1817 types,
1818 generics,
1819 )?);
1820 }
1821
1822 // match &t.elements()[..] {
1823 // [] => s.push_str("null"),
1824 // elems => {
1825 // s.push_str("[");
1826 // iter_with_sep(
1827 // s,
1828 // elems.into_iter().enumerate(),
1829 // |s, (i, dt)| {
1830 // let mut location = location.clone();
1831 // location.push(i.to_string().into());
1832
1833 // datatype(s, ts, types, &dt, location, state)
1834 // },
1835 // ", ",
1836 // )?;
1837 // s.push_str("]");
1838 // }
1839 // }
1840 Ok(())
1841}
1842
1843fn reference_dt(
1844 s: &mut String,
1845 exporter: &Exporter,
1846 types: &Types,
1847 r: &Reference,
1848 location: Vec<Cow<'static, str>>,
1849 prefix: &str,
1850 generics: &[(GenericReference, DataType)],
1851) -> Result<(), Error> {
1852 match r {
1853 Reference::Named(r) => {
1854 reference_named_dt(s, exporter, types, r, location, prefix, generics)
1855 }
1856 Reference::Generic(g) => {
1857 if let Some((_, resolved_dt)) = generics.iter().find(|(ge, _)| ge == g) {
1858 if matches!(resolved_dt, DataType::Reference(Reference::Generic(inner)) if inner == g)
1859 {
1860 write_generic_reference(s, g)?;
1861 Ok(())
1862 } else {
1863 datatype(
1864 s,
1865 exporter,
1866 types,
1867 resolved_dt,
1868 location,
1869 None,
1870 prefix,
1871 generics,
1872 )
1873 }
1874 } else {
1875 write_generic_reference(s, g)?;
1876 Ok(())
1877 }
1878 }
1879 Reference::Opaque(r) => reference_opaque_dt(s, exporter, types, r),
1880 }
1881}
1882
1883fn reference_opaque_dt(
1884 s: &mut String,
1885 exporter: &Exporter,
1886 types: &Types,
1887 r: &OpaqueReference,
1888) -> Result<(), Error> {
1889 if let Some(def) = r.downcast_ref::<opaque::Define>() {
1890 s.push_str(&def.0);
1891 return Ok(());
1892 } else if r.downcast_ref::<opaque::Any>().is_some() {
1893 s.push_str("any");
1894 return Ok(());
1895 } else if r.downcast_ref::<opaque::Unknown>().is_some() {
1896 s.push_str("unknown");
1897 return Ok(());
1898 } else if r.downcast_ref::<opaque::Never>().is_some() {
1899 s.push_str("never");
1900 return Ok(());
1901 } else if let Some(def) = r.downcast_ref::<Branded>() {
1902 let resolved_types = ResolvedTypes::from_resolved_types(types.clone());
1903
1904 if let Some(branded_type) = exporter
1905 .branded_type_impl
1906 .as_ref()
1907 .map(|builder| {
1908 (builder.0)(
1909 BrandedTypeExporter {
1910 exporter,
1911 types: &resolved_types,
1912 },
1913 def,
1914 )
1915 })
1916 .transpose()?
1917 {
1918 s.push_str(branded_type.as_ref());
1919 return Ok(());
1920 }
1921
1922 // TODO: Build onto `s` instead of appending a separate string
1923 match def.ty() {
1924 DataType::Reference(r) => reference_dt(s, exporter, types, r, vec![], "", &[])?,
1925 ty => inline_datatype(s, exporter, types, ty, vec![], None, "", 0, &[])?,
1926 }
1927 s.push_str(r#" & { { readonly __brand: ""#);
1928 s.push_str(def.brand());
1929 s.push_str("\" }");
1930 return Ok(());
1931 }
1932
1933 Err(Error::unsupported_opaque_reference(r.clone()))
1934}
1935
1936fn reference_named_dt(
1937 s: &mut String,
1938 exporter: &Exporter,
1939 types: &Types,
1940 r: &NamedReference,
1941 location: Vec<Cow<'static, str>>,
1942 prefix: &str,
1943 generics: &[(GenericReference, DataType)],
1944) -> Result<(), Error> {
1945 // TODO: Legacy stuff
1946 {
1947 let ndt = r
1948 .get(types)
1949 .ok_or_else(|| Error::dangling_named_reference(format!("{r:?}")))?;
1950 let _generic_scope = push_generic_scope(ndt.generics());
1951
1952 // Check if this reference should be inlined
1953 if r.inline() {
1954 let inline_key = (
1955 ndt.module_path().clone(),
1956 ndt.name().clone(),
1957 r.generics().to_vec(),
1958 );
1959 let already_inlining = INLINE_REFERENCE_STACK
1960 .with(|stack| stack.borrow().iter().any(|key| key == &inline_key));
1961
1962 if already_inlining {
1963 // Fall through and emit a named reference to break recursive inline expansions.
1964 } else {
1965 INLINE_REFERENCE_STACK.with(|stack| stack.borrow_mut().push(inline_key));
1966 let combined_generics = merged_generics(generics, r.generics());
1967 let resolved = resolve_generics_in_datatype(ndt.ty(), &combined_generics);
1968 let result = datatype(
1969 s,
1970 exporter,
1971 types,
1972 &resolved,
1973 location,
1974 None,
1975 prefix,
1976 &combined_generics,
1977 );
1978 INLINE_REFERENCE_STACK.with(|stack| {
1979 stack.borrow_mut().pop();
1980 });
1981 return result;
1982 }
1983 }
1984
1985 // We check it's valid before tracking
1986 crate::references::track_nr(r);
1987
1988 let name = match exporter.layout {
1989 Layout::ModulePrefixedName => {
1990 let mut s = ndt.module_path().split("::").collect::<Vec<_>>().join("_");
1991 s.push('_');
1992 s.push_str(ndt.name());
1993 Cow::Owned(s)
1994 }
1995 Layout::Namespaces => {
1996 if ndt.module_path().is_empty() {
1997 ndt.name().clone()
1998 } else {
1999 let mut path =
2000 ndt.module_path()
2001 .split("::")
2002 .fold("$s$.".to_string(), |mut s, segment| {
2003 s.push_str(segment);
2004 s.push('.');
2005 s
2006 });
2007 path.push_str(ndt.name());
2008 Cow::Owned(path)
2009 }
2010 }
2011 Layout::Files => {
2012 let current_module_path =
2013 crate::references::current_module_path().unwrap_or_default();
2014
2015 if ndt.module_path() == ¤t_module_path {
2016 ndt.name().clone()
2017 } else {
2018 let mut path = crate::exporter::module_alias(ndt.module_path());
2019 path.push('.');
2020 path.push_str(ndt.name());
2021 Cow::Owned(path)
2022 }
2023 }
2024 _ => ndt.name().clone(),
2025 };
2026
2027 let scoped_generics = generics
2028 .iter()
2029 .filter(|(parent_generic, _)| {
2030 !r.generics()
2031 .iter()
2032 .any(|(child_generic, _)| child_generic == parent_generic)
2033 })
2034 .cloned()
2035 .collect::<Vec<_>>();
2036
2037 s.push_str(&name);
2038 if !r.generics().is_empty() {
2039 s.push('<');
2040
2041 for (i, (_, v)) in r.generics().iter().enumerate() {
2042 if i != 0 {
2043 s.push_str(", ");
2044 }
2045
2046 crate::legacy::datatype_inner(
2047 crate::legacy::ExportContext {
2048 cfg: exporter,
2049 path: vec![],
2050 },
2051 v,
2052 types,
2053 s,
2054 &scoped_generics,
2055 )?;
2056 }
2057
2058 s.push('>');
2059 }
2060 }
2061
2062 // let ndt = types
2063 // .get(r.sid())
2064 // // Should be impossible without a bug in Specta.
2065 // .unwrap_or_else(|| panic!("Missing {:?} in `Types`", r.sid()));
2066
2067 // if r.inline() {
2068 // todo!("inline reference!");
2069 // }
2070
2071 // s.push_str(ndt.name());
2072 // // TODO: We could possible break this out, the root `export` function also has to emit generics.
2073 // match r.generics() {
2074 // [] => {}
2075 // generics => {
2076 // s.push('<');
2077 // // TODO: Should we push a location for which generic?
2078 // iter_with_sep(
2079 // s,
2080 // generics,
2081 // |s, dt| datatype(s, ts, types, &dt, location.clone(), state),
2082 // ", ",
2083 // )?;
2084 // s.push('>');
2085 // }
2086 // }
2087
2088 Ok(())
2089}
2090
2091// fn validate_name(
2092// ident: &Cow<'static, str>,
2093// location: &Vec<Cow<'static, str>>,
2094// ) -> Result<(), Error> {
2095// // TODO: Use a perfect hash-map for faster lookups?
2096// if let Some(name) = RESERVED_TYPE_NAMES.iter().find(|v| **v == ident) {
2097// return Err(Error::ForbiddenName {
2098// path: location.join("."),
2099// name,
2100// });
2101// }
2102
2103// if ident.is_empty() {
2104// return Err(Error::InvalidName {
2105// path: location.join("."),
2106// name: ident.clone(),
2107// });
2108// }
2109
2110// if let Some(first_char) = ident.chars().next() {
2111// if !first_char.is_alphabetic() && first_char != '_' {
2112// return Err(Error::InvalidName {
2113// path: location.join("."),
2114// name: ident.clone(),
2115// });
2116// }
2117// }
2118
2119// if ident
2120// .find(|c: char| !c.is_alphanumeric() && c != '_')
2121// .is_some()
2122// {
2123// return Err(Error::InvalidName {
2124// path: location.join("."),
2125// name: ident.clone(),
2126// });
2127// }
2128
2129// Ok(())
2130// }
2131
2132// fn escape_key(name: &Cow<'static, str>) -> Cow<'static, str> {
2133// let needs_escaping = name
2134// .chars()
2135// .all(|c| c.is_alphanumeric() || c == '_' || c == '$')
2136// && name
2137// .chars()
2138// .next()
2139// .map(|first| !first.is_numeric())
2140// .unwrap_or(true);
2141
2142// if !needs_escaping {
2143// format!(r#""{name}""#).into()
2144// } else {
2145// name.clone()
2146// }
2147// }
2148
2149// fn comment() {
2150// // TODO: Different JSDoc modes
2151
2152// // TODO: Regular comments
2153// // TODO: Deprecated
2154
2155// // TODO: When enabled: arguments, result types
2156// }
2157
2158// /// Iterate with separate and error handling
2159// fn iter_with_sep<T>(
2160// s: &mut String,
2161// i: impl IntoIterator<Item = T>,
2162// mut item: impl FnMut(&mut String, T) -> Result<(), Error>,
2163// sep: &'static str,
2164// ) -> Result<(), Error> {
2165// for (i, e) in i.into_iter().enumerate() {
2166// if i != 0 {
2167// s.push_str(sep);
2168// }
2169// (item)(s, e)?;
2170// }
2171// Ok(())
2172// }
2173
2174// A smaller helper until this is stablised into the Rust standard library.
2175fn intersperse<T: Clone>(iter: impl Iterator<Item = T>, sep: T) -> impl Iterator<Item = T> {
2176 iter.enumerate().flat_map(move |(i, item)| {
2177 if i == 0 {
2178 vec![item]
2179 } else {
2180 vec![sep.clone(), item]
2181 }
2182 })
2183}