Skip to main content

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() == &current_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}