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