use super::Context;
use itertools::Itertools;
use serde::*;
use std::error;
use std::fmt;
#[derive(Serialize, Deserialize, PartialEq, Clone, Eq, Hash)]
pub struct CustomError {
content: Box<InnerError>,
}
#[derive(Serialize, Deserialize, PartialEq, Clone, Eq, Hash)]
struct InnerError {
warning: bool,
short_description: String,
long_description: String,
suggestions: Vec<String>,
version: String,
context: Context,
underlying_errors: Vec<CustomError>,
}
#[expect(clippy::needless_pass_by_value)] impl CustomError {
pub fn error(
short_desc: impl std::string::ToString,
long_desc: impl std::string::ToString,
context: Context,
) -> Self {
Self {
content: Box::new(InnerError {
warning: false,
short_description: short_desc.to_string(),
long_description: long_desc.to_string(),
suggestions: Vec::new(),
version: String::new(),
context,
underlying_errors: Vec::new(),
}),
}
}
pub fn warning(
short_desc: impl std::string::ToString,
long_desc: impl std::string::ToString,
context: Context,
) -> Self {
Self {
content: Box::new(InnerError {
warning: true,
short_description: short_desc.to_string(),
long_description: long_desc.to_string(),
suggestions: Vec::new(),
version: String::new(),
context,
underlying_errors: Vec::new(),
}),
}
}
pub const fn level(&self) -> &str {
if self.content.warning {
"warning"
} else {
"error"
}
}
pub fn suggestions(&self) -> &[String] {
&self.content.suggestions
}
pub const fn is_warning(&self) -> bool {
self.content.warning
}
pub fn short_description(&self) -> &str {
&self.content.short_description
}
pub fn long_description(&self) -> &str {
&self.content.long_description
}
#[must_use]
pub fn with_long_description(&self, long_desc: impl std::string::ToString) -> Self {
Self {
content: Box::new(InnerError {
long_description: long_desc.to_string(),
..(*self.content).clone()
}),
}
}
#[must_use]
pub fn with_suggestions(
&self,
suggestions: impl IntoIterator<Item = impl std::string::ToString>,
) -> Self {
Self {
content: Box::new(InnerError {
suggestions: suggestions.into_iter().map(|s| s.to_string()).collect(),
..(*self.content).clone()
}),
}
}
#[must_use]
pub fn with_version(self, version: impl std::string::ToString) -> Self {
Self {
content: Box::new(InnerError {
version: version.to_string(),
..(*self.content)
}),
}
}
#[must_use]
pub fn with_context(&self, context: Context) -> Self {
Self {
content: Box::new(InnerError {
context,
..(*self.content).clone()
}),
}
}
#[must_use]
pub fn with_underlying_errors(&self, underlying_errors: Vec<Self>) -> Self {
Self {
content: Box::new(InnerError {
underlying_errors,
..(*self.content).clone()
}),
}
}
#[must_use]
pub fn overwrite_line_number(&self, line_number: usize) -> Self {
Self {
content: Box::new(InnerError {
context: self
.content
.context
.clone()
.overwrite_line_number(line_number),
..(*self.content).clone()
}),
}
}
pub const fn context(&self) -> &Context {
&self.content.context
}
}
impl fmt::Debug for CustomError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"{}: {}{}\n{}",
self.level(),
self.content.short_description,
self.content.context,
self.content.long_description
)?;
match self.content.suggestions.len() {
0 => Ok(()),
1 => writeln!(f, "Did you mean: {}?", self.content.suggestions[0]),
_ => writeln!(
f,
"Did you mean any of: {}?",
self.content.suggestions.join(", ")
),
}?;
if !self.content.version.is_empty() {
writeln!(f, "Version: {}", self.content.version)?;
}
match self.content.underlying_errors.len() {
0 => Ok(()),
1 => writeln!(
f,
"Underlying error:\n{}",
self.content.underlying_errors[0]
),
_ => writeln!(
f,
"Underlying errors:\n{}",
self.content.underlying_errors.iter().join("\n")
),
}
}
}
impl fmt::Display for CustomError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}
impl error::Error for CustomError {}
#[cfg(test)]
#[expect(clippy::print_stdout, clippy::missing_panics_doc)]
mod tests {
use super::*;
use crate::error::FilePosition;
#[test]
fn create_empty_error() {
let a = CustomError::error("test", "test", Context::none());
println!("{a}");
assert_eq!(format!("{a}"), "error: test\ntest\n");
assert!(!a.is_warning());
}
#[test]
fn create_full_line_error() {
let a = CustomError::warning("test", "test", Context::full_line(0, "testing line"));
println!("{a}");
assert_eq!(
format!("{a}"),
"warning: test\n ╷\n1 │ testing line\n ╵\ntest\n"
);
assert!(a.is_warning());
}
#[test]
fn create_range_error() {
let pos1 = FilePosition {
text: "hello world\nthis is a multiline\npiece of teXt",
line_index: 0,
column: 0,
};
let pos2 = FilePosition {
text: "",
line_index: 3,
column: 13,
};
let a = CustomError::warning("test", "test error", Context::range(&pos1, &pos2));
println!("{a}");
assert_eq!(format!("{a}"), "warning: test\n ╷\n1 │ hello world\n2 │ this is a multiline\n3 │ piece of teXt\n ╵\ntest error\n");
assert!(a.is_warning());
assert_eq!(pos2.text, "");
assert_eq!(pos2.line_index, 3);
assert_eq!(pos2.column, 13);
}
}