use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ExprError {
Syntax {
message: String,
position: Option<usize>,
},
UnknownFunction {
name: String,
},
Arity {
name: String,
min: usize,
max: Option<usize>,
given: usize,
},
Type {
message: String,
},
DivisionByZero,
Call {
message: String,
},
}
impl ExprError {
pub(crate) fn syntax_at(message: impl Into<String>, position: usize) -> Self {
ExprError::Syntax {
message: message.into(),
position: Some(position),
}
}
pub(crate) fn type_error(message: impl Into<String>) -> Self {
ExprError::Type {
message: message.into(),
}
}
pub fn call(message: impl Into<String>) -> Self {
ExprError::Call {
message: message.into(),
}
}
pub fn is_compile_error(&self) -> bool {
matches!(
self,
ExprError::Syntax { .. } | ExprError::UnknownFunction { .. } | ExprError::Arity { .. }
)
}
}
impl fmt::Display for ExprError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ExprError::Syntax {
message,
position: Some(p),
} => write!(f, "syntax error at {p}: {message}"),
ExprError::Syntax {
message,
position: None,
} => write!(f, "syntax error: {message}"),
ExprError::UnknownFunction { name } => write!(f, "unknown function '{name}'"),
ExprError::Arity { name, min, max, given } => match max {
Some(max) if max == min => write!(f, "{name}() takes {min} argument(s), got {given}"),
Some(max) => write!(f, "{name}() takes {min} to {max} arguments, got {given}"),
None => write!(f, "{name}() takes at least {min} argument(s), got {given}"),
},
ExprError::Type { message } => write!(f, "type error: {message}"),
ExprError::DivisionByZero => write!(f, "division by zero"),
ExprError::Call { message } => write!(f, "{message}"),
}
}
}
impl std::error::Error for ExprError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_and_classification() {
assert_eq!(
ExprError::syntax_at("expected ')'", 7).to_string(),
"syntax error at 7: expected ')'"
);
assert_eq!(
ExprError::UnknownFunction { name: "nope".into() }.to_string(),
"unknown function 'nope'"
);
let a = ExprError::Arity {
name: "round".into(),
min: 1,
max: Some(2),
given: 3,
};
assert_eq!(a.to_string(), "round() takes 1 to 2 arguments, got 3");
let v = ExprError::Arity {
name: "concat".into(),
min: 1,
max: None,
given: 0,
};
assert_eq!(v.to_string(), "concat() takes at least 1 argument(s), got 0");
let x = ExprError::Arity {
name: "abs".into(),
min: 1,
max: Some(1),
given: 2,
};
assert_eq!(x.to_string(), "abs() takes 1 argument(s), got 2");
assert!(a.is_compile_error() && !ExprError::DivisionByZero.is_compile_error());
assert_eq!(
ExprError::call("upper: expected a string").to_string(),
"upper: expected a string"
);
assert_eq!(
ExprError::type_error("'-': operands must be numbers").to_string(),
"type error: '-': operands must be numbers"
);
}
}