concuring 0.1.0

A synchronous, concurrent HTTP client library for Rust that uses io_uring.
Documentation
use color_eyre::eyre::Result;
use flume::Sender;
use http_types::{Method, Request, Response, StatusCode};
use smol::net::unix::UnixListener;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::{Arc, Condvar, Mutex};
use std::thread;
use tempfile::TempDir;

enum ControlMessage {
    Shutdown,
}

pub struct TestHttpServer {
    pub socket_path: PathBuf,
    _temp_dir: TempDir,
    control_tx: Sender<ControlMessage>,
    ready_signal: Arc<(Mutex<bool>, Condvar)>,
}

impl TestHttpServer {
    pub fn spawn<'scope, F, Fut>(
        scope: &'scope thread::Scope<'scope, '_>,
        handler: F,
    ) -> Result<TestHttpServer>
    where
        F: Fn(Request) -> Fut + Clone + Send + 'scope,
        Fut: Future<Output = http_types::Result<Response>> + Send + 'scope,
    {
        let temp_dir = tempfile::tempdir().unwrap();
        let socket_path = temp_dir.path().join("test.sock");
        let socket_path_clone = socket_path.clone();
        let (control_tx, control_rx) = flume::bounded(1);
        let ready_signal = Arc::new((Mutex::new(false), Condvar::new()));
        let ready_signal_clone = Arc::clone(&ready_signal);

        let _ = scope.spawn(move || {
            let handler = Rc::new(handler);
            let local_executor = smol::LocalExecutor::new();
            futures_lite::future::block_on(local_executor.run(async {
                let listener =
                    UnixListener::bind(&socket_path_clone).expect("test server binds to uds");

                // Signal that the listener is ready
                {
                    let (lock, cvar) = &*ready_signal_clone;
                    let mut ready = lock.lock().unwrap();
                    *ready = true;
                    cvar.notify_one();
                }

                loop {
                    let do_shutdown = smol::future::race(
                        async {
                            let (stream, _) =
                                listener.accept().await.expect("server accepting sock conn");

                            let handler = handler.clone();
                            local_executor
                                .spawn(async move {
                                    async_h1::server::accept(stream, |req| handler(req))
                                        .await
                                        .expect("handling connection");
                                })
                                .detach();
                            false
                        },
                        async {
                            if let Ok(ControlMessage::Shutdown) = control_rx.recv_async().await {
                                true
                            } else {
                                false
                            }
                        },
                    )
                    .await;
                    if do_shutdown {
                        break;
                    }
                }
            }));
        });

        Ok(Self {
            socket_path,
            _temp_dir: temp_dir,
            control_tx,
            ready_signal,
        })
    }

    /// Block until the server listener is online and ready to accept connections
    pub fn wait_until_ready(&self) {
        let (lock, cvar) = &*self.ready_signal;
        let mut ready = lock.lock().unwrap();
        while !*ready {
            ready = cvar.wait(ready).unwrap();
        }
    }
}

impl Drop for TestHttpServer {
    fn drop(&mut self) {
        let _ = self.control_tx.send(ControlMessage::Shutdown);
    }
}

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

    #[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();

            // Create a reqwest client that can handle Unix sockets
            let client = reqwest::blocking::Client::builder()
                .unix_socket(server.socket_path.clone())
                .build()
                .expect("reqwest client");

            // Make request to the Unix socket
            let socket_url = format!("http://localhost/world");
            let response = client.get(&socket_url).send().unwrap();

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

            // Test with different name
            let response = client.get("http://localhost/Alice").send().unwrap();

            assert_eq!(response.status(), 200);
            let body = response.text().unwrap();
            assert_eq!(body, "Hello, Alice!");
        });
    }
}