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::DynamicFontCollision`]: Font filename collision
31//! - [`RenderError::InputTooLarge`]: Input size limits exceeded
32//! - [`RenderError::YamlTooLarge`]: YAML size exceeded maximum
33//! - [`RenderError::NestingTooDeep`]: Nesting depth exceeded maximum
34//! - [`RenderError::OutputTooLarge`]: Template output exceeded maximum size
35//!
36//! ## Examples
37//!
38//! ### Error Handling
39//!
40//! ```no_run
41//! use quillmark_core::{RenderError, error::print_errors};
42//! # use quillmark_core::{RenderResult, OutputFormat};
43//! # struct Workflow;
44//! # impl Workflow {
45//! #     fn render(&self, _: &str, _: Option<()>) -> Result<RenderResult, RenderError> {
46//! #         Ok(RenderResult::new(vec![], OutputFormat::Pdf))
47//! #     }
48//! # }
49//! # let workflow = Workflow;
50//! # let markdown = "";
51//!
52//! match workflow.render(markdown, None) {
53//!     Ok(result) => {
54//!         // Process artifacts
55//!         for artifact in result.artifacts {
56//!             std::fs::write(
57//!                 format!("output.{:?}", artifact.output_format),
58//!                 &artifact.bytes
59//!             )?;
60//!         }
61//!     }
62//!     Err(e) => {
63//!         // Print structured diagnostics
64//!         print_errors(&e);
65//!         
66//!         // Match specific error types
67//!         match e {
68//!             RenderError::CompilationFailed { diags } => {
69//!                 eprintln!("Compilation failed with {} errors:", diags.len());
70//!                 for diag in diags {
71//!                     eprintln!("{}", diag.fmt_pretty());
72//!                 }
73//!             }
74//!             RenderError::InvalidFrontmatter { diag } => {
75//!                 eprintln!("Frontmatter error: {}", diag.message);
76//!             }
77//!             _ => eprintln!("Error: {}", e),
78//!         }
79//!     }
80//! }
81//! # Ok::<(), Box<dyn std::error::Error>>(())
82//! ```
83//!
84//! ### Creating Diagnostics
85//!
86//! ```
87//! use quillmark_core::{Diagnostic, Location, Severity};
88//!
89//! let diag = Diagnostic::new(Severity::Error, "Undefined variable".to_string())
90//!     .with_code("E001".to_string())
91//!     .with_location(Location {
92//!         file: "template.typ".to_string(),
93//!         line: 10,
94//!         col: 5,
95//!     })
96//!     .with_hint("Check variable spelling".to_string());
97//!
98//! println!("{}", diag.fmt_pretty());
99//! ```
100//!
101//! Example output:
102//! ```text
103//! [ERROR] Undefined variable (E001) at template.typ:10:5
104//!   hint: Check variable spelling
105//! ```
106//!
107//! ### Result with Warnings
108//!
109//! ```no_run
110//! # use quillmark_core::{RenderResult, Diagnostic, Severity, OutputFormat};
111//! # let artifacts = vec![];
112//! let result = RenderResult::new(artifacts, OutputFormat::Pdf)
113//!     .with_warning(Diagnostic::new(
114//!         Severity::Warning,
115//!         "Deprecated field used".to_string(),
116//!     ));
117//! ```
118//!
119//! ## Pretty Printing
120//!
121//! The [`Diagnostic`] type provides [`Diagnostic::fmt_pretty()`] for human-readable output with error code, location, and hints.
122//!
123//! ## Machine-Readable Output
124//!
125//! All diagnostic types implement `serde::Serialize` for JSON export:
126//!
127//! ```no_run
128//! # use quillmark_core::{Diagnostic, Severity};
129//! # let diagnostic = Diagnostic::new(Severity::Error, "Test".to_string());
130//! let json = serde_json::to_string(&diagnostic).unwrap();
131//! ```
132
133use crate::OutputFormat;
134
135/// Maximum input size for markdown (10 MB)
136pub const MAX_INPUT_SIZE: usize = 10 * 1024 * 1024;
137
138/// Maximum YAML size (1 MB)
139pub const MAX_YAML_SIZE: usize = 1024 * 1024;
140
141/// Maximum nesting depth for markdown structures (100 levels)
142pub const MAX_NESTING_DEPTH: usize = 100;
143
144/// Maximum template output size (50 MB)
145pub const MAX_TEMPLATE_OUTPUT: usize = 50 * 1024 * 1024;
146
147/// Error severity levels
148#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
149pub enum Severity {
150    /// Fatal error that prevents completion
151    Error,
152    /// Non-fatal issue that may need attention
153    Warning,
154    /// Informational message
155    Note,
156}
157
158/// Location information for diagnostics
159#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
160pub struct Location {
161    /// Source file name (e.g., "plate.typ", "template.typ", "input.md")
162    pub file: String,
163    /// Line number (1-indexed)
164    pub line: u32,
165    /// Column number (1-indexed)
166    pub col: u32,
167}
168
169/// Structured diagnostic information
170#[derive(Debug, serde::Serialize)]
171pub struct Diagnostic {
172    /// Error severity level
173    pub severity: Severity,
174    /// Optional error code (e.g., "E001", "typst::syntax")
175    pub code: Option<String>,
176    /// Human-readable error message
177    pub message: String,
178    /// Primary source location
179    pub primary: Option<Location>,
180    /// Optional hint for fixing the error
181    pub hint: Option<String>,
182    /// Source error that caused this diagnostic (for error chaining)
183    /// Note: This field is excluded from serialization as Error trait
184    /// objects cannot be serialized
185    #[serde(skip)]
186    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
187}
188
189impl Diagnostic {
190    /// Create a new diagnostic
191    pub fn new(severity: Severity, message: String) -> Self {
192        Self {
193            severity,
194            code: None,
195            message,
196            primary: None,
197            hint: None,
198            source: None,
199        }
200    }
201
202    /// Set the error code
203    pub fn with_code(mut self, code: String) -> Self {
204        self.code = Some(code);
205        self
206    }
207
208    /// Set the primary location
209    pub fn with_location(mut self, location: Location) -> Self {
210        self.primary = Some(location);
211        self
212    }
213
214    /// Set a hint
215    pub fn with_hint(mut self, hint: String) -> Self {
216        self.hint = Some(hint);
217        self
218    }
219
220    /// Set error source (chainable)
221    pub fn with_source(mut self, source: Box<dyn std::error::Error + Send + Sync>) -> Self {
222        self.source = Some(source);
223        self
224    }
225
226    /// Get the source chain as a list of error messages
227    pub fn source_chain(&self) -> Vec<String> {
228        let mut chain = Vec::new();
229        let mut current_source = self
230            .source
231            .as_ref()
232            .map(|b| b.as_ref() as &dyn std::error::Error);
233        while let Some(err) = current_source {
234            chain.push(err.to_string());
235            current_source = err.source();
236        }
237        chain
238    }
239
240    /// Format diagnostic for pretty printing
241    pub fn fmt_pretty(&self) -> String {
242        let mut result = format!(
243            "[{}] {}",
244            match self.severity {
245                Severity::Error => "ERROR",
246                Severity::Warning => "WARN",
247                Severity::Note => "NOTE",
248            },
249            self.message
250        );
251
252        if let Some(ref code) = self.code {
253            result.push_str(&format!(" ({})", code));
254        }
255
256        if let Some(ref loc) = self.primary {
257            result.push_str(&format!("\n  --> {}:{}:{}", loc.file, loc.line, loc.col));
258        }
259
260        if let Some(ref hint) = self.hint {
261            result.push_str(&format!("\n  hint: {}", hint));
262        }
263
264        result
265    }
266
267    /// Format diagnostic with source chain for debugging
268    pub fn fmt_pretty_with_source(&self) -> String {
269        let mut result = self.fmt_pretty();
270
271        for (i, cause) in self.source_chain().iter().enumerate() {
272            result.push_str(&format!("\n  cause {}: {}", i + 1, cause));
273        }
274
275        result
276    }
277}
278
279impl std::fmt::Display for Diagnostic {
280    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281        write!(f, "{}", self.message)
282    }
283}
284
285/// Serializable diagnostic for cross-language boundaries
286///
287/// This type is used when diagnostics need to be serialized and sent across
288/// FFI boundaries (e.g., Python, WASM). Unlike `Diagnostic`, it does not
289/// contain the non-serializable `source` field, but instead includes a
290/// flattened `source_chain` for display purposes.
291#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
292pub struct SerializableDiagnostic {
293    /// Error severity level
294    pub severity: Severity,
295    /// Optional error code (e.g., "E001", "typst::syntax")
296    pub code: Option<String>,
297    /// Human-readable error message
298    pub message: String,
299    /// Primary source location
300    pub primary: Option<Location>,
301    /// Optional hint for fixing the error
302    pub hint: Option<String>,
303    /// Source chain as list of strings (for display purposes)
304    pub source_chain: Vec<String>,
305}
306
307impl From<Diagnostic> for SerializableDiagnostic {
308    fn from(diag: Diagnostic) -> Self {
309        let source_chain = diag.source_chain();
310        Self {
311            severity: diag.severity,
312            code: diag.code,
313            message: diag.message,
314            primary: diag.primary,
315            hint: diag.hint,
316            source_chain,
317        }
318    }
319}
320
321impl From<&Diagnostic> for SerializableDiagnostic {
322    fn from(diag: &Diagnostic) -> Self {
323        Self {
324            severity: diag.severity,
325            code: diag.code.clone(),
326            message: diag.message.clone(),
327            primary: diag.primary.clone(),
328            hint: diag.hint.clone(),
329            source_chain: diag.source_chain(),
330        }
331    }
332}
333
334/// Error type for parsing operations
335#[derive(thiserror::Error, Debug)]
336pub enum ParseError {
337    /// Input too large
338    #[error("Input too large: {size} bytes (max: {max} bytes)")]
339    InputTooLarge {
340        /// Actual size
341        size: usize,
342        /// Maximum allowed size
343        max: usize,
344    },
345
346    /// YAML parsing error
347    #[error("YAML parsing error: {0}")]
348    YamlError(#[from] serde_yaml::Error),
349
350    /// Invalid YAML structure
351    #[error("Invalid YAML structure: {0}")]
352    InvalidStructure(String),
353
354    /// Missing CARD directive in inline metadata block
355    #[error("{}", .diag.message)]
356    MissingCardDirective {
357        /// Diagnostic information with hint
358        diag: Diagnostic,
359    },
360
361    /// Other parsing errors
362    #[error("{0}")]
363    Other(String),
364}
365
366impl ParseError {
367    /// Create a MissingCardDirective error with helpful hint
368    pub fn missing_card_directive() -> Self {
369        let diag = Diagnostic::new(
370            Severity::Error,
371            "Inline metadata block missing CARD directive".to_string(),
372        )
373        .with_code("parse::missing_card".to_string())
374        .with_hint(
375            "Add 'CARD: <card_type>' to specify which card this block belongs to. \
376            Example:\n---\nCARD: my_card_type\nfield: value\n---"
377                .to_string(),
378        );
379        ParseError::MissingCardDirective { diag }
380    }
381}
382
383impl From<Box<dyn std::error::Error + Send + Sync>> for ParseError {
384    fn from(err: Box<dyn std::error::Error + Send + Sync>) -> Self {
385        ParseError::Other(err.to_string())
386    }
387}
388
389impl From<String> for ParseError {
390    fn from(msg: String) -> Self {
391        ParseError::Other(msg)
392    }
393}
394
395/// Main error type for rendering operations
396#[derive(thiserror::Error, Debug)]
397pub enum RenderError {
398    /// Failed to create rendering engine
399    #[error("{diag}")]
400    EngineCreation {
401        /// Diagnostic information
402        diag: Diagnostic,
403    },
404
405    /// Invalid YAML frontmatter in markdown document
406    #[error("{diag}")]
407    InvalidFrontmatter {
408        /// Diagnostic information
409        diag: Diagnostic,
410    },
411
412    /// Template rendering failed
413    #[error("{diag}")]
414    TemplateFailed {
415        /// Diagnostic information
416        diag: Diagnostic,
417    },
418
419    /// Backend compilation failed with one or more errors
420    #[error("Backend compilation failed with {} error(s)", diags.len())]
421    CompilationFailed {
422        /// List of diagnostics
423        diags: Vec<Diagnostic>,
424    },
425
426    /// Requested output format not supported by backend
427    #[error("{diag}")]
428    FormatNotSupported {
429        /// Diagnostic information
430        diag: Diagnostic,
431    },
432
433    /// Backend not registered with engine
434    #[error("{diag}")]
435    UnsupportedBackend {
436        /// Diagnostic information
437        diag: Diagnostic,
438    },
439
440    /// Dynamic asset filename collision
441    #[error("{diag}")]
442    DynamicAssetCollision {
443        /// Diagnostic information
444        diag: Diagnostic,
445    },
446
447    /// Dynamic font filename collision
448    #[error("{diag}")]
449    DynamicFontCollision {
450        /// Diagnostic information
451        diag: Diagnostic,
452    },
453
454    /// Input size limits exceeded
455    #[error("{diag}")]
456    InputTooLarge {
457        /// Diagnostic information
458        diag: Diagnostic,
459    },
460
461    /// YAML size exceeded maximum allowed
462    #[error("{diag}")]
463    YamlTooLarge {
464        /// Diagnostic information
465        diag: Diagnostic,
466    },
467
468    /// Nesting depth exceeded maximum allowed
469    #[error("{diag}")]
470    NestingTooDeep {
471        /// Diagnostic information
472        diag: Diagnostic,
473    },
474
475    /// Template output exceeded maximum size
476    #[error("{diag}")]
477    OutputTooLarge {
478        /// Diagnostic information
479        diag: Diagnostic,
480    },
481
482    /// Validation failed for parsed document
483    #[error("{diag}")]
484    ValidationFailed {
485        /// Diagnostic information
486        diag: Diagnostic,
487    },
488
489    /// Invalid schema definition
490    #[error("{diag}")]
491    InvalidSchema {
492        /// Diagnostic information
493        diag: Diagnostic,
494    },
495
496    /// Quill configuration error
497    #[error("{diag}")]
498    QuillConfig {
499        /// Diagnostic information
500        diag: Diagnostic,
501    },
502}
503
504impl RenderError {
505    /// Extract all diagnostics from this error
506    pub fn diagnostics(&self) -> Vec<&Diagnostic> {
507        match self {
508            RenderError::CompilationFailed { diags } => diags.iter().collect(),
509            RenderError::EngineCreation { diag }
510            | RenderError::InvalidFrontmatter { diag }
511            | RenderError::TemplateFailed { diag }
512            | RenderError::FormatNotSupported { diag }
513            | RenderError::UnsupportedBackend { diag }
514            | RenderError::DynamicAssetCollision { diag }
515            | RenderError::DynamicFontCollision { diag }
516            | RenderError::InputTooLarge { diag }
517            | RenderError::YamlTooLarge { diag }
518            | RenderError::NestingTooDeep { diag }
519            | RenderError::OutputTooLarge { diag }
520            | RenderError::ValidationFailed { diag }
521            | RenderError::InvalidSchema { diag }
522            | RenderError::QuillConfig { diag } => vec![diag],
523        }
524    }
525}
526
527/// Result type containing artifacts and warnings
528#[derive(Debug)]
529pub struct RenderResult {
530    /// Generated output artifacts
531    pub artifacts: Vec<crate::Artifact>,
532    /// Non-fatal diagnostic messages
533    pub warnings: Vec<Diagnostic>,
534    /// Output format that was produced
535    pub output_format: OutputFormat,
536}
537
538impl RenderResult {
539    /// Create a new result with artifacts and output format
540    pub fn new(artifacts: Vec<crate::Artifact>, output_format: OutputFormat) -> Self {
541        Self {
542            artifacts,
543            warnings: Vec::new(),
544            output_format,
545        }
546    }
547
548    /// Add a warning to the result
549    pub fn with_warning(mut self, warning: Diagnostic) -> Self {
550        self.warnings.push(warning);
551        self
552    }
553}
554
555/// Convert minijinja errors to RenderError
556impl From<minijinja::Error> for RenderError {
557    fn from(e: minijinja::Error) -> Self {
558        // Extract location with proper range information
559        let loc = e.line().map(|line| Location {
560            file: e.name().unwrap_or("template").to_string(),
561            line: line as u32,
562            // MiniJinja provides range, extract approximate column
563            col: e.range().map(|r| r.start as u32).unwrap_or(0),
564        });
565
566        // Generate helpful hints based on error kind
567        let hint = generate_minijinja_hint(&e);
568
569        // Create diagnostic with source preservation
570        let mut diag = Diagnostic::new(Severity::Error, e.to_string())
571            .with_code(format!("minijinja::{:?}", e.kind()));
572
573        if let Some(loc) = loc {
574            diag = diag.with_location(loc);
575        }
576
577        if let Some(hint) = hint {
578            diag = diag.with_hint(hint);
579        }
580
581        // Preserve the original error as source
582        diag = diag.with_source(Box::new(e));
583
584        RenderError::TemplateFailed { diag }
585    }
586}
587
588/// Generate helpful hints for common MiniJinja errors
589fn generate_minijinja_hint(e: &minijinja::Error) -> Option<String> {
590    use minijinja::ErrorKind;
591
592    match e.kind() {
593        ErrorKind::UndefinedError => {
594            Some("Check variable spelling and ensure it's defined in frontmatter".to_string())
595        }
596        ErrorKind::InvalidOperation => {
597            Some("Check that you're using the correct filter or operator for this type".to_string())
598        }
599        ErrorKind::SyntaxError => Some(
600            "Check template syntax - look for unclosed tags or invalid expressions".to_string(),
601        ),
602        _ => e.detail().map(|d| d.to_string()),
603    }
604}
605
606/// Helper to print structured errors
607pub fn print_errors(err: &RenderError) {
608    match err {
609        RenderError::CompilationFailed { diags } => {
610            for d in diags {
611                eprintln!("{}", d.fmt_pretty());
612            }
613        }
614        RenderError::TemplateFailed { diag } => eprintln!("{}", diag.fmt_pretty()),
615        RenderError::InvalidFrontmatter { diag } => eprintln!("{}", diag.fmt_pretty()),
616        RenderError::EngineCreation { diag } => eprintln!("{}", diag.fmt_pretty()),
617        RenderError::FormatNotSupported { diag } => eprintln!("{}", diag.fmt_pretty()),
618        RenderError::UnsupportedBackend { diag } => eprintln!("{}", diag.fmt_pretty()),
619        RenderError::DynamicAssetCollision { diag } => eprintln!("{}", diag.fmt_pretty()),
620        RenderError::DynamicFontCollision { diag } => eprintln!("{}", diag.fmt_pretty()),
621        RenderError::InputTooLarge { diag } => eprintln!("{}", diag.fmt_pretty()),
622        RenderError::YamlTooLarge { diag } => eprintln!("{}", diag.fmt_pretty()),
623        RenderError::NestingTooDeep { diag } => eprintln!("{}", diag.fmt_pretty()),
624        RenderError::OutputTooLarge { diag } => eprintln!("{}", diag.fmt_pretty()),
625        RenderError::ValidationFailed { diag } => eprintln!("{}", diag.fmt_pretty()),
626        RenderError::InvalidSchema { diag } => eprintln!("{}", diag.fmt_pretty()),
627        RenderError::QuillConfig { diag } => eprintln!("{}", diag.fmt_pretty()),
628    }
629}
630
631#[cfg(test)]
632mod tests {
633    use super::*;
634
635    #[test]
636    fn test_diagnostic_with_source_chain() {
637        let root_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
638        let diag = Diagnostic::new(Severity::Error, "Rendering failed".to_string())
639            .with_source(Box::new(root_err));
640
641        let chain = diag.source_chain();
642        assert_eq!(chain.len(), 1);
643        assert!(chain[0].contains("File not found"));
644    }
645
646    #[test]
647    fn test_diagnostic_serialization() {
648        let diag = Diagnostic::new(Severity::Error, "Test error".to_string())
649            .with_code("E001".to_string())
650            .with_location(Location {
651                file: "test.typ".to_string(),
652                line: 10,
653                col: 5,
654            });
655
656        let serializable: SerializableDiagnostic = diag.into();
657        let json = serde_json::to_string(&serializable).unwrap();
658        assert!(json.contains("Test error"));
659        assert!(json.contains("E001"));
660    }
661
662    #[test]
663    fn test_render_error_diagnostics_extraction() {
664        let diag1 = Diagnostic::new(Severity::Error, "Error 1".to_string());
665        let diag2 = Diagnostic::new(Severity::Error, "Error 2".to_string());
666
667        let err = RenderError::CompilationFailed {
668            diags: vec![diag1, diag2],
669        };
670
671        let diags = err.diagnostics();
672        assert_eq!(diags.len(), 2);
673    }
674
675    #[test]
676    fn test_diagnostic_fmt_pretty() {
677        let diag = Diagnostic::new(Severity::Warning, "Deprecated field used".to_string())
678            .with_code("W001".to_string())
679            .with_location(Location {
680                file: "input.md".to_string(),
681                line: 5,
682                col: 10,
683            })
684            .with_hint("Use the new field name instead".to_string());
685
686        let output = diag.fmt_pretty();
687        assert!(output.contains("[WARN]"));
688        assert!(output.contains("Deprecated field used"));
689        assert!(output.contains("W001"));
690        assert!(output.contains("input.md:5:10"));
691        assert!(output.contains("hint:"));
692    }
693
694    #[test]
695    fn test_diagnostic_fmt_pretty_with_source() {
696        let root_err = std::io::Error::new(std::io::ErrorKind::Other, "Underlying error");
697        let diag = Diagnostic::new(Severity::Error, "Top-level error".to_string())
698            .with_code("E002".to_string())
699            .with_source(Box::new(root_err));
700
701        let output = diag.fmt_pretty_with_source();
702        assert!(output.contains("[ERROR]"));
703        assert!(output.contains("Top-level error"));
704        assert!(output.contains("cause 1:"));
705        assert!(output.contains("Underlying error"));
706    }
707
708    #[test]
709    fn test_render_result_with_warnings() {
710        let artifacts = vec![];
711        let warning = Diagnostic::new(Severity::Warning, "Test warning".to_string());
712
713        let result = RenderResult::new(artifacts, OutputFormat::Pdf).with_warning(warning);
714
715        assert_eq!(result.warnings.len(), 1);
716        assert_eq!(result.warnings[0].message, "Test warning");
717    }
718
719    #[test]
720    fn test_minijinja_error_conversion() {
721        // Use undefined variable with strict mode to trigger an error
722        let template_str = "{{ undefined_var }}";
723        let mut env = minijinja::Environment::new();
724        env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
725
726        let result = env.render_str(template_str, minijinja::context! {});
727        assert!(
728            result.is_err(),
729            "Expected rendering to fail with undefined variable"
730        );
731
732        let minijinja_err = result.unwrap_err();
733        let render_err: RenderError = minijinja_err.into();
734
735        match render_err {
736            RenderError::TemplateFailed { diag } => {
737                assert_eq!(diag.severity, Severity::Error);
738                assert!(diag.code.is_some());
739                assert!(diag.hint.is_some());
740                assert!(diag.source.is_some());
741            }
742            _ => panic!("Expected TemplateFailed error"),
743        }
744    }
745}