Skip to main content

rustolio_server/
lib.rs

1//
2// SPDX-License-Identifier: MPL-2.0
3//
4// Copyright (c) 2026 Tobias Binnewies. All rights reserved.
5//
6// This Source Code Form is subject to the terms of the Mozilla Public
7// License, v. 2.0. If a copy of the MPL was not distributed with this
8// file, You can obtain one at http://mozilla.org/MPL/2.0/.
9//
10
11mod context;
12mod router;
13mod static_files;
14
15use std::{io, net::SocketAddr, path::PathBuf};
16
17use hyper::{body::Incoming, server::conn::http1, service::service_fn, StatusCode};
18use hyper_util::rt::TokioIo;
19use rustolio_utils::{http::Outgoing, prelude::Threadsafe, prelude::*};
20use tokio::net::TcpListener;
21
22use rustolio_utils::http;
23
24use context::ServerContext;
25use static_files::serve_static_dir;
26
27const DEFAULT_STATIC_PATH: &str = "pkg";
28
29pub struct Server<C> {
30    #[allow(dead_code)]
31    context: C,
32    with_static: Option<PathBuf>,
33}
34
35impl Default for Server<()> {
36    fn default() -> Self {
37        Self {
38            context: (),
39            with_static: Some(DEFAULT_STATIC_PATH.into()),
40        }
41    }
42}
43
44impl Server<()> {
45    pub fn new() -> Self {
46        Self {
47            context: (),
48            with_static: None,
49        }
50    }
51}
52
53impl Server<()> {
54    pub fn context<C>(self, context: C) -> Server<C> {
55        Server {
56            context,
57            with_static: self.with_static,
58        }
59    }
60}
61
62impl<C> Server<C> {
63    pub fn with_static(mut self, path: impl Into<PathBuf>) -> Self {
64        self.with_static = Some(path.into());
65        self
66    }
67}
68
69impl<C: Threadsafe> Server<C> {
70    pub async fn bind(self, addr: impl Into<SocketAddr>) -> Result<(), io::Error> {
71        bind(self.with_static, addr, self.context).await
72    }
73}
74
75async fn bind<C: Threadsafe>(
76    with_static: Option<PathBuf>,
77    addr: impl Into<SocketAddr>,
78    custom_ctx: C,
79) -> Result<(), io::Error> {
80    let addr = addr.into();
81    let listener = TcpListener::bind(addr).await?;
82
83    let static_path = match with_static {
84        Some(path) => path,
85        None => PathBuf::new(),
86    };
87
88    tracing::info!("Starting server on http://{}/", addr);
89
90    let ctx = ServerContext::new(static_path, custom_ctx);
91    service!(ctx as ServerContext<C>);
92
93    loop {
94        let (stream, _) = listener.accept().await?;
95        let io = TokioIo::new(stream);
96
97        tokio::task::spawn(async move {
98            if let Err(err) = http1::Builder::new()
99                .serve_connection(io, service_fn(move |req| glue(req, ctx)))
100                .await
101            {
102                eprintln!("Error serving connection: {:?}", err);
103            }
104        });
105    }
106}
107
108async fn glue<C>(
109    req: hyper::Request<Incoming>,
110    ctx: &'static ServerContext<C>,
111) -> Result<hyper::Response<Outgoing>, http::Error> {
112    let req = http::Request::from_inner(req);
113
114    let res = match req.uri().path().as_bytes() {
115        #[cfg(feature = "db")]
116        [b'/', b'd', b'b', b'/', ..] => ctx.db_router.serve(req, &ctx.db_ctx).await,
117        [b'/', b'r', b'p', b'c', b'/', ..] => ctx.rpc_router.serve(req, &ctx.rpc_ctx).await,
118        [b'/', ..] => Ok(serve_static_dir(req, ctx).await),
119        _ => Ok(http::Response::builder()
120            .status(StatusCode::NOT_FOUND)
121            .text("Not found")
122            .build()
123            .unwrap()),
124    };
125    res.map(|r| r.into_inner())
126}
127
128pub mod prelude {
129    #[doc(hidden)]
130    pub mod __server_macro {
131        pub use crate::context::ServerContext;
132        pub use inventory;
133    }
134}