navy-nvim-rs 0.0.27

A library for writing neovim rpc clients
//! Custom error payloads stored inside [`std::io::Error`].

use std::{
    error::Error,
    fmt::{self, Display},
    io,
};

use oneshot::error::RecvError;
use rmp::decode::{
    DecodeStringError, MarkerReadError, NumValueReadError, ValueReadError, bytes::BytesReadError,
};
use rmpv::Value;
use tokio::sync::oneshot;

#[derive(Debug)]
pub struct NeovimError {
    pub method: &'static str,
    pub error_type: Option<i64>,
    pub message: Value,
}

impl NeovimError {
    pub fn new(method: &'static str, error: Value) -> Self {
        let (error_type, message) = match error {
            Value::Array(mut values) => (
                values.first().and_then(Value::as_i64),
                if values.len() >= 2 {
                    values.swap_remove(1)
                } else {
                    Value::Nil
                },
            ),
            v => (None, v),
        };
        Self {
            method,
            error_type,
            message,
        }
    }
}

impl Display for NeovimError {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            fmt,
            "Neovim returned error for request {:?}: {} (type={:?})",
            self.method, self.message, self.error_type,
        )
    }
}

impl Error for NeovimError {}

#[derive(Debug)]
pub struct ResponseReceiveError {
    pub method: &'static str,
    pub error: RecvError,
}

impl ResponseReceiveError {
    pub fn new(method: &'static str, error: RecvError) -> Self {
        Self { method, error }
    }
}

impl Display for ResponseReceiveError {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            fmt,
            "Could not receive response for request '{}': {}",
            self.method, self.error
        )
    }
}

impl Error for ResponseReceiveError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        Some(&self.error)
    }
}

#[derive(Debug)]
pub struct TypeError {
    pub method: &'static str,
    pub expected: &'static str,
    pub actual: Value,
}

impl TypeError {
    pub fn new(method: &'static str, expected: &'static str, actual: Value) -> Self {
        Self {
            method,
            expected,
            actual,
        }
    }
}

impl Display for TypeError {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            fmt,
            "Wrong type of return value for request '{}': expected {}, got {}",
            self.method, self.expected, self.actual
        )
    }
}

impl Error for TypeError {}

#[derive(Debug)]
pub struct MessageIdNotFound {
    pub msgid: u32,
}

impl MessageIdNotFound {
    pub fn new(msgid: u32) -> Self {
        Self { msgid }
    }
}

impl Display for MessageIdNotFound {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(fmt, "Unknown message ID {}", self.msgid)
    }
}

impl Error for MessageIdNotFound {}

pub(crate) trait IntoIoError {
    fn into_io(self) -> io::Error;
}

impl IntoIoError for BytesReadError {
    #[inline]
    fn into_io(self) -> io::Error {
        match self {
            BytesReadError::InsufficientBytes { .. } => io::ErrorKind::UnexpectedEof.into(),
            _ => io::ErrorKind::InvalidData.into(),
        }
    }
}

impl IntoIoError for MarkerReadError<BytesReadError> {
    #[inline]
    fn into_io(self) -> io::Error {
        self.0.into_io()
    }
}

impl IntoIoError for ValueReadError<BytesReadError> {
    #[inline]
    fn into_io(self) -> io::Error {
        match self {
            ValueReadError::InvalidMarkerRead(err) | ValueReadError::InvalidDataRead(err) => {
                err.into_io()
            }
            ValueReadError::TypeMismatch(_) => io::ErrorKind::InvalidData.into(),
        }
    }
}

impl IntoIoError for NumValueReadError<BytesReadError> {
    #[inline]
    fn into_io(self) -> io::Error {
        match self {
            NumValueReadError::InvalidMarkerRead(err) | NumValueReadError::InvalidDataRead(err) => {
                err.into_io()
            }
            NumValueReadError::TypeMismatch(_) | NumValueReadError::OutOfRange => {
                io::ErrorKind::InvalidData.into()
            }
        }
    }
}

impl IntoIoError for DecodeStringError<'_, BytesReadError> {
    #[inline]
    fn into_io(self) -> io::Error {
        match self {
            DecodeStringError::InvalidMarkerRead(err) | DecodeStringError::InvalidDataRead(err) => {
                err.into_io()
            }
            DecodeStringError::BufferSizeTooSmall(_) => io::ErrorKind::UnexpectedEof.into(),
            DecodeStringError::TypeMismatch(_) | DecodeStringError::InvalidUtf8(_, _) => {
                io::ErrorKind::InvalidData.into()
            }
        }
    }
}

impl IntoIoError for rmpv::decode::Error {
    #[inline]
    fn into_io(self) -> io::Error {
        match self {
            Self::InvalidMarkerRead(err) | Self::InvalidDataRead(err) => err,
            Self::DepthLimitExceeded => io::ErrorKind::InvalidData.into(),
        }
    }
}

pub(crate) trait IntoIoResult<T> {
    fn into_io(self) -> io::Result<T>;
}

