use std::fmt;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorCategory {
Syntax,
Encoding,
Io,
Limit,
Reference,
DuplicateKey,
Data,
Unsupported,
Lossless,
Other,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Span {
pub start: usize,
pub end: usize,
pub line: usize,
pub column: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Location {
index: usize,
line: usize,
column: usize,
}
impl Location {
pub fn new(index: usize, line: usize, column: usize) -> Self {
Self {
index,
line,
column,
}
}
pub fn index(&self) -> usize {
self.index
}
pub fn line(&self) -> usize {
self.line
}
pub fn column(&self) -> usize {
self.column
}
}
impl Span {
pub fn new(start: usize, end: usize, line: usize, column: usize) -> Self {
Self {
start,
end,
line,
column,
}
}
pub fn point(offset: usize, line: usize, column: usize) -> Self {
Self::new(offset, offset, line, column)
}
}
pub(crate) fn utf8_error_span(input: &[u8], error: std::str::Utf8Error) -> Span {
let offset = error.valid_up_to();
let mut line = 1usize;
let mut column = 1usize;
for byte in &input[..offset] {
if *byte == b'\n' {
line += 1;
column = 1;
} else {
column += 1;
}
}
Span::point(offset, line, column)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RelatedDiagnostic {
pub message: String,
pub span: Span,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ErrorPath {
segments: Vec<ErrorPathSegment>,
}
impl ErrorPath {
pub fn new() -> Self {
Self {
segments: Vec::new(),
}
}
pub fn from_segments(segments: Vec<ErrorPathSegment>) -> Self {
Self { segments }
}
pub fn segments(&self) -> &[ErrorPathSegment] {
&self.segments
}
pub fn is_empty(&self) -> bool {
self.segments.is_empty()
}
pub(crate) fn prepend(&mut self, segment: ErrorPathSegment) {
self.segments.insert(0, segment);
}
}
impl fmt::Display for ErrorPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (index, segment) in self.segments.iter().enumerate() {
match segment {
ErrorPathSegment::Field(field) | ErrorPathSegment::Key(field)
if is_plain_path_key(field) =>
{
if index > 0 {
f.write_str(".")?;
}
f.write_str(field)?;
}
ErrorPathSegment::Field(field) | ErrorPathSegment::Key(field) => {
write!(f, "[\"{}\"]", EscapedPathString(field))?;
}
ErrorPathSegment::Index(index) => write!(f, "[{index}]")?,
ErrorPathSegment::ScalarKey(key) => write!(f, "[{key}]")?,
ErrorPathSegment::ComplexKey => f.write_str("[{complex key}]")?,
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorPathSegment {
Field(String),
Key(String),
Index(usize),
ScalarKey(String),
ComplexKey,
}
fn is_plain_path_key(value: &str) -> bool {
let mut chars = value.chars();
let Some(first) = chars.next() else {
return false;
};
matches!(first, 'A'..='Z' | 'a'..='z' | '_')
&& chars.all(|ch| matches!(ch, 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '-'))
}
struct EscapedPathString<'a>(&'a str);
impl fmt::Display for EscapedPathString<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for ch in self.0.chars() {
match ch {
'\\' => f.write_str("\\\\")?,
'"' => f.write_str("\\\"")?,
'\n' => f.write_str("\\n")?,
'\r' => f.write_str("\\r")?,
'\t' => f.write_str("\\t")?,
ch if ch.is_control() => write!(f, "\\u{:04X}", ch as u32)?,
ch => f.write_str(ch.encode_utf8(&mut [0; 4]))?,
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Diagnostic {
pub message: String,
pub span: Span,
pub related: Vec<RelatedDiagnostic>,
pub category: ErrorCategory,
pub document_index: Option<usize>,
pub path: Option<ErrorPath>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Error {
diagnostic: Box<Diagnostic>,
}
pub type Result<T> = std::result::Result<T, Error>;
impl Error {
pub fn new(message: impl Into<String>, span: impl Into<Option<Span>>) -> Self {
Self::with_category(message, span, ErrorCategory::Other)
}
pub fn with_category(
message: impl Into<String>,
span: impl Into<Option<Span>>,
category: ErrorCategory,
) -> Self {
Self {
diagnostic: Box::new(Diagnostic {
message: message.into(),
span: span.into().unwrap_or_default(),
related: Vec::new(),
category,
document_index: None,
path: None,
}),
}
}
pub(crate) fn data(message: impl Into<String>, span: impl Into<Option<Span>>) -> Self {
Self::with_category(message, span, ErrorCategory::Data)
}
pub(crate) fn syntax(message: impl Into<String>, span: impl Into<Option<Span>>) -> Self {
Self::with_category(message, span, ErrorCategory::Syntax)
}
pub(crate) fn encoding(message: impl Into<String>, span: impl Into<Option<Span>>) -> Self {
Self::with_category(message, span, ErrorCategory::Encoding)
}
pub(crate) fn io(message: impl Into<String>, span: impl Into<Option<Span>>) -> Self {
Self::with_category(message, span, ErrorCategory::Io)
}
pub(crate) fn limit(message: impl Into<String>, span: impl Into<Option<Span>>) -> Self {
Self::with_category(message, span, ErrorCategory::Limit)
}
pub(crate) fn reference(message: impl Into<String>, span: impl Into<Option<Span>>) -> Self {
Self::with_category(message, span, ErrorCategory::Reference)
}
pub fn with_related(
message: impl Into<String>,
span: Span,
related_message: impl Into<String>,
related_span: Span,
) -> Self {
Self {
diagnostic: Box::new(Diagnostic {
message: message.into(),
span,
related: vec![RelatedDiagnostic {
message: related_message.into(),
span: related_span,
}],
category: ErrorCategory::Other,
document_index: None,
path: None,
}),
}
}
pub(crate) fn with_related_category(
message: impl Into<String>,
span: Span,
related_message: impl Into<String>,
related_span: Span,
category: ErrorCategory,
) -> Self {
let mut error = Self::with_related(message, span, related_message, related_span);
error.diagnostic.category = category;
error
}
pub fn diagnostic(&self) -> &Diagnostic {
&self.diagnostic
}
pub fn span(&self) -> Span {
self.diagnostic.span
}
pub fn category(&self) -> ErrorCategory {
self.diagnostic.category
}
pub fn document_index(&self) -> Option<usize> {
self.diagnostic.document_index
}
pub fn path(&self) -> Option<&ErrorPath> {
self.diagnostic.path.as_ref()
}
pub fn location(&self) -> Option<Location> {
let span = self.span();
(span.line > 0 && span.column > 0).then_some(Location::new(
span.start,
span.line,
span.column,
))
}
pub fn line(&self) -> Option<usize> {
self.location().map(|location| location.line())
}
pub fn column(&self) -> Option<usize> {
self.location().map(|location| location.column())
}
pub fn render_source<'a>(&'a self, source: &'a str) -> SourceDiagnostic<'a> {
self.render_source_with_options(source, SourceRenderOptions::default())
}
pub fn render_source_with_options<'a>(
&'a self,
source: &'a str,
options: SourceRenderOptions,
) -> SourceDiagnostic<'a> {
self.diagnostic.render_source_with_options(source, options)
}
pub(crate) fn with_span_if_missing(mut self, span: Span) -> Self {
if self.location().is_none() {
self.diagnostic.span = span;
}
self
}
pub(crate) fn with_document_index(mut self, index: usize) -> Self {
if index > 0 {
self.diagnostic.document_index.get_or_insert(index);
}
self
}
pub(crate) fn with_path_segment_if_empty(mut self, segment: ErrorPathSegment) -> Self {
if self
.diagnostic
.path
.as_ref()
.is_none_or(ErrorPath::is_empty)
{
self.diagnostic.path = Some(ErrorPath::from_segments(vec![segment]));
}
self
}
pub(crate) fn prepend_path_segment(mut self, segment: ErrorPathSegment) -> Self {
match &mut self.diagnostic.path {
Some(path) => path.prepend(segment),
None => self.diagnostic.path = Some(ErrorPath::from_segments(vec![segment])),
}
self
}
}
impl Diagnostic {
pub fn category(&self) -> ErrorCategory {
self.category
}
pub fn document_index(&self) -> Option<usize> {
self.document_index
}
pub fn path(&self) -> Option<&ErrorPath> {
self.path.as_ref()
}
pub fn render_source<'a>(&'a self, source: &'a str) -> SourceDiagnostic<'a> {
self.render_source_with_options(source, SourceRenderOptions::default())
}
pub fn render_source_with_options<'a>(
&'a self,
source: &'a str,
options: SourceRenderOptions,
) -> SourceDiagnostic<'a> {
SourceDiagnostic {
diagnostic: self,
source,
options,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct SourceRenderOptions {
pub context_lines: usize,
}
#[derive(Clone, Copy, Debug)]
pub struct SourceDiagnostic<'a> {
diagnostic: &'a Diagnostic,
source: &'a str,
options: SourceRenderOptions,
}
impl fmt::Display for SourceDiagnostic<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match diagnostic_location(self.diagnostic) {
Some(location) => write!(
f,
"{} at line {}, column {}",
self.diagnostic.message,
location.line(),
location.column()
)?,
None => f.write_str(&self.diagnostic.message)?,
}
if let Some(path) = &self.diagnostic.path
&& !path.is_empty()
{
write!(f, "\npath: {path}")?;
}
if let Some(index) = self.diagnostic.document_index {
write!(f, "\ndocument: {index}")?;
}
render_span_block(f, self.source, self.diagnostic.span, self.options)?;
for related in &self.diagnostic.related {
write!(f, "\n{}", related.message)?;
render_span_block(f, self.source, related.span, self.options)?;
}
Ok(())
}
}
fn diagnostic_location(diagnostic: &Diagnostic) -> Option<Location> {
let span = diagnostic.span;
(span.line > 0 && span.column > 0).then_some(Location::new(span.start, span.line, span.column))
}
fn render_span_block(
f: &mut fmt::Formatter<'_>,
source: &str,
span: Span,
options: SourceRenderOptions,
) -> fmt::Result {
if span.line == 0 || span.column == 0 || span.start > source.len() {
return Ok(());
}
let Some((line_start, line_end, _)) = line_bounds(source, span.start) else {
return Ok(());
};
let line_number = span.line;
let context_start = line_number.saturating_sub(options.context_lines).max(1);
let context_end = line_number.saturating_add(options.context_lines);
let width = context_end.to_string().len();
writeln!(f)?;
writeln!(f, "{:>width$} |", "", width = width)?;
let caret_start = span.start.clamp(line_start, line_end);
let caret_end = floor_char_boundary(source, span.end.clamp(caret_start, line_end));
let mut rendered_line = false;
for current_line in context_start..=context_end {
let Some((current_start, _, line_text)) =
line_bounds_for_line(source, current_line, current_line == line_number)
else {
continue;
};
if rendered_line {
writeln!(f)?;
}
write!(f, "{current_line:>width$} | {line_text}", width = width)?;
if current_line == line_number {
writeln!(f)?;
write!(f, "{:>width$} | ", "", width = width)?;
for byte in source.as_bytes()[current_start..caret_start]
.iter()
.copied()
{
if byte == b'\t' {
f.write_str("\t")?;
} else {
f.write_str(" ")?;
}
}
let caret_count = caret_end.saturating_sub(caret_start).max(1);
for _ in 0..caret_count {
f.write_str("^")?;
}
}
rendered_line = true;
}
Ok(())
}
fn line_bounds(source: &str, offset: usize) -> Option<(usize, usize, &str)> {
if offset > source.len() || !source.is_char_boundary(offset) {
return None;
}
let line_start = source[..offset]
.rfind('\n')
.map_or(0, |index| index.saturating_add(1));
let line_end = source[offset..]
.find('\n')
.map_or(source.len(), |index| offset + index);
Some((line_start, line_end, &source[line_start..line_end]))
}
fn line_bounds_for_line(
source: &str,
target_line: usize,
include_trailing_empty_line: bool,
) -> Option<(usize, usize, &str)> {
if target_line == 0 {
return None;
}
let mut line = 1usize;
let mut start = 0usize;
for part in source.split_inclusive('\n') {
let end = start + part.len();
let text_end = end.saturating_sub(usize::from(part.ends_with('\n')));
if line == target_line {
return Some((start, text_end, &source[start..text_end]));
}
start = end;
line += 1;
}
if include_trailing_empty_line && source.ends_with('\n') && line == target_line {
return Some((source.len(), source.len(), ""));
}
None
}
fn floor_char_boundary(source: &str, mut offset: usize) -> usize {
while offset > 0 && !source.is_char_boundary(offset) {
offset -= 1;
}
offset
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.location() {
Some(location) => write!(
f,
"{} at line {}, column {}",
self.diagnostic.message,
location.line(),
location.column()
),
None => f.write_str(&self.diagnostic.message),
}
}
}
impl std::error::Error for Error {}
impl serde::de::Error for Error {
fn custom<T: fmt::Display>(msg: T) -> Self {
Self::data(msg.to_string(), Span::default())
}
fn unknown_field(field: &str, expected: &'static [&'static str]) -> Self {
let message = if expected.is_empty() {
format!("unknown field `{field}`")
} else {
format!(
"unknown field `{}`, expected one of {}",
field,
expected
.iter()
.map(|field| format!("`{field}`"))
.collect::<Vec<_>>()
.join(", ")
)
};
Self::data(message, Span::default())
.with_path_segment_if_empty(ErrorPathSegment::Field(field.to_string()))
}
fn missing_field(field: &'static str) -> Self {
Self::data(format!("missing field `{field}`"), Span::default())
.with_path_segment_if_empty(ErrorPathSegment::Field(field.to_string()))
}
fn duplicate_field(field: &'static str) -> Self {
Self::data(format!("duplicate field `{field}`"), Span::default())
.with_path_segment_if_empty(ErrorPathSegment::Field(field.to_string()))
}
}
impl serde::ser::Error for Error {
fn custom<T>(msg: T) -> Self
where
T: fmt::Display,
{
Self::new(msg.to_string(), Span::default())
}
}