concuring 0.1.0

A synchronous, concurrent HTTP client library for Rust that uses io_uring.
Documentation
#[path = "server.rs"]
mod server;

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use super::server::TestHttpServer;
    use concuring::{concuring::Concuring, request::RequestBuilder};
    use http_types::{Method, Request, Response, StatusCode};

    #[test]
    fn test_hello_name_endpoint() {
        // Handler that responds to GET /<name> with "Hello, <name>!"
        let handler = |req: Request| async move {
            if req.method() == Method::Get {
                let path = req.url().path();
                if let Some(name) = path.strip_prefix('/') {
                    if !name.is_empty() {
                        let body = format!("Hello, {}!", name);
                        let mut response = Response::new(StatusCode::Ok);
                        response.set_body(body);
                        response.insert_header("content-type", "text/plain");
                        return Ok(response);
                    }
                }
            }
            Ok(Response::new(StatusCode::NotFound))
        };
        std::thread::scope(|scope| {
            // Spawn the test server
            let server = TestHttpServer::spawn(scope, handler).unwrap();

            // Give the server a moment to start
            server.wait_until_ready();

            let mut client = Concuring::<8>::new().unwrap();

            let socket_path = format!("unix://{}//world", server.socket_path.to_str().unwrap());
            let request = RequestBuilder::get(&socket_path)
                .unwrap()
                .append_header("Connection", "close")
                .unwrap()
                .build_without_body();

            client
                .try_submit(1, request)
                .expect("has space for 1 request");
            client.wait().expect("should get 1 resp at least");
            let (_, response) = client.finished_responses.pop_front().unwrap();
            let response = response.unwrap();

            // Check the response
            assert_eq!(response.status(), 200);
            let body = str::from_utf8(response.body()).unwrap();
            assert_eq!(body, "Hello, world!");

            // Test with different name
            let socket_path = format!("unix://{}//Alice", server.socket_path.to_str().unwrap());
            let request = RequestBuilder::get(&socket_path)
                .unwrap()
                .append_header("Connection", "close")
                .unwrap()
                .build_without_body();
            client
                .try_submit(2, request)
                .expect("has space for 1 request");
            client.wait().expect("should get 1 resp at least");
            let (_, response) = client.finished_responses.pop_front().unwrap();
            let response = response.unwrap();

            // Check the response
            assert_eq!(response.status(), 200);
            let body = str::from_utf8(response.body()).unwrap();
            assert_eq!(body, "Hello, Alice!");
        });
    }

    #[test]
    fn test_concurrent_barrier() {
        // Create a barrier that waits for 8 requests
        let barrier = Arc::new(smol::lock::Barrier::new(8));

        // Simple handler that waits on the barrier then responds
        let handler = {
            let barrier = barrier.clone();
            move |req: Request| {
                let barrier = barrier.clone();
                async move {
                    if req.method() == Method::Get && req.url().path() == "/concurrent" {
                        // Wait for all 8 requests to arrive
                        barrier.wait();

                        let mut response = Response::new(StatusCode::Ok);
                        response.set_body("concurrent response");
                        response.insert_header("content-type", "text/plain");
                        return Ok(response);
                    }
                    Ok(Response::new(StatusCode::NotFound))
                }
            }
        };

        std::thread::scope(|scope| {
            let server = TestHttpServer::spawn(scope, handler).unwrap();
            server.wait_until_ready();

            let mut client = Concuring::<8>::new().unwrap();

            // Submit 8 concurrent requests
            for i in 0..8 {
                let socket_path = format!(
                    "unix://{}//concurrent",
                    server.socket_path.to_str().unwrap()
                );
                let request = RequestBuilder::get(&socket_path)
                    .unwrap()
                    .append_header("Connection", "close")
                    .unwrap()
                    .build_without_body();

                client
                    .try_submit(i, request)
                    .expect("should have space for request");
            }

            // Wait for all responses
            for _ in 0..8 {
                client.wait().expect("should get response");
            }

            // Verify all responses
            assert_eq!(client.finished_responses.len(), 8);

            for _ in 0..8 {
                let (_, response) = client.finished_responses.pop_front().unwrap();
                let response = response.unwrap();
                assert_eq!(response.status(), 200);
                let body = str::from_utf8(response.body()).unwrap();
                assert_eq!(body, "concurrent response");
            }
        });
    }
}