use console::{style, StyledObject};
use once_cell::sync::Lazy;
use regex::Regex;
use std::{
any::type_name,
borrow::Cow,
collections::VecDeque,
fmt::{self, Debug},
io,
ops::Range,
};
#[derive(Clone, Debug)]
pub(crate) enum InptContext<'s> {
Within(&'s str),
InptType(&'static str),
FromStrType(&'static str),
Regex(&'static str),
Literal(&'static dyn Debug),
RegexGroup(usize),
Message(String),
Recursion(&'static str),
AtStart,
AtEnd,
}
impl<'s> InptContext<'s> {
fn within(&self) -> Option<&'s str> {
match self {
InptContext::Within(text) => Some(text),
_ => None,
}
}
}
#[derive(Clone, Debug)]
pub struct InptError<'s> {
pub(crate) context: Vec<InptContext<'s>>,
}
impl std::error::Error for InptError<'static> {}
impl<'s> InptError<'s> {
pub fn expected_regex(re: &'static str, found: &'s str) -> Self {
InptError {
context: vec![InptContext::Within(found), InptContext::Regex(re)],
}
}
pub fn expected_regex_at_start(re: &'static str) -> Self {
InptError {
context: vec![InptContext::AtStart, InptContext::Regex(re)],
}
}
pub fn expected_regex_at_end(re: &'static str) -> Self {
InptError {
context: vec![InptContext::AtEnd, InptContext::Regex(re)],
}
}
pub fn expected_lit(lit: &'static impl Debug, found: &'s str) -> Self {
InptError {
context: vec![InptContext::Within(found), InptContext::Literal(lit)],
}
}
pub fn expected_lit_at_start(lit: &'static impl Debug) -> Self {
InptError {
context: vec![InptContext::AtStart, InptContext::Literal(lit)],
}
}
pub fn expected_lit_at_end(lit: &'static impl Debug) -> Self {
InptError {
context: vec![InptContext::AtEnd, InptContext::Literal(lit)],
}
}
pub fn expected<T>(found: &'s str) -> Self {
InptError {
context: vec![
InptContext::Within(found),
InptContext::InptType(type_name::<T>()),
],
}
}
pub fn expected_at_start<T>() -> Self {
InptError {
context: vec![
InptContext::AtStart,
InptContext::InptType(type_name::<T>()),
],
}
}
pub fn expected_at_end<T>() -> Self {
InptError {
context: vec![InptContext::AtEnd, InptContext::InptType(type_name::<T>())],
}
}
pub fn expected_regex_group(index: usize) -> Self {
InptError {
context: vec![InptContext::AtEnd, InptContext::RegexGroup(index)],
}
}
pub fn expected_from_str<T>(found: &'s str) -> Self {
InptError {
context: vec![
InptContext::Within(found),
InptContext::FromStrType(type_name::<T>()),
],
}
}
pub fn recursion_at_start<T>() -> Self {
InptError {
context: vec![
InptContext::AtStart,
InptContext::Recursion(type_name::<T>()),
],
}
}
}
pub type InptResult<'s, T> = Result<T, InptError<'s>>;
fn context_outside<'s, T>(mut this: InptResult<'s, T>, ctx: InptContext<'s>) -> InptResult<'s, T> {
if let Err(error) = &mut this {
error.context.push(ctx);
}
this
}
fn context_inside<'s, T>(mut this: InptResult<'s, T>, ctx: InptContext<'s>) -> InptResult<'s, T> {
if let Err(error) = &mut this {
if let Some((idx, _)) = error.context.iter().enumerate().rfind(|(_, ctx)| {
matches!(
ctx,
InptContext::Within(_) | InptContext::AtStart | InptContext::AtEnd
)
}) {
error.context.insert(idx + 1, ctx);
} else {
error.context.push(ctx);
}
}
this
}
pub trait ResultExt<'s> {
fn within(self, text: &'s str) -> Self;
fn regex(self, re: &'static str) -> Self;
fn regex_group(self, group: usize) -> Self;
fn message_outside(self, msg: impl fmt::Display) -> Self;
fn message_inside(self, msg: impl fmt::Display) -> Self;
fn expected<T>(self) -> Self;
fn replace_expected<A, B>(self) -> Self;
fn at_start(self) -> Self;
fn at_end(self) -> Self;
}
impl<'s, R> ResultExt<'s> for InptResult<'s, R> {
fn within(self, text: &'s str) -> Self {
context_outside(self, InptContext::Within(text))
}
fn regex(self, re: &'static str) -> Self {
context_outside(self, InptContext::Regex(re))
}
fn regex_group(self, group: usize) -> Self {
context_outside(self, InptContext::RegexGroup(group))
}
fn message_outside(self, msg: impl fmt::Display) -> Self {
context_outside(self, InptContext::Message(msg.to_string()))
}
fn message_inside(self, msg: impl fmt::Display) -> Self {
context_inside(self, InptContext::Message(msg.to_string()))
}
fn expected<T>(self) -> Self {
context_outside(self, InptContext::InptType(type_name::<T>()))
}
fn replace_expected<A, B>(mut self) -> Self {
if let Err(error) = &mut self {
for ctx in error.context.iter_mut().rev() {
if let InptContext::InptType(ty) = ctx {
if *ty == type_name::<A>() {
*ty = type_name::<B>();
}
break;
}
}
}
self
}
fn at_start(self) -> Self {
context_outside(self, InptContext::AtStart)
}
fn at_end(self) -> Self {
context_outside(self, InptContext::AtEnd)
}
}
static SHORTEN_TYPES: Lazy<Regex> = Lazy::new(|| {
Regex::new("(std|alloc|core)::(vec|collections|boxed|sync|slice|rc|str|string)::").unwrap()
});
fn shorten_type(name: &str) -> Cow<str> {
SHORTEN_TYPES.replace_all(name, "")
}
impl<'s> InptError<'s> {
pub fn row_col(&self) -> Option<Range<(usize, usize)>> {
let inner = self.context.iter().find_map(|c| c.within())?;
let all = self.context.iter().rev().find_map(|c| c.within())?;
let begin_loc = inner.as_ptr() as usize - all.as_ptr() as usize;
let end_loc = begin_loc + inner.len();
let mut begin_row = 0;
let mut begin_col = 0;
let mut end_row = 0;
let mut end_col = 0;
for (i, c) in all.char_indices() {
if c == '\n' {
if i < begin_loc {
begin_col = 0;
begin_row += 1;
}
if i < end_loc {
end_col = 0;
end_row += 1;
}
} else {
if i < begin_loc {
begin_col += 1;
}
if i < end_loc {
end_col += 1;
}
}
}
Some((begin_row, begin_col)..(end_row, end_col))
}
pub fn unparsable_text(&self) -> Option<&'s str> {
self.context.iter().find_map(|c| c.within())
}
fn annotated_impl(&self) -> VecDeque<StyledObject<String>> {
let mut out = VecDeque::new();
let mut inner: Option<&str> = None;
let mut at_end = false;
let mut at_start = false;
let mut sty: fn(String) -> StyledObject<String> = |s| style(s).bold().red();
for ctx in &self.context {
match ctx {
InptContext::Within(outer) => {
let (split_loc, skip_len) = match (at_start, at_end, inner) {
(false, false, Some(inner)) => (
inner.as_ptr() as usize - outer.as_ptr() as usize,
inner.len(),
),
(false, false, None) => (0, 0),
(true, false, _) => (0, 0),
(false, true, _) => (outer.len(), 0),
_ => unreachable!(),
};
if split_loc != 0 {
out.push_front(style(outer[..split_loc].to_string()));
}
if split_loc + skip_len != outer.len() {
out.push_back(style(outer[split_loc + skip_len..].to_string()));
}
inner = Some(outer);
at_end = false;
at_start = false;
}
InptContext::InptType(ty) => {
let ty = shorten_type(ty);
out.push_front(sty(format!("<{ty}>")));
out.push_back(sty(format!("</{ty}>")));
sty = |s| style(s).bold().yellow();
}
InptContext::FromStrType(ty) => {
let ty = shorten_type(ty);
out.push_front(sty(format!("<{ty}::from_str>")));
out.push_back(sty(format!("</{ty}::from_str>")));
sty = |s| style(s).bold().yellow();
}
InptContext::Regex(reg) => {
out.push_front(sty(format!(" >")));
out.push_front(style(format!("/{reg}/")).blue().italic());
out.push_front(sty(format!("< ")));
out.push_back(sty(format!("</regex>")));
sty = |s| style(s).bold().yellow();
}
InptContext::Literal(lit) => {
out.push_front(sty(format!(" >")));
out.push_front(style(format!("{lit:?}")).blue().italic());
out.push_front(sty(format!("< ")));
out.push_back(sty(format!("</ {lit:?} >")));
sty = |s| style(s).bold().yellow();
}
InptContext::RegexGroup(idx) => {
out.push_front(sty(format!("<${idx}>")));
out.push_back(sty(format!("</${idx}>")));
sty = |s| style(s).bold().yellow();
}
InptContext::Message(msg) => {
out.push_front(sty(format!("<! {msg} >")));
}
InptContext::Recursion(ty) => {
out.push_front(sty(format!("<^ {ty} />")));
}
InptContext::AtStart if !at_end => at_start = true,
InptContext::AtEnd if !at_start => at_end = true,
_ => (),
}
}
out
}
pub fn annotated(
&self,
mut output: impl io::Write,
force_styling: Option<bool>,
) -> io::Result<()> {
for mut chunk in self.annotated_impl() {
if let Some(force_styling) = force_styling {
chunk = chunk.force_styling(force_styling);
}
write!(output, "{chunk}")?;
}
Ok(())
}
pub fn annotated_stdout(&self, source: &str) -> io::Result<()> {
use io::Write;
let mut stdout = console::Term::buffered_stdout();
if let Some((row, col)) = self.row_col().map(|range| range.start) {
writeln!(
stdout,
"{} in {}:{}:{}",
style("INPT ERROR").red().for_stdout(),
source,
row + 1,
col + 1,
)?;
}
for mut chunk in self.annotated_impl() {
chunk = chunk.for_stdout();
write!(stdout, "{chunk}")?;
}
write!(stdout, "\n")?;
stdout.flush()
}
pub fn annotated_stderr(&self, source: &str) -> io::Result<()> {
use io::Write;
let mut stderr = console::Term::buffered_stderr();
if let Some((row, col)) = self.row_col().map(|range| range.start) {
writeln!(
stderr,
"{} in {}:{}:{}",
style("INPT ERROR").red().for_stderr(),
source,
row + 1,
col + 1,
)?;
}
for mut chunk in self.annotated_impl() {
chunk = chunk.for_stderr();
write!(stderr, "{chunk}")?;
}
write!(stderr, "\n")?;
stderr.flush()
}
}
impl<'a> fmt::Display for InptError<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for mut chunk in self.annotated_impl() {
chunk = chunk.force_styling(false);
write!(f, "{chunk}")?;
}
Ok(())
}
}