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