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 UnrepresentableAliasCycle {
190 path: String,
191 reason: Cow<'static, str>,
192 },
193}
194
195impl Error {
196 fn new(kind: ErrorKind) -> Self {
197 Self {
198 kind,
199 named_datatype: None,
200 trace: Vec::new(),
201 }
202 }
203
204 pub fn named_datatype(&self) -> Option<&NamedDataType> {
206 self.named_datatype.as_deref()
207 }
208
209 pub fn trace(&self) -> &[ErrorTraceFrame] {
211 &self.trace
212 }
213
214 pub(crate) fn with_named_datatype(mut self, ndt: &NamedDataType) -> Self {
215 self.named_datatype
216 .get_or_insert_with(|| Box::new(ndt.clone()));
217 self
218 }
219
220 pub(crate) fn with_inline_trace(
221 mut self,
222 ndt: Option<&NamedDataType>,
223 path: impl Into<String>,
224 ) -> Self {
225 self.trace.push(ErrorTraceFrame::Inlined {
226 named_datatype: ndt.map(|ndt| Box::new(ndt.clone())),
227 path: path.into(),
228 });
229 self
230 }
231
232 pub(crate) fn invalid_map_key(
233 path: impl Into<String>,
234 reason: impl Into<Cow<'static, str>>,
235 ) -> Self {
236 Self::new(ErrorKind::InvalidMapKey {
237 path: path.into(),
238 reason: reason.into(),
239 })
240 }
241
242 pub fn framework(
244 message: impl Into<Cow<'static, str>>,
245 source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
246 ) -> Self {
247 Self::new(ErrorKind::Framework {
248 message: message.into(),
249 source: source.into(),
250 })
251 }
252
253 pub(crate) fn format(
255 message: impl Into<Cow<'static, str>>,
256 source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
257 ) -> Self {
258 Self::new(ErrorKind::Format {
259 message: message.into(),
260 path: None,
261 source: source.into(),
262 })
263 }
264
265 pub(crate) fn format_at(
266 message: impl Into<Cow<'static, str>>,
267 path: impl Into<String>,
268 source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
269 ) -> Self {
270 Self::new(ErrorKind::Format {
271 message: message.into(),
272 path: Some(path.into()),
273 source: source.into(),
274 })
275 }
276
277 pub(crate) fn bigint_forbidden(path: String) -> Self {
278 Self::new(ErrorKind::BigIntForbidden { path })
279 }
280
281 pub(crate) fn invalid_name(path: String, name: impl Into<Cow<'static, str>>) -> Self {
282 Self::new(ErrorKind::InvalidName {
283 path,
284 name: name.into(),
285 })
286 }
287
288 pub(crate) fn empty_name(path: String) -> Self {
289 Self::new(ErrorKind::EmptyName { path })
290 }
291
292 pub(crate) fn unsupported_anonymous_enum_variant(
293 path: String,
294 variant_kind: &'static str,
295 ) -> Self {
296 Self::new(ErrorKind::UnsupportedAnonymousEnumVariant { path, variant_kind })
297 }
298
299 pub(crate) fn forbidden_name(path: String, name: &'static str) -> Self {
300 Self::new(ErrorKind::ForbiddenName { path, name })
301 }
302
303 pub(crate) fn duplicate_type_name(
304 name: Cow<'static, str>,
305 first: Location<'static>,
306 second: Location<'static>,
307 ) -> Self {
308 Self::new(ErrorKind::DuplicateTypeName {
309 name,
310 first: format_location(first),
311 second: format_location(second),
312 })
313 }
314
315 pub(crate) fn read_dir(path: PathBuf, source: io::Error) -> Self {
316 Self::new(ErrorKind::ReadDir { path, source })
317 }
318
319 pub(crate) fn metadata(path: PathBuf, source: io::Error) -> Self {
320 Self::new(ErrorKind::Metadata { path, source })
321 }
322
323 pub(crate) fn remove_file(path: PathBuf, source: io::Error) -> Self {
324 Self::new(ErrorKind::RemoveFile { path, source })
325 }
326
327 pub(crate) fn remove_dir(path: PathBuf, source: io::Error) -> Self {
328 Self::new(ErrorKind::RemoveDir { path, source })
329 }
330
331 pub(crate) fn create_dir(path: PathBuf, source: io::Error) -> Self {
332 Self::new(ErrorKind::CreateDir { path, source })
333 }
334
335 pub(crate) fn write_file(path: PathBuf, source: io::Error) -> Self {
336 Self::new(ErrorKind::WriteFile { path, source })
337 }
338
339 pub(crate) fn read_file(path: PathBuf, source: io::Error) -> Self {
340 Self::new(ErrorKind::ReadFile { path, source })
341 }
342
343 pub(crate) fn unsupported_opaque_reference(path: String, reference: OpaqueReference) -> Self {
344 Self::new(ErrorKind::UnsupportedOpaqueReference { path, reference })
345 }
346
347 pub(crate) fn dangling_named_reference(path: String, reference: String) -> Self {
348 Self::new(ErrorKind::DanglingNamedReference { path, reference })
349 }
350
351 pub(crate) fn infinite_recursive_inline_type(
352 path: String,
353 reference: String,
354 cycle: RecursiveInlineType,
355 ) -> Self {
356 Self::new(ErrorKind::InfiniteRecursiveInlineType {
357 path,
358 reference,
359 cycle,
360 })
361 }
362
363 pub(crate) fn inline_recursion_limit_exceeded(path: String) -> Self {
364 Self::new(ErrorKind::InlineRecursionLimitExceeded { path })
365 }
366
367 pub(crate) fn export_requires_export_to(layout: Layout) -> Self {
368 Self::new(ErrorKind::ExportRequiresExportTo(layout))
369 }
370
371 pub(crate) fn jsdoc_namespaces_unsupported() -> Self {
372 Self::new(ErrorKind::JsdocNamespacesUnsupported)
373 }
374
375 pub(crate) fn unrepresentable_alias_cycle(
376 path: String,
377 reason: impl Into<Cow<'static, str>>,
378 ) -> Self {
379 Self::new(ErrorKind::UnrepresentableAliasCycle {
380 path,
381 reason: reason.into(),
382 })
383 }
384}
385
386impl From<io::Error> for Error {
387 fn from(error: io::Error) -> Self {
388 Self::new(ErrorKind::Io(error))
389 }
390}
391
392impl fmt::Display for Error {
393 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
394 match &self.kind {
395 ErrorKind::InvalidMapKey { path, reason } => {
396 write!(f, "Invalid map key at '{path}': {reason}")
397 }
398 ErrorKind::BigIntForbidden { path } => write!(
399 f,
400 "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."
401 ),
402 ErrorKind::ForbiddenName { path, name } => write!(
403 f,
404 "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\")]`",
405 display_path(path)
406 ),
407 ErrorKind::InvalidName { path, name } => write!(
408 f,
409 "Attempted to export {} but was unable to due to name {name:?} containing an invalid character. Try renaming it or using `#[specta(rename = \"new name\")]`",
410 display_path(path)
411 ),
412 ErrorKind::EmptyName { path } => write!(
413 f,
414 "Attempted to export {} but was unable to because the Typescript type name is empty. Try renaming it or using `#[specta(rename = \"new name\")]`",
415 display_path(path)
416 ),
417 ErrorKind::UnsupportedAnonymousEnumVariant { path, variant_kind } => write!(
418 f,
419 "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.",
420 display_path(path)
421 ),
422 ErrorKind::DuplicateTypeName {
423 name,
424 first,
425 second,
426 } => write!(
427 f,
428 "Detected multiple types with the same name: {name:?} at {first} and {second}"
429 ),
430 ErrorKind::Io(err) => write!(f, "IO error: {err}"),
431 ErrorKind::ReadDir { path, source } => {
432 write!(f, "Failed to read directory '{}': {source}", path.display())
433 }
434 ErrorKind::Metadata { path, source } => {
435 write!(
436 f,
437 "Failed to read metadata for '{}': {source}",
438 path.display()
439 )
440 }
441 ErrorKind::RemoveFile { path, source } => {
442 write!(f, "Failed to remove file '{}': {source}", path.display())
443 }
444 ErrorKind::RemoveDir { path, source } => {
445 write!(
446 f,
447 "Failed to remove directory '{}': {source}",
448 path.display()
449 )
450 }
451 ErrorKind::CreateDir { path, source } => {
452 write!(
453 f,
454 "Failed to create directory '{}': {source}",
455 path.display()
456 )
457 }
458 ErrorKind::WriteFile { path, source } => {
459 write!(f, "Failed to write file '{}': {source}", path.display())
460 }
461 ErrorKind::ReadFile { path, source } => {
462 write!(f, "Failed to read file '{}': {source}", path.display())
463 }
464 ErrorKind::UnsupportedOpaqueReference { path, reference } => write!(
465 f,
466 "Found unsupported opaque reference '{}' at {}. It is not supported by the Typescript exporter.",
467 reference.type_name(),
468 display_path(path)
469 ),
470 ErrorKind::DanglingNamedReference { path, reference } => write!(
471 f,
472 "Found dangling named reference {reference} at {}. The referenced type is missing from the resolved type collection.",
473 display_path(path)
474 ),
475 ErrorKind::InfiniteRecursiveInlineType {
476 path,
477 reference,
478 cycle,
479 } => {
480 write!(
481 f,
482 "Found infinitely recursive inline named reference {reference} at {}. Recursive inline types cannot be expanded because they would produce an infinite Typescript type.",
483 display_path(path)
484 )?;
485 write!(f, "\nInline cycle:\n {cycle:?}")?;
486 Ok(())
487 }
488 ErrorKind::InlineRecursionLimitExceeded { path } if path.is_empty() => write!(
489 f,
490 "Type recursion limit exceeded while expanding the provided inline type. Recursive inline types cannot be expanded because they would produce an infinite Typescript type."
491 ),
492 ErrorKind::InlineRecursionLimitExceeded { path } => write!(
493 f,
494 "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.",
495 display_path(path)
496 ),
497 ErrorKind::Framework { message, source } => {
498 let source = source.to_string();
499 if message.is_empty() && source.is_empty() {
500 write!(f, "Framework error")
501 } else if source.is_empty() {
502 write!(f, "Framework error: {message}")
503 } else {
504 write!(f, "Framework error: {message}: {source}")
505 }
506 }
507 ErrorKind::Format {
508 message,
509 path,
510 source,
511 } => {
512 let source = source.to_string();
513 let location = path
514 .as_deref()
515 .filter(|path| !path.is_empty())
516 .map(|path| format!(" at {}", display_path(path)))
517 .unwrap_or_default();
518 if message.is_empty() && source.is_empty() {
519 write!(f, "Format error{location}")
520 } else if source.is_empty() {
521 write!(f, "Format error{location}: {message}")
522 } else {
523 write!(f, "Format error{location}: {message}: {source}")
524 }
525 }
526 ErrorKind::ExportRequiresExportTo(layout) => write!(
527 f,
528 "Unable to export layout {layout} as a single string. Use `Exporter::export_to` with a directory path for file-based exports."
529 ),
530 ErrorKind::JsdocNamespacesUnsupported => write!(
531 f,
532 "Unable to export JSDoc with the Namespaces layout. Disable JSDoc or use FlatFile, ModulePrefixedName, or Files layout."
533 ),
534 ErrorKind::UnrepresentableAliasCycle { path, reason } => write!(
535 f,
536 "Attempted to export {} but it is part of a recursive type-alias cycle that cannot be collapsed into valid TypeScript: {reason}. TypeScript rejects self-referential type aliases (TS2456); restructure the type so the recursion passes through an object, array, or tuple instead of a bare (untagged/transparent) reference.",
537 display_path(path)
538 ),
539 }?;
540
541 if let Some(ndt) = self.named_datatype() {
542 write!(
543 f,
544 "\nRust type: {}::{} at {}",
545 ndt.module_path,
546 ndt.name,
547 format_location(ndt.location)
548 )?;
549 }
550
551 if !self.trace.is_empty() {
552 write!(f, "\nWhile inlining:")?;
553 for frame in self.trace.iter().rev() {
554 match frame {
555 ErrorTraceFrame::Inlined {
556 named_datatype,
557 path,
558 } => {
559 write!(f, "\n {path} -> ")?;
560 if let Some(ndt) = named_datatype.as_deref() {
561 write!(f, "{}::{}", ndt.module_path, ndt.name)?;
562 } else {
563 write!(f, "<unresolved named type>")?;
564 }
565 }
566 }
567 }
568 }
569
570 Ok(())
571 }
572}
573
574impl fmt::Debug for Error {
575 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
576 fmt::Display::fmt(self, f)
577 }
578}
579
580impl error::Error for Error {
581 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
582 match &self.kind {
583 ErrorKind::Io(error) => Some(error),
584 ErrorKind::ReadDir { source, .. }
585 | ErrorKind::Metadata { source, .. }
586 | ErrorKind::RemoveFile { source, .. }
587 | ErrorKind::RemoveDir { source, .. }
588 | ErrorKind::CreateDir { source, .. }
589 | ErrorKind::WriteFile { source, .. }
590 | ErrorKind::ReadFile { source, .. } => Some(source),
591 ErrorKind::Framework { source, .. } | ErrorKind::Format { source, .. } => {
592 Some(source.as_ref())
593 }
594 _ => None,
595 }
596 }
597}
598
599fn format_location(location: Location<'static>) -> String {
600 format!(
601 "{}:{}:{}",
602 location.file(),
603 location.line(),
604 location.column()
605 )
606}
607
608fn display_path(path: &str) -> Cow<'_, str> {
609 if path.is_empty() {
610 Cow::Borrowed("<unknown path>")
611 } else {
612 Cow::Owned(format!("{path:?}"))
613 }
614}