Skip to main content

lino_rest_api/
serve.rs

1//! Running an application over HTTP.
2//!
3//! The application itself knows nothing about sockets: it turns a [`RawRequest`]
4//! into [`crate::middleware::ResponseParts`]. This module is the shell that
5//! carries those over HTTP, built on axum, and it is the only place in the crate
6//! that depends on a server.
7
8use std::net::SocketAddr;
9use std::sync::Arc;
10
11use axum::Router;
12use axum::body::{Body, Bytes};
13use axum::extract::State;
14use axum::http::{HeaderName, HeaderValue, Method, Response, StatusCode, Uri};
15use tokio::net::TcpListener;
16
17use crate::app::{LinoApp, RawRequest};
18
19/// Build the axum router that serves an application.
20///
21/// Every method and every path reaches the application, which does its own
22/// routing and answers a `404` or a `405` itself, with problem details and an
23/// `Allow` header, as the specification requires.
24pub fn router(app: Arc<LinoApp>) -> Router {
25    Router::new().fallback(dispatch).with_state(app)
26}
27
28/// Serve one request through the application.
29async fn dispatch(
30    State(app): State<Arc<LinoApp>>,
31    method: Method,
32    uri: Uri,
33    headers: axum::http::HeaderMap,
34    body: Bytes,
35) -> Response<Body> {
36    let mut raw = RawRequest::new(method.as_str(), uri.path());
37    raw.query_string = uri.query().unwrap_or_default().to_string();
38    for (name, value) in &headers {
39        raw.headers
40            .insert(name.as_str(), value.to_str().unwrap_or_default());
41    }
42    raw.body = body.to_vec();
43
44    let parts = app.respond(raw).await;
45    let status = StatusCode::from_u16(parts.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
46    let mut response = Response::builder().status(status);
47    for (name, value) in parts.headers.iter() {
48        // A header the application could not have produced is dropped rather
49        // than allowed to fail the whole response.
50        if let (Ok(name), Ok(value)) = (
51            HeaderName::from_bytes(name.as_bytes()),
52            HeaderValue::from_str(value),
53        ) {
54            response = response.header(name, value);
55        }
56    }
57    response
58        .body(Body::from(parts.body))
59        .unwrap_or_else(|_| Response::new(Body::empty()))
60}
61
62/// An application bound to a port, before it starts serving.
63///
64/// Binding and serving are separate so that a test can bind port `0`, read the
65/// port that was chosen and then serve in the background.
66pub struct BoundServer {
67    /// The address the server listens on.
68    pub address: SocketAddr,
69    listener: TcpListener,
70    router: Router,
71}
72
73impl BoundServer {
74    /// Bind an application to an address.
75    ///
76    /// # Errors
77    ///
78    /// Whatever binding the socket reports.
79    pub async fn bind(app: LinoApp, address: &str) -> std::io::Result<Self> {
80        let listener = TcpListener::bind(address).await?;
81        Ok(Self {
82            address: listener.local_addr()?,
83            listener,
84            router: router(Arc::new(app)),
85        })
86    }
87
88    /// The base URL of the bound server, as a client needs it.
89    pub fn base_url(&self) -> String {
90        format!("http://{}", self.address)
91    }
92
93    /// Serve until the process ends.
94    ///
95    /// # Errors
96    ///
97    /// Whatever the server reports while accepting connections.
98    pub async fn serve(self) -> std::io::Result<()> {
99        axum::serve(self.listener, self.router).await
100    }
101}
102
103/// Bind an application to an address and serve it.
104///
105/// # Errors
106///
107/// Whatever binding the socket or accepting a connection reports.
108///
109/// # Examples
110///
111/// ```no_run
112/// use lino_rest_api::app::LinoApp;
113/// use lino_rest_api::serve::serve;
114///
115/// # async fn run() -> std::io::Result<()> {
116/// serve(LinoApp::new(), "0.0.0.0:8000").await
117/// # }
118/// ```
119pub async fn serve(app: LinoApp, address: &str) -> std::io::Result<()> {
120    BoundServer::bind(app, address).await?.serve().await
121}