1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
use std::{
    borrow::Cow,
    io,
    path::{Path, PathBuf},
};

use specta::{datatype::DeprecatedType, Language, TypeMap};
use specta_serde::is_valid_ty;

use crate::{comments, detect_duplicate_type_names, export_named_datatype, ExportError};

#[derive(Debug)]
#[non_exhaustive]
pub struct CommentFormatterArgs<'a> {
    pub docs: &'a Cow<'static, str>,
    pub deprecated: Option<&'a DeprecatedType>,
}

/// The signature for a function responsible for exporting Typescript comments.
pub type CommentFormatterFn = fn(CommentFormatterArgs) -> String; // TODO: Returning `Cow`???

/// The signature for a function responsible for formatter a Typescript file.
pub type FormatterFn = fn(&Path) -> io::Result<()>;

/// Allows you to configure how Specta's Typescript exporter will deal with BigInt types ([i64], [i128] etc).
///
/// WARNING: None of these settings affect how your data is actually ser/deserialized.
/// It's up to you to adjust your ser/deserialize settings.
#[derive(Debug, Clone, Default)]
pub enum BigIntExportBehavior {
    /// Export BigInt as a Typescript `string`
    ///
    /// Doing this is serde is [pretty simple](https://github.com/serde-rs/json/issues/329#issuecomment-305608405).
    String,
    /// Export BigInt as a Typescript `number`.
    ///
    /// WARNING: `JSON.parse` in JS will truncate your number resulting in data loss so ensure your deserializer supports large numbers.
    Number,
    /// Export BigInt as a Typescript `BigInt`.
    BigInt,
    /// Abort the export with an error.
    ///
    /// This is the default behavior because without integration from your serializer and deserializer we can't guarantee data loss won't occur.
    #[default]
    Fail,
    /// Same as `Self::Fail` but it allows a library to configure the message shown to the end user.
    #[doc(hidden)]
    FailWithReason(&'static str),
}

/// Typescript language exporter.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Typescript {
    /// The file's header
    pub header: Cow<'static, str>,
    /// Should we remove the default header?
    pub remove_default_header: bool,
    /// How BigInts should be exported.
    pub bigint: BigIntExportBehavior,
    /// How comments should be rendered.
    pub comment_exporter: Option<CommentFormatterFn>,
    /// How the resulting file should be formatted.
    pub formatter: Option<FormatterFn>,
}

impl Default for Typescript {
    fn default() -> Self {
        Self {
            header: Cow::Borrowed(""),
            remove_default_header: false,
            bigint: Default::default(),
            comment_exporter: Some(comments::js_doc),
            formatter: None,
        }
    }
}

impl Typescript {
    /// Construct a new Typescript exporter with the default options configured.
    pub fn new() -> Self {
        Default::default()
    }

    /// Configure a header for the file.
    ///
    /// This is perfect for configuring lint ignore rules or other file-level comments.
    pub fn header(mut self, header: impl Into<Cow<'static, str>>) -> Self {
        self.header = header.into();
        self
    }

    // TODO: Only keep this is TS stays responsible for exporting which it probs won't.
    /// Removes the default Specta header from the output.
    pub fn remove_default_header(mut self) -> Self {
        self.remove_default_header = true;
        self
    }

    /// Configure the BigInt handling behaviour
    pub fn bigint(mut self, bigint: BigIntExportBehavior) -> Self {
        self.bigint = bigint;
        self
    }

    /// Configure a function which is responsible for styling the comments to be exported
    ///
    /// Implementations:
    ///  - [`js_doc`](specta_typescript::js_doc)
    ///
    /// Not calling this method will default to the [`js_doc`](specta_typescript::js_doc) exporter.
    /// `None` will disable comment exporting.
    /// `Some(exporter)` will enable comment exporting using the provided exporter.
    pub fn comment_style(mut self, exporter: CommentFormatterFn) -> Self {
        self.comment_exporter = Some(exporter);
        self
    }

    /// Configure a function which is responsible for formatting the result file or files
    ///
    ///
    /// Built-in implementations:
    ///  - [`prettier`](crate:formatter:::prettier)
    ///  - [`ESLint`](crate::formatter::eslint)
    ///  - [`Biome`](crate::formatter::biome)e
    pub fn formatter(mut self, formatter: FormatterFn) -> Self {
        self.formatter = Some(formatter);
        self
    }
}

impl Language for Typescript {
    type Error = ExportError;

    fn export(&self, type_map: TypeMap) -> Result<String, Self::Error> {
        let mut out = self.header.to_string();
        if !self.remove_default_header {
            out += "// This file has been generated by Specta. DO NOT EDIT.\n\n";
        }

        if let Some((ty_name, l0, l1)) = detect_duplicate_type_names(&type_map).into_iter().next() {
            return Err(ExportError::DuplicateTypeName(ty_name, l0, l1));
        }

        for (_, ty) in type_map.iter() {
            is_valid_ty(&ty.inner, &type_map)?;

            out += &export_named_datatype(self, ty, &type_map)?;
            out += "\n\n";
        }

        Ok(out)
    }

    fn format(&self, path: &Path) -> Result<(), Self::Error> {
        if let Some(formatter) = self.formatter {
            formatter(path)?;
        }
        Ok(())
    }
}