specta_typescript/
typescript.rs

1use std::{borrow::Cow, io, path::Path};
2
3use specta::{datatype::DeprecatedType, Language, TypeCollection};
4use specta_serde::is_valid_ty;
5
6use crate::{comments, detect_duplicate_type_names, export_named_datatype, ExportError};
7
8#[derive(Debug)]
9#[non_exhaustive]
10pub struct CommentFormatterArgs<'a> {
11    pub docs: &'a Cow<'static, str>,
12    pub deprecated: Option<&'a DeprecatedType>,
13}
14
15/// The signature for a function responsible for exporting Typescript comments.
16pub type CommentFormatterFn = fn(CommentFormatterArgs) -> String; // TODO: Returning `Cow`???
17
18/// The signature for a function responsible for formatter a Typescript file.
19pub type FormatterFn = fn(&Path) -> io::Result<()>;
20
21/// Allows you to configure how Specta's Typescript exporter will deal with BigInt types ([i64], [i128] etc).
22///
23/// WARNING: None of these settings affect how your data is actually ser/deserialized.
24/// It's up to you to adjust your ser/deserialize settings.
25#[derive(Debug, Clone, Default)]
26pub enum BigIntExportBehavior {
27    /// Export BigInt as a Typescript `string`
28    ///
29    /// Doing this is serde is [pretty simple](https://github.com/serde-rs/json/issues/329#issuecomment-305608405).
30    String,
31    /// Export BigInt as a Typescript `number`.
32    ///
33    /// WARNING: `JSON.parse` in JS will truncate your number resulting in data loss so ensure your deserializer supports large numbers.
34    Number,
35    /// Export BigInt as a Typescript `BigInt`.
36    BigInt,
37    /// Abort the export with an error.
38    ///
39    /// This is the default behavior because without integration from your serializer and deserializer we can't guarantee data loss won't occur.
40    #[default]
41    Fail,
42    /// Same as `Self::Fail` but it allows a library to configure the message shown to the end user.
43    #[doc(hidden)]
44    FailWithReason(&'static str),
45}
46
47/// Typescript language exporter.
48#[derive(Debug, Clone)]
49#[non_exhaustive]
50pub struct Typescript {
51    /// The users file header
52    pub header: Cow<'static, str>,
53    /// The framework's header
54    pub framework_header: Cow<'static, str>,
55    /// How BigInts should be exported.
56    pub bigint: BigIntExportBehavior,
57    /// How comments should be rendered.
58    pub comment_exporter: Option<CommentFormatterFn>,
59    /// How the resulting file should be formatted.
60    pub formatter: Option<FormatterFn>,
61}
62
63impl Default for Typescript {
64    fn default() -> Self {
65        Self {
66            header: Cow::Borrowed(""),
67            framework_header: Cow::Borrowed(
68                "// This file has been generated by Specta. DO NOT EDIT.",
69            ),
70            bigint: Default::default(),
71            comment_exporter: Some(comments::js_doc),
72            formatter: None,
73        }
74    }
75}
76
77impl Typescript {
78    /// Construct a new Typescript exporter with the default options configured.
79    pub fn new() -> Self {
80        Default::default()
81    }
82
83    /// Override the header for the exported file.
84    /// You should prefer `Self::header` instead unless your a framework.
85    #[doc(hidden)] // Although this is hidden it's still public API.
86    pub fn framework_header(mut self, header: impl Into<Cow<'static, str>>) -> Self {
87        self.framework_header = header.into();
88        self
89    }
90
91    /// Configure a header for the file.
92    ///
93    /// This is perfect for configuring lint ignore rules or other file-level comments.
94    pub fn header(mut self, header: impl Into<Cow<'static, str>>) -> Self {
95        self.header = header.into();
96        self
97    }
98
99    /// Configure the BigInt handling behaviour
100    pub fn bigint(mut self, bigint: BigIntExportBehavior) -> Self {
101        self.bigint = bigint;
102        self
103    }
104
105    /// Configure a function which is responsible for styling the comments to be exported
106    ///
107    /// Implementations:
108    ///  - [`js_doc`](specta_typescript::js_doc)
109    ///
110    /// Not calling this method will default to the [`js_doc`](specta_typescript::js_doc) exporter.
111    /// `None` will disable comment exporting.
112    /// `Some(exporter)` will enable comment exporting using the provided exporter.
113    pub fn comment_style(mut self, exporter: CommentFormatterFn) -> Self {
114        self.comment_exporter = Some(exporter);
115        self
116    }
117
118    /// Configure a function which is responsible for formatting the result file or files
119    ///
120    ///
121    /// Built-in implementations:
122    ///  - [`prettier`](crate:formatter:::prettier)
123    ///  - [`ESLint`](crate::formatter::eslint)
124    ///  - [`Biome`](crate::formatter::biome)e
125    pub fn formatter(mut self, formatter: FormatterFn) -> Self {
126        self.formatter = Some(formatter);
127        self
128    }
129
130    /// TODO
131    pub fn export(&self, types: &TypeCollection) -> Result<String, ExportError> {
132        let mut out = self.header.to_string();
133        if !out.is_empty() {
134            out.push('\n');
135        }
136        out += &self.framework_header;
137        out.push_str("\n\n");
138
139        if let Some((ty_name, l0, l1)) = detect_duplicate_type_names(&types).into_iter().next() {
140            return Err(ExportError::DuplicateTypeName(ty_name, l0, l1));
141        }
142
143        for (_, ty) in types.into_iter() {
144            is_valid_ty(&ty.inner, &types)?;
145
146            out += &export_named_datatype(self, ty, &types)?;
147            out += "\n\n";
148        }
149
150        Ok(out)
151    }
152
153    /// TODO
154    pub fn export_to(
155        &self,
156        path: impl AsRef<Path>,
157        types: &TypeCollection,
158    ) -> Result<(), ExportError> {
159        let path = path.as_ref();
160        if let Some(parent) = path.parent() {
161            std::fs::create_dir_all(parent)?;
162        }
163        std::fs::write(
164            &path,
165            self.export(types).map(|s| format!("{}{s}", self.header))?,
166        )?;
167        if let Some(formatter) = self.formatter {
168            formatter(path)?;
169        }
170        Ok(())
171    }
172
173    /// TODO
174    pub fn format(&self, path: impl AsRef<Path>) -> Result<(), ExportError> {
175        if let Some(formatter) = self.formatter {
176            formatter(path.as_ref())?;
177        }
178        Ok(())
179    }
180}