1mod recovery;
2
3pub use recovery::{ErrorRecoverable, ErrorRecovery};
4
5use std::fmt;
6use std::path::PathBuf;
7use thiserror::Error;
8
9#[allow(clippy::result_large_err)]
12pub type KrikResult<T> = Result<T, KrikError>;
13
14#[derive(Debug, Error)]
16pub enum KrikError {
17 #[error(transparent)]
19 Cli(#[from] Box<CliError>),
20 #[error(transparent)]
22 Config(#[from] Box<ConfigError>),
23 #[error(transparent)]
25 Io(#[from] Box<IoError>),
26 #[error(transparent)]
28 Markdown(#[from] Box<MarkdownError>),
29 #[error(transparent)]
31 Template(#[from] Box<TemplateError>),
32 #[error(transparent)]
34 Theme(#[from] Box<ThemeError>),
35 #[error(transparent)]
37 Server(#[from] Box<ServerError>),
38 #[error(transparent)]
40 Content(#[from] Box<ContentError>),
41 #[error(transparent)]
43 Generation(#[from] Box<GenerationError>),
44}
45#[derive(Debug)]
47pub struct CliError {
48 pub kind: CliErrorKind,
49 pub path: Option<PathBuf>,
50 pub context: String,
51}
52
53#[derive(Debug)]
54pub enum CliErrorKind {
55 PathDoesNotExist,
57 NotADirectory,
59 PermissionDenied,
61 CreateDirFailed(std::io::Error),
63 CanonicalizeFailed(std::io::Error),
65 InvalidPort(String),
67 ThemeNotFound,
69}
70
71#[derive(Debug)]
73pub struct ConfigError {
74 pub kind: ConfigErrorKind,
75 pub path: Option<PathBuf>,
76 pub context: String,
77}
78
79#[derive(Debug)]
80pub enum ConfigErrorKind {
81 NotFound,
83 InvalidToml(toml::de::Error),
85 InvalidYaml(serde_yaml::Error),
87 MissingField(String),
89 InvalidValue {
91 field: String,
92 expected: String,
93 found: String,
94 },
95 PermissionDenied,
97}
98
99#[derive(Debug)]
101pub struct IoError {
102 pub kind: IoErrorKind,
103 pub path: PathBuf,
104 pub context: String,
105}
106
107#[derive(Debug)]
108pub enum IoErrorKind {
109 NotFound,
111 PermissionDenied,
113 AlreadyExists,
115 InvalidPath,
117 WriteFailed(std::io::Error),
119 ReadFailed(std::io::Error),
121}
122
123#[derive(Debug)]
125pub struct MarkdownError {
126 pub kind: MarkdownErrorKind,
127 pub file: PathBuf,
128 pub line: Option<usize>,
129 pub column: Option<usize>,
130 pub context: String,
131}
132
133#[derive(Debug)]
134pub enum MarkdownErrorKind {
135 InvalidFrontMatter(serde_yaml::Error),
137 MissingFrontMatterField(String),
139 InvalidDate(String),
141 ParseError(String),
143 InvalidLanguage(String),
145 CircularReference(PathBuf),
147}
148
149#[derive(Debug)]
151pub struct TemplateError {
152 pub kind: TemplateErrorKind,
153 pub template: String,
154 pub context: String,
155}
156
157#[derive(Debug)]
158pub enum TemplateErrorKind {
159 NotFound,
161 SyntaxError(tera::Error),
163 MissingVariable(String),
165 RenderError(tera::Error),
167 CompileError(tera::Error),
169}
170
171#[derive(Debug)]
173pub struct ThemeError {
174 pub kind: ThemeErrorKind,
175 pub theme_path: PathBuf,
176 pub context: String,
177}
178
179#[derive(Debug)]
180pub enum ThemeErrorKind {
181 NotFound,
183 InvalidConfig(ConfigError),
185 MissingTemplate(String),
187 AssetError(String),
189}
190
191#[derive(Debug)]
193pub struct ServerError {
194 pub kind: ServerErrorKind,
195 pub context: String,
196}
197
198#[derive(Debug)]
199pub enum ServerErrorKind {
200 BindError { port: u16, source: std::io::Error },
202 WatchError(notify::Error),
204 WebSocketError(String),
206 LiveReloadError(String),
208}
209
210#[derive(Debug)]
212pub struct ContentError {
213 pub kind: ContentErrorKind,
214 pub path: Option<PathBuf>,
215 pub context: String,
216}
217
218#[derive(Debug)]
219pub enum ContentErrorKind {
220 InvalidType(String),
222 DuplicateSlug(String),
224 InvalidFileName(String),
226 ValidationFailed(Vec<String>),
228}
229
230#[derive(Debug)]
232pub struct GenerationError {
233 pub kind: GenerationErrorKind,
234 pub context: String,
235}
236
237#[derive(Debug)]
238pub enum GenerationErrorKind {
239 NoContent,
241 OutputDirError(std::io::Error),
243 AssetCopyError {
245 source: PathBuf,
246 target: PathBuf,
247 error: std::io::Error,
248 },
249 FeedError(String),
251 SitemapError(String),
253}
254
255impl fmt::Display for CliError {
258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259 let path_str = self
260 .path
261 .as_ref()
262 .map(|p| p.to_string_lossy().to_string())
263 .unwrap_or_else(|| "<unknown>".to_string());
264
265 match &self.kind {
266 CliErrorKind::PathDoesNotExist => write!(
267 f,
268 "Path does not exist: {}\n Context: {}\n Suggestion: Create it with `mkdir -p {}` or double-check the --path argument",
269 path_str, self.context, path_str
270 ),
271 CliErrorKind::NotADirectory => write!(
272 f,
273 "Not a directory: {}\n Context: {}\n Suggestion: Provide a directory path, not a file",
274 path_str, self.context
275 ),
276 CliErrorKind::PermissionDenied => write!(
277 f,
278 "Permission denied for: {}\n Context: {}\n Suggestion: Check permissions or run with appropriate privileges",
279 path_str, self.context
280 ),
281 CliErrorKind::CreateDirFailed(e) => write!(
282 f,
283 "Failed to create directory: {}\n Error: {}\n Context: {}\n Suggestion: Ensure parent directory exists and you have write permissions",
284 path_str, e, self.context
285 ),
286 CliErrorKind::CanonicalizeFailed(e) => write!(
287 f,
288 "Failed to resolve absolute path: {}\n Error: {}\n Context: {}\n Suggestion: Ensure the path exists and is accessible",
289 path_str, e, self.context
290 ),
291 CliErrorKind::InvalidPort(value) => write!(
292 f,
293 "Invalid port number: {}\n Context: {}\n Suggestion: Use a value between 1 and 65535 (e.g., --port 3000)",
294 value, self.context
295 ),
296 CliErrorKind::ThemeNotFound => write!(
297 f,
298 "Theme directory not found: {}\n Context: {}\n Suggestion: Ensure the theme exists or run `kk init` to install the default theme",
299 path_str, self.context
300 ),
301 }
302 }
303}
304
305impl fmt::Display for ConfigError {
306 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307 let path_str = self
308 .path
309 .as_ref()
310 .map(|p| p.to_string_lossy().to_string())
311 .unwrap_or_else(|| "<unknown>".to_string());
312
313 match &self.kind {
314 ConfigErrorKind::NotFound => {
315 write!(f, "Configuration file not found: {path_str}")
316 }
317 ConfigErrorKind::InvalidToml(e) => {
318 write!(
319 f,
320 "Invalid TOML in {}: {}\n Context: {}",
321 path_str, e, self.context
322 )
323 }
324 ConfigErrorKind::InvalidYaml(e) => {
325 write!(
326 f,
327 "Invalid YAML in {}: {}\n Context: {}",
328 path_str, e, self.context
329 )
330 }
331 ConfigErrorKind::MissingField(field) => {
332 write!(
333 f,
334 "Missing required field '{}' in {}\n Context: {}",
335 field, path_str, self.context
336 )
337 }
338 ConfigErrorKind::InvalidValue {
339 field,
340 expected,
341 found,
342 } => {
343 write!(f, "Invalid value for field '{}' in {}\n Expected: {}\n Found: {}\n Context: {}",
344 field, path_str, expected, found, self.context)
345 }
346 ConfigErrorKind::PermissionDenied => {
347 write!(
348 f,
349 "Permission denied accessing configuration file: {path_str}"
350 )
351 }
352 }
353 }
354}
355
356impl fmt::Display for IoError {
357 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358 let path_str = self.path.to_string_lossy();
359
360 match &self.kind {
361 IoErrorKind::NotFound => {
362 write!(
363 f,
364 "File or directory not found: {}\n Context: {}",
365 path_str, self.context
366 )
367 }
368 IoErrorKind::PermissionDenied => {
369 write!(
370 f,
371 "Permission denied: {}\n Context: {}",
372 path_str, self.context
373 )
374 }
375 IoErrorKind::AlreadyExists => {
376 write!(
377 f,
378 "File already exists: {}\n Context: {}",
379 path_str, self.context
380 )
381 }
382 IoErrorKind::InvalidPath => {
383 write!(
384 f,
385 "Invalid file path: {}\n Context: {}",
386 path_str, self.context
387 )
388 }
389 IoErrorKind::WriteFailed(e) => {
390 write!(
391 f,
392 "Failed to write file: {}\n Error: {}\n Context: {}",
393 path_str, e, self.context
394 )
395 }
396 IoErrorKind::ReadFailed(e) => {
397 write!(
398 f,
399 "Failed to read file: {}\n Error: {}\n Context: {}",
400 path_str, e, self.context
401 )
402 }
403 }
404 }
405}
406
407impl fmt::Display for MarkdownError {
408 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
409 let file_str = self.file.to_string_lossy();
410 let location = match (self.line, self.column) {
411 (Some(line), Some(col)) => format!(" at line {line}, column {col}"),
412 (Some(line), None) => format!(" at line {line}"),
413 _ => String::new(),
414 };
415
416 match &self.kind {
417 MarkdownErrorKind::InvalidFrontMatter(e) => {
418 write!(
419 f,
420 "Invalid front matter in {}{}\n Error: {}\n Context: {}",
421 file_str, location, e, self.context
422 )
423 }
424 MarkdownErrorKind::MissingFrontMatterField(field) => {
425 write!(
426 f,
427 "Missing required front matter field '{}' in {}{}\n Context: {}",
428 field, file_str, location, self.context
429 )
430 }
431 MarkdownErrorKind::InvalidDate(date) => {
432 write!(f, "Invalid date format '{}' in {}{}\n Expected ISO 8601 format (e.g., 2024-01-15T10:30:00Z)\n Context: {}",
433 date, file_str, location, self.context)
434 }
435 MarkdownErrorKind::ParseError(msg) => {
436 write!(
437 f,
438 "Markdown parsing error in {}{}\n Error: {}\n Context: {}",
439 file_str, location, msg, self.context
440 )
441 }
442 MarkdownErrorKind::InvalidLanguage(lang) => {
443 write!(f, "Invalid language code '{}' in {}{}\n Supported languages: en, it, es, fr, de, pt, ja, zh, ru, ar\n Context: {}",
444 lang, file_str, location, self.context)
445 }
446 MarkdownErrorKind::CircularReference(ref_path) => {
447 write!(
448 f,
449 "Circular reference detected: {} references {}\n Context: {}",
450 file_str,
451 ref_path.to_string_lossy(),
452 self.context
453 )
454 }
455 }
456 }
457}
458
459impl fmt::Display for TemplateError {
460 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
461 match &self.kind {
462 TemplateErrorKind::NotFound => {
463 write!(
464 f,
465 "Template not found: {}\n Context: {}",
466 self.template, self.context
467 )
468 }
469 TemplateErrorKind::SyntaxError(e) => {
470 write!(
471 f,
472 "Template syntax error in {}\n Error: {}\n Context: {}",
473 self.template, e, self.context
474 )
475 }
476 TemplateErrorKind::MissingVariable(var) => {
477 write!(
478 f,
479 "Missing template variable '{}' in {}\n Context: {}",
480 var, self.template, self.context
481 )
482 }
483 TemplateErrorKind::RenderError(e) => {
484 write!(
485 f,
486 "Template rendering failed for {}\n Error: {}\n Context: {}",
487 self.template, e, self.context
488 )
489 }
490 TemplateErrorKind::CompileError(e) => {
491 write!(
492 f,
493 "Template compilation failed for {}\n Error: {}\n Context: {}",
494 self.template, e, self.context
495 )
496 }
497 }
498 }
499}
500
501impl fmt::Display for ThemeError {
502 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
503 let theme_str = self.theme_path.to_string_lossy();
504
505 match &self.kind {
506 ThemeErrorKind::NotFound => {
507 write!(
508 f,
509 "Theme not found: {}\n Context: {}",
510 theme_str, self.context
511 )
512 }
513 ThemeErrorKind::InvalidConfig(e) => {
514 write!(
515 f,
516 "Invalid theme configuration in {}\n Error: {}\n Context: {}",
517 theme_str, e, self.context
518 )
519 }
520 ThemeErrorKind::MissingTemplate(template) => {
521 write!(
522 f,
523 "Missing required template '{}' in theme {}\n Context: {}",
524 template, theme_str, self.context
525 )
526 }
527 ThemeErrorKind::AssetError(msg) => {
528 write!(
529 f,
530 "Asset processing error in theme {}\n Error: {}\n Context: {}",
531 theme_str, msg, self.context
532 )
533 }
534 }
535 }
536}
537
538impl fmt::Display for ServerError {
539 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
540 match &self.kind {
541 ServerErrorKind::BindError { port, source } => {
542 write!(f, "Failed to bind to port {}\n Error: {}\n Context: {}\n Suggestion: Try a different port with --port <PORT>",
543 port, source, self.context)
544 }
545 ServerErrorKind::WatchError(e) => {
546 write!(
547 f,
548 "File watching failed\n Error: {}\n Context: {}",
549 e, self.context
550 )
551 }
552 ServerErrorKind::WebSocketError(msg) => {
553 write!(f, "WebSocket error: {}\n Context: {}", msg, self.context)
554 }
555 ServerErrorKind::LiveReloadError(msg) => {
556 write!(
557 f,
558 "Live reload error: {}\n Context: {}\n Suggestion: Try --no-live-reload flag",
559 msg, self.context
560 )
561 }
562 }
563 }
564}
565
566impl fmt::Display for ContentError {
567 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
568 let path_str = self
569 .path
570 .as_ref()
571 .map(|p| p.to_string_lossy().to_string())
572 .unwrap_or_else(|| "<unknown>".to_string());
573
574 match &self.kind {
575 ContentErrorKind::InvalidType(content_type) => {
576 write!(
577 f,
578 "Invalid content type '{}' for {}\n Context: {}",
579 content_type, path_str, self.context
580 )
581 }
582 ContentErrorKind::DuplicateSlug(slug) => {
583 write!(
584 f,
585 "Duplicate slug '{}' found\n Path: {}\n Context: {}",
586 slug, path_str, self.context
587 )
588 }
589 ContentErrorKind::InvalidFileName(filename) => {
590 write!(f, "Invalid file name '{}'\n Context: {}\n Suggestion: Use alphanumeric characters, hyphens, and underscores only",
591 filename, self.context)
592 }
593 ContentErrorKind::ValidationFailed(errors) => {
594 write!(f, "Content validation failed for {path_str}\n Issues:\n")?;
595 for error in errors {
596 writeln!(f, " - {error}")?;
597 }
598 write!(f, " Context: {}", self.context)
599 }
600 }
601 }
602}
603
604impl fmt::Display for GenerationError {
605 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
606 match &self.kind {
607 GenerationErrorKind::NoContent => {
608 write!(f, "No content found to generate\n Context: {}\n Suggestion: Add .md files to your content directory",
609 self.context)
610 }
611 GenerationErrorKind::OutputDirError(e) => {
612 write!(
613 f,
614 "Failed to create output directory\n Error: {}\n Context: {}",
615 e, self.context
616 )
617 }
618 GenerationErrorKind::AssetCopyError {
619 source,
620 target,
621 error,
622 } => {
623 write!(
624 f,
625 "Failed to copy asset\n From: {}\n To: {}\n Error: {}\n Context: {}",
626 source.to_string_lossy(),
627 target.to_string_lossy(),
628 error,
629 self.context
630 )
631 }
632 GenerationErrorKind::FeedError(msg) => {
633 write!(
634 f,
635 "Feed generation failed\n Error: {}\n Context: {}",
636 msg, self.context
637 )
638 }
639 GenerationErrorKind::SitemapError(msg) => {
640 write!(
641 f,
642 "Sitemap generation failed\n Error: {}\n Context: {}",
643 msg, self.context
644 )
645 }
646 }
647 }
648}
649
650impl std::error::Error for CliError {}
653impl std::error::Error for ConfigError {}
654impl std::error::Error for IoError {}
655impl std::error::Error for MarkdownError {}
656impl std::error::Error for TemplateError {}
657impl std::error::Error for ThemeError {}
658impl std::error::Error for ServerError {}
659impl std::error::Error for ContentError {}
660impl std::error::Error for GenerationError {}
661
662impl From<std::io::Error> for KrikError {
665 fn from(e: std::io::Error) -> Self {
666 KrikError::Io(Box::new(IoError {
667 kind: match e.kind() {
668 std::io::ErrorKind::NotFound => IoErrorKind::NotFound,
669 std::io::ErrorKind::PermissionDenied => IoErrorKind::PermissionDenied,
670 std::io::ErrorKind::AlreadyExists => IoErrorKind::AlreadyExists,
671 _ => IoErrorKind::ReadFailed(e),
672 },
673 path: PathBuf::new(), context: "I/O operation".to_string(),
675 }))
676 }
677}
678
679impl From<toml::de::Error> for KrikError {
680 fn from(e: toml::de::Error) -> Self {
681 KrikError::Config(Box::new(ConfigError {
682 kind: ConfigErrorKind::InvalidToml(e),
683 path: None,
684 context: "TOML parsing".to_string(),
685 }))
686 }
687}
688
689impl From<serde_yaml::Error> for KrikError {
690 fn from(e: serde_yaml::Error) -> Self {
691 KrikError::Config(Box::new(ConfigError {
692 kind: ConfigErrorKind::InvalidYaml(e),
693 path: None,
694 context: "YAML parsing".to_string(),
695 }))
696 }
697}
698
699impl From<tera::Error> for KrikError {
700 fn from(e: tera::Error) -> Self {
701 KrikError::Template(Box::new(TemplateError {
702 kind: TemplateErrorKind::RenderError(e),
703 template: "<unknown>".to_string(),
704 context: "Template processing".to_string(),
705 }))
706 }
707}
708
709#[macro_export]
713macro_rules! io_error {
714 ($kind:expr, $path:expr, $context:expr) => {
715 $crate::error::KrikError::Io(Box::new($crate::error::IoError {
716 kind: $kind,
717 path: $path.into(),
718 context: $context.to_string(),
719 }))
720 };
721}
722
723#[macro_export]
725macro_rules! markdown_error {
726 ($kind:expr, $file:expr, $context:expr) => {
727 $crate::error::KrikError::Markdown(Box::new($crate::error::MarkdownError {
728 kind: $kind,
729 file: $file.into(),
730 line: None,
731 column: None,
732 context: $context.to_string(),
733 }))
734 };
735 ($kind:expr, $file:expr, $line:expr, $context:expr) => {
736 $crate::error::KrikError::Markdown(Box::new($crate::error::MarkdownError {
737 kind: $kind,
738 file: $file.into(),
739 line: Some($line),
740 column: None,
741 context: $context.to_string(),
742 }))
743 };
744}
745
746#[macro_export]
748macro_rules! template_error {
749 ($kind:expr, $template:expr, $context:expr) => {
750 $crate::error::KrikError::Template(Box::new($crate::error::TemplateError {
751 kind: $kind,
752 template: $template.to_string(),
753 context: $context.to_string(),
754 }))
755 };
756}
757
758#[macro_export]
760macro_rules! config_error {
761 ($kind:expr, $path:expr, $context:expr) => {
762 $crate::error::KrikError::Config(Box::new($crate::error::ConfigError {
763 kind: $kind,
764 path: Some($path.into()),
765 context: $context.to_string(),
766 }))
767 };
768}