krik/error/
mod.rs

1mod recovery;
2
3pub use recovery::{ErrorRecoverable, ErrorRecovery};
4
5use std::fmt;
6use std::path::PathBuf;
7use thiserror::Error;
8
9/// Result type alias for Krik operations  
10/// Large error types are intentional for detailed error context
11#[allow(clippy::result_large_err)]
12pub type KrikResult<T> = Result<T, KrikError>;
13
14/// Main error type for the Krik static site generator
15#[derive(Debug, Error)]
16pub enum KrikError {
17    /// CLI argument and validation errors
18    #[error(transparent)]
19    Cli(#[from] Box<CliError>),
20    /// Configuration-related errors
21    #[error(transparent)]
22    Config(#[from] Box<ConfigError>),
23    /// File I/O errors
24    #[error(transparent)]
25    Io(#[from] Box<IoError>),
26    /// Markdown parsing errors
27    #[error(transparent)]
28    Markdown(#[from] Box<MarkdownError>),
29    /// Template processing errors
30    #[error(transparent)]
31    Template(#[from] Box<TemplateError>),
32    /// Theme-related errors
33    #[error(transparent)]
34    Theme(#[from] Box<ThemeError>),
35    /// Server-related errors
36    #[error(transparent)]
37    Server(#[from] Box<ServerError>),
38    /// Content creation errors
39    #[error(transparent)]
40    Content(#[from] Box<ContentError>),
41    /// Site generation errors
42    #[error(transparent)]
43    Generation(#[from] Box<GenerationError>),
44}
45/// CLI validation and argument parsing errors
46#[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    /// Provided path does not exist
56    PathDoesNotExist,
57    /// Provided path exists but is not a directory
58    NotADirectory,
59    /// Permissions do not allow the requested operation
60    PermissionDenied,
61    /// Failed to create a directory
62    CreateDirFailed(std::io::Error),
63    /// Failed to canonicalize a path
64    CanonicalizeFailed(std::io::Error),
65    /// Invalid port number provided to the CLI
66    InvalidPort(String),
67    /// Theme directory not found or invalid
68    ThemeNotFound,
69}
70
71/// Configuration file and parsing errors
72#[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    /// Configuration file not found
82    NotFound,
83    /// Invalid TOML syntax
84    InvalidToml(toml::de::Error),
85    /// Invalid YAML syntax  
86    InvalidYaml(serde_yaml::Error),
87    /// Missing required field
88    MissingField(String),
89    /// Invalid field value
90    InvalidValue {
91        field: String,
92        expected: String,
93        found: String,
94    },
95    /// File permissions error
96    PermissionDenied,
97}
98
99/// File I/O related errors
100#[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    /// File or directory not found
110    NotFound,
111    /// Permission denied
112    PermissionDenied,
113    /// File already exists when it shouldn't
114    AlreadyExists,
115    /// Invalid file name or path
116    InvalidPath,
117    /// Disk full or write error
118    WriteFailed(std::io::Error),
119    /// Read operation failed
120    ReadFailed(std::io::Error),
121}
122
123/// Markdown processing errors
124#[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    /// Invalid front matter YAML
136    InvalidFrontMatter(serde_yaml::Error),
137    /// Missing required front matter field
138    MissingFrontMatterField(String),
139    /// Invalid date format
140    InvalidDate(String),
141    /// Malformed markdown content
142    ParseError(String),
143    /// Invalid language code
144    InvalidLanguage(String),
145    /// Circular reference in content
146    CircularReference(PathBuf),
147}
148
149/// Template processing errors
150#[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    /// Template file not found
160    NotFound,
161    /// Template syntax error
162    SyntaxError(tera::Error),
163    /// Missing template variable
164    MissingVariable(String),
165    /// Template rendering failed
166    RenderError(tera::Error),
167    /// Template compilation failed
168    CompileError(tera::Error),
169}
170
171/// Theme-related errors
172#[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    /// Theme directory not found
182    NotFound,
183    /// Invalid theme.toml configuration
184    InvalidConfig(ConfigError),
185    /// Missing required template
186    MissingTemplate(String),
187    /// Asset processing failed
188    AssetError(String),
189}
190
191/// Development server errors
192#[derive(Debug)]
193pub struct ServerError {
194    pub kind: ServerErrorKind,
195    pub context: String,
196}
197
198#[derive(Debug)]
199pub enum ServerErrorKind {
200    /// Failed to bind to port
201    BindError { port: u16, source: std::io::Error },
202    /// File watching failed
203    WatchError(notify::Error),
204    /// WebSocket error
205    WebSocketError(String),
206    /// Live reload failed
207    LiveReloadError(String),
208}
209
210/// Content creation and management errors
211#[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    /// Invalid content type
221    InvalidType(String),
222    /// Duplicate slug
223    DuplicateSlug(String),
224    /// Invalid file name
225    InvalidFileName(String),
226    /// Content validation failed
227    ValidationFailed(Vec<String>),
228}
229
230/// Site generation errors
231#[derive(Debug)]
232pub struct GenerationError {
233    pub kind: GenerationErrorKind,
234    pub context: String,
235}
236
237#[derive(Debug)]
238pub enum GenerationErrorKind {
239    /// No content found to generate
240    NoContent,
241    /// Output directory creation failed
242    OutputDirError(std::io::Error),
243    /// Asset copying failed
244    AssetCopyError {
245        source: PathBuf,
246        target: PathBuf,
247        error: std::io::Error,
248    },
249    /// Feed generation failed
250    FeedError(String),
251    /// Sitemap generation failed
252    SitemapError(String),
253}
254
255// Display implementations for user-friendly error messages (inner types)
256
257impl 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
650// Standard Error trait implementations
651
652impl 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
662// Conversion implementations from external error types
663
664impl 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(), // Will be set by context
674            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// Helper macros for creating contextual errors
710
711/// Create a context-aware I/O error
712#[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/// Create a context-aware markdown error
724#[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/// Create a context-aware template error
747#[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/// Create a context-aware config error
759#[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}