Skip to main content

specta_typescript/
error.rs

1use std::{borrow::Cow, error, fmt, io, panic::Location, path::PathBuf};
2
3use specta::datatype::OpaqueReference;
4
5use crate::Layout;
6
7use super::legacy::ExportPath;
8
9/// The error type for the TypeScript exporter.
10///
11/// ## BigInt Forbidden
12///
13/// Specta Typescript intentionally forbids exporting BigInt-style Rust integer types.
14/// This includes [usize], [isize], [i64], [u64], [u128], [i128] and [f128].
15///
16/// This guard exists because `JSON.parse` will truncate large integers to fit into a JavaScript `number` type so we explicitly forbid exporting them.
17///
18/// If you encounter this error, there are a few common migration paths (in order of preference):
19///
20/// 1. Use a smaller integer types (any of `u8`/`i8`/`u16`/`i16`/`u32`/`i32`/`f64`).
21///    - Only possible when the biggest integer you need to represent is small enough to be represented by a `number` in JS.
22///    - This approach forces your application code to handle overflow/underflow values explicitly
23///    - Downside is that it can introduce annoying glue code and doesn't actually work if your need large values.
24///
25/// 2. Serialize the value as a string
26///     - This can be done using `#[specta(type = String)]` combined with a Serde `#[serde(with = "...")]` attribute.
27///     - Downside is that it can introduce annoying glue code, both on in Rust and in JS as you will need to turn it back into a `new BigInt(myString)` in JS.
28///
29/// 3. Use a Specta-based framework
30///     - Frameworks like [Tauri Specta](https://github.com/specta-rs/tauri-specta) and [TauRPC](https://github.com/MatsDK/TauRPC) take care of this for you.
31///     - They use special internals to preserve the values and make use of [`specta-tags`](http://docs.rs/specta-tags) for generating glue-code automatically.
32///
33/// 4. UNSAFE: Accept precision loss
34///     - Accept that large numbers may be deserialized differently and use `#[specta(type = f64)]` to bypass this warning on a per-field basis.
35///     - This can't be set globally as it is designed intentionally to introduce friction, as you are accepting the risk of data loss which is not okay.
36///
37#[non_exhaustive]
38pub struct Error {
39    kind: ErrorKind,
40}
41
42type FrameworkSource = Box<dyn error::Error + Send + Sync + 'static>;
43const BIGINT_DOCS_URL: &str =
44    "https://docs.rs/specta-typescript/latest/specta_typescript/struct.Error.html#bigint-forbidden";
45
46#[allow(dead_code)]
47enum ErrorKind {
48    InvalidMapKey {
49        path: String,
50        reason: Cow<'static, str>,
51    },
52    /// Attempted to export a bigint type but the configuration forbids it.
53    BigIntForbidden {
54        path: String,
55    },
56    /// A type's name conflicts with a reserved keyword in Typescript.
57    ForbiddenName {
58        path: String,
59        name: &'static str,
60    },
61    /// A type's name contains invalid characters or is not valid.
62    InvalidName {
63        path: String,
64        name: Cow<'static, str>,
65    },
66    /// Detected multiple items within the same scope with the same name.
67    /// Typescript doesn't support this so we error out.
68    ///
69    /// Using anything other than [Layout::FlatFile] should make this basically impossible.
70    DuplicateTypeName {
71        name: Cow<'static, str>,
72        first: String,
73        second: String,
74    },
75    /// An filesystem IO error.
76    /// This is possible when using `Typescript::export_to` when writing to a file or formatting the file.
77    Io(io::Error),
78    /// Failed to read a directory while exporting files.
79    ReadDir {
80        path: PathBuf,
81        source: io::Error,
82    },
83    /// Failed to inspect filesystem metadata while exporting files.
84    Metadata {
85        path: PathBuf,
86        source: io::Error,
87    },
88    /// Failed to remove a stale file while exporting files.
89    RemoveFile {
90        path: PathBuf,
91        source: io::Error,
92    },
93    /// Failed to remove an empty directory while exporting files.
94    RemoveDir {
95        path: PathBuf,
96        source: io::Error,
97    },
98    /// Found an opaque reference which the Typescript exporter doesn't know how to handle.
99    /// You may be referencing a type which is not supported by the Typescript exporter.
100    UnsupportedOpaqueReference(OpaqueReference),
101    /// Found a named reference that cannot be resolved from the provided
102    /// [`ResolvedTypes`](specta::ResolvedTypes).
103    DanglingNamedReference {
104        reference: String,
105    },
106    /// Found a generic reference that cannot be resolved to a declared generic name.
107    UnresolvedGenericReference {
108        reference: String,
109    },
110    /// An error occurred in your exporter framework.
111    Framework {
112        message: Cow<'static, str>,
113        source: FrameworkSource,
114    },
115
116    //
117    //
118    // TODO: Break
119    //
120    //
121    BigIntForbiddenLegacy(ExportPath),
122    ForbiddenNameLegacy(ExportPath, &'static str),
123    InvalidNameLegacy(ExportPath, String),
124    FmtLegacy(std::fmt::Error),
125    UnableToExport(Layout),
126}
127
128impl Error {
129    pub(crate) fn invalid_map_key(
130        path: impl Into<String>,
131        reason: impl Into<Cow<'static, str>>,
132    ) -> Self {
133        Self {
134            kind: ErrorKind::InvalidMapKey {
135                path: path.into(),
136                reason: reason.into(),
137            },
138        }
139    }
140
141    /// Construct an error for framework-specific logic.
142    pub fn framework(
143        message: impl Into<Cow<'static, str>>,
144        source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
145    ) -> Self {
146        Self {
147            kind: ErrorKind::Framework {
148                message: message.into(),
149                source: source.into(),
150            },
151        }
152    }
153
154    pub(crate) fn bigint_forbidden(path: String) -> Self {
155        Self {
156            kind: ErrorKind::BigIntForbidden { path },
157        }
158    }
159
160    pub(crate) fn invalid_name(path: String, name: impl Into<Cow<'static, str>>) -> Self {
161        Self {
162            kind: ErrorKind::InvalidName {
163                path,
164                name: name.into(),
165            },
166        }
167    }
168
169    pub(crate) fn duplicate_type_name(
170        name: Cow<'static, str>,
171        first: Location<'static>,
172        second: Location<'static>,
173    ) -> Self {
174        Self {
175            kind: ErrorKind::DuplicateTypeName {
176                name,
177                first: format_location(first),
178                second: format_location(second),
179            },
180        }
181    }
182
183    pub(crate) fn read_dir(path: PathBuf, source: io::Error) -> Self {
184        Self {
185            kind: ErrorKind::ReadDir { path, source },
186        }
187    }
188
189    pub(crate) fn metadata(path: PathBuf, source: io::Error) -> Self {
190        Self {
191            kind: ErrorKind::Metadata { path, source },
192        }
193    }
194
195    pub(crate) fn remove_file(path: PathBuf, source: io::Error) -> Self {
196        Self {
197            kind: ErrorKind::RemoveFile { path, source },
198        }
199    }
200
201    pub(crate) fn remove_dir(path: PathBuf, source: io::Error) -> Self {
202        Self {
203            kind: ErrorKind::RemoveDir { path, source },
204        }
205    }
206
207    pub(crate) fn unsupported_opaque_reference(reference: OpaqueReference) -> Self {
208        Self {
209            kind: ErrorKind::UnsupportedOpaqueReference(reference),
210        }
211    }
212
213    pub(crate) fn dangling_named_reference(reference: String) -> Self {
214        Self {
215            kind: ErrorKind::DanglingNamedReference { reference },
216        }
217    }
218
219    pub(crate) fn unresolved_generic_reference(reference: String) -> Self {
220        Self {
221            kind: ErrorKind::UnresolvedGenericReference { reference },
222        }
223    }
224
225    pub(crate) fn forbidden_name_legacy(path: ExportPath, name: &'static str) -> Self {
226        Self {
227            kind: ErrorKind::ForbiddenNameLegacy(path, name),
228        }
229    }
230
231    pub(crate) fn invalid_name_legacy(path: ExportPath, name: String) -> Self {
232        Self {
233            kind: ErrorKind::InvalidNameLegacy(path, name),
234        }
235    }
236
237    pub(crate) fn unable_to_export(layout: Layout) -> Self {
238        Self {
239            kind: ErrorKind::UnableToExport(layout),
240        }
241    }
242}
243
244impl From<io::Error> for Error {
245    fn from(error: io::Error) -> Self {
246        Self {
247            kind: ErrorKind::Io(error),
248        }
249    }
250}
251
252impl From<std::fmt::Error> for Error {
253    fn from(error: std::fmt::Error) -> Self {
254        Self {
255            kind: ErrorKind::FmtLegacy(error),
256        }
257    }
258}
259
260impl fmt::Display for Error {
261    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262        match &self.kind {
263            ErrorKind::InvalidMapKey { path, reason } => {
264                write!(f, "Invalid map key at '{path}': {reason}")
265            }
266            ErrorKind::BigIntForbidden { path } => write!(
267                f,
268                "Attempted to export {path:?} but Specta forbids exporting BigInt-style types (usize, isize, i64, u64, i128, u128) to avoid precision loss. See {BIGINT_DOCS_URL} for a full explanation."
269            ),
270            ErrorKind::ForbiddenName { path, name } => write!(
271                f,
272                "Attempted to export {path:?} but was unable to due toname {name:?} conflicting with a reserved keyword in Typescript. Try renaming it or using `#[specta(rename = \"new name\")]`"
273            ),
274            ErrorKind::InvalidName { path, name } => write!(
275                f,
276                "Attempted to export {path:?} but was unable to due to name {name:?} containing an invalid character. Try renaming it or using `#[specta(rename = \"new name\")]`"
277            ),
278            ErrorKind::DuplicateTypeName {
279                name,
280                first,
281                second,
282            } => write!(
283                f,
284                "Detected multiple types with the same name: {name:?} at {first} and {second}"
285            ),
286            ErrorKind::Io(err) => write!(f, "IO error: {err}"),
287            ErrorKind::ReadDir { path, source } => {
288                write!(f, "Failed to read directory '{}': {source}", path.display())
289            }
290            ErrorKind::Metadata { path, source } => {
291                write!(
292                    f,
293                    "Failed to read metadata for '{}': {source}",
294                    path.display()
295                )
296            }
297            ErrorKind::RemoveFile { path, source } => {
298                write!(f, "Failed to remove file '{}': {source}", path.display())
299            }
300            ErrorKind::RemoveDir { path, source } => {
301                write!(
302                    f,
303                    "Failed to remove directory '{}': {source}",
304                    path.display()
305                )
306            }
307            ErrorKind::UnsupportedOpaqueReference(reference) => write!(
308                f,
309                "Found unsupported opaque reference '{}'. It is not supported by the Typescript exporter.",
310                reference.type_name()
311            ),
312            ErrorKind::DanglingNamedReference { reference } => write!(
313                f,
314                "Found dangling named reference {reference}. The referenced type is missing from the resolved type collection."
315            ),
316            ErrorKind::UnresolvedGenericReference { reference } => write!(
317                f,
318                "Found unresolved generic reference {reference}. The generic is missing from the active named type scope."
319            ),
320            ErrorKind::Framework { message, source } => {
321                let source = source.to_string();
322                if message.is_empty() && source.is_empty() {
323                    write!(f, "Framework error")
324                } else if source.is_empty() {
325                    write!(f, "Framework error: {message}")
326                } else {
327                    write!(f, "Framework error: {message}: {source}")
328                }
329            }
330            ErrorKind::BigIntForbiddenLegacy(path) => write!(
331                f,
332                "Attempted to export {path:?} but Specta forbids exporting BigInt-style types (usize, isize, i64, u64, i128, u128) to avoid precision loss. See {BIGINT_DOCS_URL} for a full explanation."
333            ),
334            ErrorKind::ForbiddenNameLegacy(path, name) => write!(
335                f,
336                "Attempted to export {path:?} but was unable to due to name {name:?} conflicting with a reserved keyword in Typescript. Try renaming it or using `#[specta(rename = \"new name\")]`"
337            ),
338            ErrorKind::InvalidNameLegacy(path, name) => write!(
339                f,
340                "Attempted to export {path:?} but was unable to due to name {name:?} containing an invalid character. Try renaming it or using `#[specta(rename = \"new name\")]`"
341            ),
342            ErrorKind::FmtLegacy(err) => write!(f, "formatter: {err:?}"),
343            ErrorKind::UnableToExport(layout) => write!(
344                f,
345                "Unable to export layout {layout} with the current configuration. Maybe try `Exporter::export_to` or switching to Typescript."
346            ),
347        }
348    }
349}
350
351impl fmt::Debug for Error {
352    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353        fmt::Display::fmt(self, f)
354    }
355}
356
357impl error::Error for Error {
358    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
359        match &self.kind {
360            ErrorKind::Io(error) => Some(error),
361            ErrorKind::ReadDir { source, .. }
362            | ErrorKind::Metadata { source, .. }
363            | ErrorKind::RemoveFile { source, .. }
364            | ErrorKind::RemoveDir { source, .. } => Some(source),
365            ErrorKind::Framework { source, .. } => Some(source.as_ref()),
366            ErrorKind::FmtLegacy(error) => Some(error),
367            _ => None,
368        }
369    }
370}
371
372fn format_location(location: Location<'static>) -> String {
373    format!(
374        "{}:{}:{}",
375        location.file(),
376        location.line(),
377        location.column()
378    )
379}