jpx-core 0.2.2

Complete JMESPath implementation with 400+ extension functions
Documentation
//! Error types for jpx-core.

use std::fmt;

/// A JMESPath error with position information.
#[derive(Debug, Clone, PartialEq)]
pub struct JmespathError {
    /// Character offset in the expression where the error occurred.
    pub offset: usize,
    /// The expression that caused the error.
    pub expression: String,
    /// The reason for the error.
    pub reason: ErrorReason,
}

impl JmespathError {
    /// Creates a new error.
    pub fn new(expression: &str, offset: usize, reason: ErrorReason) -> Self {
        Self {
            offset,
            expression: expression.to_owned(),
            reason,
        }
    }

    /// Creates an error from a Context, using its current offset and expression.
    pub fn from_ctx(ctx: &crate::Context<'_>, reason: ErrorReason) -> Self {
        Self {
            offset: ctx.offset,
            expression: ctx.expression.to_owned(),
            reason,
        }
    }

    /// Returns the line number of the error (1-indexed).
    pub fn line(&self) -> usize {
        self.expression[..self.offset.min(self.expression.len())]
            .chars()
            .filter(|c| *c == '\n')
            .count()
            + 1
    }

    /// Returns the column number of the error (0-indexed).
    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 {}

/// The reason for a JMESPath error.
#[derive(Debug, Clone, PartialEq)]
pub enum ErrorReason {
    /// A parse-time error.
    Parse(String),
    /// A runtime error.
    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}"),
        }
    }
}

/// Runtime errors that can occur during expression evaluation.
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum RuntimeError {
    /// A slice expression with step of 0.
    #[error("Invalid slice: step cannot be 0")]
    InvalidSlice,
    /// Too many arguments provided to a function.
    #[error("Too many arguments: expected {expected}, got {actual}")]
    TooManyArguments { expected: usize, actual: usize },
    /// Not enough arguments provided to a function.
    #[error("Not enough arguments: expected {expected}, got {actual}")]
    NotEnoughArguments { expected: usize, actual: usize },
    /// An unknown function was called.
    #[error("Unknown function: {0}")]
    UnknownFunction(String),
    /// Invalid type provided to a function.
    #[error("Invalid type at position {position}: expected {expected}, got {actual}")]
    InvalidType {
        expected: String,
        actual: String,
        position: usize,
    },
    /// Invalid return type from an expression reference.
    #[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()));
        // line() clamps to expression length, so still line 1
        assert_eq!(err.line(), 1);
        // column() returns the raw offset when no newline is found
        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);
    }
}