Skip to main content

ullun/
app.rs

1//! Main application orchestrator
2
3use crate::error::{Error, Result};
4use crate::handler::{DynHandler, Handler, HandlerWithParams};
5use crate::middleware::{Layered, Middleware, Next};
6use crate::openapi::{self, OpenApiInfo, Operation, RouteKind};
7use crate::request::{Params, Request};
8use crate::response::Response;
9use crate::router::Router;
10use http_body_util::BodyExt;
11use hyper::server::conn::http1;
12use hyper::service::service_fn;
13use hyper_util::rt::TokioIo;
14use std::collections::HashMap;
15use std::convert::Infallible;
16use std::net::SocketAddr;
17use std::sync::Arc;
18use tokio::net::TcpListener;
19
20/// Main application struct
21pub struct App {
22    router: Router,
23    middlewares: Vec<Arc<dyn Middleware>>,
24    max_body_size: Option<usize>, // in bytes
25    docs: HashMap<(String, String), Operation>,
26    openapi: Option<OpenApiInfo>,
27}
28
29/// Route group builder
30pub struct RouteGroup {
31    prefix: String,
32    routes: Vec<(String, String, DynHandler)>, // (method, path, handler)
33    middlewares: Vec<Arc<dyn Middleware>>,
34    docs: Vec<(String, String, Operation)>, // (method, path relative to prefix, docs)
35}
36
37impl App {
38    pub fn new() -> Self {
39        Self {
40            router: Router::new(),
41            middlewares: Vec::new(),
42            max_body_size: Some(10 * 1024 * 1024), // 10 MB default
43            docs: HashMap::new(),
44            openapi: None,
45        }
46    }
47
48    /// Set maximum request body size in bytes (None = unlimited)
49    pub fn max_body_size(mut self, size: Option<usize>) -> Self {
50        self.max_body_size = size;
51        self
52    }
53
54    /// Create a route group with a common prefix
55    pub fn group(mut self, prefix: &str, configure: impl FnOnce(RouteGroup) -> RouteGroup) -> Self {
56        let group = RouteGroup::new(prefix);
57        let group = configure(group);
58
59        for (method, path, op) in group.docs {
60            let full_path = format!("{}{}", group.prefix, path);
61            self.docs.insert((method, full_path), op);
62        }
63
64        // Group middlewares wrap only this group's routes (inside the global ones)
65        let middlewares = group.middlewares;
66        for (method, path, handler) in group.routes {
67            let full_path = format!("{}{}", group.prefix, path);
68            let handler: DynHandler = if middlewares.is_empty() {
69                handler
70            } else {
71                Arc::new(Layered::new(middlewares.clone(), handler))
72            };
73            if let Err(e) = self.router.insert(&method, &full_path, handler) {
74                panic!("Failed to register {} route {}: {}", method, full_path, e);
75            }
76        }
77
78        self
79    }
80
81    /// Describe a registered route for the OpenAPI document
82    ///
83    /// `path` is the path exactly as registered, e.g. `/users/{id}`.
84    pub fn doc(mut self, method: &str, path: &str, operation: Operation) -> Self {
85        self.docs
86            .insert((method.to_ascii_uppercase(), path.to_string()), operation);
87        self
88    }
89
90    /// Publish an OpenAPI 3.1 document of all routes (and optionally Swagger UI)
91    ///
92    /// See the [`openapi`] module.
93    pub fn openapi(mut self, info: OpenApiInfo) -> Self {
94        self.openapi = Some(info);
95        self
96    }
97
98    /// Add GET route
99    pub fn get<H>(mut self, path: &str, handler: H) -> Self
100    where
101        H: Handler,
102    {
103        if let Err(e) = self
104            .router
105            .insert("GET", path, Arc::new(handler) as DynHandler)
106        {
107            panic!("Failed to register GET route {}: {}", path, e);
108        }
109        self
110    }
111
112    /// Add GET route with path params extractor
113    pub fn get_with_params<F, Fut>(mut self, path: &str, handler: F) -> Self
114    where
115        F: Fn(Params) -> Fut + Send + Sync + 'static,
116        Fut: std::future::Future<Output = Result<Response>> + Send + 'static,
117    {
118        if let Err(e) = self.router.insert(
119            "GET",
120            path,
121            Arc::new(HandlerWithParams::new(handler)) as DynHandler,
122        ) {
123            panic!("Failed to register GET route {}: {}", path, e);
124        }
125        self
126    }
127
128    /// Add POST route
129    pub fn post<H>(mut self, path: &str, handler: H) -> Self
130    where
131        H: Handler,
132    {
133        if let Err(e) = self
134            .router
135            .insert("POST", path, Arc::new(handler) as DynHandler)
136        {
137            panic!("Failed to register POST route {}: {}", path, e);
138        }
139        self
140    }
141
142    /// Add PUT route
143    pub fn put<H>(mut self, path: &str, handler: H) -> Self
144    where
145        H: Handler,
146    {
147        if let Err(e) = self
148            .router
149            .insert("PUT", path, Arc::new(handler) as DynHandler)
150        {
151            panic!("Failed to register PUT route {}: {}", path, e);
152        }
153        self
154    }
155
156    /// Add DELETE route
157    pub fn delete<H>(mut self, path: &str, handler: H) -> Self
158    where
159        H: Handler,
160    {
161        if let Err(e) = self
162            .router
163            .insert("DELETE", path, Arc::new(handler) as DynHandler)
164        {
165            panic!("Failed to register DELETE route {}: {}", path, e);
166        }
167        self
168    }
169
170    /// Add PATCH route
171    pub fn patch<H>(mut self, path: &str, handler: H) -> Self
172    where
173        H: Handler,
174    {
175        if let Err(e) = self
176            .router
177            .insert("PATCH", path, Arc::new(handler) as DynHandler)
178        {
179            panic!("Failed to register PATCH route {}: {}", path, e);
180        }
181        self
182    }
183
184    /// Accept WebSocket connections on `path` (feature `websocket`)
185    ///
186    /// The handler receives the original request (path params, query, headers,
187    /// extensions set by middleware) and the upgraded socket, and runs on its
188    /// own task until it returns.
189    #[cfg(feature = "websocket")]
190    pub fn ws<F, Fut>(self, path: &str, handler: F) -> Self
191    where
192        F: Fn(Request, crate::websocket::WebSocket) -> Fut + Send + Sync + 'static,
193        Fut: std::future::Future<Output = ()> + Send + 'static,
194    {
195        self.ws_with(path, crate::websocket::WsOptions::default(), handler)
196    }
197
198    /// Like [`App::ws`] with explicit size limit and allowed origins
199    #[cfg(feature = "websocket")]
200    pub fn ws_with<F, Fut>(
201        mut self,
202        path: &str,
203        options: crate::websocket::WsOptions,
204        handler: F,
205    ) -> Self
206    where
207        F: Fn(Request, crate::websocket::WebSocket) -> Fut + Send + Sync + 'static,
208        Fut: std::future::Future<Output = ()> + Send + 'static,
209    {
210        let route = crate::websocket::WsRoute::new(handler, options);
211        if let Err(e) = self.router.insert_as(
212            RouteKind::WebSocket,
213            "GET",
214            path,
215            Arc::new(route) as DynHandler,
216        ) {
217            panic!("Failed to register WebSocket route {}: {}", path, e);
218        }
219        self
220    }
221
222    /// Add middleware
223    pub fn middleware<M>(mut self, middleware: M) -> Self
224    where
225        M: Middleware,
226    {
227        self.middlewares.push(Arc::new(middleware));
228        self
229    }
230
231    /// Serve static files from a directory
232    pub fn serve_static(mut self, route_prefix: &str, dir_path: &str) -> Self {
233        use crate::static_files::StaticFileHandler;
234        let handler = StaticFileHandler::new(dir_path);
235        // matchit 0.9 catch-all syntax: {*param} matches the rest of the path
236        let prefix = route_prefix.trim_end_matches('/');
237        let handler: DynHandler = Arc::new(handler);
238        let route = format!("{}/{{*filepath}}", prefix);
239        if let Err(e) = self
240            .router
241            .insert_as(RouteKind::Hidden, "GET", &route, handler.clone())
242        {
243            panic!("Failed to register static file route {}: {}", route, e);
244        }
245        // The catch-all needs at least one segment, so also serve the prefix
246        // root (-> index.html). Skipped when the user already owns that route.
247        let root = if prefix.is_empty() { "/" } else { prefix };
248        let _ = self
249            .router
250            .insert_as(RouteKind::Hidden, "GET", root, handler.clone());
251        if !prefix.is_empty() {
252            let _ =
253                self.router
254                    .insert_as(RouteKind::Hidden, "GET", &format!("{}/", prefix), handler);
255        }
256        self
257    }
258
259    /// Run the server with graceful shutdown on CTRL+C
260    pub async fn run(self, addr: &str) -> Result<()> {
261        let this = self.finalize();
262        let addr: SocketAddr = addr
263            .parse()
264            .map_err(|e| Error::internal(format!("Invalid address: {}", e)))?;
265
266        let listener = TcpListener::bind(addr)
267            .await
268            .map_err(|e| Error::internal(format!("Failed to bind: {}", e)))?;
269
270        println!("šŸš€ ullun-api listening on http://{}", addr);
271
272        let router = Arc::new(this.router);
273        let middlewares = Arc::new(this.middlewares);
274        let max_body_size = this.max_body_size;
275
276        // Graceful shutdown signal
277        let shutdown = async {
278            tokio::signal::ctrl_c()
279                .await
280                .expect("Failed to install CTRL+C handler");
281            println!("\nšŸ›‘ Shutting down gracefully...");
282        };
283
284        tokio::select! {
285            _ = shutdown => {
286                println!("āœ… Server stopped");
287                Ok(())
288            }
289            result = Self::serve_loop(listener, router, middlewares, max_body_size) => result
290        }
291    }
292
293    /// Serve on an already bound listener until the future is dropped.
294    ///
295    /// Useful for tests, benchmarks and embedding: bind to port `0`, read the
296    /// assigned port from `listener.local_addr()`, then call `serve`.
297    /// Unlike [`App::run`], this does not install a CTRL+C handler.
298    pub async fn serve(self, listener: TcpListener) -> Result<()> {
299        let this = self.finalize();
300        Self::serve_loop(
301            listener,
302            Arc::new(this.router),
303            Arc::new(this.middlewares),
304            this.max_body_size,
305        )
306        .await
307    }
308
309    /// Last step before serving: publish the OpenAPI document, now that all
310    /// routes are known.
311    fn finalize(mut self) -> Self {
312        let Some(info) = self.openapi.take() else {
313            return self;
314        };
315        let spec = openapi::build(&info, self.router.registered(), &self.docs);
316        let spec = bytes::Bytes::from(serde_json::to_vec(&spec).unwrap_or_default());
317
318        let spec_handler = {
319            let spec = spec.clone();
320            move |_req: Request| {
321                let spec = spec.clone();
322                async move {
323                    Ok(Response::ok()
324                        .with_header("content-type", "application/json")
325                        .with_body(spec))
326                }
327            }
328        };
329        self.insert_hidden("GET", &info.spec_path, Arc::new(spec_handler));
330
331        if let Some(docs_path) = &info.docs_path {
332            let page = openapi::swagger_ui_html(&info.title, &info.spec_path);
333            let page_handler = move |_req: Request| {
334                let page = page.clone();
335                async move { Ok(Response::html(page)) }
336            };
337            self.insert_hidden("GET", docs_path, Arc::new(page_handler));
338        }
339        self
340    }
341
342    fn insert_hidden(&mut self, method: &str, path: &str, handler: DynHandler) {
343        if let Err(e) = self
344            .router
345            .insert_as(RouteKind::Hidden, method, path, handler)
346        {
347            panic!("Failed to register {} route {}: {}", method, path, e);
348        }
349    }
350
351    async fn serve_loop(
352        listener: TcpListener,
353        router: Arc<Router>,
354        middlewares: Arc<Vec<Arc<dyn Middleware>>>,
355        max_body_size: Option<usize>,
356    ) -> Result<()> {
357        loop {
358            let (stream, peer) = listener
359                .accept()
360                .await
361                .map_err(|e| Error::internal(format!("Failed to accept: {}", e)))?;
362
363            let io = TokioIo::new(stream);
364            let router = router.clone();
365            let middlewares = middlewares.clone();
366
367            tokio::task::spawn(async move {
368                if let Err(err) = http1::Builder::new()
369                    .serve_connection(
370                        io,
371                        service_fn(|req| {
372                            let router = router.clone();
373                            let middlewares = middlewares.clone();
374                            async move {
375                                Ok::<_, Infallible>(
376                                    handle_request(req, peer, router, middlewares, max_body_size)
377                                        .await,
378                                )
379                            }
380                        }),
381                    )
382                    .with_upgrades()
383                    .await
384                {
385                    eprintln!("Error serving connection: {:?}", err);
386                }
387            });
388        }
389    }
390}
391
392impl Default for App {
393    fn default() -> Self {
394        Self::new()
395    }
396}
397
398impl RouteGroup {
399    fn new(prefix: &str) -> Self {
400        let prefix = prefix.trim_end_matches('/');
401        Self {
402            prefix: prefix.to_string(),
403            routes: Vec::new(),
404            middlewares: Vec::new(),
405            docs: Vec::new(),
406        }
407    }
408
409    /// Describe a route of this group for the OpenAPI document; `path` is
410    /// relative to the group prefix, as given to `get`/`post`/...
411    pub fn doc(mut self, method: &str, path: &str, operation: Operation) -> Self {
412        self.docs
413            .push((method.to_ascii_uppercase(), path.to_string(), operation));
414        self
415    }
416
417    pub fn get<H>(mut self, path: &str, handler: H) -> Self
418    where
419        H: Handler,
420    {
421        self.routes.push((
422            "GET".to_string(),
423            path.to_string(),
424            Arc::new(handler) as DynHandler,
425        ));
426        self
427    }
428
429    pub fn post<H>(mut self, path: &str, handler: H) -> Self
430    where
431        H: Handler,
432    {
433        self.routes.push((
434            "POST".to_string(),
435            path.to_string(),
436            Arc::new(handler) as DynHandler,
437        ));
438        self
439    }
440
441    pub fn put<H>(mut self, path: &str, handler: H) -> Self
442    where
443        H: Handler,
444    {
445        self.routes.push((
446            "PUT".to_string(),
447            path.to_string(),
448            Arc::new(handler) as DynHandler,
449        ));
450        self
451    }
452
453    pub fn delete<H>(mut self, path: &str, handler: H) -> Self
454    where
455        H: Handler,
456    {
457        self.routes.push((
458            "DELETE".to_string(),
459            path.to_string(),
460            Arc::new(handler) as DynHandler,
461        ));
462        self
463    }
464
465    pub fn patch<H>(mut self, path: &str, handler: H) -> Self
466    where
467        H: Handler,
468    {
469        self.routes.push((
470            "PATCH".to_string(),
471            path.to_string(),
472            Arc::new(handler) as DynHandler,
473        ));
474        self
475    }
476
477    /// Add middleware that runs only for the routes of this group
478    ///
479    /// It runs after the application-wide middleware, in registration order,
480    /// regardless of whether it is added before or after the routes.
481    pub fn middleware<M>(mut self, middleware: M) -> Self
482    where
483        M: Middleware,
484    {
485        self.middlewares.push(Arc::new(middleware));
486        self
487    }
488}
489
490async fn handle_request(
491    req: hyper::Request<hyper::body::Incoming>,
492    peer: SocketAddr,
493    router: Arc<Router>,
494    middlewares: Arc<Vec<Arc<dyn Middleware>>>,
495    max_body_size: Option<usize>,
496) -> hyper::Response<http_body_util::Full<bytes::Bytes>> {
497    let method = req.method().to_string();
498    let path = req.uri().path().to_string();
499
500    // Convert hyper request to our Request type
501    let (parts, body) = req.into_parts();
502
503    // Enforce body size limit during streaming
504    let body_bytes = if let Some(max_size) = max_body_size {
505        // Check Content-Length header first (fast path)
506        if let Some(content_length) = parts.headers.get(http::header::CONTENT_LENGTH) {
507            if let Ok(length_str) = content_length.to_str() {
508                if let Ok(length) = length_str.parse::<usize>() {
509                    if length > max_size {
510                        let error_response = Response::from_error(Error::custom(
511                            413,
512                            format!("Request body too large (max {} bytes)", max_size),
513                        ));
514                        return error_response.into_hyper();
515                    }
516                }
517            }
518        }
519
520        // Limit actual stream reading (prevents bypass via chunked encoding)
521        let limited = http_body_util::Limited::new(body, max_size);
522        match limited.collect().await {
523            Ok(collected) => collected.to_bytes(),
524            Err(_) => {
525                // Body exceeded limit during reading
526                let error_response = Response::from_error(Error::custom(
527                    413,
528                    format!("Request body too large (max {} bytes)", max_size),
529                ));
530                return error_response.into_hyper();
531            }
532        }
533    } else {
534        match body.collect().await {
535            Ok(collected) => collected.to_bytes(),
536            Err(_) => bytes::Bytes::new(),
537        }
538    };
539
540    let mut request = Request::new(parts.method, parts.uri, parts.headers, body_bytes);
541    request.extensions = parts.extensions;
542    request.remote_addr = Some(peer);
543
544    // Match route. A miss still runs the middleware chain (CORS preflight,
545    // request ids, logging, security headers) and ends in a 404.
546    let handler: Option<DynHandler> = match router.match_route(&method, &path) {
547        Ok((handler, params)) => {
548            request.params = params;
549            Some(handler)
550        }
551        Err(_) => None,
552    };
553
554    let result = if middlewares.is_empty() {
555        dispatch(handler, request).await
556    } else {
557        // Route handler (or the 404) runs as the last link of the chain
558        let mut chain = middlewares.to_vec();
559        let final_handler = Arc::new(move |req: Request, _next: Next| {
560            let handler = handler.clone();
561            async move { dispatch(handler, req).await }
562        });
563        chain.push(final_handler as Arc<dyn Middleware>);
564
565        Next::new(Arc::new(chain)).run(request).await
566    };
567
568    // Convert Result to Response
569    let response = match result {
570        Ok(resp) => resp,
571        Err(err) => Response::from_error(err),
572    };
573
574    response.into_hyper()
575}
576
577async fn dispatch(handler: Option<DynHandler>, req: Request) -> Result<Response> {
578    match handler {
579        Some(handler) => handler.call(req).await,
580        None => Err(Error::not_found("Route not found")),
581    }
582}