Skip to main content

aam_core/
error.rs

1#![allow(clippy::format_push_string, clippy::too_many_lines)]
2
3//! Error types for the AAML parser and validation pipeline with beautiful colored output.
4
5use colored::Colorize;
6use smol_str::SmolStr;
7use std::cell::RefCell;
8use std::fmt;
9use std::io;
10
11#[derive(Debug, Clone)]
12pub(crate) struct ErrorRenderContext {
13    path: String,
14    source: String,
15}
16
17thread_local! {
18    static ERROR_RENDER_CONTEXT: RefCell<ErrorRenderContext> = RefCell::new(ErrorRenderContext {
19        path: "<raw_string>".to_string(),
20        source: String::new(),
21    });
22}
23
24pub(crate) struct ErrorRenderContextGuard {
25    previous: ErrorRenderContext,
26}
27
28impl Drop for ErrorRenderContextGuard {
29    fn drop(&mut self) {
30        let previous = self.previous.clone();
31        ERROR_RENDER_CONTEXT.with(|ctx| {
32            *ctx.borrow_mut() = previous;
33        });
34    }
35}
36
37pub fn set_error_render_context(path: impl Into<String>, source: impl Into<String>) {
38    let path = path.into();
39    let source = source.into();
40    ERROR_RENDER_CONTEXT.with(|ctx| {
41        let mut ctx_mut = ctx.borrow_mut();
42        ctx_mut.path = path;
43        ctx_mut.source = source;
44    });
45}
46
47pub(crate) fn push_error_render_context(
48    path: impl Into<String>,
49    source: impl Into<String>,
50) -> ErrorRenderContextGuard {
51    let previous = get_error_render_context();
52    set_error_render_context(path, source);
53    ErrorRenderContextGuard { previous }
54}
55
56fn get_error_render_context() -> ErrorRenderContext {
57    ERROR_RENDER_CONTEXT.with(|ctx| ctx.borrow().clone())
58}
59
60fn source_line(source: &str, line: usize) -> Option<&str> {
61    if line == 0 {
62        return None;
63    }
64    source.lines().nth(line - 1)
65}
66
67fn type_alias_line_info(source: &str, alias_name: &str) -> Option<(usize, String, usize)> {
68    for (idx, raw) in source.lines().enumerate() {
69        let line_num = idx + 1;
70        let trimmed = raw.trim_start();
71        if !trimmed.starts_with("@type") {
72            continue;
73        }
74
75        let rest = trimmed.trim_start_matches("@type").trim_start();
76        let Some((lhs, _rhs)) = rest.split_once('=') else {
77            continue;
78        };
79        if lhs.trim() != alias_name {
80            continue;
81        }
82
83        let col = raw.find(alias_name).map_or(1, |p| p + 1);
84        return Some((line_num, raw.to_string(), col));
85    }
86    None
87}
88
89/// Error diagnostics with "What", "Why", and "Fix" guidance (inspired by Cargo).
90#[derive(Debug, Clone)]
91pub struct ErrorDiagnostics {
92    /// What went wrong (short title).
93    pub what: SmolStr,
94    /// Why it happened (detailed explanation).
95    pub why: SmolStr,
96    /// How to fix it (suggested resolution).
97    pub fix: SmolStr,
98}
99
100impl ErrorDiagnostics {
101    /// Create new diagnostics.
102    pub fn new(what: impl Into<SmolStr>, why: impl Into<SmolStr>, fix: impl Into<SmolStr>) -> Self {
103        Self {
104            what: what.into(),
105            why: why.into(),
106            fix: fix.into(),
107        }
108    }
109
110    /// Pretty-print with colors (like Cargo).
111    #[must_use]
112    pub fn pretty_print(&self) -> String {
113        format!(
114            "{}\n{}\n\n{}\n{}\n\n{}\n{}\n",
115            "error".red().bold(),
116            format!("  {}", self.what).red(),
117            "why".cyan().bold(),
118            format!("  {}", self.why).cyan(),
119            "fix".green().bold(),
120            format!("  {}", self.fix).green(),
121        )
122    }
123}
124
125/// All errors that can be produced while parsing or validating an AAML document.
126#[derive(Debug)]
127pub enum AamlError {
128    /// An I/O error occurred while reading a file.
129    IoError {
130        details: String,
131        diagnostics: Option<Box<ErrorDiagnostics>>,
132    },
133
134    /// A line could not be parsed as a valid AAML statement.
135    ParseError {
136        /// 1-based line number in the source file.
137        line: usize,
138        /// Raw content of the offending line.
139        content: String,
140        /// Human-readable explanation of why parsing failed.
141        details: String,
142        /// Diagnostic guidance.
143        diagnostics: Option<Box<ErrorDiagnostics>>,
144    },
145
146    /// A key or type name was not found in the registry or map.
147    NotFound {
148        key: String,
149        context: String,
150        diagnostics: Option<Box<ErrorDiagnostics>>,
151    },
152
153    /// A value does not satisfy a basic type constraint (not schema-specific).
154    InvalidValue {
155        details: String,
156        expected: String,
157        diagnostics: Option<Box<ErrorDiagnostics>>,
158    },
159
160    /// A value failed validation against a registered or built-in type.
161    InvalidType {
162        /// Name of the type that rejected the value.
163        type_name: String,
164        /// Details from the type validator.
165        details: String,
166        /// What was provided.
167        provided: String,
168        /// Diagnostic guidance.
169        diagnostics: Option<Box<ErrorDiagnostics>>,
170    },
171
172    /// A directive (`@import`, `@derive`, …) encountered an error in its arguments.
173    DirectiveError {
174        directive: String,
175        message: String,
176        diagnostics: Option<Box<ErrorDiagnostics>>,
177    },
178
179    /// A schema constraint was violated during parsing or explicit validation.
180    SchemaValidationError {
181        /// Name of the schema that declared the field.
182        schema: String,
183        /// Name of the field that failed validation.
184        field: String,
185        /// Declared type of the field.
186        type_name: String,
187        /// Human-readable description of the failure.
188        details: String,
189        /// Diagnostic guidance.
190        diagnostics: Option<Box<ErrorDiagnostics>>,
191    },
192
193    /// Missing the required field in schema.
194    MissingRequiredField {
195        schema: String,
196        field: String,
197        field_type: String,
198        diagnostics: Option<Box<ErrorDiagnostics>>,
199    },
200
201    /// Circular dependency detected.
202    CircularDependency {
203        path: String,
204        diagnostics: Option<Box<ErrorDiagnostics>>,
205    },
206
207    /// Type registration conflict.
208    TypeRegistrationConflict {
209        type_name: String,
210        existing: String,
211        new: String,
212        diagnostics: Option<Box<ErrorDiagnostics>>,
213    },
214
215    /// Nesting depth exceeded (possible infinite loop).
216    NestingDepthExceeded {
217        depth: usize,
218        context: String,
219        diagnostics: Option<Box<ErrorDiagnostics>>,
220    },
221
222    /// Malformed inline object or array literal.
223    MalformedLiteral {
224        literal_type: String,
225        content: String,
226        diagnostics: Option<Box<ErrorDiagnostics>>,
227    },
228
229    /// Directive syntax is incorrect.
230    DirectiveSyntaxError {
231        directive: String,
232        provided_syntax: String,
233        expected_syntax: String,
234        diagnostics: Option<Box<ErrorDiagnostics>>,
235    },
236
237    /// Type conversion failed.
238    TypeConversionError {
239        from_type: String,
240        to_type: String,
241        value: String,
242        diagnostics: Option<Box<ErrorDiagnostics>>,
243    },
244
245    /// Lexical analysis error (invalid character or token).
246    LexError {
247        /// Line number where the error occurred
248        line: usize,
249        /// Column number where the error occurred
250        column: usize,
251        /// The invalid character
252        character: String,
253        /// Diagnostic guidance
254        diagnostics: Option<Box<ErrorDiagnostics>>,
255    },
256}
257
258/// Backward-compatible alias used by some bindings.
259pub type AamError = AamlError;
260
261impl AamlError {
262    const fn code(&self) -> &'static str {
263        match self {
264            Self::CircularDependency { .. } => "E001",
265            Self::ParseError { .. } => "E002",
266            Self::InvalidType { .. } => "E003",
267            Self::SchemaValidationError { .. } => "E004",
268            Self::MissingRequiredField { .. } => "E005",
269            Self::NotFound { .. } => "E006",
270            Self::DirectiveError { .. } => "E007",
271            Self::DirectiveSyntaxError { .. } => "E008",
272            Self::MalformedLiteral { .. } => "E009",
273            Self::TypeRegistrationConflict { .. } => "E010",
274            Self::TypeConversionError { .. } => "E011",
275            Self::InvalidValue { .. } => "E012",
276            Self::NestingDepthExceeded { .. } => "E013",
277            Self::LexError { .. } => "E014",
278            Self::IoError { .. } => "E015",
279        }
280    }
281
282    const fn title(&self) -> &'static str {
283        match self {
284            Self::CircularDependency { .. } => "cyclic dependency detected",
285            Self::ParseError { .. } => "parse error",
286            Self::InvalidType { .. } => "type validation failed",
287            Self::SchemaValidationError { .. } => "schema validation failed",
288            Self::MissingRequiredField { .. } => "missing required field",
289            Self::NotFound { .. } => "entry not found",
290            Self::DirectiveError { .. } => "directive execution failed",
291            Self::DirectiveSyntaxError { .. } => "directive syntax error",
292            Self::MalformedLiteral { .. } => "malformed literal",
293            Self::TypeRegistrationConflict { .. } => "type registration conflict",
294            Self::TypeConversionError { .. } => "type conversion failed",
295            Self::InvalidValue { .. } => "invalid value",
296            Self::NestingDepthExceeded { .. } => "nesting depth exceeded",
297            Self::LexError { .. } => "lexical analysis failed",
298            Self::IoError { .. } => "I/O operation failed",
299        }
300    }
301
302    const fn default_help(&self) -> &'static str {
303        match self {
304            Self::CircularDependency { .. } => {
305                "types in AAM must be acyclic. Consider using a primitive type or breaking the loop."
306            }
307            Self::ParseError { .. } => {
308                "check assignment/directive syntax near the highlighted line."
309            }
310            Self::InvalidType { .. } => {
311                "ensure the value matches the declared type or update the type declaration."
312            }
313            Self::SchemaValidationError { .. } | Self::MissingRequiredField { .. } => {
314                "fill required fields and ensure each field value matches its declared type."
315            }
316            Self::NotFound { .. } => {
317                "verify the referenced key/type/schema exists and is in scope."
318            }
319            Self::DirectiveError { .. } | Self::DirectiveSyntaxError { .. } => {
320                "check directive name and argument format."
321            }
322            Self::MalformedLiteral { .. } => {
323                "ensure object/list literals are balanced and well-formed."
324            }
325            Self::TypeRegistrationConflict { .. } => {
326                "rename the type or remove duplicate @type declarations."
327            }
328            Self::TypeConversionError { .. } => {
329                "provide a value that can be converted to the requested type."
330            }
331            Self::InvalidValue { .. } => "provide a value in the expected format.",
332            Self::NestingDepthExceeded { .. } => "reduce recursion depth or split nested data.",
333            Self::LexError { .. } => "remove or replace unsupported characters.",
334            Self::IoError { .. } => "check file path and permissions.",
335        }
336    }
337
338    fn render_error_header(&self, ctx: &ErrorRenderContext, out: &mut String) {
339        use std::fmt::Write;
340        let _ = writeln!(
341            out,
342            "{}[{}]: {}",
343            "error".red().bold(),
344            self.code().red().bold(),
345            self.title().bold()
346        );
347        let _ = writeln!(out, " {} {}:?:?", "-->".blue().bold(), ctx.path);
348        let _ = writeln!(out, " {} {}", "|".blue().bold(), self.short_message());
349    }
350
351    fn render_circular_dep(&self, ctx: &ErrorRenderContext, out: &mut String) {
352        use std::fmt::Write;
353        let Self::CircularDependency { path, .. } = self else {
354            return;
355        };
356        let nodes: Vec<String> = path
357            .split("->")
358            .map(str::trim)
359            .filter(|s| !s.is_empty())
360            .map(ToString::to_string)
361            .collect();
362        let cycle_start = nodes
363            .first()
364            .cloned()
365            .unwrap_or_else(|| "unknown".to_string());
366        let chain = if nodes.is_empty() {
367            path.clone()
368        } else {
369            nodes.join(" -> ")
370        };
371
372        let first_alias = nodes.first().cloned().unwrap_or_else(|| "?".to_string());
373        let first_loc = type_alias_line_info(&ctx.source, &first_alias);
374
375        if let Some((line, _, col)) = first_loc {
376            let _ = writeln!(
377                out,
378                " {} {}:{}:{}",
379                "-->".blue().bold(),
380                ctx.path,
381                line,
382                col
383            );
384        }
385        let _ = writeln!(out, " {}", "|".blue().bold());
386
387        for (idx, alias) in nodes.iter().enumerate() {
388            if let Some((line, src, col)) = type_alias_line_info(&ctx.source, alias) {
389                let _ = writeln!(
390                    out,
391                    " {} {} {}",
392                    line.to_string().blue().bold(),
393                    "|".blue().bold(),
394                    src
395                );
396                let marker = if idx == 0 {
397                    "cycle starts here"
398                } else if idx == nodes.len().saturating_sub(1) {
399                    "cycle closes here"
400                } else {
401                    "part of cycle"
402                };
403                let _ = writeln!(
404                    out,
405                    " {} {}{} {}",
406                    "|".blue().bold(),
407                    " ".repeat(col.saturating_sub(1)),
408                    "^".red().bold(),
409                    marker
410                );
411            }
412        }
413
414        let _ = writeln!(out, " {}", "|".blue().bold());
415        let _ = writeln!(out, " {} {} {}", "=".blue().bold(), "cycle:".bold(), chain);
416        let _ = writeln!(out, " {} returns to '{}'", "=".blue().bold(), cycle_start);
417    }
418
419    fn render_parse_error(&self, ctx: &ErrorRenderContext, out: &mut String) {
420        use std::fmt::Write;
421        let Self::ParseError {
422            line,
423            content,
424            details,
425            ..
426        } = self
427        else {
428            return;
429        };
430        let _ = writeln!(out, " {} {}:{}:{}", "-->".blue().bold(), ctx.path, line, 1);
431        let _ = writeln!(out, " {}", "|".blue().bold());
432        let display_line = source_line(&ctx.source, *line).unwrap_or(content.as_str());
433        let caret_col = display_line.find(content.trim()).map_or(1, |v| v + 1);
434        let _ = writeln!(
435            out,
436            " {} {} {}",
437            line.to_string().blue().bold(),
438            "|".blue().bold(),
439            display_line
440        );
441        let _ = writeln!(
442            out,
443            " {} {} {}",
444            "|".blue().bold(),
445            "^".red().bold(),
446            details
447        );
448        let _ = writeln!(
449            out,
450            " {} {}{}",
451            "|".blue().bold(),
452            " ".repeat(caret_col.saturating_sub(1)),
453            "^-- here".red().bold()
454        );
455    }
456
457    fn render_lex_error(&self, ctx: &ErrorRenderContext, out: &mut String) {
458        use std::fmt::Write;
459        let Self::LexError {
460            line,
461            column,
462            character,
463            ..
464        } = self
465        else {
466            return;
467        };
468        let _ = writeln!(
469            out,
470            " {} {}:{}:{}",
471            "-->".blue().bold(),
472            ctx.path,
473            line,
474            column
475        );
476        if let Some(src) = source_line(&ctx.source, *line) {
477            let _ = writeln!(
478                out,
479                " {} {} {}",
480                line.to_string().blue().bold(),
481                "|".blue().bold(),
482                src
483            );
484            let _ = writeln!(
485                out,
486                " {} {}{} invalid character '{}'",
487                "|".blue().bold(),
488                " ".repeat(column.saturating_sub(1)),
489                "^--".red().bold(),
490                character
491            );
492        } else {
493            let _ = writeln!(
494                out,
495                " {} invalid character '{}'",
496                "|".blue().bold(),
497                character
498            );
499        }
500    }
501
502    fn render_compiler_style(&self) -> String {
503        use std::fmt::Write;
504        let ctx = get_error_render_context();
505        let mut out = String::new();
506        let _ = writeln!(
507            out,
508            "{}[{}]: {}",
509            "error".red().bold(),
510            self.code().red().bold(),
511            self.title().bold()
512        );
513
514        match self {
515            Self::CircularDependency { .. } => self.render_circular_dep(&ctx, &mut out),
516            Self::ParseError { .. } => self.render_parse_error(&ctx, &mut out),
517            Self::LexError { .. } => self.render_lex_error(&ctx, &mut out),
518            _ => self.render_error_header(&ctx, &mut out),
519        }
520
521        let _ = writeln!(out, " {}", "|".blue().bold());
522        if let Some(diag) = self.diagnostics() {
523            let _ = writeln!(out, " {} {}", "help:".green().bold(), diag.fix);
524        } else {
525            let _ = writeln!(out, " {} {}", "help:".green().bold(), self.default_help());
526        }
527
528        out
529    }
530
531    /// Get the primary error message (short form).
532    #[must_use]
533    pub fn short_message(&self) -> String {
534        match self {
535            Self::IoError { details, .. } => format!("IO error: {details}"),
536            Self::ParseError {
537                line,
538                content,
539                details,
540                ..
541            } => {
542                format!("Parse error at line {line}: {content} ({details})")
543            }
544            Self::NotFound { key, context, .. } => {
545                format!("Key '{key}' not found ({context})")
546            }
547            Self::InvalidValue {
548                details, expected, ..
549            } => {
550                format!("Invalid value: {details} (expected: {expected})")
551            }
552            Self::InvalidType {
553                type_name,
554                provided,
555                details,
556                ..
557            } => {
558                format!("Invalid type '{type_name}': {details} (got: {provided})")
559            }
560            Self::DirectiveError {
561                directive, message, ..
562            } => {
563                format!("Directive '@{directive}' error: {message}")
564            }
565            Self::SchemaValidationError {
566                schema,
567                field,
568                type_name,
569                details,
570                ..
571            } => {
572                format!("Schema '{schema}' field '{field}' ({type_name}): {details}")
573            }
574            Self::MissingRequiredField {
575                schema,
576                field,
577                field_type,
578                ..
579            } => {
580                format!(
581                    "Missing required field '{field}' in schema '{schema}' (type: {field_type})"
582                )
583            }
584            Self::CircularDependency { path, .. } => {
585                format!("Circular dependency detected: {path}")
586            }
587            Self::TypeRegistrationConflict {
588                type_name,
589                existing,
590                new,
591                ..
592            } => {
593                format!(
594                    "Type '{type_name}' already defined as '{existing}', cannot redefine as '{new}'"
595                )
596            }
597            Self::NestingDepthExceeded { depth, context, .. } => {
598                format!("Nesting depth exceeded ({depth}): {context}")
599            }
600            Self::MalformedLiteral {
601                literal_type,
602                content,
603                ..
604            } => {
605                format!("Malformed {literal_type} literal: {content}")
606            }
607            Self::DirectiveSyntaxError {
608                directive,
609                provided_syntax,
610                expected_syntax,
611                ..
612            } => {
613                format!(
614                    "Directive '@{directive}' syntax error: got '{provided_syntax}', expected '{expected_syntax}'"
615                )
616            }
617            Self::TypeConversionError {
618                from_type,
619                to_type,
620                value,
621                ..
622            } => {
623                format!("Cannot convert '{value}' from {from_type} to {to_type}")
624            }
625            Self::LexError {
626                line,
627                column,
628                character,
629                ..
630            } => {
631                format!("Lexical error at {line}:{column}: invalid character '{character}'")
632            }
633        }
634    }
635
636    /// Get the detailed diagnostics if available.
637    #[must_use]
638    pub fn diagnostics(&self) -> Option<&ErrorDiagnostics> {
639        match self {
640            Self::IoError { diagnostics, .. }
641            | Self::ParseError { diagnostics, .. }
642            | Self::NotFound { diagnostics, .. }
643            | Self::InvalidValue { diagnostics, .. }
644            | Self::InvalidType { diagnostics, .. }
645            | Self::DirectiveError { diagnostics, .. }
646            | Self::SchemaValidationError { diagnostics, .. }
647            | Self::MissingRequiredField { diagnostics, .. }
648            | Self::CircularDependency { diagnostics, .. }
649            | Self::TypeRegistrationConflict { diagnostics, .. }
650            | Self::NestingDepthExceeded { diagnostics, .. }
651            | Self::MalformedLiteral { diagnostics, .. }
652            | Self::DirectiveSyntaxError { diagnostics, .. }
653            | Self::TypeConversionError { diagnostics, .. }
654            | Self::LexError { diagnostics, .. } => diagnostics.as_deref(),
655        }
656    }
657}
658
659impl fmt::Display for AamlError {
660    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
661        write!(f, "{}", self.render_compiler_style())
662    }
663}
664
665impl std::error::Error for AamlError {}
666
667#[cfg(feature = "serde")]
668impl serde::de::Error for AamlError {
669    fn custom<T: fmt::Display>(msg: T) -> Self {
670        AamlError::InvalidValue {
671            details: msg.to_string(),
672            expected: String::new(),
673            diagnostics: None,
674        }
675    }
676}
677
678impl From<io::Error> for AamlError {
679    fn from(err: io::Error) -> Self {
680        let details = err.to_string();
681        let diagnostics = Some(Box::new(ErrorDiagnostics::new(
682            "I/O operation failed",
683            format!("Could not read or write file: {details}"),
684            "Check file permissions and ensure the path exists",
685        )));
686        Self::IoError {
687            details,
688            diagnostics,
689        }
690    }
691}