1use std::{borrow::Cow, error, fmt, io, panic::Location, path::PathBuf};
2
3use specta::datatype::{NamedDataType, OpaqueReference, RecursiveInlineType};
4
5use crate::types::NDT;
6
7#[non_exhaustive]
8pub struct Error {
9 kind: ErrorKind,
10 named_datatype: Option<Box<NamedDataType>>,
11 trace: Vec<ErrorTraceFrame>,
12}
13
14#[derive(Debug, Clone)]
16#[non_exhaustive]
17pub enum ErrorTraceFrame {
18 Inlined {
20 named_datatype: Option<Box<NamedDataType>>,
22 path: String,
24 },
25}
26
27type FrameworkSource = Box<dyn error::Error + Send + Sync + 'static>;
28const BIGINT_DOCS_URL: &str =
29 "https://docs.rs/specta-typescript/latest/specta_typescript/struct.Error.html#bigint-forbidden";
30
31#[allow(dead_code)]
32enum ErrorKind {
33 InvalidMapKey {
38 path: String,
39 reason: Cow<'static, str>,
40 },
41 BigIntForbidden { path: String },
43 ForbiddenName { path: String, name: &'static str },
45 InvalidName {
47 path: String,
48 name: Cow<'static, str>,
49 },
50 EmptyName { path: String },
52 UnsupportedAnonymousEnumVariant {
54 path: String,
55 variant_kind: &'static str,
56 },
57 DuplicateTypeName {
62 name: Cow<'static, str>,
63 first: String,
64 second: String,
65 },
66 Io(io::Error),
69 ReadDir { path: PathBuf, source: io::Error },
71 Metadata { path: PathBuf, source: io::Error },
73 RemoveFile { path: PathBuf, source: io::Error },
75 RemoveDir { path: PathBuf, source: io::Error },
77 CreateDir { path: PathBuf, source: io::Error },
79 WriteFile { path: PathBuf, source: io::Error },
81 ReadFile { path: PathBuf, source: io::Error },
83 UnsupportedOpaqueReference {
86 path: String,
87 reference: OpaqueReference,
88 },
89 DanglingNamedReference { path: String, reference: String },
92 InfiniteRecursiveInlineType {
94 path: String,
95 reference: String,
96 cycle: RecursiveInlineType,
97 },
98 InlineRecursionLimitExceeded { path: String },
100 Framework {
102 message: Cow<'static, str>,
103 source: FrameworkSource,
104 },
105 Format {
107 message: Cow<'static, str>,
108 path: Option<String>,
109 source: FrameworkSource,
110 },
111 UnrepresentableAliasCycle {
112 path: String,
113 reason: Cow<'static, str>,
114 },
115}
116
117impl Error {
118 fn new(kind: ErrorKind) -> Self {
119 Self {
120 kind,
121 named_datatype: None,
122 trace: Vec::new(),
123 }
124 }
125
126 pub fn named_datatype(&self) -> Option<&NamedDataType> {
128 self.named_datatype.as_deref()
129 }
130
131 pub fn trace(&self) -> &[ErrorTraceFrame] {
133 &self.trace
134 }
135
136 pub(crate) fn with_named_datatype(mut self, ndt: &NDT) -> Self {
137 self.named_datatype
138 .get_or_insert_with(|| Box::new(ndt.inner().clone()));
139 self
140 }
141
142 pub(crate) fn with_inline_trace(
143 mut self,
144 ndt: Option<&NamedDataType>,
145 path: impl Into<String>,
146 ) -> Self {
147 self.trace.push(ErrorTraceFrame::Inlined {
148 named_datatype: ndt.map(|ndt| Box::new(ndt.clone())),
149 path: path.into(),
150 });
151 self
152 }
153
154 pub(crate) fn invalid_map_key(
155 path: impl Into<String>,
156 reason: impl Into<Cow<'static, str>>,
157 ) -> Self {
158 Self::new(ErrorKind::InvalidMapKey {
159 path: path.into(),
160 reason: reason.into(),
161 })
162 }
163
164 pub fn framework(
166 message: impl Into<Cow<'static, str>>,
167 source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
168 ) -> Self {
169 Self::new(ErrorKind::Framework {
170 message: message.into(),
171 source: source.into(),
172 })
173 }
174
175 pub(crate) fn bigint_forbidden(path: String) -> Self {
200 Self::new(ErrorKind::BigIntForbidden { path })
201 }
202
203 pub(crate) fn invalid_name(path: String, name: impl Into<Cow<'static, str>>) -> Self {
204 Self::new(ErrorKind::InvalidName {
205 path,
206 name: name.into(),
207 })
208 }
209
210 pub(crate) fn empty_name(path: String) -> Self {
211 Self::new(ErrorKind::EmptyName { path })
212 }
213
214 pub(crate) fn unsupported_anonymous_enum_variant(
215 path: String,
216 variant_kind: &'static str,
217 ) -> Self {
218 Self::new(ErrorKind::UnsupportedAnonymousEnumVariant { path, variant_kind })
219 }
220
221 pub(crate) fn forbidden_name(path: String, name: &'static str) -> Self {
222 Self::new(ErrorKind::ForbiddenName { path, name })
223 }
224
225 pub(crate) fn read_dir(path: PathBuf, source: io::Error) -> Self {
238 Self::new(ErrorKind::ReadDir { path, source })
239 }
240
241 pub(crate) fn metadata(path: PathBuf, source: io::Error) -> Self {
242 Self::new(ErrorKind::Metadata { path, source })
243 }
244
245 pub(crate) fn remove_file(path: PathBuf, source: io::Error) -> Self {
246 Self::new(ErrorKind::RemoveFile { path, source })
247 }
248
249 pub(crate) fn remove_dir(path: PathBuf, source: io::Error) -> Self {
250 Self::new(ErrorKind::RemoveDir { path, source })
251 }
252
253 pub(crate) fn create_dir(path: PathBuf, source: io::Error) -> Self {
254 Self::new(ErrorKind::CreateDir { path, source })
255 }
256
257 pub(crate) fn write_file(path: PathBuf, source: io::Error) -> Self {
258 Self::new(ErrorKind::WriteFile { path, source })
259 }
260
261 pub(crate) fn read_file(path: PathBuf, source: io::Error) -> Self {
262 Self::new(ErrorKind::ReadFile { path, source })
263 }
264
265 pub(crate) fn unsupported_opaque_reference(path: String, reference: OpaqueReference) -> Self {
266 Self::new(ErrorKind::UnsupportedOpaqueReference { path, reference })
267 }
268
269 pub(crate) fn dangling_named_reference(path: String, reference: String) -> Self {
270 Self::new(ErrorKind::DanglingNamedReference { path, reference })
271 }
272
273 pub(crate) fn infinite_recursive_inline_type(
274 path: String,
275 reference: String,
276 cycle: RecursiveInlineType,
277 ) -> Self {
278 Self::new(ErrorKind::InfiniteRecursiveInlineType {
279 path,
280 reference,
281 cycle,
282 })
283 }
284
285 }
299
300impl From<io::Error> for Error {
301 fn from(error: io::Error) -> Self {
302 Self::new(ErrorKind::Io(error))
303 }
304}
305
306impl fmt::Display for Error {
307 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308 match &self.kind {
309 ErrorKind::InvalidMapKey { path, reason } => {
310 write!(f, "Invalid map key at '{path}': {reason}")
311 }
312 ErrorKind::BigIntForbidden { path } => write!(
313 f,
314 "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."
315 ),
316 ErrorKind::ForbiddenName { path, name } => write!(
317 f,
318 "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\")]`",
319 display_path(path)
320 ),
321 ErrorKind::InvalidName { path, name } => write!(
322 f,
323 "Attempted to export {} but was unable to due to name {name:?} containing an invalid character. Try renaming it or using `#[specta(rename = \"new name\")]`",
324 display_path(path)
325 ),
326 ErrorKind::EmptyName { path } => write!(
327 f,
328 "Attempted to export {} but was unable to because the Typescript type name is empty. Try renaming it or using `#[specta(rename = \"new name\")]`",
329 display_path(path)
330 ),
331 ErrorKind::UnsupportedAnonymousEnumVariant { path, variant_kind } => write!(
332 f,
333 "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.",
334 display_path(path)
335 ),
336 ErrorKind::DuplicateTypeName {
337 name,
338 first,
339 second,
340 } => write!(
341 f,
342 "Detected multiple types with the same name: {name:?} at {first} and {second}"
343 ),
344 ErrorKind::Io(err) => write!(f, "IO error: {err}"),
345 ErrorKind::ReadDir { path, source } => {
346 write!(f, "Failed to read directory '{}': {source}", path.display())
347 }
348 ErrorKind::Metadata { path, source } => {
349 write!(
350 f,
351 "Failed to read metadata for '{}': {source}",
352 path.display()
353 )
354 }
355 ErrorKind::RemoveFile { path, source } => {
356 write!(f, "Failed to remove file '{}': {source}", path.display())
357 }
358 ErrorKind::RemoveDir { path, source } => {
359 write!(
360 f,
361 "Failed to remove directory '{}': {source}",
362 path.display()
363 )
364 }
365 ErrorKind::CreateDir { path, source } => {
366 write!(
367 f,
368 "Failed to create directory '{}': {source}",
369 path.display()
370 )
371 }
372 ErrorKind::WriteFile { path, source } => {
373 write!(f, "Failed to write file '{}': {source}", path.display())
374 }
375 ErrorKind::ReadFile { path, source } => {
376 write!(f, "Failed to read file '{}': {source}", path.display())
377 }
378 ErrorKind::UnsupportedOpaqueReference { path, reference } => write!(
379 f,
380 "Found unsupported opaque reference '{}' at {}. It is not supported by the Elm exporter.",
381 reference.type_name(),
382 display_path(path)
383 ),
384 ErrorKind::DanglingNamedReference { path, reference } => write!(
385 f,
386 "Found dangling named reference {reference} at {}. The referenced type is missing from the resolved type collection.",
387 display_path(path)
388 ),
389 ErrorKind::InfiniteRecursiveInlineType {
390 path,
391 reference,
392 cycle,
393 } => {
394 write!(
395 f,
396 "Found infinitely recursive inline named reference {reference} at {}. Recursive inline types cannot be expanded because they would produce an infinite Elm type.",
397 display_path(path)
398 )?;
399 write!(f, "\nInline cycle:\n {cycle:?}")?;
400 Ok(())
401 }
402 ErrorKind::InlineRecursionLimitExceeded { path } if path.is_empty() => write!(
403 f,
404 "Type recursion limit exceeded while expanding the provided inline type. Recursive inline types cannot be expanded because they would produce an infinite Typescript type."
405 ),
406 ErrorKind::InlineRecursionLimitExceeded { path } => write!(
407 f,
408 "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.",
409 display_path(path)
410 ),
411 ErrorKind::Framework { message, source } => {
412 let source = source.to_string();
413 if message.is_empty() && source.is_empty() {
414 write!(f, "Framework error")
415 } else if source.is_empty() {
416 write!(f, "Framework error: {message}")
417 } else {
418 write!(f, "Framework error: {message}: {source}")
419 }
420 }
421 ErrorKind::Format {
422 message,
423 path,
424 source,
425 } => {
426 let source = source.to_string();
427 let location = path
428 .as_deref()
429 .filter(|path| !path.is_empty())
430 .map(|path| format!(" at {}", display_path(path)))
431 .unwrap_or_default();
432 if message.is_empty() && source.is_empty() {
433 write!(f, "Format error{location}")
434 } else if source.is_empty() {
435 write!(f, "Format error{location}: {message}")
436 } else {
437 write!(f, "Format error{location}: {message}: {source}")
438 }
439 }
440 ErrorKind::UnrepresentableAliasCycle { path, reason } => write!(
441 f,
442 "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.",
443 display_path(path)
444 ),
445 }?;
446
447 if let Some(ndt) = self.named_datatype() {
448 write!(
449 f,
450 "\nRust type: {}::{} at {}",
451 ndt.module_path,
452 ndt.name,
453 format_location(ndt.location)
454 )?;
455 }
456
457 if !self.trace.is_empty() {
458 write!(f, "\nWhile inlining:")?;
459 for frame in self.trace.iter().rev() {
460 match frame {
461 ErrorTraceFrame::Inlined {
462 named_datatype,
463 path,
464 } => {
465 write!(f, "\n {path} -> ")?;
466 if let Some(ndt) = named_datatype.as_deref() {
467 write!(f, "{}::{}", ndt.module_path, ndt.name)?;
468 } else {
469 write!(f, "<unresolved named type>")?;
470 }
471 }
472 }
473 }
474 }
475
476 Ok(())
477 }
478}
479
480impl fmt::Debug for Error {
481 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
482 fmt::Display::fmt(self, f)
483 }
484}
485
486impl error::Error for Error {
487 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
488 match &self.kind {
489 ErrorKind::Io(error) => Some(error),
490 ErrorKind::ReadDir { source, .. }
491 | ErrorKind::Metadata { source, .. }
492 | ErrorKind::RemoveFile { source, .. }
493 | ErrorKind::RemoveDir { source, .. }
494 | ErrorKind::CreateDir { source, .. }
495 | ErrorKind::WriteFile { source, .. }
496 | ErrorKind::ReadFile { source, .. } => Some(source),
497 ErrorKind::Framework { source, .. } | ErrorKind::Format { source, .. } => {
498 Some(source.as_ref())
499 }
500 _ => None,
501 }
502 }
503}
504
505fn format_location(location: Location<'static>) -> String {
506 format!(
507 "{}:{}:{}",
508 location.file(),
509 location.line(),
510 location.column()
511 )
512}
513
514fn display_path(path: &str) -> Cow<'_, str> {
515 if path.is_empty() {
516 Cow::Borrowed("<unknown path>")
517 } else {
518 Cow::Owned(format!("{path:?}"))
519 }
520}