navy-nvim-rs 0.0.18

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

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

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: oneshot::error::RecvError,
}

impl ResponseReceiveError {
    pub fn new(method: &'static str, error: oneshot::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 {}

#[derive(Debug)]
pub struct ResponseSendError {
    pub msgid: u32,
    pub response: Result<Value, Value>,
}

impl ResponseSendError {
    pub fn new(msgid: u32, response: Result<Value, Value>) -> Self {
        Self { msgid, response }
    }
}

impl Display for ResponseSendError {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            fmt,
            "Could not send response for message ID {}: {:?}",
            self.msgid, self.response
        )
    }
}

impl Error for ResponseSendError {}

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

    #[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);
    }

    #[test]
    fn response_send_error_retains_response() {
        let err = ResponseSendError::new(7, Err(Value::from("failed")));

        assert_eq!(err.msgid, 7);
        assert_eq!(err.response, Err(Value::from("failed")));
    }
}