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