nnrp-runtime 1.0.0-preview.4.5

Transport-neutral NNRP client/server session runtime over framed async transport slots.
Documentation
use async_trait::async_trait;
use nnrp_core::{CommonHeader, COMMON_HEADER_LEN};
use tokio::{
    io::{AsyncReadExt, AsyncWriteExt},
    net::{TcpListener, TcpStream, ToSocketAddrs},
};

use crate::{RuntimeError, RuntimePacket};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RuntimeFrameLimits {
    pub max_packet_bytes: usize,
}

impl RuntimeFrameLimits {
    pub const DEFAULT_MAX_PACKET_BYTES: usize = 64 * 1024 * 1024;

    pub const fn new(max_packet_bytes: usize) -> Self {
        Self { max_packet_bytes }
    }

    pub fn validate_packet_len(&self, declared: usize) -> Result<(), RuntimeError> {
        if declared > self.max_packet_bytes {
            Err(RuntimeError::FrameTooLarge {
                declared,
                max: self.max_packet_bytes,
            })
        } else {
            Ok(())
        }
    }
}

impl Default for RuntimeFrameLimits {
    fn default() -> Self {
        Self::new(Self::DEFAULT_MAX_PACKET_BYTES)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimeTransportKind {
    Tcp,
    Quic,
    Ipc,
    WebSocket,
}

impl RuntimeTransportKind {
    pub fn transport_id(self) -> nnrp_core::TransportId {
        match self {
            Self::Tcp => nnrp_core::TransportId::Tcp,
            Self::Quic => nnrp_core::TransportId::Quic,
            Self::Ipc => nnrp_core::TransportId::Ipc,
            Self::WebSocket => nnrp_core::TransportId::WebSocket,
        }
    }
}

pub type BoxedFramedTransport = Box<dyn FramedTransport>;
pub type BoxedFramedListener = Box<dyn FramedListener>;

#[async_trait]
pub trait FramedTransport: Send {
    fn transport_kind(&self) -> RuntimeTransportKind;
    async fn read_packet(&mut self) -> Result<RuntimePacket, RuntimeError>;
    async fn write_packet(&mut self, packet: &RuntimePacket) -> Result<(), RuntimeError>;
    async fn close(&mut self) -> Result<(), RuntimeError>;
}

#[async_trait]
pub trait FramedListener: Send + Sync {
    fn transport_kind(&self) -> RuntimeTransportKind;
    fn local_addr(&self) -> Result<std::net::SocketAddr, RuntimeError>;
    async fn accept(&self) -> Result<BoxedFramedTransport, RuntimeError>;
}

#[derive(Debug)]
pub struct TcpTransport {
    stream: TcpStream,
    limits: RuntimeFrameLimits,
}

impl TcpTransport {
    pub fn new(stream: TcpStream) -> Self {
        Self::new_with_limits(stream, RuntimeFrameLimits::default())
    }

    pub fn new_with_limits(stream: TcpStream, limits: RuntimeFrameLimits) -> Self {
        Self { stream, limits }
    }

    pub async fn connect(addr: impl ToSocketAddrs) -> Result<Self, RuntimeError> {
        Self::connect_with_limits(addr, RuntimeFrameLimits::default()).await
    }

    pub async fn connect_with_limits(
        addr: impl ToSocketAddrs,
        limits: RuntimeFrameLimits,
    ) -> Result<Self, RuntimeError> {
        Ok(Self {
            stream: TcpStream::connect(addr).await?,
            limits,
        })
    }
}

#[async_trait]
impl FramedTransport for TcpTransport {
    fn transport_kind(&self) -> RuntimeTransportKind {
        RuntimeTransportKind::Tcp
    }

    async fn read_packet(&mut self) -> Result<RuntimePacket, RuntimeError> {
        let mut header_bytes = [0u8; COMMON_HEADER_LEN];
        self.stream.read_exact(&mut header_bytes).await?;
        let header = CommonHeader::parse(&header_bytes)?;
        self.limits.validate_packet_len(header.packet_len()?)?;

        let mut metadata = vec![0u8; header.meta_len as usize];
        if !metadata.is_empty() {
            self.stream.read_exact(&mut metadata).await?;
        }

        let mut body = vec![0u8; header.body_len as usize];
        if !body.is_empty() {
            self.stream.read_exact(&mut body).await?;
        }

        Ok(RuntimePacket::from_parts(header, metadata, body)?)
    }

    async fn write_packet(&mut self, packet: &RuntimePacket) -> Result<(), RuntimeError> {
        let bytes = packet.to_bytes()?;
        self.limits.validate_packet_len(bytes.len())?;
        self.stream.write_all(&bytes).await?;
        self.stream.flush().await?;
        Ok(())
    }

    async fn close(&mut self) -> Result<(), RuntimeError> {
        self.stream.shutdown().await?;
        Ok(())
    }
}

#[derive(Debug)]
pub struct TcpFramedListener {
    listener: TcpListener,
    limits: RuntimeFrameLimits,
}

impl TcpFramedListener {
    pub fn new(listener: TcpListener) -> Self {
        Self::new_with_limits(listener, RuntimeFrameLimits::default())
    }

    pub fn new_with_limits(listener: TcpListener, limits: RuntimeFrameLimits) -> Self {
        Self { listener, limits }
    }

    pub async fn bind(addr: impl ToSocketAddrs) -> Result<Self, RuntimeError> {
        Self::bind_with_limits(addr, RuntimeFrameLimits::default()).await
    }

    pub async fn bind_with_limits(
        addr: impl ToSocketAddrs,
        limits: RuntimeFrameLimits,
    ) -> Result<Self, RuntimeError> {
        Ok(Self {
            listener: TcpListener::bind(addr).await?,
            limits,
        })
    }
}

#[async_trait]
impl FramedListener for TcpFramedListener {
    fn transport_kind(&self) -> RuntimeTransportKind {
        RuntimeTransportKind::Tcp
    }

    fn local_addr(&self) -> Result<std::net::SocketAddr, RuntimeError> {
        Ok(self.listener.local_addr()?)
    }

    async fn accept(&self) -> Result<BoxedFramedTransport, RuntimeError> {
        let (stream, _) = self.listener.accept().await?;
        Ok(Box::new(TcpTransport::new_with_limits(stream, self.limits)))
    }
}

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

    #[test]
    fn runtime_transport_kinds_map_to_frozen_transport_ids() {
        assert_eq!(RuntimeTransportKind::Tcp.transport_id(), TransportId::Tcp);
        assert_eq!(RuntimeTransportKind::Quic.transport_id(), TransportId::Quic);
        assert_eq!(RuntimeTransportKind::Ipc.transport_id(), TransportId::Ipc);
        assert_eq!(
            RuntimeTransportKind::WebSocket.transport_id(),
            TransportId::WebSocket
        );
    }
}