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, OutputFormat};
41//! # struct Workflow;
42//! # impl Workflow {
43//! #     fn render(&self, _: &str, _: Option<()>) -> Result<RenderResult, RenderError> {
44//! #         Ok(RenderResult::new(vec![], OutputFormat::Pdf))
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, OutputFormat};
109//! # let artifacts = vec![];
110//! let result = RenderResult::new(artifacts, OutputFormat::Pdf)
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/// Maximum input size for markdown (10 MB)
134pub const MAX_INPUT_SIZE: usize = 10 * 1024 * 1024;
135
136/// Maximum YAML size (1 MB)
137pub const MAX_YAML_SIZE: usize = 1 * 1024 * 1024;
138
139/// Maximum nesting depth for markdown structures (100 levels)
140pub const MAX_NESTING_DEPTH: usize = 100;
141
142/// Maximum template output size (50 MB)
143pub const MAX_TEMPLATE_OUTPUT: usize = 50 * 1024 * 1024;
144
145/// Error severity levels
146#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
147pub enum Severity {
148    /// Fatal error that prevents completion
149    Error,
150    /// Non-fatal issue that may need attention
151    Warning,
152    /// Informational message
153    Note,
154}
155
156/// Location information for diagnostics
157#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
158pub struct Location {
159    /// Source file name (e.g., "glue.typ", "template.typ", "input.md")
160    pub file: String,
161    /// Line number (1-indexed)
162    pub line: u32,
163    /// Column number (1-indexed)
164    pub col: u32,
165}
166
167/// Structured diagnostic information
168#[derive(Debug, Clone, serde::Serialize)]
169pub struct Diagnostic {
170    /// Error severity level
171    pub severity: Severity,
172    /// Optional error code (e.g., "E001", "typst::syntax")
173    pub code: Option<String>,
174    /// Human-readable error message
175    pub message: String,
176    /// Primary source location
177    pub primary: Option<Location>,
178    /// Related source locations for context
179    pub related: Vec<Location>,
180    /// Optional hint for fixing the error
181    pub hint: Option<String>,
182}
183
184impl Diagnostic {
185    /// Create a new diagnostic
186    pub fn new(severity: Severity, message: String) -> Self {
187        Self {
188            severity,
189            code: None,
190            message,
191            primary: None,
192            related: Vec::new(),
193            hint: None,
194        }
195    }
196
197    /// Set the error code
198    pub fn with_code(mut self, code: String) -> Self {
199        self.code = Some(code);
200        self
201    }
202
203    /// Set the primary location
204    pub fn with_location(mut self, location: Location) -> Self {
205        self.primary = Some(location);
206        self
207    }
208
209    /// Add a related location
210    pub fn with_related(mut self, location: Location) -> Self {
211        self.related.push(location);
212        self
213    }
214
215    /// Set a hint
216    pub fn with_hint(mut self, hint: String) -> Self {
217        self.hint = Some(hint);
218        self
219    }
220
221    /// Format diagnostic for pretty printing
222    pub fn fmt_pretty(&self) -> String {
223        let mut result = format!(
224            "[{}] {}",
225            match self.severity {
226                Severity::Error => "ERROR",
227                Severity::Warning => "WARN",
228                Severity::Note => "NOTE",
229            },
230            self.message
231        );
232
233        if let Some(ref code) = self.code {
234            result.push_str(&format!(" ({})", code));
235        }
236
237        if let Some(ref loc) = self.primary {
238            result.push_str(&format!("\n  --> {}:{}:{}", loc.file, loc.line, loc.col));
239        }
240
241        // Add related locations (trace)
242        for (i, related) in self.related.iter().enumerate() {
243            result.push_str(&format!(
244                "\n  {} {}:{}:{}",
245                if i == 0 { "trace:" } else { "      " },
246                related.file,
247                related.line,
248                related.col
249            ));
250        }
251
252        if let Some(ref hint) = self.hint {
253            result.push_str(&format!("\n  hint: {}", hint));
254        }
255
256        result
257    }
258}
259
260/// Error type for parsing operations
261#[derive(thiserror::Error, Debug)]
262pub enum ParseError {
263    /// Input too large
264    #[error("Input too large: {size} bytes (max: {max} bytes)")]
265    InputTooLarge {
266        /// Actual size
267        size: usize,
268        /// Maximum allowed size
269        max: usize,
270    },
271
272    /// YAML parsing error
273    #[error("YAML parsing error: {0}")]
274    YamlError(#[from] serde_yaml::Error),
275
276    /// Invalid YAML structure
277    #[error("Invalid YAML structure: {0}")]
278    InvalidStructure(String),
279
280    /// Other parsing errors
281    #[error("{0}")]
282    Other(String),
283}
284
285impl From<Box<dyn std::error::Error + Send + Sync>> for ParseError {
286    fn from(err: Box<dyn std::error::Error + Send + Sync>) -> Self {
287        ParseError::Other(err.to_string())
288    }
289}
290
291impl From<String> for ParseError {
292    fn from(msg: String) -> Self {
293        ParseError::Other(msg)
294    }
295}
296
297/// Main error type for rendering operations
298#[derive(thiserror::Error, Debug)]
299pub enum RenderError {
300    /// Failed to create rendering engine
301    #[error("Engine creation failed")]
302    EngineCreation {
303        /// Diagnostic information
304        diag: Diagnostic,
305        #[source]
306        /// Optional source error
307        source: Option<anyhow::Error>,
308    },
309
310    /// Invalid YAML frontmatter in markdown document
311    #[error("Invalid YAML frontmatter")]
312    InvalidFrontmatter {
313        /// Diagnostic information
314        diag: Diagnostic,
315        #[source]
316        /// Optional source error
317        source: Option<anyhow::Error>,
318    },
319
320    /// Template rendering failed
321    #[error("Template rendering failed")]
322    TemplateFailed {
323        #[source]
324        /// MiniJinja error
325        source: minijinja::Error,
326        /// Diagnostic information
327        diag: Diagnostic,
328    },
329
330    /// Backend compilation failed with one or more errors
331    #[error("Backend compilation failed with {0} error(s)")]
332    CompilationFailed(
333        /// Number of errors
334        usize,
335        /// List of diagnostics
336        Vec<Diagnostic>,
337    ),
338
339    /// Requested output format not supported by backend
340    #[error("{format:?} not supported by {backend}")]
341    FormatNotSupported {
342        /// Backend identifier
343        backend: String,
344        /// Requested format
345        format: OutputFormat,
346    },
347
348    /// Backend not registered with engine
349    #[error("Unsupported backend: {0}")]
350    UnsupportedBackend(String),
351
352    /// Dynamic asset filename collision
353    #[error("Dynamic asset collision: {filename}")]
354    DynamicAssetCollision {
355        /// Filename that collided
356        filename: String,
357        /// Error message
358        message: String,
359    },
360
361    /// Dynamic font filename collision
362    #[error("Dynamic font collision: {filename}")]
363    DynamicFontCollision {
364        /// Filename that collided
365        filename: String,
366        /// Error message
367        message: String,
368    },
369
370    /// Internal error (wraps anyhow::Error)
371    #[error(transparent)]
372    Internal(#[from] anyhow::Error),
373
374    /// Other errors (boxed trait object)
375    #[error("{0}")]
376    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
377
378    /// Template-related error
379    #[error("Template error: {0}")]
380    Template(#[from] crate::templating::TemplateError),
381
382    /// Input size exceeded maximum allowed
383    #[error("Input too large: {size} bytes (max: {max} bytes)")]
384    InputTooLarge {
385        /// Actual size
386        size: usize,
387        /// Maximum allowed size
388        max: usize,
389    },
390
391    /// YAML size exceeded maximum allowed
392    #[error("YAML block too large: {size} bytes (max: {max} bytes)")]
393    YamlTooLarge {
394        /// Actual size
395        size: usize,
396        /// Maximum allowed size
397        max: usize,
398    },
399
400    /// Nesting depth exceeded maximum allowed
401    #[error("Nesting too deep: {depth} levels (max: {max} levels)")]
402    NestingTooDeep {
403        /// Actual depth
404        depth: usize,
405        /// Maximum allowed depth
406        max: usize,
407    },
408
409    /// Template output exceeded maximum size
410    #[error("Template output too large: {size} bytes (max: {max} bytes)")]
411    OutputTooLarge {
412        /// Actual size
413        size: usize,
414        /// Maximum allowed size
415        max: usize,
416    },
417}
418
419/// Result type containing artifacts and warnings
420#[derive(Debug)]
421pub struct RenderResult {
422    /// Generated output artifacts
423    pub artifacts: Vec<crate::Artifact>,
424    /// Non-fatal diagnostic messages
425    pub warnings: Vec<Diagnostic>,
426    /// Output format that was produced
427    pub output_format: OutputFormat,
428}
429
430impl RenderResult {
431    /// Create a new result with artifacts and output format
432    pub fn new(artifacts: Vec<crate::Artifact>, output_format: OutputFormat) -> Self {
433        Self {
434            artifacts,
435            warnings: Vec::new(),
436            output_format,
437        }
438    }
439
440    /// Add a warning to the result
441    pub fn with_warning(mut self, warning: Diagnostic) -> Self {
442        self.warnings.push(warning);
443        self
444    }
445}
446
447/// Convert minijinja errors to RenderError
448impl From<minijinja::Error> for RenderError {
449    fn from(e: minijinja::Error) -> Self {
450        // Extract location with proper range information
451        let loc = e.line().map(|line| {
452            Location {
453                file: e.name().unwrap_or("template").to_string(),
454                line: line as u32,
455                // MiniJinja provides range, extract approximate column
456                col: e.range().map(|r| r.start as u32).unwrap_or(0),
457            }
458        });
459
460        // Generate helpful hints based on error kind
461        let hint = generate_minijinja_hint(&e);
462
463        let diag = Diagnostic {
464            severity: Severity::Error,
465            code: Some(format!("minijinja::{:?}", e.kind())),
466            message: e.to_string(),
467            primary: loc,
468            related: vec![],
469            hint,
470        };
471
472        RenderError::TemplateFailed { source: e, diag }
473    }
474}
475
476/// Generate helpful hints for common MiniJinja errors
477fn generate_minijinja_hint(e: &minijinja::Error) -> Option<String> {
478    use minijinja::ErrorKind;
479
480    match e.kind() {
481        ErrorKind::UndefinedError => {
482            Some("Check variable spelling and ensure it's defined in frontmatter".to_string())
483        }
484        ErrorKind::InvalidOperation => {
485            Some("Check that you're using the correct filter or operator for this type".to_string())
486        }
487        ErrorKind::SyntaxError => Some(
488            "Check template syntax - look for unclosed tags or invalid expressions".to_string(),
489        ),
490        _ => e.detail().map(|d| d.to_string()),
491    }
492}
493
494/// Helper to print structured errors
495pub fn print_errors(err: &RenderError) {
496    match err {
497        RenderError::CompilationFailed(_, diags) => {
498            for d in diags {
499                eprintln!("{}", d.fmt_pretty());
500            }
501        }
502        RenderError::TemplateFailed { diag, .. } => eprintln!("{}", diag.fmt_pretty()),
503        RenderError::InvalidFrontmatter { diag, .. } => eprintln!("{}", diag.fmt_pretty()),
504        RenderError::EngineCreation { diag, .. } => eprintln!("{}", diag.fmt_pretty()),
505        RenderError::FormatNotSupported { backend, format } => {
506            eprintln!(
507                "[ERROR] Format {:?} not supported by {} backend",
508                format, backend
509            );
510        }
511        RenderError::UnsupportedBackend(name) => {
512            eprintln!("[ERROR] Unsupported backend: {}", name);
513        }
514        RenderError::DynamicAssetCollision { filename, message } => {
515            eprintln!(
516                "[ERROR] Dynamic asset collision: {}\n  {}",
517                filename, message
518            );
519        }
520        RenderError::DynamicFontCollision { filename, message } => {
521            eprintln!(
522                "[ERROR] Dynamic font collision: {}\n  {}",
523                filename, message
524            );
525        }
526        RenderError::Internal(e) => {
527            eprintln!("[ERROR] Internal error: {}", e);
528        }
529        RenderError::Template(e) => {
530            eprintln!("[ERROR] Template error: {}", e);
531        }
532        RenderError::Other(e) => {
533            eprintln!("[ERROR] {}", e);
534        }
535        RenderError::InputTooLarge { size, max } => {
536            eprintln!(
537                "[ERROR] Input too large: {} bytes (maximum: {} bytes)",
538                size, max
539            );
540        }
541        RenderError::YamlTooLarge { size, max } => {
542            eprintln!(
543                "[ERROR] YAML block too large: {} bytes (maximum: {} bytes)",
544                size, max
545            );
546        }
547        RenderError::NestingTooDeep { depth, max } => {
548            eprintln!(
549                "[ERROR] Nesting too deep: {} levels (maximum: {} levels)",
550                depth, max
551            );
552        }
553        RenderError::OutputTooLarge { size, max } => {
554            eprintln!(
555                "[ERROR] Template output too large: {} bytes (maximum: {} bytes)",
556                size, max
557            );
558        }
559    }
560}