use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub struct JmespathError {
pub offset: usize,
pub expression: String,
pub reason: ErrorReason,
}
impl JmespathError {
pub fn new(expression: &str, offset: usize, reason: ErrorReason) -> Self {
Self {
offset,
expression: expression.to_owned(),
reason,
}
}
pub fn from_ctx(ctx: &crate::Context<'_>, reason: ErrorReason) -> Self {
Self {
offset: ctx.offset,
expression: ctx.expression.to_owned(),
reason,
}
}
pub fn line(&self) -> usize {
self.expression[..self.offset.min(self.expression.len())]
.chars()
.filter(|c| *c == '\n')
.count()
+ 1
}
pub fn column(&self) -> usize {
let before = &self.expression[..self.offset.min(self.expression.len())];
match before.rfind('\n') {
Some(pos) => self.offset - pos - 1,
None => self.offset,
}
}
}
impl fmt::Display for JmespathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let col = self.column();
write!(
f,
"{}\n{}\n{}",
self.reason,
self.expression,
" ".repeat(col)
)?;
write!(f, "^")
}
}
impl std::error::Error for JmespathError {}
#[derive(Debug, Clone, PartialEq)]
pub enum ErrorReason {
Parse(String),
Runtime(RuntimeError),
}
impl fmt::Display for ErrorReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErrorReason::Parse(msg) => write!(f, "Parse error: {msg}"),
ErrorReason::Runtime(err) => write!(f, "Runtime error: {err}"),
}
}
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum RuntimeError {
#[error("Invalid slice: step cannot be 0")]
InvalidSlice,
#[error("Too many arguments: expected {expected}, got {actual}")]
TooManyArguments { expected: usize, actual: usize },
#[error("Not enough arguments: expected {expected}, got {actual}")]
NotEnoughArguments { expected: usize, actual: usize },
#[error("Unknown function: {0}")]
UnknownFunction(String),
#[error("Invalid type at position {position}: expected {expected}, got {actual}")]
InvalidType {
expected: String,
actual: String,
position: usize,
},
#[error(
"Invalid return type at position {position}, invocation {invocation}: expected {expected}, got {actual}"
)]
InvalidReturnType {
expected: String,
actual: String,
position: usize,
invocation: usize,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn line_single_line_expression() {
let err = JmespathError::new("foo.bar", 4, ErrorReason::Parse("test".into()));
assert_eq!(err.line(), 1);
}
#[test]
fn column_single_line_expression() {
let err = JmespathError::new("foo.bar", 4, ErrorReason::Parse("test".into()));
assert_eq!(err.column(), 4);
}
#[test]
fn line_multi_line_expression() {
let err = JmespathError::new("foo\nbar\nbaz", 8, ErrorReason::Parse("test".into()));
assert_eq!(err.line(), 3);
}
#[test]
fn column_multi_line_expression() {
let err = JmespathError::new("foo\nbar\nbaz", 8, ErrorReason::Parse("test".into()));
assert_eq!(err.column(), 0);
}
#[test]
fn column_mid_second_line() {
let err = JmespathError::new("foo\nbar.baz", 6, ErrorReason::Parse("test".into()));
assert_eq!(err.line(), 2);
assert_eq!(err.column(), 2);
}
#[test]
fn offset_beyond_expression_length() {
let err = JmespathError::new("foo", 100, ErrorReason::Parse("test".into()));
assert_eq!(err.line(), 1);
assert_eq!(err.column(), 100);
}
#[test]
fn display_format_parse_error() {
let err = JmespathError::new("foo.bar", 4, ErrorReason::Parse("bad token".into()));
let display = format!("{err}");
assert!(display.contains("Parse error: bad token"));
assert!(display.contains("foo.bar"));
assert!(display.contains("^"));
}
#[test]
fn display_format_runtime_error() {
let err = JmespathError::new(
"foo()",
0,
ErrorReason::Runtime(RuntimeError::UnknownFunction("foo".into())),
);
let display = format!("{err}");
assert!(display.contains("Runtime error"));
assert!(display.contains("Unknown function: foo"));
}
#[test]
fn runtime_error_invalid_slice_display() {
let err = RuntimeError::InvalidSlice;
assert_eq!(format!("{err}"), "Invalid slice: step cannot be 0");
}
#[test]
fn runtime_error_too_many_args_display() {
let err = RuntimeError::TooManyArguments {
expected: 2,
actual: 5,
};
assert_eq!(format!("{err}"), "Too many arguments: expected 2, got 5");
}
#[test]
fn runtime_error_not_enough_args_display() {
let err = RuntimeError::NotEnoughArguments {
expected: 3,
actual: 1,
};
assert_eq!(format!("{err}"), "Not enough arguments: expected 3, got 1");
}
#[test]
fn runtime_error_invalid_type_display() {
let err = RuntimeError::InvalidType {
expected: "string".into(),
actual: "number".into(),
position: 0,
};
let display = format!("{err}");
assert!(display.contains("expected string"));
assert!(display.contains("got number"));
}
#[test]
fn runtime_error_invalid_return_type_display() {
let err = RuntimeError::InvalidReturnType {
expected: "number".into(),
actual: "string".into(),
position: 1,
invocation: 2,
};
let display = format!("{err}");
assert!(display.contains("expected number"));
assert!(display.contains("got string"));
assert!(display.contains("position 1"));
assert!(display.contains("invocation 2"));
}
#[test]
fn error_reason_parse_display() {
let reason = ErrorReason::Parse("unexpected token".into());
assert_eq!(format!("{reason}"), "Parse error: unexpected token");
}
#[test]
fn error_reason_runtime_display() {
let reason = ErrorReason::Runtime(RuntimeError::InvalidSlice);
assert!(format!("{reason}").contains("Invalid slice"));
}
#[test]
fn from_ctx_uses_context_offset() {
let runtime = crate::Runtime::new();
let mut ctx = crate::Context::new("test_expr", &runtime);
ctx.offset = 5;
let err = JmespathError::from_ctx(&ctx, ErrorReason::Parse("test".into()));
assert_eq!(err.offset, 5);
assert_eq!(err.expression, "test_expr");
}
#[test]
fn jmespath_error_implements_std_error() {
let err = JmespathError::new("foo", 0, ErrorReason::Parse("test".into()));
let _: &dyn std::error::Error = &err;
}
#[test]
fn jmespath_error_clone_and_eq() {
let err = JmespathError::new("foo", 0, ErrorReason::Parse("test".into()));
let cloned = err.clone();
assert_eq!(err, cloned);
}
}