mod recovery;
pub use recovery::{ErrorRecoverable, ErrorRecovery};
use std::fmt;
use std::path::PathBuf;
use thiserror::Error;
#[allow(clippy::result_large_err)]
pub type KrikResult<T> = Result<T, KrikError>;
#[derive(Debug, Error)]
pub enum KrikError {
#[error(transparent)]
Cli(#[from] Box<CliError>),
#[error(transparent)]
Config(#[from] Box<ConfigError>),
#[error(transparent)]
Io(#[from] Box<IoError>),
#[error(transparent)]
Markdown(#[from] Box<MarkdownError>),
#[error(transparent)]
Template(#[from] Box<TemplateError>),
#[error(transparent)]
Theme(#[from] Box<ThemeError>),
#[error(transparent)]
Server(#[from] Box<ServerError>),
#[error(transparent)]
Content(#[from] Box<ContentError>),
#[error(transparent)]
Generation(#[from] Box<GenerationError>),
}
#[derive(Debug)]
pub struct CliError {
pub kind: CliErrorKind,
pub path: Option<PathBuf>,
pub context: String,
}
#[derive(Debug)]
pub enum CliErrorKind {
PathDoesNotExist,
NotADirectory,
PermissionDenied,
CreateDirFailed(std::io::Error),
CanonicalizeFailed(std::io::Error),
InvalidPort(String),
ThemeNotFound,
}
#[derive(Debug)]
pub struct ConfigError {
pub kind: ConfigErrorKind,
pub path: Option<PathBuf>,
pub context: String,
}
#[derive(Debug)]
pub enum ConfigErrorKind {
NotFound,
InvalidToml(toml::de::Error),
InvalidYaml(serde_yaml::Error),
MissingField(String),
InvalidValue {
field: String,
expected: String,
found: String,
},
PermissionDenied,
}
#[derive(Debug)]
pub struct IoError {
pub kind: IoErrorKind,
pub path: PathBuf,
pub context: String,
}
#[derive(Debug)]
pub enum IoErrorKind {
NotFound,
PermissionDenied,
AlreadyExists,
InvalidPath,
WriteFailed(std::io::Error),
ReadFailed(std::io::Error),
}
#[derive(Debug)]
pub struct MarkdownError {
pub kind: MarkdownErrorKind,
pub file: PathBuf,
pub line: Option<usize>,
pub column: Option<usize>,
pub context: String,
}
#[derive(Debug)]
pub enum MarkdownErrorKind {
InvalidFrontMatter(serde_yaml::Error),
MissingFrontMatterField(String),
InvalidDate(String),
ParseError(String),
InvalidLanguage(String),
CircularReference(PathBuf),
}
#[derive(Debug)]
pub struct TemplateError {
pub kind: TemplateErrorKind,
pub template: String,
pub context: String,
}
#[derive(Debug)]
pub enum TemplateErrorKind {
NotFound,
SyntaxError(tera::Error),
MissingVariable(String),
RenderError(tera::Error),
CompileError(tera::Error),
}
#[derive(Debug)]
pub struct ThemeError {
pub kind: ThemeErrorKind,
pub theme_path: PathBuf,
pub context: String,
}
#[derive(Debug)]
pub enum ThemeErrorKind {
NotFound,
InvalidConfig(ConfigError),
MissingTemplate(String),
AssetError(String),
}
#[derive(Debug)]
pub struct ServerError {
pub kind: ServerErrorKind,
pub context: String,
}
#[derive(Debug)]
pub enum ServerErrorKind {
BindError { port: u16, source: std::io::Error },
WatchError(notify::Error),
WebSocketError(String),
LiveReloadError(String),
}
#[derive(Debug)]
pub struct ContentError {
pub kind: ContentErrorKind,
pub path: Option<PathBuf>,
pub context: String,
}
#[derive(Debug)]
pub enum ContentErrorKind {
InvalidType(String),
DuplicateSlug(String),
InvalidFileName(String),
ValidationFailed(Vec<String>),
}
#[derive(Debug)]
pub struct GenerationError {
pub kind: GenerationErrorKind,
pub context: String,
}
#[derive(Debug)]
pub enum GenerationErrorKind {
NoContent,
OutputDirError(std::io::Error),
AssetCopyError {
source: PathBuf,
target: PathBuf,
error: std::io::Error,
},
FeedError(String),
SitemapError(String),
}
impl fmt::Display for CliError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let path_str = self
.path
.as_ref()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| "<unknown>".to_string());
match &self.kind {
CliErrorKind::PathDoesNotExist => write!(
f,
"Path does not exist: {}\n Context: {}\n Suggestion: Create it with `mkdir -p {}` or double-check the --path argument",
path_str, self.context, path_str
),
CliErrorKind::NotADirectory => write!(
f,
"Not a directory: {}\n Context: {}\n Suggestion: Provide a directory path, not a file",
path_str, self.context
),
CliErrorKind::PermissionDenied => write!(
f,
"Permission denied for: {}\n Context: {}\n Suggestion: Check permissions or run with appropriate privileges",
path_str, self.context
),
CliErrorKind::CreateDirFailed(e) => write!(
f,
"Failed to create directory: {}\n Error: {}\n Context: {}\n Suggestion: Ensure parent directory exists and you have write permissions",
path_str, e, self.context
),
CliErrorKind::CanonicalizeFailed(e) => write!(
f,
"Failed to resolve absolute path: {}\n Error: {}\n Context: {}\n Suggestion: Ensure the path exists and is accessible",
path_str, e, self.context
),
CliErrorKind::InvalidPort(value) => write!(
f,
"Invalid port number: {}\n Context: {}\n Suggestion: Use a value between 1 and 65535 (e.g., --port 3000)",
value, self.context
),
CliErrorKind::ThemeNotFound => write!(
f,
"Theme directory not found: {}\n Context: {}\n Suggestion: Ensure the theme exists or run `kk init` to install the default theme",
path_str, self.context
),
}
}
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let path_str = self
.path
.as_ref()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| "<unknown>".to_string());
match &self.kind {
ConfigErrorKind::NotFound => {
write!(f, "Configuration file not found: {path_str}")
}
ConfigErrorKind::InvalidToml(e) => {
write!(
f,
"Invalid TOML in {}: {}\n Context: {}",
path_str, e, self.context
)
}
ConfigErrorKind::InvalidYaml(e) => {
write!(
f,
"Invalid YAML in {}: {}\n Context: {}",
path_str, e, self.context
)
}
ConfigErrorKind::MissingField(field) => {
write!(
f,
"Missing required field '{}' in {}\n Context: {}",
field, path_str, self.context
)
}
ConfigErrorKind::InvalidValue {
field,
expected,
found,
} => {
write!(f, "Invalid value for field '{}' in {}\n Expected: {}\n Found: {}\n Context: {}",
field, path_str, expected, found, self.context)
}
ConfigErrorKind::PermissionDenied => {
write!(
f,
"Permission denied accessing configuration file: {path_str}"
)
}
}
}
}
impl fmt::Display for IoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let path_str = self.path.to_string_lossy();
match &self.kind {
IoErrorKind::NotFound => {
write!(
f,
"File or directory not found: {}\n Context: {}",
path_str, self.context
)
}
IoErrorKind::PermissionDenied => {
write!(
f,
"Permission denied: {}\n Context: {}",
path_str, self.context
)
}
IoErrorKind::AlreadyExists => {
write!(
f,
"File already exists: {}\n Context: {}",
path_str, self.context
)
}
IoErrorKind::InvalidPath => {
write!(
f,
"Invalid file path: {}\n Context: {}",
path_str, self.context
)
}
IoErrorKind::WriteFailed(e) => {
write!(
f,
"Failed to write file: {}\n Error: {}\n Context: {}",
path_str, e, self.context
)
}
IoErrorKind::ReadFailed(e) => {
write!(
f,
"Failed to read file: {}\n Error: {}\n Context: {}",
path_str, e, self.context
)
}
}
}
}
impl fmt::Display for MarkdownError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let file_str = self.file.to_string_lossy();
let location = match (self.line, self.column) {
(Some(line), Some(col)) => format!(" at line {line}, column {col}"),
(Some(line), None) => format!(" at line {line}"),
_ => String::new(),
};
match &self.kind {
MarkdownErrorKind::InvalidFrontMatter(e) => {
write!(
f,
"Invalid front matter in {}{}\n Error: {}\n Context: {}",
file_str, location, e, self.context
)
}
MarkdownErrorKind::MissingFrontMatterField(field) => {
write!(
f,
"Missing required front matter field '{}' in {}{}\n Context: {}",
field, file_str, location, self.context
)
}
MarkdownErrorKind::InvalidDate(date) => {
write!(f, "Invalid date format '{}' in {}{}\n Expected ISO 8601 format (e.g., 2024-01-15T10:30:00Z)\n Context: {}",
date, file_str, location, self.context)
}
MarkdownErrorKind::ParseError(msg) => {
write!(
f,
"Markdown parsing error in {}{}\n Error: {}\n Context: {}",
file_str, location, msg, self.context
)
}
MarkdownErrorKind::InvalidLanguage(lang) => {
write!(f, "Invalid language code '{}' in {}{}\n Supported languages: en, it, es, fr, de, pt, ja, zh, ru, ar\n Context: {}",
lang, file_str, location, self.context)
}
MarkdownErrorKind::CircularReference(ref_path) => {
write!(
f,
"Circular reference detected: {} references {}\n Context: {}",
file_str,
ref_path.to_string_lossy(),
self.context
)
}
}
}
}
impl fmt::Display for TemplateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
TemplateErrorKind::NotFound => {
write!(
f,
"Template not found: {}\n Context: {}",
self.template, self.context
)
}
TemplateErrorKind::SyntaxError(e) => {
write!(
f,
"Template syntax error in {}\n Error: {}\n Context: {}",
self.template, e, self.context
)
}
TemplateErrorKind::MissingVariable(var) => {
write!(
f,
"Missing template variable '{}' in {}\n Context: {}",
var, self.template, self.context
)
}
TemplateErrorKind::RenderError(e) => {
write!(
f,
"Template rendering failed for {}\n Error: {}\n Context: {}",
self.template, e, self.context
)
}
TemplateErrorKind::CompileError(e) => {
write!(
f,
"Template compilation failed for {}\n Error: {}\n Context: {}",
self.template, e, self.context
)
}
}
}
}
impl fmt::Display for ThemeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let theme_str = self.theme_path.to_string_lossy();
match &self.kind {
ThemeErrorKind::NotFound => {
write!(
f,
"Theme not found: {}\n Context: {}",
theme_str, self.context
)
}
ThemeErrorKind::InvalidConfig(e) => {
write!(
f,
"Invalid theme configuration in {}\n Error: {}\n Context: {}",
theme_str, e, self.context
)
}
ThemeErrorKind::MissingTemplate(template) => {
write!(
f,
"Missing required template '{}' in theme {}\n Context: {}",
template, theme_str, self.context
)
}
ThemeErrorKind::AssetError(msg) => {
write!(
f,
"Asset processing error in theme {}\n Error: {}\n Context: {}",
theme_str, msg, self.context
)
}
}
}
}
impl fmt::Display for ServerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
ServerErrorKind::BindError { port, source } => {
write!(f, "Failed to bind to port {}\n Error: {}\n Context: {}\n Suggestion: Try a different port with --port <PORT>",
port, source, self.context)
}
ServerErrorKind::WatchError(e) => {
write!(
f,
"File watching failed\n Error: {}\n Context: {}",
e, self.context
)
}
ServerErrorKind::WebSocketError(msg) => {
write!(f, "WebSocket error: {}\n Context: {}", msg, self.context)
}
ServerErrorKind::LiveReloadError(msg) => {
write!(
f,
"Live reload error: {}\n Context: {}\n Suggestion: Try --no-live-reload flag",
msg, self.context
)
}
}
}
}
impl fmt::Display for ContentError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let path_str = self
.path
.as_ref()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| "<unknown>".to_string());
match &self.kind {
ContentErrorKind::InvalidType(content_type) => {
write!(
f,
"Invalid content type '{}' for {}\n Context: {}",
content_type, path_str, self.context
)
}
ContentErrorKind::DuplicateSlug(slug) => {
write!(
f,
"Duplicate slug '{}' found\n Path: {}\n Context: {}",
slug, path_str, self.context
)
}
ContentErrorKind::InvalidFileName(filename) => {
write!(f, "Invalid file name '{}'\n Context: {}\n Suggestion: Use alphanumeric characters, hyphens, and underscores only",
filename, self.context)
}
ContentErrorKind::ValidationFailed(errors) => {
write!(f, "Content validation failed for {path_str}\n Issues:\n")?;
for error in errors {
writeln!(f, " - {error}")?;
}
write!(f, " Context: {}", self.context)
}
}
}
}
impl fmt::Display for GenerationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
GenerationErrorKind::NoContent => {
write!(f, "No content found to generate\n Context: {}\n Suggestion: Add .md files to your content directory",
self.context)
}
GenerationErrorKind::OutputDirError(e) => {
write!(
f,
"Failed to create output directory\n Error: {}\n Context: {}",
e, self.context
)
}
GenerationErrorKind::AssetCopyError {
source,
target,
error,
} => {
write!(
f,
"Failed to copy asset\n From: {}\n To: {}\n Error: {}\n Context: {}",
source.to_string_lossy(),
target.to_string_lossy(),
error,
self.context
)
}
GenerationErrorKind::FeedError(msg) => {
write!(
f,
"Feed generation failed\n Error: {}\n Context: {}",
msg, self.context
)
}
GenerationErrorKind::SitemapError(msg) => {
write!(
f,
"Sitemap generation failed\n Error: {}\n Context: {}",
msg, self.context
)
}
}
}
}
impl std::error::Error for CliError {}
impl std::error::Error for ConfigError {}
impl std::error::Error for IoError {}
impl std::error::Error for MarkdownError {}
impl std::error::Error for TemplateError {}
impl std::error::Error for ThemeError {}
impl std::error::Error for ServerError {}
impl std::error::Error for ContentError {}
impl std::error::Error for GenerationError {}
impl From<std::io::Error> for KrikError {
fn from(e: std::io::Error) -> Self {
KrikError::Io(Box::new(IoError {
kind: match e.kind() {
std::io::ErrorKind::NotFound => IoErrorKind::NotFound,
std::io::ErrorKind::PermissionDenied => IoErrorKind::PermissionDenied,
std::io::ErrorKind::AlreadyExists => IoErrorKind::AlreadyExists,
_ => IoErrorKind::ReadFailed(e),
},
path: PathBuf::new(), context: "I/O operation".to_string(),
}))
}
}
impl From<toml::de::Error> for KrikError {
fn from(e: toml::de::Error) -> Self {
KrikError::Config(Box::new(ConfigError {
kind: ConfigErrorKind::InvalidToml(e),
path: None,
context: "TOML parsing".to_string(),
}))
}
}
impl From<serde_yaml::Error> for KrikError {
fn from(e: serde_yaml::Error) -> Self {
KrikError::Config(Box::new(ConfigError {
kind: ConfigErrorKind::InvalidYaml(e),
path: None,
context: "YAML parsing".to_string(),
}))
}
}
impl From<tera::Error> for KrikError {
fn from(e: tera::Error) -> Self {
KrikError::Template(Box::new(TemplateError {
kind: TemplateErrorKind::RenderError(e),
template: "<unknown>".to_string(),
context: "Template processing".to_string(),
}))
}
}
#[macro_export]
macro_rules! io_error {
($kind:expr, $path:expr, $context:expr) => {
$crate::error::KrikError::Io(Box::new($crate::error::IoError {
kind: $kind,
path: $path.into(),
context: $context.to_string(),
}))
};
}
#[macro_export]
macro_rules! markdown_error {
($kind:expr, $file:expr, $context:expr) => {
$crate::error::KrikError::Markdown(Box::new($crate::error::MarkdownError {
kind: $kind,
file: $file.into(),
line: None,
column: None,
context: $context.to_string(),
}))
};
($kind:expr, $file:expr, $line:expr, $context:expr) => {
$crate::error::KrikError::Markdown(Box::new($crate::error::MarkdownError {
kind: $kind,
file: $file.into(),
line: Some($line),
column: None,
context: $context.to_string(),
}))
};
}
#[macro_export]
macro_rules! template_error {
($kind:expr, $template:expr, $context:expr) => {
$crate::error::KrikError::Template(Box::new($crate::error::TemplateError {
kind: $kind,
template: $template.to_string(),
context: $context.to_string(),
}))
};
}
#[macro_export]
macro_rules! config_error {
($kind:expr, $path:expr, $context:expr) => {
$crate::error::KrikError::Config(Box::new($crate::error::ConfigError {
kind: $kind,
path: Some($path.into()),
context: $context.to_string(),
}))
};
}