iris-core 1.1.3

Iris engine core: cross-platform windowing, async runtime, memory pool, IO and networking
Documentation
//! Async networking utilities built on Tokio.
//!
//! Provides TCP connection and listener helpers, plus a minimal HTTP
//! GET/POST client for common use cases.

use std::io;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, TcpStream};

/// Connect to a TCP server.
pub async fn tcp_connect(addr: &str) -> Result<TcpStream, io::Error> {
    TcpStream::connect(addr).await
}

/// Bind a TCP listener on the given address.
pub async fn tcp_bind(addr: &str) -> Result<TcpListener, io::Error> {
    TcpListener::bind(addr).await
}

/// Read all available data from a TCP stream into a `Vec<u8>`.
pub async fn read_stream(stream: &mut TcpStream) -> Result<Vec<u8>, io::Error> {
    let mut buf = Vec::with_capacity(4096);
    stream.read_to_end(&mut buf).await?;
    Ok(buf)
}

/// Write data to a TCP stream.
pub async fn write_stream(stream: &mut TcpStream, data: &[u8]) -> Result<(), io::Error> {
    stream.write_all(data).await
}

// ─── Minimal HTTP client ─────────────────────────────────────────────

/// A minimal async HTTP response.
pub struct HttpResponse {
    pub status: u16,
    pub body: String,
    pub headers: Vec<(String, String)>,
}

/// Perform a minimal HTTP GET request.
///
/// This is intentionally simple — it speaks HTTP/1.1 over TCP.
/// For production use, depend on `reqwest` directly.
pub async fn http_get(url: &str) -> Result<HttpResponse, String> {
    let (host, path) = parse_url(url)?;
    let addr = format!("{}:80", host);

    let mut stream = TcpStream::connect(&addr)
        .await
        .map_err(|e| format!("connect {}: {}", addr, e))?;

    let request = format!(
        "GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
        path, host
    );
    stream
        .write_all(request.as_bytes())
        .await
        .map_err(|e| format!("send: {}", e))?;

    let mut reader = BufReader::new(&mut stream);
    let mut status_line = String::new();
    reader
        .read_line(&mut status_line)
        .await
        .map_err(|e| format!("read status: {}", e))?;

    let status = status_line
        .split_whitespace()
        .nth(1)
        .and_then(|s| s.parse::<u16>().ok())
        .unwrap_or(0);

    let mut headers = Vec::new();
    loop {
        let mut line = String::new();
        reader.read_line(&mut line).await.map_err(|e| format!("read header: {}", e))?;
        let trimmed = line.trim();
        if trimmed.is_empty() {
            break;
        }
        if let Some((k, v)) = trimmed.split_once(':') {
            headers.push((k.trim().to_string(), v.trim().to_string()));
        }
    }

    let mut body = String::new();
    reader
        .read_to_string(&mut body)
        .await
        .map_err(|e| format!("read body: {}", e))?;

    Ok(HttpResponse { status, body, headers })
}

fn parse_url(url: &str) -> Result<(&str, &str), String> {
    let url = url
        .strip_prefix("http://")
        .ok_or_else(|| format!("Only http:// URLs supported: {}", url))?;
    match url.find('/') {
        Some(pos) => Ok((&url[..pos], &url[pos..])),
        None => Ok((url, "/")),
    }
}

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

    #[test]
    fn test_parse_url() {
        let (host, path) = parse_url("http://example.com/api").unwrap();
        assert_eq!(host, "example.com");
        assert_eq!(path, "/api");
    }

    #[test]
    fn test_parse_url_root() {
        let (host, path) = parse_url("http://example.com").unwrap();
        assert_eq!(host, "example.com");
        assert_eq!(path, "/");
    }
}