#[cfg(feature = "std")]
use std::fmt;
#[cfg(not(feature = "std"))]
use core::fmt;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::error::YamlError;
#[cfg(test)]
use crate::error::ErrorKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorCode {
E001,
E002,
E003,
E004,
E005,
E006,
E007,
E008,
E009,
E010,
E011,
E012,
E013,
E014,
E015,
}
impl ErrorCode {
pub fn as_str(&self) -> &'static str {
match self {
ErrorCode::E001 => "E001",
ErrorCode::E002 => "E002",
ErrorCode::E003 => "E003",
ErrorCode::E004 => "E004",
ErrorCode::E005 => "E005",
ErrorCode::E006 => "E006",
ErrorCode::E007 => "E007",
ErrorCode::E008 => "E008",
ErrorCode::E009 => "E009",
ErrorCode::E010 => "E010",
ErrorCode::E011 => "E011",
ErrorCode::E012 => "E012",
ErrorCode::E013 => "E013",
ErrorCode::E014 => "E014",
ErrorCode::E015 => "E015",
}
}
pub fn description(&self) -> &'static str {
match self {
ErrorCode::E001 => "Missing colon separator in mapping",
ErrorCode::E002 => "Quoted string is not properly terminated",
ErrorCode::E003 => "Escape sequence is invalid or malformed",
ErrorCode::E004 => "Alias references an undefined anchor",
ErrorCode::E005 => "Anchor name is used more than once",
ErrorCode::E006 => "Tag syntax is invalid or unsupported",
ErrorCode::E007 => "Indentation is inconsistent or unexpected",
ErrorCode::E008 => "Key contains invalid characters",
ErrorCode::E009 => "Flow collection ([], {}) is not closed",
ErrorCode::E010 => "Document marker (---/...) is malformed",
ErrorCode::E011 => "Circular reference in aliases detected",
ErrorCode::E012 => "Nesting depth exceeds configured limit",
ErrorCode::E013 => "Boolean value must be true/false/yes/no/on/off",
ErrorCode::E014 => "Numeric value format is invalid",
ErrorCode::E015 => "Null value must be null/~ or empty",
}
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
pub start_line: usize,
pub start_col: usize,
pub end_line: usize,
pub end_col: usize,
}
impl Span {
pub fn new(start_line: usize, start_col: usize, end_line: usize, end_col: usize) -> Self {
Self {
start_line,
start_col,
end_line,
end_col,
}
}
pub fn point(line: usize, col: usize) -> Self {
Self {
start_line: line,
start_col: col,
end_line: line,
end_col: col,
}
}
pub fn is_point(&self) -> bool {
self.start_line == self.end_line && self.start_col == self.end_col
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ErrorSuggestion {
pub message: String,
pub replacement: Option<String>,
pub span: Option<Span>,
}
impl ErrorSuggestion {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
replacement: None,
span: None,
}
}
pub fn with_replacement(mut self, replacement: impl Into<String>) -> Self {
self.replacement = Some(replacement.into());
self
}
pub fn with_span(mut self, span: Span) -> Self {
self.span = Some(span);
self
}
}
#[derive(Debug, Clone)]
pub struct EnhancedError {
base: YamlError,
code: Option<ErrorCode>,
snippet: Option<String>,
span: Option<Span>,
suggestions: Vec<ErrorSuggestion>,
notes: Vec<String>,
}
impl EnhancedError {
pub fn new(base: YamlError) -> Self {
Self {
base,
code: None,
snippet: None,
span: None,
suggestions: Vec::new(),
notes: Vec::new(),
}
}
pub fn with_code(mut self, code: ErrorCode) -> Self {
self.code = Some(code);
self
}
pub fn with_snippet(mut self, snippet: impl Into<String>) -> Self {
self.snippet = Some(snippet.into());
self
}
pub fn with_span(mut self, span: Span) -> Self {
self.span = Some(span);
self
}
pub fn with_suggestion(mut self, suggestion: ErrorSuggestion) -> Self {
self.suggestions.push(suggestion);
self
}
pub fn with_note(mut self, note: impl Into<String>) -> Self {
self.notes.push(note.into());
self
}
pub fn base(&self) -> &YamlError {
&self.base
}
pub fn code(&self) -> Option<ErrorCode> {
self.code
}
pub fn suggestions(&self) -> &[ErrorSuggestion] {
&self.suggestions
}
pub fn notes(&self) -> &[String] {
&self.notes
}
pub fn format_detailed(&self) -> String {
let mut output = String::new();
if let Some(code) = self.code {
output.push_str(&format!("[{}] ", code));
}
output.push_str(&self.base.to_string());
output.push('\n');
if let Some(snippet) = &self.snippet {
output.push_str("\n");
output.push_str(snippet);
output.push_str("\n");
}
if !self.suggestions.is_empty() {
output.push_str("\nSuggestions:\n");
for (i, suggestion) in self.suggestions.iter().enumerate() {
output.push_str(&format!(" {}. {}\n", i + 1, suggestion.message));
if let Some(replacement) = &suggestion.replacement {
output.push_str(&format!(" Try: {}\n", replacement));
}
}
}
if !self.notes.is_empty() {
output.push_str("\nNote:\n");
for note in &self.notes {
output.push_str(&format!(" {}\n", note));
}
}
output
}
}
impl fmt::Display for EnhancedError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.format_detailed())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecoveryStrategy {
SkipLine,
SkipToNextMapping,
SkipCollection,
InsertDefault,
Abort,
}
#[derive(Debug)]
pub struct ErrorRecovery {
pub strategy: RecoveryStrategy,
pub recovered: bool,
pub message: String,
}
impl ErrorRecovery {
pub fn new(strategy: RecoveryStrategy) -> Self {
Self {
strategy,
recovered: false,
message: String::new(),
}
}
pub fn success(mut self, message: impl Into<String>) -> Self {
self.recovered = true;
self.message = message.into();
self
}
pub fn failed(mut self, message: impl Into<String>) -> Self {
self.recovered = false;
self.message = message.into();
self
}
}
pub struct SuggestionBuilder;
impl SuggestionBuilder {
pub fn missing_colon(key: &str) -> ErrorSuggestion {
ErrorSuggestion::new(format!("Add ':' after the key '{}'", key))
.with_replacement(format!("{}: ", key))
}
pub fn unclosed_quote(quote_char: char) -> ErrorSuggestion {
ErrorSuggestion::new(format!(
"Add closing {} to terminate the string",
quote_char
))
}
pub fn boolean_typo(actual: &str) -> ErrorSuggestion {
let suggestion = match actual.to_lowercase().as_str() {
"ture" | "tru" => "true",
"flase" | "fals" => "false",
"ye" | "ys" => "yes",
"n" => "no",
_ => return ErrorSuggestion::new("Use one of: true, false, yes, no, on, off"),
};
ErrorSuggestion::new(format!("Did you mean '{}'?", suggestion))
.with_replacement(suggestion.to_string())
}
pub fn null_typo(actual: &str) -> ErrorSuggestion {
let suggestion = match actual.to_lowercase().as_str() {
"nul" | "nil" | "none" => "null",
_ => return ErrorSuggestion::new("Use 'null' or '~' for null values"),
};
ErrorSuggestion::new(format!("Did you mean '{}'?", suggestion))
.with_replacement(suggestion.to_string())
}
pub fn undefined_alias(alias: &str, available: &[String]) -> ErrorSuggestion {
if available.is_empty() {
return ErrorSuggestion::new("No anchors are defined in this document")
.with_replacement(format!("&{} ... *{}", alias, alias));
}
let closest = available
.iter()
.min_by_key(|anchor| edit_distance(alias, anchor))
.unwrap();
if edit_distance(alias, closest) <= 3 {
ErrorSuggestion::new(format!("Did you mean '*{}'?", closest))
.with_replacement(format!("*{}", closest))
} else {
ErrorSuggestion::new(format!("Available anchors: {}", available.join(", ")))
}
}
pub fn indentation(expected: usize, actual: usize) -> ErrorSuggestion {
let action = if actual < expected {
format!("Increase indentation by {} spaces", expected - actual)
} else {
format!("Decrease indentation by {} spaces", actual - expected)
};
ErrorSuggestion::new(format!(
"Expected {} spaces, found {}. {}",
expected, actual, action
))
}
}
fn edit_distance(a: &str, b: &str) -> usize {
let len_a = a.len();
let len_b = b.len();
if len_a == 0 {
return len_b;
}
if len_b == 0 {
return len_a;
}
let mut matrix = vec![vec![0; len_b + 1]; len_a + 1];
for i in 0..=len_a {
matrix[i][0] = i;
}
for j in 0..=len_b {
matrix[0][j] = j;
}
for (i, ca) in a.chars().enumerate() {
for (j, cb) in b.chars().enumerate() {
let cost = if ca == cb { 0 } else { 1 };
matrix[i + 1][j + 1] = core::cmp::min(
core::cmp::min(
matrix[i][j + 1] + 1, matrix[i + 1][j] + 1, ),
matrix[i][j] + cost, );
}
}
matrix[len_a][len_b]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_code() {
assert_eq!(ErrorCode::E001.as_str(), "E001");
assert_eq!(ErrorCode::E001.to_string(), "E001");
assert!(ErrorCode::E001.description().contains("colon"));
}
#[test]
fn test_span() {
let span = Span::new(1, 5, 1, 10);
assert_eq!(span.start_line, 1);
assert_eq!(span.end_col, 10);
assert!(!span.is_point());
let point = Span::point(5, 10);
assert!(point.is_point());
}
#[test]
fn test_error_suggestion() {
let suggestion = ErrorSuggestion::new("Add colon").with_replacement("key: value");
assert_eq!(suggestion.message, "Add colon");
assert_eq!(suggestion.replacement, Some("key: value".to_string()));
}
#[test]
fn test_enhanced_error() {
let base = YamlError::new(ErrorKind::SyntaxError, "test error").with_position(5, 10);
let enhanced = EnhancedError::new(base)
.with_code(ErrorCode::E001)
.with_suggestion(ErrorSuggestion::new("Try this"))
.with_note("Additional context");
assert_eq!(enhanced.code(), Some(ErrorCode::E001));
assert_eq!(enhanced.suggestions().len(), 1);
assert_eq!(enhanced.notes().len(), 1);
}
#[test]
fn test_suggestion_builder_boolean() {
let suggestion = SuggestionBuilder::boolean_typo("ture");
assert!(suggestion.message.contains("true"));
assert_eq!(suggestion.replacement, Some("true".to_string()));
let suggestion = SuggestionBuilder::boolean_typo("flase");
assert!(suggestion.message.contains("false"));
}
#[test]
fn test_suggestion_builder_null() {
let suggestion = SuggestionBuilder::null_typo("nil");
assert!(suggestion.message.contains("null"));
assert_eq!(suggestion.replacement, Some("null".to_string()));
}
#[test]
fn test_suggestion_builder_alias() {
let available = vec!["anchor1".to_string(), "myanchor".to_string()];
let suggestion = SuggestionBuilder::undefined_alias("ancor1", &available);
assert!(suggestion.message.contains("anchor1"));
}
#[test]
fn test_edit_distance() {
assert_eq!(edit_distance("kitten", "sitting"), 3);
assert_eq!(edit_distance("hello", "hello"), 0);
assert_eq!(edit_distance("", "test"), 4);
assert_eq!(edit_distance("test", ""), 4);
}
#[test]
fn test_recovery_strategy() {
let recovery =
ErrorRecovery::new(RecoveryStrategy::SkipLine).success("Skipped invalid line");
assert!(recovery.recovered);
assert_eq!(recovery.strategy, RecoveryStrategy::SkipLine);
}
#[test]
fn test_enhanced_error_format() {
let base = YamlError::new(ErrorKind::SyntaxError, "Missing colon");
let enhanced = EnhancedError::new(base)
.with_code(ErrorCode::E001)
.with_suggestion(ErrorSuggestion::new("Add ':' after key"))
.with_note("Mappings require key: value pairs");
let formatted = enhanced.format_detailed();
assert!(formatted.contains("[E001]"));
assert!(formatted.contains("Suggestions:"));
assert!(formatted.contains("Note:"));
}
#[test]
fn test_span_invalid() {
let span = Span::new(2, 3, 1, 1); assert_eq!(span.start_line, 2);
assert_eq!(span.end_line, 1);
assert!(!span.is_point());
}
#[test]
fn test_error_suggestion_empty() {
let suggestion = ErrorSuggestion::new("");
assert_eq!(suggestion.message, "");
assert_eq!(suggestion.replacement, None);
assert_eq!(suggestion.span, None);
}
#[test]
fn test_error_suggestion_with_span() {
let span = Span::point(1, 1);
let suggestion = ErrorSuggestion::new("Fix here").with_span(span);
assert_eq!(suggestion.span, Some(span));
}
#[test]
fn test_enhanced_error_no_code() {
let base = YamlError::new(ErrorKind::SyntaxError, "no code");
let enhanced = EnhancedError::new(base);
assert_eq!(enhanced.code(), None);
}
#[test]
fn test_enhanced_error_multiple_notes() {
let base = YamlError::new(ErrorKind::SyntaxError, "multi notes");
let enhanced = EnhancedError::new(base)
.with_note("First note")
.with_note("Second note");
assert_eq!(enhanced.notes().len(), 2);
}
#[test]
fn test_recovery_failure() {
let recovery = ErrorRecovery::new(RecoveryStrategy::Abort).failed("Could not recover");
assert!(!recovery.recovered);
assert_eq!(recovery.strategy, RecoveryStrategy::Abort);
assert_eq!(recovery.message, "Could not recover");
}
}