touche 0.0.15

Synchronous HTTP library
Documentation
use std::{
    collections::HashMap,
    sync::{
        atomic::{AtomicUsize, Ordering},
        mpsc::{self, Sender},
        Arc, Mutex,
    },
    thread,
};

use indoc::formatdoc;
use touche::{body::HttpBody, Body, Method, Request, Response, Server, StatusCode};

#[derive(Debug)]
enum Event {
    User(usize),
    Message(usize, String),
}

type Users = Arc<Mutex<HashMap<usize, Sender<Event>>>>;

fn main() -> std::io::Result<()> {
    static NEXT_USER_ID: AtomicUsize = AtomicUsize::new(1);

    let users: Users = Arc::new(Mutex::new(HashMap::new()));

    Server::bind("0.0.0.0:4444").serve(move |req: Request<Body>| {
        let users = users.clone();

        match req.uri().path() {
            "/messages" if req.method() == Method::POST => {
                let user_id = req
                    .headers()
                    .get("x-user-id")
                    .and_then(|h| h.to_str().ok())
                    .and_then(|user_id| user_id.parse::<usize>().ok());

                match user_id {
                    Some(user_id) => {
                        let text = req.into_body().into_bytes().unwrap_or_default();
                        let text = std::str::from_utf8(&text).unwrap_or_default();

                        users.lock().unwrap().retain(|id, tx| {
                            if user_id != *id {
                                tx.send(Event::Message(user_id, text.to_string())).is_ok()
                            } else {
                                true
                            }
                        });

                        Response::builder()
                            .status(StatusCode::CREATED)
                            .body(Body::empty())
                    }

                    None => Response::builder()
                        .status(StatusCode::UNAUTHORIZED)
                        .body("Missing x-user-id header".into()),
                }
            }

            "/messages" => {
                let user_id = NEXT_USER_ID.fetch_add(1, Ordering::Relaxed);

                let (tx, rx) = mpsc::channel();
                tx.send(Event::User(user_id)).unwrap();
                users.lock().unwrap().insert(user_id, tx);

                let (sender, body) = Body::channel();
                thread::spawn(move || {
                    for event in rx {
                        let message = match event {
                            Event::User(id) => formatdoc! {r#"
                                event: user
                                data: {{"id": "{id}"}}

                            "#},
                            Event::Message(user_id, text) => formatdoc! {r#"
                                event: message
                                data: {{"userId": {user_id}, "message": "{text}"}}

                            "#},
                        };

                        if let Err(_err) = sender.send(message) {
                            break;
                        }
                    }
                });

                Response::builder()
                    .status(StatusCode::OK)
                    .header("content-type", "text/event-stream")
                    .header("connection", "close")
                    .body(body)
            }

            "/" => Response::builder()
                .status(StatusCode::OK)
                .header("content-type", "text/html")
                .body(include_str!("sse-chat.html").into()),

            _ => Response::builder()
                .status(StatusCode::NOT_FOUND)
                .body(Body::empty()),
        }
    })
}