1use std::{borrow::Cow, error, fmt, io, panic::Location, path::PathBuf};
2
3use specta::datatype::{NamedDataType, OpaqueReference, RecursiveInlineType};
4
5use crate::Layout;
6
7#[non_exhaustive]
43pub struct Error {
44 kind: ErrorKind,
45 named_datatype: Option<Box<NamedDataType>>,
46 trace: Vec<ErrorTraceFrame>,
47}
48
49#[derive(Debug, Clone)]
51#[non_exhaustive]
52pub enum ErrorTraceFrame {
53 Inlined {
55 named_datatype: Option<Box<NamedDataType>>,
57 path: String,
59 },
60}
61
62type FrameworkSource = Box<dyn error::Error + Send + Sync + 'static>;
63const BIGINT_DOCS_URL: &str =
64 "https://docs.rs/specta-typescript/latest/specta_typescript/struct.Error.html#bigint-forbidden";
65
66#[allow(dead_code)]
67enum ErrorKind {
68 InvalidMapKey {
73 path: String,
74 reason: Cow<'static, str>,
75 },
76 BigIntForbidden {
78 path: String,
79 },
80 ForbiddenName {
82 path: String,
83 name: &'static str,
84 },
85 InvalidName {
87 path: String,
88 name: Cow<'static, str>,
89 },
90 EmptyName {
92 path: String,
93 },
94 UnsupportedAnonymousEnumVariant {
96 path: String,
97 variant_kind: &'static str,
98 },
99 DuplicateTypeName {
104 name: Cow<'static, str>,
105 first: String,
106 second: String,
107 },
108 Io(io::Error),
111 ReadDir {
113 path: PathBuf,
114 source: io::Error,
115 },
116 Metadata {
118 path: PathBuf,
119 source: io::Error,
120 },
121 RemoveFile {
123 path: PathBuf,
124 source: io::Error,
125 },
126 RemoveDir {
128 path: PathBuf,
129 source: io::Error,
130 },
131 CreateDir {
133 path: PathBuf,
134 source: io::Error,
135 },
136 WriteFile {
138 path: PathBuf,
139 source: io::Error,
140 },
141 ReadFile {
143 path: PathBuf,
144 source: io::Error,
145 },
146 UnsupportedOpaqueReference {
149 path: String,
150 reference: OpaqueReference,
151 },
152 DanglingNamedReference {
155 path: String,
156 reference: String,
157 },
158 InfiniteRecursiveInlineType {
160 path: String,
161 reference: String,
162 cycle: RecursiveInlineType,
163 },
164 InlineRecursionLimitExceeded {
166 path: String,
167 },
168 Framework {
170 message: Cow<'static, str>,
171 source: FrameworkSource,
172 },
173 Format {
175 message: Cow<'static, str>,
176 path: Option<String>,
177 source: FrameworkSource,
178 },
179 ExportRequiresExportTo(Layout),
184 JsdocNamespacesUnsupported,
185}
186
187impl Error {
188 fn new(kind: ErrorKind) -> Self {
189 Self {
190 kind,
191 named_datatype: None,
192 trace: Vec::new(),
193 }
194 }
195
196 pub fn named_datatype(&self) -> Option<&NamedDataType> {
198 self.named_datatype.as_deref()
199 }
200
201 pub fn trace(&self) -> &[ErrorTraceFrame] {
203 &self.trace
204 }
205
206 pub(crate) fn with_named_datatype(mut self, ndt: &NamedDataType) -> Self {
207 self.named_datatype
208 .get_or_insert_with(|| Box::new(ndt.clone()));
209 self
210 }
211
212 pub(crate) fn with_inline_trace(
213 mut self,
214 ndt: Option<&NamedDataType>,
215 path: impl Into<String>,
216 ) -> Self {
217 self.trace.push(ErrorTraceFrame::Inlined {
218 named_datatype: ndt.map(|ndt| Box::new(ndt.clone())),
219 path: path.into(),
220 });
221 self
222 }
223
224 pub(crate) fn invalid_map_key(
225 path: impl Into<String>,
226 reason: impl Into<Cow<'static, str>>,
227 ) -> Self {
228 Self::new(ErrorKind::InvalidMapKey {
229 path: path.into(),
230 reason: reason.into(),
231 })
232 }
233
234 pub fn framework(
236 message: impl Into<Cow<'static, str>>,
237 source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
238 ) -> Self {
239 Self::new(ErrorKind::Framework {
240 message: message.into(),
241 source: source.into(),
242 })
243 }
244
245 pub(crate) fn format(
247 message: impl Into<Cow<'static, str>>,
248 source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
249 ) -> Self {
250 Self::new(ErrorKind::Format {
251 message: message.into(),
252 path: None,
253 source: source.into(),
254 })
255 }
256
257 pub(crate) fn format_at(
258 message: impl Into<Cow<'static, str>>,
259 path: impl Into<String>,
260 source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
261 ) -> Self {
262 Self::new(ErrorKind::Format {
263 message: message.into(),
264 path: Some(path.into()),
265 source: source.into(),
266 })
267 }
268
269 pub(crate) fn bigint_forbidden(path: String) -> Self {
270 Self::new(ErrorKind::BigIntForbidden { path })
271 }
272
273 pub(crate) fn invalid_name(path: String, name: impl Into<Cow<'static, str>>) -> Self {
274 Self::new(ErrorKind::InvalidName {
275 path,
276 name: name.into(),
277 })
278 }
279
280 pub(crate) fn empty_name(path: String) -> Self {
281 Self::new(ErrorKind::EmptyName { path })
282 }
283
284 pub(crate) fn unsupported_anonymous_enum_variant(
285 path: String,
286 variant_kind: &'static str,
287 ) -> Self {
288 Self::new(ErrorKind::UnsupportedAnonymousEnumVariant { path, variant_kind })
289 }
290
291 pub(crate) fn forbidden_name(path: String, name: &'static str) -> Self {
292 Self::new(ErrorKind::ForbiddenName { path, name })
293 }
294
295 pub(crate) fn duplicate_type_name(
296 name: Cow<'static, str>,
297 first: Location<'static>,
298 second: Location<'static>,
299 ) -> Self {
300 Self::new(ErrorKind::DuplicateTypeName {
301 name,
302 first: format_location(first),
303 second: format_location(second),
304 })
305 }
306
307 pub(crate) fn read_dir(path: PathBuf, source: io::Error) -> Self {
308 Self::new(ErrorKind::ReadDir { path, source })
309 }
310
311 pub(crate) fn metadata(path: PathBuf, source: io::Error) -> Self {
312 Self::new(ErrorKind::Metadata { path, source })
313 }
314
315 pub(crate) fn remove_file(path: PathBuf, source: io::Error) -> Self {
316 Self::new(ErrorKind::RemoveFile { path, source })
317 }
318
319 pub(crate) fn remove_dir(path: PathBuf, source: io::Error) -> Self {
320 Self::new(ErrorKind::RemoveDir { path, source })
321 }
322
323 pub(crate) fn create_dir(path: PathBuf, source: io::Error) -> Self {
324 Self::new(ErrorKind::CreateDir { path, source })
325 }
326
327 pub(crate) fn write_file(path: PathBuf, source: io::Error) -> Self {
328 Self::new(ErrorKind::WriteFile { path, source })
329 }
330
331 pub(crate) fn read_file(path: PathBuf, source: io::Error) -> Self {
332 Self::new(ErrorKind::ReadFile { path, source })
333 }
334
335 pub(crate) fn unsupported_opaque_reference(path: String, reference: OpaqueReference) -> Self {
336 Self::new(ErrorKind::UnsupportedOpaqueReference { path, reference })
337 }
338
339 pub(crate) fn dangling_named_reference(path: String, reference: String) -> Self {
340 Self::new(ErrorKind::DanglingNamedReference { path, reference })
341 }
342
343 pub(crate) fn infinite_recursive_inline_type(
344 path: String,
345 reference: String,
346 cycle: RecursiveInlineType,
347 ) -> Self {
348 Self::new(ErrorKind::InfiniteRecursiveInlineType {
349 path,
350 reference,
351 cycle,
352 })
353 }
354
355 pub(crate) fn inline_recursion_limit_exceeded(path: String) -> Self {
356 Self::new(ErrorKind::InlineRecursionLimitExceeded { path })
357 }
358
359 pub(crate) fn export_requires_export_to(layout: Layout) -> Self {
360 Self::new(ErrorKind::ExportRequiresExportTo(layout))
361 }
362
363 pub(crate) fn jsdoc_namespaces_unsupported() -> Self {
364 Self::new(ErrorKind::JsdocNamespacesUnsupported)
365 }
366}
367
368impl From<io::Error> for Error {
369 fn from(error: io::Error) -> Self {
370 Self::new(ErrorKind::Io(error))
371 }
372}
373
374impl fmt::Display for Error {
375 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
376 match &self.kind {
377 ErrorKind::InvalidMapKey { path, reason } => {
378 write!(f, "Invalid map key at '{path}': {reason}")
379 }
380 ErrorKind::BigIntForbidden { path } => write!(
381 f,
382 "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."
383 ),
384 ErrorKind::ForbiddenName { path, name } => write!(
385 f,
386 "Attempted to export {} but was unable to due to name {name:?} conflicting with a reserved keyword in Typescript. Try renaming it or using `#[specta(rename = \"new name\")]`",
387 display_path(path)
388 ),
389 ErrorKind::InvalidName { path, name } => write!(
390 f,
391 "Attempted to export {} but was unable to due to name {name:?} containing an invalid character. Try renaming it or using `#[specta(rename = \"new name\")]`",
392 display_path(path)
393 ),
394 ErrorKind::EmptyName { path } => write!(
395 f,
396 "Attempted to export {} but was unable to because the Typescript type name is empty. Try renaming it or using `#[specta(rename = \"new name\")]`",
397 display_path(path)
398 ),
399 ErrorKind::UnsupportedAnonymousEnumVariant { path, variant_kind } => write!(
400 f,
401 "Attempted to export {} but anonymous {variant_kind} enum variants cannot be exported to Typescript. Try giving the variant a name or changing the enum representation.",
402 display_path(path)
403 ),
404 ErrorKind::DuplicateTypeName {
405 name,
406 first,
407 second,
408 } => write!(
409 f,
410 "Detected multiple types with the same name: {name:?} at {first} and {second}"
411 ),
412 ErrorKind::Io(err) => write!(f, "IO error: {err}"),
413 ErrorKind::ReadDir { path, source } => {
414 write!(f, "Failed to read directory '{}': {source}", path.display())
415 }
416 ErrorKind::Metadata { path, source } => {
417 write!(
418 f,
419 "Failed to read metadata for '{}': {source}",
420 path.display()
421 )
422 }
423 ErrorKind::RemoveFile { path, source } => {
424 write!(f, "Failed to remove file '{}': {source}", path.display())
425 }
426 ErrorKind::RemoveDir { path, source } => {
427 write!(
428 f,
429 "Failed to remove directory '{}': {source}",
430 path.display()
431 )
432 }
433 ErrorKind::CreateDir { path, source } => {
434 write!(
435 f,
436 "Failed to create directory '{}': {source}",
437 path.display()
438 )
439 }
440 ErrorKind::WriteFile { path, source } => {
441 write!(f, "Failed to write file '{}': {source}", path.display())
442 }
443 ErrorKind::ReadFile { path, source } => {
444 write!(f, "Failed to read file '{}': {source}", path.display())
445 }
446 ErrorKind::UnsupportedOpaqueReference { path, reference } => write!(
447 f,
448 "Found unsupported opaque reference '{}' at {}. It is not supported by the Typescript exporter.",
449 reference.type_name(),
450 display_path(path)
451 ),
452 ErrorKind::DanglingNamedReference { path, reference } => write!(
453 f,
454 "Found dangling named reference {reference} at {}. The referenced type is missing from the resolved type collection.",
455 display_path(path)
456 ),
457 ErrorKind::InfiniteRecursiveInlineType {
458 path,
459 reference,
460 cycle,
461 } => {
462 write!(
463 f,
464 "Found infinitely recursive inline named reference {reference} at {}. Recursive inline types cannot be expanded because they would produce an infinite Typescript type.",
465 display_path(path)
466 )?;
467 write!(f, "\nInline cycle:\n {cycle:?}")?;
468 Ok(())
469 }
470 ErrorKind::InlineRecursionLimitExceeded { path } if path.is_empty() => write!(
471 f,
472 "Type recursion limit exceeded while expanding the provided inline type. Recursive inline types cannot be expanded because they would produce an infinite Typescript type."
473 ),
474 ErrorKind::InlineRecursionLimitExceeded { path } => write!(
475 f,
476 "Type recursion limit exceeded while expanding an inline Typescript type at {}. Recursive inline types cannot be expanded because they would produce an infinite Typescript type.",
477 display_path(path)
478 ),
479 ErrorKind::Framework { message, source } => {
480 let source = source.to_string();
481 if message.is_empty() && source.is_empty() {
482 write!(f, "Framework error")
483 } else if source.is_empty() {
484 write!(f, "Framework error: {message}")
485 } else {
486 write!(f, "Framework error: {message}: {source}")
487 }
488 }
489 ErrorKind::Format {
490 message,
491 path,
492 source,
493 } => {
494 let source = source.to_string();
495 let location = path
496 .as_deref()
497 .filter(|path| !path.is_empty())
498 .map(|path| format!(" at {}", display_path(path)))
499 .unwrap_or_default();
500 if message.is_empty() && source.is_empty() {
501 write!(f, "Format error{location}")
502 } else if source.is_empty() {
503 write!(f, "Format error{location}: {message}")
504 } else {
505 write!(f, "Format error{location}: {message}: {source}")
506 }
507 }
508 ErrorKind::ExportRequiresExportTo(layout) => write!(
509 f,
510 "Unable to export layout {layout} as a single string. Use `Exporter::export_to` with a directory path for file-based exports."
511 ),
512 ErrorKind::JsdocNamespacesUnsupported => write!(
513 f,
514 "Unable to export JSDoc with the Namespaces layout. Disable JSDoc or use FlatFile, ModulePrefixedName, or Files layout."
515 ),
516 }?;
517
518 if let Some(ndt) = self.named_datatype() {
519 write!(
520 f,
521 "\nRust type: {}::{} at {}",
522 ndt.module_path,
523 ndt.name,
524 format_location(ndt.location)
525 )?;
526 }
527
528 if !self.trace.is_empty() {
529 write!(f, "\nWhile inlining:")?;
530 for frame in self.trace.iter().rev() {
531 match frame {
532 ErrorTraceFrame::Inlined {
533 named_datatype,
534 path,
535 } => {
536 write!(f, "\n {path} -> ")?;
537 if let Some(ndt) = named_datatype.as_deref() {
538 write!(f, "{}::{}", ndt.module_path, ndt.name)?;
539 } else {
540 write!(f, "<unresolved named type>")?;
541 }
542 }
543 }
544 }
545 }
546
547 Ok(())
548 }
549}
550
551impl fmt::Debug for Error {
552 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
553 fmt::Display::fmt(self, f)
554 }
555}
556
557impl error::Error for Error {
558 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
559 match &self.kind {
560 ErrorKind::Io(error) => Some(error),
561 ErrorKind::ReadDir { source, .. }
562 | ErrorKind::Metadata { source, .. }
563 | ErrorKind::RemoveFile { source, .. }
564 | ErrorKind::RemoveDir { source, .. }
565 | ErrorKind::CreateDir { source, .. }
566 | ErrorKind::WriteFile { source, .. }
567 | ErrorKind::ReadFile { source, .. } => Some(source),
568 ErrorKind::Framework { source, .. } | ErrorKind::Format { source, .. } => {
569 Some(source.as_ref())
570 }
571 _ => None,
572 }
573 }
574}
575
576fn format_location(location: Location<'static>) -> String {
577 format!(
578 "{}:{}:{}",
579 location.file(),
580 location.line(),
581 location.column()
582 )
583}
584
585fn display_path(path: &str) -> Cow<'_, str> {
586 if path.is_empty() {
587 Cow::Borrowed("<unknown path>")
588 } else {
589 Cow::Owned(format!("{path:?}"))
590 }
591}