use std::{borrow::Cow, error, fmt, io, panic::Location, path::PathBuf};
use specta::datatype::{NamedDataType, OpaqueReference, RecursiveInlineType};
use crate::Layout;
#[non_exhaustive]
pub struct Error {
kind: ErrorKind,
named_datatype: Option<Box<NamedDataType>>,
trace: Vec<ErrorTraceFrame>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum ErrorTraceFrame {
Inlined {
named_datatype: Option<Box<NamedDataType>>,
path: String,
},
}
type FrameworkSource = Box<dyn error::Error + Send + Sync + 'static>;
const BIGINT_DOCS_URL: &str =
"https://docs.rs/specta-typescript/latest/specta_typescript/struct.Error.html#bigint-forbidden";
#[allow(dead_code)]
enum ErrorKind {
InvalidMapKey {
path: String,
reason: Cow<'static, str>,
},
BigIntForbidden {
path: String,
},
ForbiddenName {
path: String,
name: &'static str,
},
InvalidName {
path: String,
name: Cow<'static, str>,
},
EmptyName {
path: String,
},
UnsupportedAnonymousEnumVariant {
path: String,
variant_kind: &'static str,
},
DuplicateTypeName {
name: Cow<'static, str>,
first: String,
second: String,
},
Io(io::Error),
ReadDir {
path: PathBuf,
source: io::Error,
},
Metadata {
path: PathBuf,
source: io::Error,
},
RemoveFile {
path: PathBuf,
source: io::Error,
},
RemoveDir {
path: PathBuf,
source: io::Error,
},
CreateDir {
path: PathBuf,
source: io::Error,
},
WriteFile {
path: PathBuf,
source: io::Error,
},
ReadFile {
path: PathBuf,
source: io::Error,
},
UnsupportedOpaqueReference {
path: String,
reference: OpaqueReference,
},
DanglingNamedReference {
path: String,
reference: String,
},
InfiniteRecursiveInlineType {
path: String,
reference: String,
cycle: RecursiveInlineType,
},
InlineRecursionLimitExceeded {
path: String,
},
Framework {
message: Cow<'static, str>,
source: FrameworkSource,
},
Format {
message: Cow<'static, str>,
path: Option<String>,
source: FrameworkSource,
},
ExportRequiresExportTo(Layout),
JsdocNamespacesUnsupported,
}
impl Error {
fn new(kind: ErrorKind) -> Self {
Self {
kind,
named_datatype: None,
trace: Vec::new(),
}
}
pub fn named_datatype(&self) -> Option<&NamedDataType> {
self.named_datatype.as_deref()
}
pub fn trace(&self) -> &[ErrorTraceFrame] {
&self.trace
}
pub(crate) fn with_named_datatype(mut self, ndt: &NamedDataType) -> Self {
self.named_datatype
.get_or_insert_with(|| Box::new(ndt.clone()));
self
}
pub(crate) fn with_inline_trace(
mut self,
ndt: Option<&NamedDataType>,
path: impl Into<String>,
) -> Self {
self.trace.push(ErrorTraceFrame::Inlined {
named_datatype: ndt.map(|ndt| Box::new(ndt.clone())),
path: path.into(),
});
self
}
pub(crate) fn invalid_map_key(
path: impl Into<String>,
reason: impl Into<Cow<'static, str>>,
) -> Self {
Self::new(ErrorKind::InvalidMapKey {
path: path.into(),
reason: reason.into(),
})
}
pub fn framework(
message: impl Into<Cow<'static, str>>,
source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
) -> Self {
Self::new(ErrorKind::Framework {
message: message.into(),
source: source.into(),
})
}
pub(crate) fn format(
message: impl Into<Cow<'static, str>>,
source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
) -> Self {
Self::new(ErrorKind::Format {
message: message.into(),
path: None,
source: source.into(),
})
}
pub(crate) fn format_at(
message: impl Into<Cow<'static, str>>,
path: impl Into<String>,
source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
) -> Self {
Self::new(ErrorKind::Format {
message: message.into(),
path: Some(path.into()),
source: source.into(),
})
}
pub(crate) fn bigint_forbidden(path: String) -> Self {
Self::new(ErrorKind::BigIntForbidden { path })
}
pub(crate) fn invalid_name(path: String, name: impl Into<Cow<'static, str>>) -> Self {
Self::new(ErrorKind::InvalidName {
path,
name: name.into(),
})
}
pub(crate) fn empty_name(path: String) -> Self {
Self::new(ErrorKind::EmptyName { path })
}
pub(crate) fn unsupported_anonymous_enum_variant(
path: String,
variant_kind: &'static str,
) -> Self {
Self::new(ErrorKind::UnsupportedAnonymousEnumVariant { path, variant_kind })
}
pub(crate) fn forbidden_name(path: String, name: &'static str) -> Self {
Self::new(ErrorKind::ForbiddenName { path, name })
}
pub(crate) fn duplicate_type_name(
name: Cow<'static, str>,
first: Location<'static>,
second: Location<'static>,
) -> Self {
Self::new(ErrorKind::DuplicateTypeName {
name,
first: format_location(first),
second: format_location(second),
})
}
pub(crate) fn read_dir(path: PathBuf, source: io::Error) -> Self {
Self::new(ErrorKind::ReadDir { path, source })
}
pub(crate) fn metadata(path: PathBuf, source: io::Error) -> Self {
Self::new(ErrorKind::Metadata { path, source })
}
pub(crate) fn remove_file(path: PathBuf, source: io::Error) -> Self {
Self::new(ErrorKind::RemoveFile { path, source })
}
pub(crate) fn remove_dir(path: PathBuf, source: io::Error) -> Self {
Self::new(ErrorKind::RemoveDir { path, source })
}
pub(crate) fn create_dir(path: PathBuf, source: io::Error) -> Self {
Self::new(ErrorKind::CreateDir { path, source })
}
pub(crate) fn write_file(path: PathBuf, source: io::Error) -> Self {
Self::new(ErrorKind::WriteFile { path, source })
}
pub(crate) fn read_file(path: PathBuf, source: io::Error) -> Self {
Self::new(ErrorKind::ReadFile { path, source })
}
pub(crate) fn unsupported_opaque_reference(path: String, reference: OpaqueReference) -> Self {
Self::new(ErrorKind::UnsupportedOpaqueReference { path, reference })
}
pub(crate) fn dangling_named_reference(path: String, reference: String) -> Self {
Self::new(ErrorKind::DanglingNamedReference { path, reference })
}
pub(crate) fn infinite_recursive_inline_type(
path: String,
reference: String,
cycle: RecursiveInlineType,
) -> Self {
Self::new(ErrorKind::InfiniteRecursiveInlineType {
path,
reference,
cycle,
})
}
pub(crate) fn inline_recursion_limit_exceeded(path: String) -> Self {
Self::new(ErrorKind::InlineRecursionLimitExceeded { path })
}
pub(crate) fn export_requires_export_to(layout: Layout) -> Self {
Self::new(ErrorKind::ExportRequiresExportTo(layout))
}
pub(crate) fn jsdoc_namespaces_unsupported() -> Self {
Self::new(ErrorKind::JsdocNamespacesUnsupported)
}
}
impl From<io::Error> for Error {
fn from(error: io::Error) -> Self {
Self::new(ErrorKind::Io(error))
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
ErrorKind::InvalidMapKey { path, reason } => {
write!(f, "Invalid map key at '{path}': {reason}")
}
ErrorKind::BigIntForbidden { path } => write!(
f,
"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."
),
ErrorKind::ForbiddenName { path, name } => write!(
f,
"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\")]`",
display_path(path)
),
ErrorKind::InvalidName { path, name } => write!(
f,
"Attempted to export {} but was unable to due to name {name:?} containing an invalid character. Try renaming it or using `#[specta(rename = \"new name\")]`",
display_path(path)
),
ErrorKind::EmptyName { path } => write!(
f,
"Attempted to export {} but was unable to because the Typescript type name is empty. Try renaming it or using `#[specta(rename = \"new name\")]`",
display_path(path)
),
ErrorKind::UnsupportedAnonymousEnumVariant { path, variant_kind } => write!(
f,
"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.",
display_path(path)
),
ErrorKind::DuplicateTypeName {
name,
first,
second,
} => write!(
f,
"Detected multiple types with the same name: {name:?} at {first} and {second}"
),
ErrorKind::Io(err) => write!(f, "IO error: {err}"),
ErrorKind::ReadDir { path, source } => {
write!(f, "Failed to read directory '{}': {source}", path.display())
}
ErrorKind::Metadata { path, source } => {
write!(
f,
"Failed to read metadata for '{}': {source}",
path.display()
)
}
ErrorKind::RemoveFile { path, source } => {
write!(f, "Failed to remove file '{}': {source}", path.display())
}
ErrorKind::RemoveDir { path, source } => {
write!(
f,
"Failed to remove directory '{}': {source}",
path.display()
)
}
ErrorKind::CreateDir { path, source } => {
write!(
f,
"Failed to create directory '{}': {source}",
path.display()
)
}
ErrorKind::WriteFile { path, source } => {
write!(f, "Failed to write file '{}': {source}", path.display())
}
ErrorKind::ReadFile { path, source } => {
write!(f, "Failed to read file '{}': {source}", path.display())
}
ErrorKind::UnsupportedOpaqueReference { path, reference } => write!(
f,
"Found unsupported opaque reference '{}' at {}. It is not supported by the Typescript exporter.",
reference.type_name(),
display_path(path)
),
ErrorKind::DanglingNamedReference { path, reference } => write!(
f,
"Found dangling named reference {reference} at {}. The referenced type is missing from the resolved type collection.",
display_path(path)
),
ErrorKind::InfiniteRecursiveInlineType {
path,
reference,
cycle,
} => {
write!(
f,
"Found infinitely recursive inline named reference {reference} at {}. Recursive inline types cannot be expanded because they would produce an infinite Typescript type.",
display_path(path)
)?;
write!(f, "\nInline cycle:\n {cycle:?}")?;
Ok(())
}
ErrorKind::InlineRecursionLimitExceeded { path } if path.is_empty() => write!(
f,
"Type recursion limit exceeded while expanding the provided inline type. Recursive inline types cannot be expanded because they would produce an infinite Typescript type."
),
ErrorKind::InlineRecursionLimitExceeded { path } => write!(
f,
"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.",
display_path(path)
),
ErrorKind::Framework { message, source } => {
let source = source.to_string();
if message.is_empty() && source.is_empty() {
write!(f, "Framework error")
} else if source.is_empty() {
write!(f, "Framework error: {message}")
} else {
write!(f, "Framework error: {message}: {source}")
}
}
ErrorKind::Format {
message,
path,
source,
} => {
let source = source.to_string();
let location = path
.as_deref()
.filter(|path| !path.is_empty())
.map(|path| format!(" at {}", display_path(path)))
.unwrap_or_default();
if message.is_empty() && source.is_empty() {
write!(f, "Format error{location}")
} else if source.is_empty() {
write!(f, "Format error{location}: {message}")
} else {
write!(f, "Format error{location}: {message}: {source}")
}
}
ErrorKind::ExportRequiresExportTo(layout) => write!(
f,
"Unable to export layout {layout} as a single string. Use `Exporter::export_to` with a directory path for file-based exports."
),
ErrorKind::JsdocNamespacesUnsupported => write!(
f,
"Unable to export JSDoc with the Namespaces layout. Disable JSDoc or use FlatFile, ModulePrefixedName, or Files layout."
),
}?;
if let Some(ndt) = self.named_datatype() {
write!(
f,
"\nRust type: {}::{} at {}",
ndt.module_path,
ndt.name,
format_location(ndt.location)
)?;
}
if !self.trace.is_empty() {
write!(f, "\nWhile inlining:")?;
for frame in self.trace.iter().rev() {
match frame {
ErrorTraceFrame::Inlined {
named_datatype,
path,
} => {
write!(f, "\n {path} -> ")?;
if let Some(ndt) = named_datatype.as_deref() {
write!(f, "{}::{}", ndt.module_path, ndt.name)?;
} else {
write!(f, "<unresolved named type>")?;
}
}
}
}
}
Ok(())
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match &self.kind {
ErrorKind::Io(error) => Some(error),
ErrorKind::ReadDir { source, .. }
| ErrorKind::Metadata { source, .. }
| ErrorKind::RemoveFile { source, .. }
| ErrorKind::RemoveDir { source, .. }
| ErrorKind::CreateDir { source, .. }
| ErrorKind::WriteFile { source, .. }
| ErrorKind::ReadFile { source, .. } => Some(source),
ErrorKind::Framework { source, .. } | ErrorKind::Format { source, .. } => {
Some(source.as_ref())
}
_ => None,
}
}
}
fn format_location(location: Location<'static>) -> String {
format!(
"{}:{}:{}",
location.file(),
location.line(),
location.column()
)
}
fn display_path(path: &str) -> Cow<'_, str> {
if path.is_empty() {
Cow::Borrowed("<unknown path>")
} else {
Cow::Owned(format!("{path:?}"))
}
}