use std::{borrow::Cow, fmt};
use annotate_snippets::{renderer::DecorStyle, AnnotationKind, Level, Renderer, Snippet};
use chumsky::prelude::*;
use derive_more::Display;
const NOT_POPULATED: &str = "** not yet populated **";
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct MetarError<'a> {
pub string: &'a str,
pub start: usize,
pub end: usize,
pub variant: ErrorVariant,
}
impl std::error::Error for MetarError<'_> {}
impl<'a> chumsky::error::Error<'a, &'a str> for MetarError<'a> {
fn merge(mut self, other: Self) -> Self {
if let (
ErrorVariant::ExpectedFound { expected, .. },
ErrorVariant::ExpectedFound {
expected: expected_other,
..
},
) = (&mut self.variant, &other.variant)
{
for item in expected_other {
if !expected.contains(item) {
expected.push(item.clone());
}
}
expected.sort();
}
self
}
}
impl<'a> chumsky::error::LabelError<'a, &'a str, chumsky::DefaultExpected<'a, char>>
for MetarError<'a>
{
fn expected_found<E: IntoIterator<Item = chumsky::DefaultExpected<'a, char>>>(
expected: E,
found: Option<chumsky::util::MaybeRef<'a, char>>,
span: SimpleSpan,
) -> Self {
MetarError {
string: NOT_POPULATED,
start: span.start,
end: span.end,
variant: ErrorVariant::ExpectedFound {
expected: expected
.into_iter()
.map(|i| match i {
chumsky::DefaultExpected::Token(t) => ExpectedNext::Literal {
value: (*t).to_string(),
},
chumsky::DefaultExpected::EndOfInput => ExpectedNext::EndOfInput,
_ => ExpectedNext::SomethingElse,
})
.collect(),
found: found.map(|inner| *inner),
},
}
}
}
impl<'a> chumsky::error::LabelError<'a, &'a str, chumsky::text::TextExpected<()>>
for MetarError<'a>
{
fn expected_found<E: IntoIterator<Item = chumsky::text::TextExpected<()>>>(
expected: E,
found: Option<chumsky::util::MaybeRef<'a, char>>,
span: SimpleSpan,
) -> Self {
MetarError {
string: NOT_POPULATED,
start: span.start,
end: span.end,
variant: ErrorVariant::ExpectedFound {
expected: expected
.into_iter()
.map(|i| match i {
chumsky::text::TextExpected::Digit(..) => ExpectedNext::Digits,
_ => unimplemented!(),
})
.collect(),
found: found.map(|inner| *inner),
},
}
}
}
impl fmt::Display for MetarError<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let report = &[Level::ERROR
.primary_title(self.variant.to_string())
.element(
Snippet::source(self.string).annotation(
AnnotationKind::Primary
.span(self.start..self.end)
.label(self.variant.help()),
),
)];
let renderer = Renderer::styled().decor_style(DecorStyle::Unicode);
writeln!(f, "{}", renderer.render(report))
}
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct OwnedMetarError {
pub string: String,
pub start: usize,
pub end: usize,
pub variant: ErrorVariant,
}
impl fmt::Display for OwnedMetarError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let report = &[Level::ERROR
.primary_title(self.variant.to_string())
.element(
Snippet::source(&self.string).annotation(
AnnotationKind::Primary
.span(self.start..self.end)
.label(self.variant.help()),
),
)];
let renderer = Renderer::styled().decor_style(DecorStyle::Unicode);
writeln!(f, "{}", renderer.render(report))
}
}
impl MetarError<'_> {
#[must_use]
pub fn into_owned(&self) -> OwnedMetarError {
OwnedMetarError {
string: self.string.to_string(),
start: self.start,
end: self.end,
variant: self.variant.clone(),
}
}
}
#[derive(PartialEq, Eq, Clone, Debug, Display, PartialOrd, Ord)]
pub enum ExpectedNext {
#[display("\"{value}\"")]
Literal { value: String },
#[display("a number")]
Digits,
#[display("something else")]
SomethingElse,
#[display("end of input")]
EndOfInput,
}
#[derive(PartialEq, Eq, Clone, Debug, Display)]
#[allow(missing_docs, reason = "self-documenting and with display strings")]
pub enum ErrorVariant {
#[display(
"expected one of: {}; {}",
expected.iter().map(ToString::to_string).collect::<Vec<_>>().join(", "),
if let Some(found) = found {
format!(r#"found "{found}""#)
} else {
"reached end of input".to_string()
}
)]
ExpectedFound {
expected: Vec<ExpectedNext>,
found: Option<char>,
},
#[display("invalid observation date")]
InvalidDate,
#[display("invalid observation hour")]
InvalidHour,
#[display("invalid observation minute")]
InvalidMinute,
#[display("invalid wind heading")]
InvalidWindHeading,
#[display("invalid runway number in RVR")]
InvalidRvrRunwayNumber,
#[display("invalid distance in RVR")]
InvalidRvrDistance,
}
impl ErrorVariant {
pub(crate) fn into_err(self, span: SimpleSpan) -> MetarError<'static> {
MetarError {
string: NOT_POPULATED,
start: span.start,
end: span.end,
variant: self,
}
}
fn help(&self) -> Cow<'_, str> {
match self {
Self::ExpectedFound { expected, .. } => Cow::Owned(format!(
"must be one of {}",
expected
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
)),
Self::InvalidDate => Cow::Borrowed(
"the observation date must be a two digit number less than or equal to 31 ",
),
Self::InvalidHour => {
Cow::Borrowed("the observation date must be a two digit number less than 24")
}
Self::InvalidMinute => {
Cow::Borrowed("the observation date must be a two digit number less than 60")
}
Self::InvalidWindHeading => {
Cow::Borrowed("the wind heading must be three digits between 000 and 360 inclusive")
}
Self::InvalidRvrRunwayNumber => Cow::Borrowed(
r#"the runway number must be between 00 and 36, and may be suffixed with "L", "C" or "R""#,
),
Self::InvalidRvrDistance => Cow::Borrowed("the RVR distance must be a 4 digit number"),
}
}
}