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#[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 BigIntForbidden {
54 path: String,
55 },
56 ForbiddenName {
58 path: String,
59 name: &'static str,
60 },
61 InvalidName {
63 path: String,
64 name: Cow<'static, str>,
65 },
66 DuplicateTypeName {
71 name: Cow<'static, str>,
72 first: String,
73 second: String,
74 },
75 Io(io::Error),
78 ReadDir {
80 path: PathBuf,
81 source: io::Error,
82 },
83 Metadata {
85 path: PathBuf,
86 source: io::Error,
87 },
88 RemoveFile {
90 path: PathBuf,
91 source: io::Error,
92 },
93 RemoveDir {
95 path: PathBuf,
96 source: io::Error,
97 },
98 UnsupportedOpaqueReference(OpaqueReference),
101 DanglingNamedReference {
104 reference: String,
105 },
106 UnresolvedGenericReference {
108 reference: String,
109 },
110 Framework {
112 message: Cow<'static, str>,
113 source: FrameworkSource,
114 },
115
116 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 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}