quillmark_core/
error.rs

1//! # Error Handling
2//!
3//! Structured error handling with diagnostics and source location tracking.
4//!
5//! ## Overview
6//!
7//! The `error` module provides error types and diagnostic types for actionable
8//! error reporting with source location tracking.
9//!
10//! ## Key Types
11//!
12//! - [`RenderError`]: Main error enum for rendering operations
13//! - [`crate::TemplateError`]: Template-specific errors
14//! - [`Diagnostic`]: Structured diagnostic information
15//! - [`Location`]: Source file location (file, line, column)
16//! - [`Severity`]: Error severity levels (Error, Warning, Note)
17//! - [`RenderResult`]: Result type with artifacts and warnings
18//!
19//! ## Error Hierarchy
20//!
21//! ### RenderError Variants
22//!
23//! - [`RenderError::EngineCreation`]: Failed to create rendering engine
24//! - [`RenderError::InvalidFrontmatter`]: Malformed YAML frontmatter
25//! - [`RenderError::TemplateFailed`]: Template rendering error
26//! - [`RenderError::CompilationFailed`]: Backend compilation errors
27//! - [`RenderError::FormatNotSupported`]: Requested format not supported
28//! - [`RenderError::UnsupportedBackend`]: Backend not registered
29//! - [`RenderError::DynamicAssetCollision`]: Asset filename collision
30//! - [`RenderError::Internal`]: Internal error
31//! - [`RenderError::Other`]: Other errors
32//! - [`RenderError::Template`]: Template error
33//!
34//! ## Examples
35//!
36//! ### Error Handling
37//!
38//! ```no_run
39//! use quillmark_core::{RenderError, error::print_errors};
40//! # use quillmark_core::RenderResult;
41//! # struct Workflow;
42//! # impl Workflow {
43//! #     fn render(&self, _: &str, _: Option<()>) -> Result<RenderResult, RenderError> {
44//! #         Ok(RenderResult::new(vec![]))
45//! #     }
46//! # }
47//! # let workflow = Workflow;
48//! # let markdown = "";
49//!
50//! match workflow.render(markdown, None) {
51//!     Ok(result) => {
52//!         // Process artifacts
53//!         for artifact in result.artifacts {
54//!             std::fs::write(
55//!                 format!("output.{:?}", artifact.output_format),
56//!                 &artifact.bytes
57//!             )?;
58//!         }
59//!     }
60//!     Err(e) => {
61//!         // Print structured diagnostics
62//!         print_errors(&e);
63//!         
64//!         // Match specific error types
65//!         match e {
66//!             RenderError::CompilationFailed(count, diags) => {
67//!                 eprintln!("Compilation failed with {} errors:", count);
68//!                 for diag in diags {
69//!                     eprintln!("{}", diag.fmt_pretty());
70//!                 }
71//!             }
72//!             RenderError::InvalidFrontmatter { diag, .. } => {
73//!                 eprintln!("Frontmatter error: {}", diag.message);
74//!             }
75//!             _ => eprintln!("Error: {}", e),
76//!         }
77//!     }
78//! }
79//! # Ok::<(), Box<dyn std::error::Error>>(())
80//! ```
81//!
82//! ### Creating Diagnostics
83//!
84//! ```
85//! use quillmark_core::{Diagnostic, Location, Severity};
86//!
87//! let diag = Diagnostic::new(Severity::Error, "Undefined variable".to_string())
88//!     .with_code("E001".to_string())
89//!     .with_location(Location {
90//!         file: "template.typ".to_string(),
91//!         line: 10,
92//!         col: 5,
93//!     })
94//!     .with_hint("Check variable spelling".to_string());
95//!
96//! println!("{}", diag.fmt_pretty());
97//! ```
98//!
99//! Example output:
100//! ```text
101//! [ERROR] Undefined variable (E001) at template.typ:10:5
102//!   hint: Check variable spelling
103//! ```
104//!
105//! ### Result with Warnings
106//!
107//! ```no_run
108//! # use quillmark_core::{RenderResult, Diagnostic, Severity};
109//! # let artifacts = vec![];
110//! let result = RenderResult::new(artifacts)
111//!     .with_warning(Diagnostic::new(
112//!         Severity::Warning,
113//!         "Deprecated field used".to_string(),
114//!     ));
115//! ```
116//!
117//! ## Pretty Printing
118//!
119//! The [`Diagnostic`] type provides [`Diagnostic::fmt_pretty()`] for human-readable output with error code, location, and hints.
120//!
121//! ## Machine-Readable Output
122//!
123//! All diagnostic types implement `serde::Serialize` for JSON export:
124//!
125//! ```no_run
126//! # use quillmark_core::{Diagnostic, Severity};
127//! # let diagnostic = Diagnostic::new(Severity::Error, "Test".to_string());
128//! let json = serde_json::to_string(&diagnostic).unwrap();
129//! ```
130
131use crate::OutputFormat;
132
133/// Error severity levels
134#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
135pub enum Severity {
136    /// Fatal error that prevents completion
137    Error,
138    /// Non-fatal issue that may need attention
139    Warning,
140    /// Informational message
141    Note,
142}
143
144/// Location information for diagnostics
145#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
146pub struct Location {
147    /// Source file name (e.g., "glue.typ", "template.typ", "input.md")
148    pub file: String,
149    /// Line number (1-indexed)
150    pub line: u32,
151    /// Column number (1-indexed)
152    pub col: u32,
153}
154
155/// Structured diagnostic information
156#[derive(Debug, Clone, serde::Serialize)]
157pub struct Diagnostic {
158    /// Error severity level
159    pub severity: Severity,
160    /// Optional error code (e.g., "E001", "typst::syntax")
161    pub code: Option<String>,
162    /// Human-readable error message
163    pub message: String,
164    /// Primary source location
165    pub primary: Option<Location>,
166    /// Related source locations for context
167    pub related: Vec<Location>,
168    /// Optional hint for fixing the error
169    pub hint: Option<String>,
170}
171
172impl Diagnostic {
173    /// Create a new diagnostic
174    pub fn new(severity: Severity, message: String) -> Self {
175        Self {
176            severity,
177            code: None,
178            message,
179            primary: None,
180            related: Vec::new(),
181            hint: None,
182        }
183    }
184
185    /// Set the error code
186    pub fn with_code(mut self, code: String) -> Self {
187        self.code = Some(code);
188        self
189    }
190
191    /// Set the primary location
192    pub fn with_location(mut self, location: Location) -> Self {
193        self.primary = Some(location);
194        self
195    }
196
197    /// Add a related location
198    pub fn with_related(mut self, location: Location) -> Self {
199        self.related.push(location);
200        self
201    }
202
203    /// Set a hint
204    pub fn with_hint(mut self, hint: String) -> Self {
205        self.hint = Some(hint);
206        self
207    }
208
209    /// Format diagnostic for pretty printing
210    pub fn fmt_pretty(&self) -> String {
211        let mut result = format!(
212            "[{}] {}",
213            match self.severity {
214                Severity::Error => "ERROR",
215                Severity::Warning => "WARN",
216                Severity::Note => "NOTE",
217            },
218            self.message
219        );
220
221        if let Some(ref code) = self.code {
222            result.push_str(&format!(" ({})", code));
223        }
224
225        if let Some(ref loc) = self.primary {
226            result.push_str(&format!("\n  --> {}:{}:{}", loc.file, loc.line, loc.col));
227        }
228
229        // Add related locations (trace)
230        for (i, related) in self.related.iter().enumerate() {
231            result.push_str(&format!(
232                "\n  {} {}:{}:{}",
233                if i == 0 { "trace:" } else { "      " },
234                related.file,
235                related.line,
236                related.col
237            ));
238        }
239
240        if let Some(ref hint) = self.hint {
241            result.push_str(&format!("\n  hint: {}", hint));
242        }
243
244        result
245    }
246}
247
248/// Main error type for rendering operations
249#[derive(thiserror::Error, Debug)]
250pub enum RenderError {
251    /// Failed to create rendering engine
252    #[error("Engine creation failed")]
253    EngineCreation {
254        /// Diagnostic information
255        diag: Diagnostic,
256        #[source]
257        /// Optional source error
258        source: Option<anyhow::Error>,
259    },
260
261    /// Invalid YAML frontmatter in markdown document
262    #[error("Invalid YAML frontmatter")]
263    InvalidFrontmatter {
264        /// Diagnostic information
265        diag: Diagnostic,
266        #[source]
267        /// Optional source error
268        source: Option<anyhow::Error>,
269    },
270
271    /// Template rendering failed
272    #[error("Template rendering failed")]
273    TemplateFailed {
274        #[source]
275        /// MiniJinja error
276        source: minijinja::Error,
277        /// Diagnostic information
278        diag: Diagnostic,
279    },
280
281    /// Backend compilation failed with one or more errors
282    #[error("Backend compilation failed with {0} error(s)")]
283    CompilationFailed(
284        /// Number of errors
285        usize,
286        /// List of diagnostics
287        Vec<Diagnostic>,
288    ),
289
290    /// Requested output format not supported by backend
291    #[error("{format:?} not supported by {backend}")]
292    FormatNotSupported {
293        /// Backend identifier
294        backend: String,
295        /// Requested format
296        format: OutputFormat,
297    },
298
299    /// Backend not registered with engine
300    #[error("Unsupported backend: {0}")]
301    UnsupportedBackend(String),
302
303    /// Dynamic asset filename collision
304    #[error("Dynamic asset collision: {filename}")]
305    DynamicAssetCollision {
306        /// Filename that collided
307        filename: String,
308        /// Error message
309        message: String,
310    },
311
312    /// Internal error (wraps anyhow::Error)
313    #[error(transparent)]
314    Internal(#[from] anyhow::Error),
315
316    /// Other errors (boxed trait object)
317    #[error("{0}")]
318    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
319
320    /// Template-related error
321    #[error("Template error: {0}")]
322    Template(#[from] crate::templating::TemplateError),
323}
324
325/// Result type containing artifacts and warnings
326#[derive(Debug)]
327pub struct RenderResult {
328    /// Generated output artifacts
329    pub artifacts: Vec<crate::Artifact>,
330    /// Non-fatal diagnostic messages
331    pub warnings: Vec<Diagnostic>,
332}
333
334impl RenderResult {
335    /// Create a new result with artifacts
336    pub fn new(artifacts: Vec<crate::Artifact>) -> Self {
337        Self {
338            artifacts,
339            warnings: Vec::new(),
340        }
341    }
342
343    /// Add a warning to the result
344    pub fn with_warning(mut self, warning: Diagnostic) -> Self {
345        self.warnings.push(warning);
346        self
347    }
348}
349
350/// Convert minijinja errors to RenderError
351impl From<minijinja::Error> for RenderError {
352    fn from(e: minijinja::Error) -> Self {
353        // Extract location with proper range information
354        let loc = e.line().map(|line| {
355            Location {
356                file: e.name().unwrap_or("template").to_string(),
357                line: line as u32,
358                // MiniJinja provides range, extract approximate column
359                col: e.range().map(|r| r.start as u32).unwrap_or(0),
360            }
361        });
362
363        // Generate helpful hints based on error kind
364        let hint = generate_minijinja_hint(&e);
365
366        let diag = Diagnostic {
367            severity: Severity::Error,
368            code: Some(format!("minijinja::{:?}", e.kind())),
369            message: e.to_string(),
370            primary: loc,
371            related: vec![],
372            hint,
373        };
374
375        RenderError::TemplateFailed { source: e, diag }
376    }
377}
378
379/// Generate helpful hints for common MiniJinja errors
380fn generate_minijinja_hint(e: &minijinja::Error) -> Option<String> {
381    use minijinja::ErrorKind;
382
383    match e.kind() {
384        ErrorKind::UndefinedError => {
385            Some("Check variable spelling and ensure it's defined in frontmatter".to_string())
386        }
387        ErrorKind::InvalidOperation => {
388            Some("Check that you're using the correct filter or operator for this type".to_string())
389        }
390        ErrorKind::SyntaxError => Some(
391            "Check template syntax - look for unclosed tags or invalid expressions".to_string(),
392        ),
393        _ => e.detail().map(|d| d.to_string()),
394    }
395}
396
397/// Helper to print structured errors
398pub fn print_errors(err: &RenderError) {
399    match err {
400        RenderError::CompilationFailed(_, diags) => {
401            for d in diags {
402                eprintln!("{}", d.fmt_pretty());
403            }
404        }
405        RenderError::TemplateFailed { diag, .. } => eprintln!("{}", diag.fmt_pretty()),
406        RenderError::InvalidFrontmatter { diag, .. } => eprintln!("{}", diag.fmt_pretty()),
407        RenderError::EngineCreation { diag, .. } => eprintln!("{}", diag.fmt_pretty()),
408        RenderError::FormatNotSupported { backend, format } => {
409            eprintln!(
410                "[ERROR] Format {:?} not supported by {} backend",
411                format, backend
412            );
413        }
414        RenderError::UnsupportedBackend(name) => {
415            eprintln!("[ERROR] Unsupported backend: {}", name);
416        }
417        RenderError::DynamicAssetCollision { filename, message } => {
418            eprintln!(
419                "[ERROR] Dynamic asset collision: {}\n  {}",
420                filename, message
421            );
422        }
423        RenderError::Internal(e) => {
424            eprintln!("[ERROR] Internal error: {}", e);
425        }
426        RenderError::Template(e) => {
427            eprintln!("[ERROR] Template error: {}", e);
428        }
429        RenderError::Other(e) => {
430            eprintln!("[ERROR] {}", e);
431        }
432    }
433}