use std::fmt;
use thiserror::Error;
use typst::diag::{FileError, SourceDiagnostic};
#[derive(Error, Debug)]
pub enum PapermakeError {
#[error("Template error: {0}")]
Template(#[from] TemplateError),
#[error("Compilation error: {0}")]
Compilation(#[from] CompilationError),
#[error("File system error: {0}")]
FileSystem(#[from] FileSystemError),
#[error("Data error: {0}")]
Data(#[from] DataError),
#[error("Configuration error: {0}")]
Config(#[from] ConfigError),
}
#[derive(Error, Debug)]
pub enum TemplateError {
#[error("Template not found: {path}")]
NotFound { path: String },
#[error("Invalid template structure: {message}")]
InvalidStructure { message: String },
#[error("Missing required file: {file}")]
MissingFile { file: String },
#[error("Invalid template content: {reason}")]
InvalidContent { reason: String },
#[error("Template dependency error: {dependency} - {reason}")]
DependencyError { dependency: String, reason: String },
}
#[derive(Error, Debug)]
pub enum CompilationError {
#[error("Typst compilation failed with {error_count} error(s)")]
TypstError {
error_count: usize,
diagnostics: Vec<DiagnosticInfo>,
},
#[error("Template compilation failed: {message}")]
TemplateCompilation { message: String },
#[error("Data injection failed: {reason}")]
DataInjection { reason: String },
#[error("Syntax error in template: {message}")]
SyntaxError { message: String },
#[error("Import resolution failed: {import_path} - {reason}")]
ImportResolution { import_path: String, reason: String },
}
#[derive(Error, Debug)]
pub enum FileSystemError {
#[error("File not found: {path}")]
NotFound { path: String },
#[error("Permission denied: {path}")]
PermissionDenied { path: String },
#[error("Invalid file path: {path}")]
InvalidPath { path: String },
#[error("File read error: {path} - {reason}")]
ReadError { path: String, reason: String },
#[error("File write error: {path} - {reason}")]
WriteError { path: String, reason: String },
#[error("Invalid UTF-8 content in file: {path}")]
InvalidUtf8 { path: String },
}
#[derive(Error, Debug)]
pub enum DataError {
#[error("JSON serialization failed: {reason}")]
Serialization { reason: String },
#[error("JSON deserialization failed: {reason}")]
Deserialization { reason: String },
#[error("Schema validation failed: {message}")]
SchemaValidation { message: String },
#[error("Invalid data format: {expected}, got {actual}")]
InvalidFormat { expected: String, actual: String },
#[error("Missing required field: {field}")]
MissingField { field: String },
#[error("Invalid field value: {field} - {reason}")]
InvalidFieldValue { field: String, reason: String },
}
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("Font loading failed: {reason}")]
FontLoading { reason: String },
#[error("Cache initialization failed: {reason}")]
CacheInit { reason: String },
#[error("Invalid configuration: {setting} - {reason}")]
InvalidConfig { setting: String, reason: String },
#[error("Environment variable error: {var} - {reason}")]
Environment { var: String, reason: String },
#[error("Runtime error: {message}")]
Runtime { message: String },
}
#[derive(Debug, Clone)]
pub struct DiagnosticInfo {
pub message: String,
pub severity: DiagnosticSeverity,
pub location: Option<SourceLocation>,
pub hints: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DiagnosticSeverity {
Error,
Warning,
Info,
}
#[derive(Debug, Clone)]
pub struct SourceLocation {
pub file: String,
pub line: usize,
pub column: usize,
pub range: Option<(usize, usize)>,
}
impl fmt::Display for DiagnosticInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.location {
Some(loc) => write!(f, "{}:{}: {}", loc.file, loc.line, self.message),
None => write!(f, "{}", self.message),
}
}
}
impl fmt::Display for DiagnosticSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DiagnosticSeverity::Error => write!(f, "error"),
DiagnosticSeverity::Warning => write!(f, "warning"),
DiagnosticSeverity::Info => write!(f, "info"),
}
}
}
pub type Result<T> = std::result::Result<T, PapermakeError>;
pub fn convert_typst_diagnostic(diagnostic: SourceDiagnostic) -> DiagnosticInfo {
DiagnosticInfo {
message: diagnostic.message.to_string(),
severity: DiagnosticSeverity::Error, location: None, hints: diagnostic.hints.into_iter().map(|h| h.to_string()).collect(),
}
}
pub fn template_missing_file<S: Into<String>>(file: S) -> PapermakeError {
PapermakeError::Template(TemplateError::MissingFile { file: file.into() })
}
pub fn compilation_error_from_diagnostics(diagnostics: Vec<SourceDiagnostic>) -> PapermakeError {
let diagnostic_infos: Vec<DiagnosticInfo> = diagnostics
.into_iter()
.map(convert_typst_diagnostic)
.collect();
let error_count = diagnostic_infos.len();
PapermakeError::Compilation(CompilationError::TypstError {
error_count,
diagnostics: diagnostic_infos,
})
}
impl From<std::io::Error> for PapermakeError {
fn from(error: std::io::Error) -> Self {
let reason = error.to_string();
match error.kind() {
std::io::ErrorKind::NotFound => {
PapermakeError::FileSystem(FileSystemError::NotFound {
path: "<unknown>".to_string(),
})
}
std::io::ErrorKind::PermissionDenied => {
PapermakeError::FileSystem(FileSystemError::PermissionDenied {
path: "<unknown>".to_string(),
})
}
_ => PapermakeError::FileSystem(FileSystemError::ReadError {
path: "<unknown>".to_string(),
reason,
}),
}
}
}
impl From<serde_json::Error> for PapermakeError {
fn from(error: serde_json::Error) -> Self {
let reason = error.to_string();
if error.is_syntax() || error.is_data() {
PapermakeError::Data(DataError::Deserialization { reason })
} else {
PapermakeError::Data(DataError::Serialization { reason })
}
}
}
impl From<FileError> for PapermakeError {
fn from(error: FileError) -> Self {
match error {
FileError::NotFound(path) => {
PapermakeError::FileSystem(FileSystemError::NotFound {
path: path.display().to_string(),
})
}
FileError::AccessDenied => {
PapermakeError::FileSystem(FileSystemError::PermissionDenied {
path: "<unknown>".to_string(),
})
}
FileError::InvalidUtf8 => {
PapermakeError::FileSystem(FileSystemError::InvalidUtf8 {
path: "<unknown>".to_string(),
})
}
FileError::Other(msg) => {
PapermakeError::FileSystem(FileSystemError::ReadError {
path: "<unknown>".to_string(),
reason: msg.map(|m| m.to_string()).unwrap_or_else(|| "Unknown error".to_string()),
})
}
FileError::IsDirectory => {
PapermakeError::FileSystem(FileSystemError::InvalidPath {
path: "<directory>".to_string(),
})
}
FileError::NotSource => {
PapermakeError::FileSystem(FileSystemError::ReadError {
path: "<unknown>".to_string(),
reason: "File is not a Typst source file".to_string(),
})
}
FileError::Package(pkg_error) => {
PapermakeError::FileSystem(FileSystemError::ReadError {
path: "<package>".to_string(),
reason: format!("Package error: {:?}", pkg_error),
})
}
}
}
}
impl From<std::string::FromUtf8Error> for PapermakeError {
fn from(_error: std::string::FromUtf8Error) -> Self {
PapermakeError::FileSystem(FileSystemError::InvalidUtf8 {
path: "<unknown>".to_string(),
})
}
}
impl PapermakeError {
pub fn user_message(&self) -> String {
match self {
PapermakeError::Template(e) => match e {
TemplateError::NotFound { path } => {
format!("Template not found: {}", path)
}
TemplateError::InvalidStructure { message } => {
format!("Invalid template structure: {}", message)
}
TemplateError::MissingFile { file } => {
format!("Template is missing required file: {}", file)
}
_ => format!("Template error: {}", e),
},
PapermakeError::Compilation(e) => match e {
CompilationError::TypstError { error_count, .. } => {
format!("Template compilation failed with {} error(s)", error_count)
}
_ => format!("Compilation error: {}", e),
},
PapermakeError::FileSystem(e) => match e {
FileSystemError::NotFound { path } => {
format!("File not found: {}", path)
}
FileSystemError::PermissionDenied { path } => {
format!("Permission denied accessing: {}", path)
}
_ => format!("File system error: {}", e),
},
PapermakeError::Data(e) => match e {
DataError::Serialization { .. } => {
"Failed to serialize data. Please check your data format.".to_string()
}
DataError::Deserialization { .. } => {
"Failed to parse data. Please check your JSON format.".to_string()
}
_ => format!("Data error: {}", e),
},
PapermakeError::Config(e) => {
format!("Configuration error: {}", e)
}
}
}
pub fn is_recoverable(&self) -> bool {
match self {
PapermakeError::Template(TemplateError::NotFound { .. }) => false,
PapermakeError::FileSystem(FileSystemError::NotFound { .. }) => false,
PapermakeError::FileSystem(FileSystemError::PermissionDenied { .. }) => false,
PapermakeError::Config(_) => false,
_ => true,
}
}
pub fn suggestions(&self) -> Vec<String> {
match self {
PapermakeError::Template(TemplateError::NotFound { .. }) => {
vec![
"Check if the template path is correct".to_string(),
"Verify the template exists in the expected location".to_string(),
]
}
PapermakeError::Data(DataError::Deserialization { .. }) => {
vec![
"Verify your JSON syntax is valid".to_string(),
"Check for missing quotes or trailing commas".to_string(),
"Validate your data against the template schema".to_string(),
]
}
PapermakeError::Compilation(CompilationError::DataInjection { .. }) => {
vec![
"Ensure your data matches the expected structure".to_string(),
"Check if required fields are present".to_string(),
]
}
_ => vec![],
}
}
}