#![deny(missing_docs)]
#![forbid(unsafe_code)]
#![warn(clippy::pedantic)]
#![warn(clippy::unwrap_used)]
#![warn(rust_2018_idioms, unused_lifetimes, missing_debug_implementations)]
#[cfg(feature = "colored")]
use colored::Colorize;
use std::{
fmt,
sync::atomic::{
AtomicBool,
AtomicUsize,
Ordering,
},
};
#[cfg(feature = "colored")]
mod control;
#[cfg(test)]
mod test;
#[cfg(feature = "colored")]
pub use control::{
always_color,
never_color,
set_coloring_mode,
use_environment,
ColoringMode,
};
pub const CONTEXTUALIZE_DEFAULT: bool = true;
static CONTEXTUALIZE: AtomicBool = AtomicBool::new(CONTEXTUALIZE_DEFAULT);
pub fn set_default_contextualize(should_contextualize: bool) {
CONTEXTUALIZE.store(should_contextualize, Ordering::Relaxed);
}
pub fn get_default_contextualize() -> usize {
CONTEXT_LINES.load(Ordering::Relaxed)
}
pub const CONTEXT_LINES_DEFAULT: usize = 3;
static CONTEXT_LINES: AtomicUsize = AtomicUsize::new(CONTEXT_LINES_DEFAULT);
pub fn set_default_context_lines(amount_of_context: usize) {
CONTEXT_LINES.store(amount_of_context, Ordering::Relaxed);
}
pub fn get_default_context_lines() -> usize {
CONTEXT_LINES.load(Ordering::Relaxed)
}
pub const CONTEXT_CHARACTERS_DEFAULT: usize = 30;
static CONTEXT_CHARACTERS: AtomicUsize = AtomicUsize::new(CONTEXT_CHARACTERS_DEFAULT);
pub fn set_default_context_characters(amount_of_context: usize) {
CONTEXT_CHARACTERS.store(amount_of_context, Ordering::Relaxed);
}
pub fn get_default_context_characters() -> usize {
CONTEXT_CHARACTERS.load(Ordering::Relaxed)
}
const SEPARATOR: &str = " | ";
const ELLIPSE: &str = "...";
#[derive(Debug)]
pub struct SerdeError {
input: String,
message: String,
line: Option<usize>,
column: Option<usize>,
contextualize: bool,
context_lines: usize,
context_characters: usize,
}
#[derive(Debug)]
pub enum ErrorTypes {
#[cfg(feature = "serde_json")]
Json(serde_json::Error),
#[cfg(feature = "serde_yaml")]
Yaml(serde_yaml::Error),
Custom {
error: Box<dyn std::error::Error>,
line: Option<usize>,
column: Option<usize>,
},
}
impl std::error::Error for SerdeError {}
impl fmt::Display for SerdeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.format(f)
}
}
#[cfg(feature = "serde_json")]
impl From<serde_json::Error> for ErrorTypes {
fn from(err: serde_json::Error) -> Self {
Self::Json(err)
}
}
#[cfg(feature = "serde_yaml")]
impl From<serde_yaml::Error> for ErrorTypes {
fn from(err: serde_yaml::Error) -> Self {
Self::Yaml(err)
}
}
impl From<(Box<dyn std::error::Error>, Option<usize>, Option<usize>)> for ErrorTypes {
fn from(value: (Box<dyn std::error::Error>, Option<usize>, Option<usize>)) -> Self {
Self::Custom {
error: value.0,
line: value.1,
column: value.2,
}
}
}
impl SerdeError {
pub fn new(input: String, err: impl Into<ErrorTypes>) -> SerdeError {
let error = err.into();
let (message, line, column) = match error {
#[cfg(feature = "serde_json")]
ErrorTypes::Json(e) => (e.to_string(), Some(e.line()), Some(e.column())),
#[cfg(feature = "serde_yaml")]
ErrorTypes::Yaml(e) => match e.location() {
None => (e.to_string(), None, None),
Some(location) => (
e.to_string(),
Some(location.line()),
Some(location.column() - 1),
),
},
ErrorTypes::Custom {
error,
line,
column,
} => (error.to_string(), line, column),
};
Self {
input,
message,
line,
column,
contextualize: CONTEXTUALIZE.load(Ordering::Relaxed),
context_lines: CONTEXT_LINES.load(Ordering::Relaxed),
context_characters: CONTEXT_CHARACTERS.load(Ordering::Relaxed),
}
}
pub fn set_contextualize(&mut self, should_contextualize: bool) -> &mut Self {
self.contextualize = should_contextualize;
self
}
#[must_use]
pub fn get_contextualize(&self) -> bool {
self.contextualize
}
pub fn set_context_lines(&mut self, amount_of_context: usize) -> &mut Self {
self.context_lines = amount_of_context;
self
}
#[must_use]
pub fn get_context_lines(&self) -> usize {
self.context_lines
}
pub fn set_context_characters(&mut self, amount_of_context: usize) -> &mut Self {
self.context_characters = amount_of_context;
self
}
#[must_use]
pub fn get_context_characters(&self) -> usize {
self.context_characters
}
fn format(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
if self.line.is_none() && self.column.is_none() {
#[cfg(feature = "colored")]
return writeln!(f, "{}", self.message.red().bold());
#[cfg(not(feature = "colored"))]
return writeln!(f, "{}", self.message);
}
let error_line = self.line.unwrap_or_default();
let error_column = self.column.unwrap_or_default();
let context_lines = self.context_lines;
let skip = usize::saturating_sub(error_line, context_lines + 1);
let take = context_lines * 2 + 1;
let minimized_input = self
.input
.lines()
.skip(skip)
.take(take)
.map(|line| line.replace("\t", " "))
.collect::<Vec<_>>();
if minimized_input.is_empty() {
#[cfg(feature = "colored")]
return writeln!(f, "{}", self.message.red().bold());
#[cfg(not(feature = "colored"))]
return writeln!(f, "{}", self.message);
}
let whitespace_count = minimized_input
.iter()
.map(|line| line.chars().take_while(|s| s.is_whitespace()).count())
.min()
.unwrap_or_default();
#[cfg(feature = "colored")]
let separator = SEPARATOR.blue().bold();
#[cfg(not(feature = "colored"))]
let separator = SEPARATOR;
let fill_line_position = format!("{: >fill$}", "", fill = error_line.to_string().len());
writeln!(f)?;
self.input
.lines()
.into_iter()
.enumerate()
.skip(skip)
.take(take)
.map(|(index, text)| {
(
index + 1,
text.chars()
.skip(whitespace_count)
.collect::<String>()
.replace("\t", " "),
)
})
.try_for_each(|(line_position, text)| {
self.format_line(
f,
line_position,
error_line,
error_column,
text,
whitespace_count,
&separator,
&fill_line_position,
)
})?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn format_line(
&self,
f: &mut fmt::Formatter<'_>,
line_position: usize,
error_line: usize,
error_column: usize,
text: String,
whitespace_count: usize,
#[cfg(feature = "colored")] separator: &colored::ColoredString,
#[cfg(not(feature = "colored"))] separator: &str,
fill_line_position: &str,
) -> Result<(), std::fmt::Error> {
if line_position == error_line {
let long_line_threshold = self.context_characters * 2 + 1;
let long_line_threshold = long_line_threshold < text.len();
let (context_line, new_error_column, context_before, context_after) =
if self.contextualize && long_line_threshold {
let context_characters = self.context_characters;
Self::context_long_line(&text, error_column, context_characters)
} else {
(text, error_column, false, false)
};
Self::format_error_line(
f,
&context_line,
line_position,
separator,
context_before,
context_after,
)?;
self.format_error_information(
f,
whitespace_count,
separator,
fill_line_position,
new_error_column,
context_before,
)
} else if self.contextualize {
Self::format_context_line(f, &text, separator, fill_line_position)
} else {
Ok(())
}
}
fn format_error_line(
f: &mut fmt::Formatter<'_>,
text: &str,
line_position: usize,
#[cfg(feature = "colored")] separator: &colored::ColoredString,
#[cfg(not(feature = "colored"))] separator: &str,
context_before: bool,
context_after: bool,
) -> Result<(), std::fmt::Error> {
#[cfg(feature = "colored")]
let line_pos = line_position.to_string().blue().bold();
#[cfg(not(feature = "colored"))]
let line_pos = line_position;
write!(f, " {}{}", line_pos, separator)?;
if context_before {
#[cfg(feature = "colored")]
write!(f, "{}", (ELLIPSE.blue().bold()))?;
#[cfg(not(feature = "colored"))]
write!(f, "{}", ELLIPSE)?;
}
write!(f, "{}", text)?;
if context_after {
#[cfg(feature = "colored")]
write!(f, "{}", (ELLIPSE.blue().bold()))?;
#[cfg(not(feature = "colored"))]
write!(f, "{}", ELLIPSE)?;
}
writeln!(f)
}
fn format_error_information(
&self,
f: &mut fmt::Formatter<'_>,
whitespace_count: usize,
#[cfg(feature = "colored")] separator: &colored::ColoredString,
#[cfg(not(feature = "colored"))] separator: &str,
fill_line_position: &str,
error_column: usize,
context_before: bool,
) -> Result<(), std::fmt::Error> {
let ellipse_space = if context_before { ELLIPSE.len() } else { 0 };
let fill_column_position = format!(
"{: >column$}^ {}",
"",
self.message,
column = error_column - whitespace_count + ellipse_space
);
#[cfg(feature = "colored")]
let fill_column_position = fill_column_position.red().bold();
writeln!(
f,
" {}{}{}",
fill_line_position, separator, fill_column_position,
)
}
fn format_context_line(
f: &mut fmt::Formatter<'_>,
text: &str,
#[cfg(feature = "colored")] separator: &colored::ColoredString,
#[cfg(not(feature = "colored"))] separator: &str,
fill_line_position: &str,
) -> Result<(), std::fmt::Error> {
#[cfg(feature = "colored")]
return writeln!(f, " {}{}{}", fill_line_position, separator, text.yellow());
#[cfg(not(feature = "colored"))]
return writeln!(f, " {}{}{}", fill_line_position, separator, text);
}
fn context_long_line(
text: &str,
error_column: usize,
context_chars: usize,
) -> (String, usize, bool, bool) {
#[cfg(feature = "graphemes_support")]
use unicode_segmentation::UnicodeSegmentation;
#[cfg(feature = "graphemes_support")]
let input = text.graphemes(true).collect::<Vec<_>>();
#[cfg(not(feature = "graphemes_support"))]
let input = text.chars().collect::<Vec<_>>();
let skip = usize::saturating_sub(error_column, context_chars + 1);
let take = context_chars * 2 + 1;
let context_before = skip != 0;
let context_after = skip + take < input.len();
let minimized_input = input.into_iter().skip(skip).take(take).collect();
let new_error_column = usize::saturating_sub(error_column, skip);
(
minimized_input,
new_error_column,
context_before,
context_after,
)
}
}