rconsole 1.1.0

A WebSocket-based logging library for Rust - send structured logs to NConsole desktop app
Documentation
//! WebSocket connection wrapper with proper error handling.
//!
//! Encapsulates `tungstenite` WebSocket to provide a clean interface
//! for the console layer, including auto-reconnect on send failure.

use std::net::TcpStream;

use tungstenite::stream::MaybeTlsStream;
use tungstenite::{connect, Message, WebSocket};

/// A thin wrapper around `tungstenite::WebSocket` providing
/// connection management and error recovery.
pub struct WsConnection {
    socket: WebSocket<MaybeTlsStream<TcpStream>>,
}

/// Errors that can occur during WebSocket operations.
#[derive(Debug)]
pub enum WsError {
    /// Failed to parse the URI
    InvalidUri(String),
    /// Failed to establish connection
    ConnectionFailed(String),
    /// Failed to send message
    SendFailed(String),
}

impl std::fmt::Display for WsError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            WsError::InvalidUri(msg) => write!(f, "Invalid URI: {}", msg),
            WsError::ConnectionFailed(msg) => write!(f, "Connection failed: {}", msg),
            WsError::SendFailed(msg) => write!(f, "Send failed: {}", msg),
        }
    }
}

impl std::error::Error for WsError {}

impl WsConnection {
    /// Attempt to connect to the given WebSocket URI.
    ///
    /// # Errors
    /// Returns `WsError::InvalidUri` if the URI cannot be parsed,
    /// or `WsError::ConnectionFailed` if the connection fails.
    pub fn connect(uri: &str) -> Result<Self, WsError> {
        let (socket, _response) =
            connect(uri).map_err(|e| WsError::ConnectionFailed(e.to_string()))?;

        Ok(WsConnection { socket })
    }

    /// Send a text message through the WebSocket.
    ///
    /// # Errors
    /// Returns `WsError::SendFailed` if the message cannot be sent.
    pub fn send_text(&mut self, text: &str) -> Result<(), WsError> {
        self.socket
            .send(Message::Text(text.to_string().into()))
            .map_err(|e| WsError::SendFailed(e.to_string()))
    }

    /// Close the WebSocket connection gracefully.
    pub fn close(&mut self) {
        let _ = self.socket.close(None);
    }
}

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

    #[test]
    fn test_connect_invalid_uri() {
        let result = WsConnection::connect("not_a_valid_uri");
        assert!(result.is_err());
        // tungstenite v0.29 treats invalid URIs as connection failures
        if let Err(WsError::ConnectionFailed(_)) = result {
            // expected
        } else {
            panic!("Expected ConnectionFailed error for invalid URI");
        }
    }

    #[test]
    fn test_connect_unreachable_server() {
        // Connect to a port that is very likely not listening
        let result = WsConnection::connect("ws://127.0.0.1:19999");
        assert!(result.is_err());
        if let Err(WsError::ConnectionFailed(_)) = result {
            // expected
        } else {
            panic!("Expected ConnectionFailed error");
        }
    }

    #[test]
    fn test_ws_error_display() {
        let err = WsError::InvalidUri("bad uri".to_string());
        assert_eq!(err.to_string(), "Invalid URI: bad uri");

        let err = WsError::ConnectionFailed("refused".to_string());
        assert_eq!(err.to_string(), "Connection failed: refused");

        let err = WsError::SendFailed("broken pipe".to_string());
        assert_eq!(err.to_string(), "Send failed: broken pipe");
    }
}