sark 0.2.2

Simple Asynchronous Rust webKit - Server
Documentation
#![allow(clippy::too_many_arguments)]

use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::Duration;

use dope_extra::testing::run_with_trigger;
use http::StatusCode;
use sark::{Build, ServerCfg};

#[sark_gen::request]
struct HelloRequest {}

#[sark_gen::response(raw)]
struct HelloReply {
    status: StatusCode,
    body: o3::buffer::Owned,
}

#[sark_gen::handler]
fn hello(_req: HelloRequest, _state: &()) -> HelloReply {
    let mut body = o3::buffer::Owned::new();
    body.extend_from_slice(b"hello");
    HelloReply {
        status: StatusCode::OK,
        body,
    }
}

sark_gen::define_route! {
    SmokeDispatch: () => {
        GET "/hello" => hello,
    }
}

fn http_get_close(addr: std::net::SocketAddr, path: &str) -> Vec<u8> {
    let mut stream = TcpStream::connect(addr).expect("connect");
    stream
        .set_read_timeout(Some(Duration::from_secs(2)))
        .expect("set timeout");
    let req = format!("GET {path} HTTP/1.1\r\nHost: {addr}\r\nConnection: close\r\n\r\n");
    stream.write_all(req.as_bytes()).expect("send request");
    let mut buf = Vec::new();
    let mut chunk = [0u8; 4096];
    loop {
        match stream.read(&mut chunk) {
            Ok(0) => break,
            Ok(n) => {
                buf.extend_from_slice(&chunk[..n]);
                if let Some(body_start) = find_double_crlf(&buf)
                    && let Some(cl) = content_length(&buf[..body_start])
                    && buf.len() >= body_start + cl
                {
                    break;
                }
            }
            Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
            Err(e) => panic!("read response: {e}"),
        }
    }
    buf
}

fn find_double_crlf(buf: &[u8]) -> Option<usize> {
    buf.windows(4).position(|w| w == b"\r\n\r\n").map(|i| i + 4)
}

fn content_length(headers: &[u8]) -> Option<usize> {
    let text = std::str::from_utf8(headers).ok()?;
    for line in text.split("\r\n") {
        if let Some(value) = line.to_ascii_lowercase().strip_prefix("content-length:") {
            return value.trim().parse().ok();
        }
    }
    None
}

#[test]
fn server_dispatches_get_hello() {
    let bind: std::net::SocketAddr = "127.0.0.1:38765".parse().unwrap();
    let cfg = ServerCfg {
        bind,
        max_conn: 16,
        backlog: 16,
    };

    run_with_trigger(
        bind,
        |ctx, trigger| Build::http(smoke_dispatch::new(&()), cfg.clone(), ctx, Some(trigger)),
        |bind| {
            let raw = http_get_close(bind, "/hello");
            let text = std::str::from_utf8(&raw).expect("utf8 response");

            assert!(text.starts_with("HTTP/1.1 200 "), "status line: {text:?}");
            let body_start = text
                .find("\r\n\r\n")
                .expect("blank line separating headers from body")
                + 4;
            assert_eq!(&text[body_start..], "hello", "body mismatch: {text:?}");
        },
    );
}