use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kind {
Lex,
Parse,
Unsupported,
Expansion,
Refused,
}
impl Kind {
fn banner(self) -> &'static str {
match self {
Kind::Lex => "lex error",
Kind::Parse => "parse error",
Kind::Unsupported => "not yet supported",
Kind::Expansion => "expansion error",
Kind::Refused => "refused",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QuarbError {
kind: Kind,
message: String,
hint: Option<String>,
at: Option<usize>,
source: Option<Box<QuarbError>>,
diag: Option<Box<DiagInfo>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
struct DiagInfo {
code: Option<String>,
site: Option<String>,
}
impl QuarbError {
fn new(kind: Kind, message: String) -> QuarbError {
QuarbError {
kind,
message,
hint: None,
at: None,
source: None,
diag: None,
}
}
pub fn with_code(mut self, code: impl Into<String>) -> QuarbError {
self.diag.get_or_insert_with(Default::default).code = Some(code.into());
self
}
pub fn with_site(mut self, site: impl Into<String>) -> QuarbError {
self.diag.get_or_insert_with(Default::default).site = Some(site.into());
self
}
pub fn code(&self) -> &str {
match self.diag.as_ref().and_then(|d| d.code.as_deref()) {
Some(c) => c,
None => match self.kind {
Kind::Lex | Kind::Parse => crate::diagnostic::SYNTAX,
Kind::Unsupported => "UNSUPPORTED",
Kind::Expansion => "EXPANSION",
Kind::Refused => crate::diagnostic::REFUSED,
},
}
}
pub fn site(&self) -> &str {
self.diag
.as_ref()
.and_then(|d| d.site.as_deref())
.unwrap_or("query")
}
pub fn diagnostic(&self) -> crate::diagnostic::Diagnostic {
crate::diagnostic::Diagnostic::error(self.code(), self.site(), self.to_string())
}
pub fn lex(message: impl Into<String>) -> QuarbError {
QuarbError::new(Kind::Lex, message.into())
}
pub fn parse(message: impl Into<String>) -> QuarbError {
QuarbError::new(Kind::Parse, message.into())
}
pub fn unsupported(message: impl Into<String>) -> QuarbError {
QuarbError::new(Kind::Unsupported, message.into())
}
pub fn expansion(message: impl Into<String>) -> QuarbError {
QuarbError::new(Kind::Expansion, message.into())
}
pub fn refused(message: impl Into<String>) -> QuarbError {
QuarbError::new(Kind::Refused, message.into())
}
pub fn with_hint(mut self, hint: impl Into<String>) -> QuarbError {
self.hint = Some(hint.into());
self
}
pub fn at(mut self, offset: usize) -> QuarbError {
self.at = Some(offset);
self
}
pub fn wrapping(mut self, inner: QuarbError) -> QuarbError {
self.source = Some(Box::new(inner));
self
}
pub fn kind(&self) -> Kind {
self.kind
}
pub fn message(&self) -> &str {
&self.message
}
pub fn hint(&self) -> Option<&str> {
self.hint.as_deref()
}
pub fn offset(&self) -> Option<usize> {
self.at
}
pub fn inner(&self) -> Option<&QuarbError> {
self.source.as_deref()
}
}
impl fmt::Display for QuarbError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.kind.banner(), self.message)?;
if let Some(hint) = &self.hint {
write!(f, "{hint}")?;
}
if let Some(inner) = &self.source {
write!(f, ": {inner}")?;
}
if let Some(at) = self.at {
write!(f, " (at character {})", at + 1)?;
}
Ok(())
}
}
impl std::error::Error for QuarbError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
pub type Result<T> = std::result::Result<T, QuarbError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_reproduces_the_banners() {
assert_eq!(QuarbError::parse("x").to_string(), "parse error: x");
assert_eq!(
QuarbError::parse("x").at(4).to_string(),
"parse error: x (at character 5)"
);
assert_eq!(
QuarbError::unsupported("pipeline command 'f'")
.with_hint(" ('f' uses '@|')")
.to_string(),
"not yet supported: pipeline command 'f' ('f' uses '@|')"
);
let e =
QuarbError::expansion("in expansion of '&m' ('/a')").wrapping(QuarbError::lex("bad"));
assert_eq!(
e.to_string(),
"expansion error: in expansion of '&m' ('/a'): lex error: bad"
);
assert_eq!(e.inner().map(|i| i.kind()), Some(Kind::Lex));
assert_eq!(QuarbError::refused("r").to_string(), "refused: r");
}
}