impl<T, E: IntoIoError> IntoIoResult<T> for Result<T, E> {
    #[inline]
    fn into_io(self) -> io::Result<T> {
        self.map_err(E::into_io)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn insufficient_bytes() -> BytesReadError {
        BytesReadError::InsufficientBytes {
            expected: 1,
            actual: 0,
            position: 0,
        }
    }

    #[test]
    fn bytes_read_error_converts_to_unexpected_eof() {
        assert_eq!(
            insufficient_bytes().into_io().kind(),
            io::ErrorKind::UnexpectedEof
        );
    }

    #[test]
    fn marker_read_error_converts_inner_error() {
        let error = MarkerReadError(insufficient_bytes()).into_io();

        assert_eq!(error.kind(), io::ErrorKind::UnexpectedEof);
    }

    #[test]
    fn value_read_error_converts_read_and_type_errors() {
        let error = ValueReadError::InvalidMarkerRead(insufficient_bytes()).into_io();
        assert_eq!(error.kind(), io::ErrorKind::UnexpectedEof);

        let error = ValueReadError::InvalidDataRead(insufficient_bytes()).into_io();
        assert_eq!(error.kind(), io::ErrorKind::UnexpectedEof);

        let error: ValueReadError<BytesReadError> = ValueReadError::TypeMismatch(rmp::Marker::Null);
        assert_eq!(error.into_io().kind(), io::ErrorKind::InvalidData);
    }

    #[test]
    fn num_value_read_error_converts_read_type_and_range_errors() {
        let error = NumValueReadError::InvalidMarkerRead(insufficient_bytes()).into_io();
        assert_eq!(error.kind(), io::ErrorKind::UnexpectedEof);

        let error = NumValueReadError::InvalidDataRead(insufficient_bytes()).into_io();
        assert_eq!(error.kind(), io::ErrorKind::UnexpectedEof);

        let error: NumValueReadError<BytesReadError> =
            NumValueReadError::TypeMismatch(rmp::Marker::Null);
        assert_eq!(error.into_io().kind(), io::ErrorKind::InvalidData);

        let error: NumValueReadError<BytesReadError> = NumValueReadError::OutOfRange;
        assert_eq!(error.into_io().kind(), io::ErrorKind::InvalidData);
    }

    #[test]
    fn decode_string_error_converts_read_buffer_type_and_utf8_errors() {
        let error = DecodeStringError::InvalidMarkerRead(insufficient_bytes()).into_io();
        assert_eq!(error.kind(), io::ErrorKind::UnexpectedEof);

        let error = DecodeStringError::InvalidDataRead(insufficient_bytes()).into_io();
        assert_eq!(error.kind(), io::ErrorKind::UnexpectedEof);

        let error: DecodeStringError<'_, BytesReadError> = DecodeStringError::BufferSizeTooSmall(1);
        assert_eq!(error.into_io().kind(), io::ErrorKind::UnexpectedEof);

        let error: DecodeStringError<'_, BytesReadError> =
            DecodeStringError::TypeMismatch(rmp::Marker::Null);
        assert_eq!(error.into_io().kind(), io::ErrorKind::InvalidData);

        let mut invalid = [0];
        invalid[0] = 0xff;
        let utf8_error = str::from_utf8(&invalid).unwrap_err();
        let error: DecodeStringError<'_, BytesReadError> =
            DecodeStringError::InvalidUtf8(&invalid, utf8_error);
        assert_eq!(error.into_io().kind(), io::ErrorKind::InvalidData);
    }

    #[test]
    fn rmpv_decode_error_converts_read_and_depth_errors() {
        let error =
            rmpv::decode::Error::InvalidMarkerRead(io::ErrorKind::UnexpectedEof.into()).into_io();
        assert_eq!(error.kind(), io::ErrorKind::UnexpectedEof);

        let error =
            rmpv::decode::Error::InvalidDataRead(io::ErrorKind::PermissionDenied.into()).into_io();
        assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);

        let error = rmpv::decode::Error::DepthLimitExceeded.into_io();
        assert_eq!(error.kind(), io::ErrorKind::InvalidData);
    }

    #[test]
    fn result_converts_to_io_result() {
        let result: Result<u32, BytesReadError> = Ok(42);
        assert_eq!(result.into_io().unwrap(), 42);

        let result: Result<u32, BytesReadError> = Err(insufficient_bytes());
        assert_eq!(
            result.into_io().unwrap_err().kind(),
            io::ErrorKind::UnexpectedEof
        );
    }

    #[test]
    fn neovim_error_extracts_type_and_message_from_error_array() {
        let err = NeovimError::new(
            "nvim_cmd",
            Value::Array(vec![Value::from(1), Value::from("failed")]),
        );

        assert_eq!(err.method, "nvim_cmd");
        assert_eq!(err.error_type, Some(1));
        assert_eq!(err.message, Value::from("failed"));
    }

    #[test]
    fn neovim_error_uses_string_value_as_message_without_error_type() {
        let err = NeovimError::new("nvim_cmd", Value::from("failed"));

        assert_eq!(err.method, "nvim_cmd");
        assert_eq!(err.error_type, None);
        assert_eq!(err.message, Value::from("failed"));
    }

    #[test]
    fn neovim_error_uses_nil_message_when_message_is_missing() {
        let err = NeovimError::new("nvim_cmd", Value::Array(vec![Value::from(1)]));

        assert_eq!(err.method, "nvim_cmd");
        assert_eq!(err.error_type, Some(1));
        assert_eq!(err.message, Value::Nil);
    }

    #[tokio::test]
    async fn response_receive_error_retains_method_and_source() {
        let (sender, receiver) = oneshot::channel::<()>();
        drop(sender);
        let source = receiver.await.unwrap_err();

        let err = ResponseReceiveError::new("nvim_input", source);

        assert_eq!(err.method, "nvim_input");
        assert!(err.source().is_some());
    }

    #[test]
    fn type_error_retains_expected_type_and_actual_value() {
        let err = TypeError::new("nvim_get_api_info", "array", Value::Nil);

        assert_eq!(err.method, "nvim_get_api_info");
        assert_eq!(err.expected, "array");
        assert_eq!(err.actual, Value::Nil);
    }
}