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#[non_exhaustive]
11pub struct Error {
12    kind: ErrorKind,
13}
14
15type FrameworkSource = Box<dyn error::Error + Send + Sync + 'static>;
16
17#[allow(dead_code)]
18enum ErrorKind {
19    /// Attempted to export a bigint type but the configuration forbids it.
20    BigIntForbidden {
21        path: String,
22    },
23    /// Failed to validate a type is Serde compatible.
24    Serde(specta_serde::Error),
25    /// A type's name conflicts with a reserved keyword in Typescript.
26    ForbiddenName {
27        path: String,
28        name: &'static str,
29    },
30    /// A type's name contains invalid characters or is not valid.
31    InvalidName {
32        path: String,
33        name: Cow<'static, str>,
34    },
35    /// Detected multiple items within the same scope with the same name.
36    /// Typescript doesn't support this so we error out.
37    ///
38    /// Using anything other than [Layout::FlatFile] should make this basically impossible.
39    DuplicateTypeName {
40        name: Cow<'static, str>,
41        first: String,
42        second: String,
43    },
44    /// An filesystem IO error.
45    /// This is possible when using `Typescript::export_to` when writing to a file or formatting the file.
46    Io(io::Error),
47    /// Failed to read a directory while exporting files.
48    ReadDir {
49        path: PathBuf,
50        source: io::Error,
51    },
52    /// Failed to inspect filesystem metadata while exporting files.
53    Metadata {
54        path: PathBuf,
55        source: io::Error,
56    },
57    /// Failed to remove a stale file while exporting files.
58    RemoveFile {
59        path: PathBuf,
60        source: io::Error,
61    },
62    /// Failed to remove an empty directory while exporting files.
63    RemoveDir {
64        path: PathBuf,
65        source: io::Error,
66    },
67    /// Found an opaque reference which the Typescript exporter doesn't know how to handle.
68    /// You may be referencing a type which is not supported by the Typescript exporter.
69    UnsupportedOpaqueReference(OpaqueReference),
70    /// Found a named reference that cannot be resolved from the provided [`TypeCollection`](specta::TypeCollection).
71    DanglingNamedReference {
72        reference: String,
73    },
74    /// An error occurred in your exporter framework.
75    Framework {
76        message: Cow<'static, str>,
77        source: FrameworkSource,
78    },
79
80    //
81    //
82    // TODO: Break
83    //
84    //
85    BigIntForbiddenLegacy(ExportPath),
86    ForbiddenNameLegacy(ExportPath, &'static str),
87    InvalidNameLegacy(ExportPath, String),
88    InvalidTaggedVariantContainingTupleStructLegacy(ExportPath),
89    FmtLegacy(std::fmt::Error),
90    UnableToExport(Layout),
91}
92
93impl Error {
94    /// Construct an error for framework-specific logic.
95    pub fn framework(
96        message: impl Into<Cow<'static, str>>,
97        source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
98    ) -> Self {
99        Self {
100            kind: ErrorKind::Framework {
101                message: message.into(),
102                source: source.into(),
103            },
104        }
105    }
106
107    pub(crate) fn bigint_forbidden(path: String) -> Self {
108        Self {
109            kind: ErrorKind::BigIntForbidden { path },
110        }
111    }
112
113    pub(crate) fn invalid_name(path: String, name: impl Into<Cow<'static, str>>) -> Self {
114        Self {
115            kind: ErrorKind::InvalidName {
116                path,
117                name: name.into(),
118            },
119        }
120    }
121
122    pub(crate) fn duplicate_type_name(
123        name: Cow<'static, str>,
124        first: Location<'static>,
125        second: Location<'static>,
126    ) -> Self {
127        Self {
128            kind: ErrorKind::DuplicateTypeName {
129                name,
130                first: format_location(first),
131                second: format_location(second),
132            },
133        }
134    }
135
136    pub(crate) fn read_dir(path: PathBuf, source: io::Error) -> Self {
137        Self {
138            kind: ErrorKind::ReadDir { path, source },
139        }
140    }
141
142    pub(crate) fn metadata(path: PathBuf, source: io::Error) -> Self {
143        Self {
144            kind: ErrorKind::Metadata { path, source },
145        }
146    }
147
148    pub(crate) fn remove_file(path: PathBuf, source: io::Error) -> Self {
149        Self {
150            kind: ErrorKind::RemoveFile { path, source },
151        }
152    }
153
154    pub(crate) fn remove_dir(path: PathBuf, source: io::Error) -> Self {
155        Self {
156            kind: ErrorKind::RemoveDir { path, source },
157        }
158    }
159
160    pub(crate) fn unsupported_opaque_reference(reference: OpaqueReference) -> Self {
161        Self {
162            kind: ErrorKind::UnsupportedOpaqueReference(reference),
163        }
164    }
165
166    pub(crate) fn dangling_named_reference(reference: String) -> Self {
167        Self {
168            kind: ErrorKind::DanglingNamedReference { reference },
169        }
170    }
171
172    pub(crate) fn forbidden_name_legacy(
173        path: ExportPath,
174        name: &'static str,
175    ) -> Self {
176        Self {
177            kind: ErrorKind::ForbiddenNameLegacy(path, name),
178        }
179    }
180
181    pub(crate) fn invalid_name_legacy(path: ExportPath, name: String) -> Self {
182        Self {
183            kind: ErrorKind::InvalidNameLegacy(path, name),
184        }
185    }
186
187    pub(crate) fn invalid_tagged_variant_containing_tuple_struct_legacy(path: ExportPath) -> Self {
188        Self {
189            kind: ErrorKind::InvalidTaggedVariantContainingTupleStructLegacy(path),
190        }
191    }
192
193    pub(crate) fn unable_to_export(layout: Layout) -> Self {
194        Self {
195            kind: ErrorKind::UnableToExport(layout),
196        }
197    }
198}
199
200impl From<io::Error> for Error {
201    fn from(error: io::Error) -> Self {
202        Self {
203            kind: ErrorKind::Io(error),
204        }
205    }
206}
207
208impl From<specta_serde::Error> for Error {
209    fn from(error: specta_serde::Error) -> Self {
210        Self {
211            kind: ErrorKind::Serde(error),
212        }
213    }
214}
215
216impl From<std::fmt::Error> for Error {
217    fn from(error: std::fmt::Error) -> Self {
218        Self {
219            kind: ErrorKind::FmtLegacy(error),
220        }
221    }
222}
223
224impl fmt::Display for Error {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        match &self.kind {
227            ErrorKind::BigIntForbidden { path } => write!(
228                f,
229                "Attempted to export {path:?} but Specta configuration forbids exporting BigInt types (i64, u64, i128, u128) because we don't know if your se/deserializer supports it. If your using a serializer/deserializer that natively has support for BigInt types you can disable this warning by editing your `ExportConfiguration`!"
230            ),
231            ErrorKind::Serde(err) => write!(f, "Detect invalid Serde type: {err}"),
232            ErrorKind::ForbiddenName { path, name } => write!(
233                f,
234                "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\")]`"
235            ),
236            ErrorKind::InvalidName { path, name } => write!(
237                f,
238                "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\")]`"
239            ),
240            ErrorKind::DuplicateTypeName {
241                name,
242                first,
243                second,
244            } => write!(
245                f,
246                "Detected multiple types with the same name: {name:?} at {first} and {second}"
247            ),
248            ErrorKind::Io(err) => write!(f, "IO error: {err}"),
249            ErrorKind::ReadDir { path, source } => {
250                write!(f, "Failed to read directory '{}': {source}", path.display())
251            }
252            ErrorKind::Metadata { path, source } => {
253                write!(
254                    f,
255                    "Failed to read metadata for '{}': {source}",
256                    path.display()
257                )
258            }
259            ErrorKind::RemoveFile { path, source } => {
260                write!(f, "Failed to remove file '{}': {source}", path.display())
261            }
262            ErrorKind::RemoveDir { path, source } => {
263                write!(
264                    f,
265                    "Failed to remove directory '{}': {source}",
266                    path.display()
267                )
268            }
269            ErrorKind::UnsupportedOpaqueReference(reference) => write!(
270                f,
271                "Found unsupported opaque reference '{}'. It is not supported by the Typescript exporter.",
272                reference.type_name()
273            ),
274            ErrorKind::DanglingNamedReference { reference } => write!(
275                f,
276                "Found dangling named reference {reference}. The referenced type is missing from `TypeCollection`."
277            ),
278            ErrorKind::Framework { message, source } => {
279                let source = source.to_string();
280                if message.is_empty() && source.is_empty() {
281                    write!(f, "Framework error")
282                } else if source.is_empty() {
283                    write!(f, "Framework error: {message}")
284                } else {
285                    write!(f, "Framework error: {message}: {source}")
286                }
287            }
288            ErrorKind::BigIntForbiddenLegacy(path) => write!(
289                f,
290                "Attempted to export {path:?} but Specta configuration forbids exporting BigInt types (i64, u64, i128, u128) because we don't know if your se/deserializer supports it. You can change this behavior by editing your `ExportConfiguration`!"
291            ),
292            ErrorKind::ForbiddenNameLegacy(path, name) => write!(
293                f,
294                "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\")]`"
295            ),
296            ErrorKind::InvalidNameLegacy(path, name) => write!(
297                f,
298                "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\")]`"
299            ),
300            ErrorKind::InvalidTaggedVariantContainingTupleStructLegacy(path) => write!(
301                f,
302                "Attempted to export {path:?} with tagging but the variant is a tuple struct."
303            ),
304            ErrorKind::FmtLegacy(err) => write!(f, "formatter: {err:?}"),
305            ErrorKind::UnableToExport(layout) => write!(
306                f,
307                "Unable to export layout {layout} with the current configuration. Maybe try `Exporter::export_to` or switching to Typescript."
308            ),
309        }
310    }
311}
312
313impl fmt::Debug for Error {
314    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315        fmt::Display::fmt(self, f)
316    }
317}
318
319impl error::Error for Error {
320    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
321        match &self.kind {
322            ErrorKind::Serde(error) => Some(error),
323            ErrorKind::Io(error) => Some(error),
324            ErrorKind::ReadDir { source, .. }
325            | ErrorKind::Metadata { source, .. }
326            | ErrorKind::RemoveFile { source, .. }
327            | ErrorKind::RemoveDir { source, .. } => Some(source),
328            ErrorKind::Framework { source, .. } => Some(source.as_ref()),
329            ErrorKind::FmtLegacy(error) => Some(error),
330            _ => None,
331        }
332    }
333}
334
335fn format_location(location: Location<'static>) -> String {
336    format!(
337        "{}:{}:{}",
338        location.file(),
339        location.line(),
340        location.column()
341    )
342}