1#![allow(clippy::format_push_string, clippy::too_many_lines)]
2
3use 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#[derive(Debug, Clone)]
91pub struct ErrorDiagnostics {
92 pub what: SmolStr,
94 pub why: SmolStr,
96 pub fix: SmolStr,
98}
99
100impl ErrorDiagnostics {
101 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 #[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#[derive(Debug)]
127pub enum AamlError {
128 IoError {
130 details: String,
131 diagnostics: Option<Box<ErrorDiagnostics>>,
132 },
133
134 ParseError {
136 line: usize,
138 content: String,
140 details: String,
142 diagnostics: Option<Box<ErrorDiagnostics>>,
144 },
145
146 NotFound {
148 key: String,
149 context: String,
150 diagnostics: Option<Box<ErrorDiagnostics>>,
151 },
152
153 InvalidValue {
155 details: String,
156 expected: String,
157 diagnostics: Option<Box<ErrorDiagnostics>>,
158 },
159
160 InvalidType {
162 type_name: String,
164 details: String,
166 provided: String,
168 diagnostics: Option<Box<ErrorDiagnostics>>,
170 },
171
172 DirectiveError {
174 directive: String,
175 message: String,
176 diagnostics: Option<Box<ErrorDiagnostics>>,
177 },
178
179 SchemaValidationError {
181 schema: String,
183 field: String,
185 type_name: String,
187 details: String,
189 diagnostics: Option<Box<ErrorDiagnostics>>,
191 },
192
193 MissingRequiredField {
195 schema: String,
196 field: String,
197 field_type: String,
198 diagnostics: Option<Box<ErrorDiagnostics>>,
199 },
200
201 CircularDependency {
203 path: String,
204 diagnostics: Option<Box<ErrorDiagnostics>>,
205 },
206
207 TypeRegistrationConflict {
209 type_name: String,
210 existing: String,
211 new: String,
212 diagnostics: Option<Box<ErrorDiagnostics>>,
213 },
214
215 NestingDepthExceeded {
217 depth: usize,
218 context: String,
219 diagnostics: Option<Box<ErrorDiagnostics>>,
220 },
221
222 MalformedLiteral {
224 literal_type: String,
225 content: String,
226 diagnostics: Option<Box<ErrorDiagnostics>>,
227 },
228
229 DirectiveSyntaxError {
231 directive: String,
232 provided_syntax: String,
233 expected_syntax: String,
234 diagnostics: Option<Box<ErrorDiagnostics>>,
235 },
236
237 TypeConversionError {
239 from_type: String,
240 to_type: String,
241 value: String,
242 diagnostics: Option<Box<ErrorDiagnostics>>,
243 },
244
245 LexError {
247 line: usize,
249 column: usize,
251 character: String,
253 diagnostics: Option<Box<ErrorDiagnostics>>,
255 },
256}
257
258pub 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 #[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 #[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}