use std::{borrow::Cow, error, fmt, io, panic::Location, path::PathBuf};
use specta::datatype::OpaqueReference;
use crate::Layout;
use super::legacy::ExportPath;
#[non_exhaustive]
pub struct Error {
kind: ErrorKind,
}
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>,
},
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,
},
UnsupportedOpaqueReference(OpaqueReference),
DanglingNamedReference {
reference: String,
},
UnresolvedGenericReference {
reference: String,
},
Framework {
message: Cow<'static, str>,
source: FrameworkSource,
},
BigIntForbiddenLegacy(ExportPath),
ForbiddenNameLegacy(ExportPath, &'static str),
InvalidNameLegacy(ExportPath, String),
FmtLegacy(std::fmt::Error),
UnableToExport(Layout),
}
impl Error {
pub(crate) fn invalid_map_key(
path: impl Into<String>,
reason: impl Into<Cow<'static, str>>,
) -> Self {
Self {
kind: 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 {
kind: ErrorKind::Framework {
message: message.into(),
source: source.into(),
},
}
}
pub(crate) fn bigint_forbidden(path: String) -> Self {
Self {
kind: ErrorKind::BigIntForbidden { path },
}
}
pub(crate) fn invalid_name(path: String, name: impl Into<Cow<'static, str>>) -> Self {
Self {
kind: ErrorKind::InvalidName {
path,
name: name.into(),
},
}
}
pub(crate) fn duplicate_type_name(
name: Cow<'static, str>,
first: Location<'static>,
second: Location<'static>,
) -> Self {
Self {
kind: ErrorKind::DuplicateTypeName {
name,
first: format_location(first),
second: format_location(second),
},
}
}
pub(crate) fn read_dir(path: PathBuf, source: io::Error) -> Self {
Self {
kind: ErrorKind::ReadDir { path, source },
}
}
pub(crate) fn metadata(path: PathBuf, source: io::Error) -> Self {
Self {
kind: ErrorKind::Metadata { path, source },
}
}
pub(crate) fn remove_file(path: PathBuf, source: io::Error) -> Self {
Self {
kind: ErrorKind::RemoveFile { path, source },
}
}
pub(crate) fn remove_dir(path: PathBuf, source: io::Error) -> Self {
Self {
kind: ErrorKind::RemoveDir { path, source },
}
}
pub(crate) fn unsupported_opaque_reference(reference: OpaqueReference) -> Self {
Self {
kind: ErrorKind::UnsupportedOpaqueReference(reference),
}
}
pub(crate) fn dangling_named_reference(reference: String) -> Self {
Self {
kind: ErrorKind::DanglingNamedReference { reference },
}
}
pub(crate) fn unresolved_generic_reference(reference: String) -> Self {
Self {
kind: ErrorKind::UnresolvedGenericReference { reference },
}
}
pub(crate) fn forbidden_name_legacy(path: ExportPath, name: &'static str) -> Self {
Self {
kind: ErrorKind::ForbiddenNameLegacy(path, name),
}
}
pub(crate) fn invalid_name_legacy(path: ExportPath, name: String) -> Self {
Self {
kind: ErrorKind::InvalidNameLegacy(path, name),
}
}
pub(crate) fn unable_to_export(layout: Layout) -> Self {
Self {
kind: ErrorKind::UnableToExport(layout),
}
}
}
impl From<io::Error> for Error {
fn from(error: io::Error) -> Self {
Self {
kind: ErrorKind::Io(error),
}
}
}
impl From<std::fmt::Error> for Error {
fn from(error: std::fmt::Error) -> Self {
Self {
kind: ErrorKind::FmtLegacy(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 {path:?} but was unable to due toname {name:?} conflicting with a reserved keyword in Typescript. Try renaming it or using `#[specta(rename = \"new name\")]`"
),
ErrorKind::InvalidName { path, name } => write!(
f,
"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\")]`"
),
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::UnsupportedOpaqueReference(reference) => write!(
f,
"Found unsupported opaque reference '{}'. It is not supported by the Typescript exporter.",
reference.type_name()
),
ErrorKind::DanglingNamedReference { reference } => write!(
f,
"Found dangling named reference {reference}. The referenced type is missing from the resolved type collection."
),
ErrorKind::UnresolvedGenericReference { reference } => write!(
f,
"Found unresolved generic reference {reference}. The generic is missing from the active named type scope."
),
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::BigIntForbiddenLegacy(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::ForbiddenNameLegacy(path, name) => write!(
f,
"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\")]`"
),
ErrorKind::InvalidNameLegacy(path, name) => write!(
f,
"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\")]`"
),
ErrorKind::FmtLegacy(err) => write!(f, "formatter: {err:?}"),
ErrorKind::UnableToExport(layout) => write!(
f,
"Unable to export layout {layout} with the current configuration. Maybe try `Exporter::export_to` or switching to Typescript."
),
}
}
}
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, .. } => Some(source),
ErrorKind::Framework { source, .. } => Some(source.as_ref()),
ErrorKind::FmtLegacy(error) => Some(error),
_ => None,
}
}
}
fn format_location(location: Location<'static>) -> String {
format!(
"{}:{}:{}",
location.file(),
location.line(),
location.column()
)
}