embers-client 0.1.0

Client rendering, input handling, configuration, and scripting support for Embers.
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};

use async_trait::async_trait;
use embers_core::{MuxError, Result};
use embers_protocol::{ClientMessage, ServerEvent, ServerResponse};

use crate::transport::Transport;

#[derive(Clone, Debug, Default)]
pub struct FakeTransport {
    requests: Arc<Mutex<Vec<ClientMessage>>>,
    responses: Arc<Mutex<VecDeque<ServerResponse>>>,
    events: Arc<Mutex<VecDeque<ServerEvent>>>,
}

impl FakeTransport {
    pub fn push_response(&self, response: ServerResponse) {
        self.responses
            .lock()
            .expect("responses lock")
            .push_back(response);
    }

    pub fn push_event(&self, event: ServerEvent) {
        self.events.lock().expect("events lock").push_back(event);
    }

    pub fn requests(&self) -> Vec<ClientMessage> {
        self.requests.lock().expect("requests lock").clone()
    }
}

#[async_trait]
impl Transport for FakeTransport {
    async fn request(&self, message: ClientMessage) -> Result<ServerResponse> {
        self.requests.lock().expect("requests lock").push(message);
        self.responses
            .lock()
            .expect("responses lock")
            .pop_front()
            .ok_or_else(|| MuxError::transport("fake transport has no queued response"))
    }

    async fn next_event(&self) -> Result<ServerEvent> {
        self.events
            .lock()
            .expect("events lock")
            .pop_front()
            .ok_or_else(|| MuxError::transport("fake transport has no queued event"))
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Exchange {
    pub request: ClientMessage,
    pub response: ServerResponse,
}

#[derive(Clone, Debug, Default)]
pub struct ScriptedTransport {
    exchanges: Arc<Mutex<VecDeque<Exchange>>>,
    events: Arc<Mutex<VecDeque<ServerEvent>>>,
}

impl ScriptedTransport {
    pub fn push_exchange(&self, request: ClientMessage, response: ServerResponse) {
        self.exchanges
            .lock()
            .expect("exchanges lock")
            .push_back(Exchange { request, response });
    }

    pub fn push_event(&self, event: ServerEvent) {
        self.events.lock().expect("events lock").push_back(event);
    }

    pub fn assert_exhausted(&self) -> Result<()> {
        let remaining = self.exchanges.lock().expect("exchanges lock").len();
        if remaining == 0 {
            Ok(())
        } else {
            Err(MuxError::transport(format!(
                "scripted transport still has {remaining} pending exchange(s)"
            )))
        }
    }
}

#[async_trait]
impl Transport for ScriptedTransport {
    async fn request(&self, message: ClientMessage) -> Result<ServerResponse> {
        let expected = self
            .exchanges
            .lock()
            .expect("exchanges lock")
            .pop_front()
            .ok_or_else(|| MuxError::transport("scripted transport has no queued exchange"))?;

        if expected.request != message {
            return Err(MuxError::transport(format!(
                "unexpected request: expected {:?}, got {:?}",
                expected.request, message
            )));
        }

        Ok(expected.response)
    }

    async fn next_event(&self) -> Result<ServerEvent> {
        self.events
            .lock()
            .expect("events lock")
            .pop_front()
            .ok_or_else(|| MuxError::transport("scripted transport has no queued event"))
    }
}

pub type TestGrid = crate::grid::RenderGrid;

#[cfg(test)]
mod tests {
    use embers_core::RequestId;
    use embers_protocol::{ClientMessage, PingRequest, PingResponse, ServerResponse};

    use super::{FakeTransport, ScriptedTransport, TestGrid};
    use crate::Transport;

    #[tokio::test]
    async fn fake_transport_records_requests() {
        let transport = FakeTransport::default();
        let request = ClientMessage::Ping(PingRequest {
            request_id: RequestId(3),
            payload: "phase0".to_owned(),
        });
        transport.push_response(ServerResponse::Pong(PingResponse {
            request_id: RequestId(3),
            payload: "phase0".to_owned(),
        }));

        let response = transport.request(request.clone()).await.expect("response");
        assert_eq!(transport.requests(), vec![request]);
        assert_eq!(
            response,
            ServerResponse::Pong(PingResponse {
                request_id: RequestId(3),
                payload: "phase0".to_owned(),
            })
        );
    }

    #[tokio::test]
    async fn scripted_transport_rejects_mismatched_requests() {
        let transport = ScriptedTransport::default();
        transport.push_exchange(
            ClientMessage::Ping(PingRequest {
                request_id: RequestId(4),
                payload: "expected".to_owned(),
            }),
            ServerResponse::Pong(PingResponse {
                request_id: RequestId(4),
                payload: "expected".to_owned(),
            }),
        );

        let error = transport
            .request(ClientMessage::Ping(PingRequest {
                request_id: RequestId(4),
                payload: "different".to_owned(),
            }))
            .await
            .expect_err("request must mismatch");
        assert!(error.to_string().contains("unexpected request"));
    }

    #[test]
    fn test_grid_renders_rows() {
        let mut grid = TestGrid::new(6, 2);
        grid.put_str(1, 0, "embers");
        grid.put_str(0, 1, "ok");

        assert_eq!(grid.render(), " ember\nok    ");
    }
}