Skip to main content

fastapi_core/
app.rs

1//! Application builder and runtime for fastapi_rust.
2//!
3//! This module provides a fluent API for building web applications with
4//! type-safe route registration, middleware ordering, and shared state.
5//!
6//! # Design Principles
7//!
8//! - **Fluent Builder API**: Chain methods to configure the application
9//! - **Type-Safe State**: Shared state is type-checked at compile time
10//! - **Explicit Middleware Order**: Middleware runs in registration order
11//! - **Compile-Time Validation**: Invalid configurations fail at compile time
12//!
13//! # Example
14//!
15//! ```ignore
16//! use fastapi_core::app::{App, AppBuilder};
17//! use fastapi_core::{Request, Response, RequestContext};
18//!
19//! async fn hello(ctx: &RequestContext, req: &mut Request) -> Response {
20//!     Response::ok().body_text("Hello, World!")
21//! }
22//!
23//! async fn health(ctx: &RequestContext, req: &mut Request) -> Response {
24//!     Response::ok().body_json(&serde_json::json!({"status": "healthy"}))
25//! }
26//!
27//! let app = App::builder()
28//!     .route("/", Method::Get, hello)
29//!     .route("/health", Method::Get, health)
30//!     .middleware(RequestIdMiddleware::new())
31//!     .middleware(LoggingMiddleware::new())
32//!     .build();
33//! ```
34
35use std::any::{Any, TypeId};
36use std::collections::HashMap;
37use std::future::Future;
38use std::pin::Pin;
39use std::sync::Arc;
40
41use crate::context::RequestContext;
42use crate::middleware::{BoxFuture, Handler, Middleware, MiddlewareStack};
43use crate::request::{Method, Request};
44use crate::response::{Response, StatusCode};
45use crate::shutdown::ShutdownController;
46use fastapi_router::{Route, RouteLookup, Router};
47
48// ============================================================================
49// Lifecycle Hook Types
50// ============================================================================
51
52/// A startup hook that runs before the server starts accepting connections.
53pub enum StartupHook {
54    /// Synchronous startup function.
55    Sync(Box<dyn FnOnce() -> Result<(), StartupHookError> + Send>),
56    /// Factory for async startup future.
57    AsyncFactory(
58        Box<
59            dyn FnOnce() -> Pin<Box<dyn Future<Output = Result<(), StartupHookError>> + Send>>
60                + Send,
61        >,
62    ),
63}
64
65impl StartupHook {
66    /// Create a synchronous startup hook.
67    pub fn sync<F>(f: F) -> Self
68    where
69        F: FnOnce() -> Result<(), StartupHookError> + Send + 'static,
70    {
71        Self::Sync(Box::new(f))
72    }
73
74    /// Create an async startup hook.
75    pub fn async_fn<F, Fut>(f: F) -> Self
76    where
77        F: FnOnce() -> Fut + Send + 'static,
78        Fut: Future<Output = Result<(), StartupHookError>> + Send + 'static,
79    {
80        Self::AsyncFactory(Box::new(move || Box::pin(f())))
81    }
82
83    /// Run the hook synchronously.
84    ///
85    /// For async hooks, this returns the future to await.
86    pub fn run(
87        self,
88    ) -> Result<
89        Option<Pin<Box<dyn Future<Output = Result<(), StartupHookError>> + Send>>>,
90        StartupHookError,
91    > {
92        match self {
93            Self::Sync(f) => f().map(|()| None),
94            Self::AsyncFactory(f) => Ok(Some(f())),
95        }
96    }
97}
98
99/// Error returned when a startup hook fails.
100#[derive(Debug)]
101pub struct StartupHookError {
102    /// Name of the hook that failed (if provided).
103    pub hook_name: Option<String>,
104    /// The underlying error message.
105    pub message: String,
106    /// Whether the application should abort startup.
107    pub abort: bool,
108}
109
110impl StartupHookError {
111    /// Create a new startup hook error.
112    pub fn new(message: impl Into<String>) -> Self {
113        Self {
114            hook_name: None,
115            message: message.into(),
116            abort: true,
117        }
118    }
119
120    /// Set the hook name.
121    #[must_use]
122    pub fn with_hook_name(mut self, name: impl Into<String>) -> Self {
123        self.hook_name = Some(name.into());
124        self
125    }
126
127    /// Set whether to abort startup.
128    #[must_use]
129    pub fn with_abort(mut self, abort: bool) -> Self {
130        self.abort = abort;
131        self
132    }
133
134    /// Create an error that doesn't abort startup (just logs warning).
135    pub fn non_fatal(message: impl Into<String>) -> Self {
136        Self {
137            hook_name: None,
138            message: message.into(),
139            abort: false,
140        }
141    }
142}
143
144impl std::fmt::Display for StartupHookError {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        if let Some(name) = &self.hook_name {
147            write!(f, "Startup hook '{}' failed: {}", name, self.message)
148        } else {
149            write!(f, "Startup hook failed: {}", self.message)
150        }
151    }
152}
153
154impl std::error::Error for StartupHookError {}
155
156/// Outcome of running all startup hooks.
157#[derive(Debug)]
158pub enum StartupOutcome {
159    /// All hooks succeeded.
160    Success,
161    /// Some hooks had non-fatal errors (logged but continued).
162    PartialSuccess {
163        /// Number of hooks that failed with non-fatal errors.
164        warnings: usize,
165    },
166    /// A fatal hook error aborted startup.
167    Aborted(StartupHookError),
168}
169
170impl StartupOutcome {
171    /// Returns true if startup can proceed (Success or PartialSuccess).
172    #[must_use]
173    pub fn can_proceed(&self) -> bool {
174        !matches!(self, Self::Aborted(_))
175    }
176
177    /// Returns the abort error, if any.
178    pub fn into_error(self) -> Option<StartupHookError> {
179        match self {
180            Self::Aborted(e) => Some(e),
181            _ => None,
182        }
183    }
184}
185
186/// A boxed handler function.
187///
188/// The returned future may borrow the context and request for the duration of
189/// the call (`BoxFuture<'a, _>`), which is what extractor-based handlers and
190/// `#[get]`-style proc-macro routes need: they `await` `FromRequest` on `req`
191/// and hand `ctx.cx()` to the handler.
192pub type BoxHandler = Box<
193    dyn for<'a> Fn(&'a RequestContext, &'a mut Request) -> BoxFuture<'a, Response> + Send + Sync,
194>;
195
196/// A boxed websocket handler function.
197pub type BoxWebSocketHandler = Box<
198    dyn Fn(
199            &RequestContext,
200            &mut Request,
201            crate::websocket::WebSocket,
202        ) -> std::pin::Pin<
203            Box<dyn Future<Output = Result<(), crate::websocket::WebSocketError>> + Send>,
204        > + Send
205        + Sync,
206>;
207
208/// A registered route with its handler.
209#[derive(Clone)]
210pub struct RouteEntry {
211    /// The HTTP method for this route.
212    pub method: Method,
213    /// The path pattern for this route.
214    pub path: String,
215    /// Optional router/OpenAPI metadata for this route.
216    ///
217    /// When routes are created by proc-macros, we preserve a full `fastapi_router::Route`
218    /// so OpenAPI generation can use stable operation IDs, tags, parameters, etc.
219    meta: Option<fastapi_router::Route>,
220    /// The handler function.
221    handler: Arc<BoxHandler>,
222}
223
224impl RouteEntry {
225    /// Creates a new route entry from a handler whose future is `'static`
226    /// (it does not borrow the context or request beyond the call).
227    ///
228    /// Plain `fn(&RequestContext, &mut Request) -> Ready<Response>` handlers and
229    /// `async fn` handlers that clone what they need fit here. Handlers that must
230    /// borrow `ctx`/`req` across an `.await` use [`RouteEntry::new_boxed`].
231    pub fn new<H, Fut>(method: Method, path: impl Into<String>, handler: H) -> Self
232    where
233        H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
234        Fut: Future<Output = Response> + Send + 'static,
235    {
236        Self::new_boxed(method, path, move |ctx, req| {
237            Box::pin(handler(ctx, req)) as BoxFuture<'_, Response>
238        })
239    }
240
241    /// Creates a new route entry from a handler that returns a [`BoxFuture`]
242    /// borrowing the context and request for the call's duration.
243    ///
244    /// This is the general form: extractor-based handlers `await` `FromRequest`
245    /// on `req` and pass `ctx.cx()` through, so their futures are tied to the
246    /// call. `#[get]`-style proc-macro routes are built this way via
247    /// [`RouteEntry::from_route`].
248    pub fn new_boxed<H>(method: Method, path: impl Into<String>, handler: H) -> Self
249    where
250        H: for<'a> Fn(&'a RequestContext, &'a mut Request) -> BoxFuture<'a, Response>
251            + Send
252            + Sync
253            + 'static,
254    {
255        let handler: BoxHandler = Box::new(handler);
256        Self {
257            method,
258            path: path.into(),
259            meta: None,
260            handler: Arc::new(handler),
261        }
262    }
263
264    /// Creates a new route entry from a `fastapi_router::Route` metadata object.
265    ///
266    /// This is the constructor proc-macro generated routes use, since it preserves
267    /// OpenAPI metadata (operation_id, tags, request body, etc). The handler has
268    /// the borrowing form of [`RouteEntry::new_boxed`].
269    pub fn from_route<H>(route: fastapi_router::Route, handler: H) -> Self
270    where
271        H: for<'a> Fn(&'a RequestContext, &'a mut Request) -> BoxFuture<'a, Response>
272            + Send
273            + Sync
274            + 'static,
275    {
276        let method = route.method;
277        let path = route.path.clone();
278        let mut entry = Self::new_boxed(method, path, handler);
279        entry.meta = Some(route);
280        entry
281    }
282
283    /// Returns the preserved route metadata, if any.
284    pub fn route_meta(&self) -> Option<&fastapi_router::Route> {
285        self.meta.as_ref()
286    }
287
288    /// Calls the handler with the given context and request.
289    pub async fn call(&self, ctx: &RequestContext, req: &mut Request) -> Response {
290        (self.handler)(ctx, req).await
291    }
292}
293
294impl std::fmt::Debug for RouteEntry {
295    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296        f.debug_struct("RouteEntry")
297            .field("method", &self.method)
298            .field("path", &self.path)
299            .field("meta", &self.meta.as_ref().map(|r| r.operation_id.as_str()))
300            .finish_non_exhaustive()
301    }
302}
303
304/// A registered websocket route with its handler.
305#[derive(Clone)]
306pub struct WebSocketRouteEntry {
307    /// The path pattern for this websocket route.
308    pub path: String,
309    /// Handler function.
310    handler: Arc<BoxWebSocketHandler>,
311}
312
313impl WebSocketRouteEntry {
314    /// Create a new websocket route entry.
315    pub fn new<H, Fut>(path: impl Into<String>, handler: H) -> Self
316    where
317        H: Fn(&RequestContext, &mut Request, crate::websocket::WebSocket) -> Fut
318            + Send
319            + Sync
320            + 'static,
321        Fut: Future<Output = Result<(), crate::websocket::WebSocketError>> + Send + 'static,
322    {
323        let handler: BoxWebSocketHandler = Box::new(move |ctx, req, ws| {
324            let fut = handler(ctx, req, ws);
325            Box::pin(fut)
326        });
327        Self {
328            path: path.into(),
329            handler: Arc::new(handler),
330        }
331    }
332
333    /// Calls the websocket handler.
334    pub async fn call(
335        &self,
336        ctx: &RequestContext,
337        req: &mut Request,
338        ws: crate::websocket::WebSocket,
339    ) -> Result<(), crate::websocket::WebSocketError> {
340        (self.handler)(ctx, req, ws).await
341    }
342}
343
344impl std::fmt::Debug for WebSocketRouteEntry {
345    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346        f.debug_struct("WebSocketRouteEntry")
347            .field("path", &self.path)
348            .finish_non_exhaustive()
349    }
350}
351
352/// Type-safe application state container.
353///
354/// State is stored by type and can be accessed by handlers through
355/// the `State<T>` extractor.
356#[derive(Default)]
357pub struct StateContainer {
358    state: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
359}
360
361impl StateContainer {
362    /// Creates a new empty state container.
363    #[must_use]
364    pub fn new() -> Self {
365        Self {
366            state: HashMap::new(),
367        }
368    }
369
370    /// Inserts a value into the state container.
371    ///
372    /// If a value of the same type already exists, it is replaced.
373    pub fn insert<T: Send + Sync + 'static>(&mut self, value: T) {
374        self.state.insert(TypeId::of::<T>(), Arc::new(value));
375    }
376
377    /// Gets a reference to a value in the state container.
378    pub fn get<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
379        self.state
380            .get(&TypeId::of::<T>())
381            .and_then(|v| Arc::clone(v).downcast::<T>().ok())
382    }
383
384    /// Returns true if the state container contains a value of type T.
385    pub fn contains<T: 'static>(&self) -> bool {
386        self.state.contains_key(&TypeId::of::<T>())
387    }
388
389    /// Returns the number of values in the state container.
390    pub fn len(&self) -> usize {
391        self.state.len()
392    }
393
394    /// Returns true if the state container is empty.
395    pub fn is_empty(&self) -> bool {
396        self.state.is_empty()
397    }
398}
399
400impl std::fmt::Debug for StateContainer {
401    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402        f.debug_struct("StateContainer")
403            .field("count", &self.state.len())
404            .finish()
405    }
406}
407
408// ============================================================================
409// Exception Handler Registry
410// ============================================================================
411
412/// A boxed exception handler function.
413///
414/// The handler receives the RequestContext and a boxed error, and returns a Response.
415pub type BoxExceptionHandler = Box<
416    dyn Fn(&RequestContext, Box<dyn std::error::Error + Send + Sync>) -> Response + Send + Sync,
417>;
418
419/// Registry for custom exception handlers.
420///
421/// This allows applications to register handlers for specific error types,
422/// converting errors into HTTP responses in a customizable way.
423///
424/// # Default Handlers
425///
426/// The registry comes with default handlers for common error types:
427/// - [`HttpError`](crate::HttpError) → JSON response with status/detail
428/// - [`ValidationErrors`](crate::ValidationErrors) → 422 with error list
429///
430/// # Example
431///
432/// ```ignore
433/// use fastapi_core::app::ExceptionHandlers;
434/// use fastapi_core::{RequestContext, Response, HttpError};
435///
436/// #[derive(Debug)]
437/// struct MyCustomError(String);
438///
439/// impl std::fmt::Display for MyCustomError {
440///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
441///         write!(f, "Custom error: {}", self.0)
442///     }
443/// }
444///
445/// impl std::error::Error for MyCustomError {}
446///
447/// let handlers = ExceptionHandlers::new()
448///     .handler(|_ctx, err: MyCustomError| {
449///         Response::with_status(StatusCode::BAD_REQUEST)
450///             .body_json(&serde_json::json!({"error": err.0}))
451///     });
452/// ```
453#[derive(Default)]
454pub struct ExceptionHandlers {
455    handlers: HashMap<TypeId, BoxExceptionHandler>,
456}
457
458impl ExceptionHandlers {
459    /// Creates a new empty exception handler registry.
460    #[must_use]
461    pub fn new() -> Self {
462        Self {
463            handlers: HashMap::new(),
464        }
465    }
466
467    /// Creates a registry with default handlers for common error types.
468    #[must_use]
469    pub fn with_defaults() -> Self {
470        let mut handlers = Self::new();
471
472        // Default handler for HttpError
473        handlers.register::<crate::HttpError>(|_ctx, err| {
474            use crate::IntoResponse;
475            err.into_response()
476        });
477
478        // Default handler for ValidationErrors
479        handlers.register::<crate::ValidationErrors>(|_ctx, err| {
480            use crate::IntoResponse;
481            err.into_response()
482        });
483
484        handlers
485    }
486
487    /// Registers a handler for a specific error type.
488    ///
489    /// The handler receives the error value directly (not boxed) for type safety.
490    /// If a handler for the same type already exists, it is replaced.
491    pub fn register<E>(
492        &mut self,
493        handler: impl Fn(&RequestContext, E) -> Response + Send + Sync + 'static,
494    ) where
495        E: std::error::Error + Send + Sync + 'static,
496    {
497        let boxed_handler: BoxExceptionHandler = Box::new(move |ctx, err| {
498            // Try to downcast the error to the expected type
499            match err.downcast::<E>() {
500                Ok(typed_err) => handler(ctx, *typed_err),
501                Err(_) => {
502                    // This shouldn't happen if the registry is used correctly
503                    Response::with_status(StatusCode::INTERNAL_SERVER_ERROR)
504                }
505            }
506        });
507        self.handlers.insert(TypeId::of::<E>(), boxed_handler);
508    }
509
510    /// Registers a handler for a specific error type (builder pattern).
511    #[must_use]
512    pub fn handler<E>(
513        mut self,
514        handler: impl Fn(&RequestContext, E) -> Response + Send + Sync + 'static,
515    ) -> Self
516    where
517        E: std::error::Error + Send + Sync + 'static,
518    {
519        self.register::<E>(handler);
520        self
521    }
522
523    /// Handles an error by finding and invoking the appropriate handler.
524    ///
525    /// Returns `Some(Response)` if a handler was found for the error type,
526    /// or `None` if no handler is registered.
527    pub fn handle<E>(&self, ctx: &RequestContext, err: E) -> Option<Response>
528    where
529        E: std::error::Error + Send + Sync + 'static,
530    {
531        let type_id = TypeId::of::<E>();
532        self.handlers
533            .get(&type_id)
534            .map(|handler| handler(ctx, Box::new(err)))
535    }
536
537    /// Handles an error, falling back to a default 500 response if no handler is found.
538    pub fn handle_or_default<E>(&self, ctx: &RequestContext, err: E) -> Response
539    where
540        E: std::error::Error + Send + Sync + 'static,
541    {
542        self.handle(ctx, err)
543            .unwrap_or_else(|| Response::with_status(StatusCode::INTERNAL_SERVER_ERROR))
544    }
545
546    /// Returns true if a handler is registered for the given error type.
547    pub fn has_handler<E: 'static>(&self) -> bool {
548        self.handlers.contains_key(&TypeId::of::<E>())
549    }
550
551    /// Returns the number of registered handlers.
552    pub fn len(&self) -> usize {
553        self.handlers.len()
554    }
555
556    /// Returns true if no handlers are registered.
557    pub fn is_empty(&self) -> bool {
558        self.handlers.is_empty()
559    }
560
561    /// Merges another handler registry into this one.
562    ///
563    /// Handlers from `other` will override handlers in `self` for the same error types.
564    pub fn merge(&mut self, other: ExceptionHandlers) {
565        self.handlers.extend(other.handlers);
566    }
567}
568
569impl std::fmt::Debug for ExceptionHandlers {
570    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
571        f.debug_struct("ExceptionHandlers")
572            .field("count", &self.handlers.len())
573            .finish()
574    }
575}
576
577/// Application configuration.
578#[derive(Debug, Clone)]
579pub struct AppConfig {
580    /// Application name (used in logging and OpenAPI).
581    pub name: String,
582    /// Application version.
583    pub version: String,
584    /// Enable debug mode.
585    pub debug: bool,
586    /// Root path for proxied deployments (FastAPI's `root_path`).
587    ///
588    /// When set, URL generation and redirects should be prefixed with this path.
589    pub root_path: String,
590    /// Whether to include the `root_path` in OpenAPI `servers` entries.
591    pub root_path_in_servers: bool,
592    /// Trailing slash normalization mode for routing.
593    pub trailing_slash_mode: crate::routing::TrailingSlashMode,
594    /// Debug surface configuration (e.g. debug endpoints / debug auth).
595    pub debug_config: crate::error::DebugConfig,
596    /// Maximum request body size in bytes.
597    pub max_body_size: usize,
598    /// Default request timeout in milliseconds.
599    pub request_timeout_ms: u64,
600}
601
602impl Default for AppConfig {
603    fn default() -> Self {
604        Self {
605            name: String::from("fastapi_rust"),
606            version: String::from("0.1.0"),
607            debug: false,
608            root_path: String::new(),
609            root_path_in_servers: false,
610            trailing_slash_mode: crate::routing::TrailingSlashMode::Strict,
611            debug_config: crate::error::DebugConfig::default(),
612            max_body_size: 1024 * 1024, // 1MB
613            request_timeout_ms: 30_000, // 30 seconds
614        }
615    }
616}
617
618impl AppConfig {
619    /// Creates a new configuration with defaults.
620    #[must_use]
621    pub fn new() -> Self {
622        Self::default()
623    }
624
625    /// Sets the application name.
626    #[must_use]
627    pub fn name(mut self, name: impl Into<String>) -> Self {
628        self.name = name.into();
629        self
630    }
631
632    /// Sets the application version.
633    #[must_use]
634    pub fn version(mut self, version: impl Into<String>) -> Self {
635        self.version = version.into();
636        self
637    }
638
639    /// Enables or disables debug mode.
640    #[must_use]
641    pub fn debug(mut self, debug: bool) -> Self {
642        self.debug = debug;
643        self
644    }
645
646    /// Set the root path for proxied deployments.
647    ///
648    /// The value is normalized: trailing slashes are stripped, and the value
649    /// must start with `/` (or be empty). This prevents misconfigured
650    /// root paths from producing broken redirects or URL generation.
651    #[must_use]
652    pub fn root_path(mut self, root_path: impl Into<String>) -> Self {
653        let mut rp = root_path.into();
654        // Strip trailing slashes (consistent with UrlRegistry::with_root_path)
655        while rp.ends_with('/') {
656            rp.pop();
657        }
658        self.root_path = rp;
659        self
660    }
661
662    /// Control whether `root_path` is included in OpenAPI servers.
663    #[must_use]
664    pub fn root_path_in_servers(mut self, enabled: bool) -> Self {
665        self.root_path_in_servers = enabled;
666        self
667    }
668
669    /// Configure trailing slash normalization for routing.
670    #[must_use]
671    pub fn trailing_slash_mode(mut self, mode: crate::routing::TrailingSlashMode) -> Self {
672        self.trailing_slash_mode = mode;
673        self
674    }
675
676    /// Configure debug behavior.
677    #[must_use]
678    pub fn debug_config(mut self, config: crate::error::DebugConfig) -> Self {
679        self.debug_config = config;
680        self
681    }
682
683    /// Sets the maximum request body size.
684    #[must_use]
685    pub fn max_body_size(mut self, size: usize) -> Self {
686        self.max_body_size = size;
687        self
688    }
689
690    /// Sets the default request timeout in milliseconds.
691    #[must_use]
692    pub fn request_timeout_ms(mut self, timeout: u64) -> Self {
693        self.request_timeout_ms = timeout;
694        self
695    }
696}
697
698// ============================================================================
699// OpenAPI Configuration
700// ============================================================================
701
702/// Configuration for OpenAPI documentation generation.
703///
704/// When enabled, the application will automatically generate an OpenAPI 3.1
705/// specification and serve it at the configured endpoint (default `/openapi.json`).
706///
707/// # Example
708///
709/// ```ignore
710/// use fastapi_core::app::{App, OpenApiConfig};
711///
712/// let app = App::builder()
713///     .openapi(OpenApiConfig::new()
714///         .title("My API")
715///         .version("1.0.0")
716///         .description("A sample API"))
717///     .build();
718/// ```
719#[derive(Debug, Clone)]
720pub struct OpenApiConfig {
721    /// Whether OpenAPI documentation is enabled.
722    pub enabled: bool,
723    /// API title for the OpenAPI spec.
724    pub title: String,
725    /// API version for the OpenAPI spec.
726    pub version: String,
727    /// API description.
728    pub description: Option<String>,
729    /// Path to serve the OpenAPI JSON (default: "/openapi.json").
730    pub openapi_path: String,
731    /// Servers to include in the spec.
732    pub servers: Vec<(String, Option<String>)>,
733    /// Tags for organizing operations.
734    pub tags: Vec<(String, Option<String>)>,
735}
736
737impl Default for OpenApiConfig {
738    fn default() -> Self {
739        Self {
740            enabled: true,
741            title: "FastAPI Rust".to_string(),
742            version: "0.1.0".to_string(),
743            description: None,
744            openapi_path: "/openapi.json".to_string(),
745            servers: Vec::new(),
746            tags: Vec::new(),
747        }
748    }
749}
750
751impl OpenApiConfig {
752    /// Create a new OpenAPI configuration with defaults.
753    #[must_use]
754    pub fn new() -> Self {
755        Self::default()
756    }
757
758    /// Set the API title.
759    #[must_use]
760    pub fn title(mut self, title: impl Into<String>) -> Self {
761        self.title = title.into();
762        self
763    }
764
765    /// Set the API version.
766    #[must_use]
767    pub fn version(mut self, version: impl Into<String>) -> Self {
768        self.version = version.into();
769        self
770    }
771
772    /// Set the API description.
773    #[must_use]
774    pub fn description(mut self, description: impl Into<String>) -> Self {
775        self.description = Some(description.into());
776        self
777    }
778
779    /// Set the path for the OpenAPI JSON endpoint.
780    #[must_use]
781    pub fn path(mut self, path: impl Into<String>) -> Self {
782        self.openapi_path = path.into();
783        self
784    }
785
786    /// Add a server to the spec.
787    #[must_use]
788    pub fn server(mut self, url: impl Into<String>, description: Option<String>) -> Self {
789        self.servers.push((url.into(), description));
790        self
791    }
792
793    /// Add a tag to the spec.
794    #[must_use]
795    pub fn tag(mut self, name: impl Into<String>, description: Option<String>) -> Self {
796        self.tags.push((name.into(), description));
797        self
798    }
799
800    /// Disable OpenAPI documentation.
801    #[must_use]
802    pub fn disable(mut self) -> Self {
803        self.enabled = false;
804        self
805    }
806}
807
808/// Builder for constructing an [`App`].
809///
810/// Use this to configure routes, middleware, and shared state before
811/// building the final application.
812///
813/// # Example
814///
815/// ```ignore
816/// let app = App::builder()
817///     .config(AppConfig::new().name("My API"))
818///     .state(DatabasePool::new())
819///     .middleware(LoggingMiddleware::new())
820///     .on_startup(|| {
821///         println!("Server starting...");
822///         Ok(())
823///     })
824///     .on_shutdown(|| {
825///         println!("Server stopping...");
826///     })
827///     .route("/", Method::Get, index_handler)
828///     .route("/items", Method::Get, list_items)
829///     .route("/items", Method::Post, create_item)
830///     .route("/items/{id}", Method::Get, get_item)
831///     .build();
832/// ```
833pub struct AppBuilder {
834    config: AppConfig,
835    routes: Vec<RouteEntry>,
836    ws_routes: Vec<WebSocketRouteEntry>,
837    middleware: Vec<Arc<dyn Middleware>>,
838    state: StateContainer,
839    exception_handlers: ExceptionHandlers,
840    startup_hooks: Vec<StartupHook>,
841    shutdown_hooks: Vec<Box<dyn FnOnce() + Send>>,
842    async_shutdown_hooks: Vec<Box<dyn FnOnce() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send>>,
843    openapi_config: Option<OpenApiConfig>,
844    docs_config: Option<crate::docs::DocsConfig>,
845    /// API metadata set via [`AppBuilder::title`] / [`AppBuilder::version`] /
846    /// [`AppBuilder::description`]; applied to the OpenAPI info at build time.
847    api_title: Option<String>,
848    api_version: Option<String>,
849    api_description: Option<String>,
850}
851
852impl Default for AppBuilder {
853    fn default() -> Self {
854        Self {
855            config: AppConfig::default(),
856            routes: Vec::new(),
857            ws_routes: Vec::new(),
858            middleware: Vec::new(),
859            state: StateContainer::default(),
860            exception_handlers: ExceptionHandlers::default(),
861            startup_hooks: Vec::new(),
862            shutdown_hooks: Vec::new(),
863            async_shutdown_hooks: Vec::new(),
864            openapi_config: None,
865            docs_config: None,
866            api_title: None,
867            api_version: None,
868            api_description: None,
869        }
870    }
871}
872
873impl AppBuilder {
874    /// Creates a new application builder.
875    #[must_use]
876    pub fn new() -> Self {
877        Self::default()
878    }
879
880    /// Sets the API title (FastAPI's `FastAPI(title=...)`).
881    ///
882    /// Updates [`AppConfig::name`] and, when OpenAPI is enabled via
883    /// [`AppBuilder::openapi`] or [`AppBuilder::docs`], the `info.title` of the
884    /// generated specification. Values set here take precedence over the same
885    /// fields on the [`OpenApiConfig`] passed to [`AppBuilder::openapi`].
886    #[must_use]
887    pub fn title(mut self, title: impl Into<String>) -> Self {
888        let title = title.into();
889        self.config.name.clone_from(&title);
890        self.api_title = Some(title);
891        self
892    }
893
894    /// Sets the API version (FastAPI's `FastAPI(version=...)`).
895    ///
896    /// Updates [`AppConfig::version`] and the OpenAPI `info.version` when OpenAPI
897    /// is enabled; see [`AppBuilder::title`] for precedence.
898    #[must_use]
899    pub fn version(mut self, version: impl Into<String>) -> Self {
900        let version = version.into();
901        self.config.version.clone_from(&version);
902        self.api_version = Some(version);
903        self
904    }
905
906    /// Sets the API description (FastAPI's `FastAPI(description=...)`), used as
907    /// the OpenAPI `info.description` when OpenAPI is enabled; see
908    /// [`AppBuilder::title`] for precedence.
909    #[must_use]
910    pub fn description(mut self, description: impl Into<String>) -> Self {
911        self.api_description = Some(description.into());
912        self
913    }
914
915    /// Sets the application configuration.
916    #[must_use]
917    pub fn config(mut self, config: AppConfig) -> Self {
918        self.config = config;
919        self
920    }
921
922    /// Enables and configures OpenAPI documentation.
923    ///
924    /// When enabled, the application will automatically generate an OpenAPI 3.1
925    /// specification and serve it at the configured endpoint.
926    ///
927    /// # Example
928    ///
929    /// ```ignore
930    /// let app = App::builder()
931    ///     .openapi(OpenApiConfig::new()
932    ///         .title("My API")
933    ///         .version("1.0.0")
934    ///         .description("A sample API"))
935    ///     .build();
936    /// ```
937    #[must_use]
938    pub fn openapi(mut self, config: OpenApiConfig) -> Self {
939        self.openapi_config = Some(config);
940        self
941    }
942
943    /// Enables and configures interactive API documentation endpoints.
944    ///
945    /// This wires [`crate::docs::DocsConfig`] into the application build and ensures an
946    /// OpenAPI JSON endpoint is served at `config.openapi_path` unless overridden later.
947    ///
948    /// Notes:
949    /// - Docs endpoints are appended during `build()` so they don't appear in the OpenAPI spec.
950    /// - If OpenAPI is already configured, its `openapi_path` is updated to match `DocsConfig`.
951    #[must_use]
952    pub fn enable_docs(mut self, mut config: crate::docs::DocsConfig) -> Self {
953        // If the user didn't customize the docs title, default it to the app name.
954        if config.title == crate::docs::DocsConfig::default().title {
955            config.title.clone_from(&self.config.name);
956        }
957
958        // Keep OpenAPI + docs in sync on the OpenAPI JSON path.
959        match self.openapi_config.take() {
960            Some(mut openapi) => {
961                openapi.openapi_path.clone_from(&config.openapi_path);
962                self.openapi_config = Some(openapi);
963            }
964            None => {
965                self.openapi_config = Some(
966                    OpenApiConfig::new()
967                        .title(self.config.name.clone())
968                        .version(self.config.version.clone())
969                        .path(config.openapi_path.clone()),
970                );
971            }
972        }
973
974        self.docs_config = Some(config);
975        self
976    }
977
978    /// Adds a route to the application.
979    ///
980    /// Routes are matched in the order they are added.
981    #[must_use]
982    pub fn route<H, Fut>(mut self, path: impl Into<String>, method: Method, handler: H) -> Self
983    where
984        H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
985        Fut: Future<Output = Response> + Send + 'static,
986    {
987        self.routes.push(RouteEntry::new(method, path, handler));
988        self
989    }
990
991    /// Adds a pre-built [`RouteEntry`] to the application.
992    ///
993    /// This is primarily used by proc-macro generated route builders that already
994    /// know the method/path and can build an extraction wrapper.
995    #[must_use]
996    pub fn route_entry(mut self, entry: RouteEntry) -> Self {
997        self.routes.push(entry);
998        self
999    }
1000
1001    /// Adds a websocket route to the application.
1002    ///
1003    /// WebSocket routes are matched only when the server receives a valid
1004    /// websocket upgrade request. They do not appear in OpenAPI output.
1005    #[must_use]
1006    pub fn websocket<H, Fut>(mut self, path: impl Into<String>, handler: H) -> Self
1007    where
1008        H: Fn(&RequestContext, &mut Request, crate::websocket::WebSocket) -> Fut
1009            + Send
1010            + Sync
1011            + 'static,
1012        Fut: Future<Output = Result<(), crate::websocket::WebSocketError>> + Send + 'static,
1013    {
1014        self.ws_routes.push(WebSocketRouteEntry::new(path, handler));
1015        self
1016    }
1017
1018    /// Adds a GET route.
1019    #[must_use]
1020    pub fn get<H, Fut>(self, path: impl Into<String>, handler: H) -> Self
1021    where
1022        H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
1023        Fut: Future<Output = Response> + Send + 'static,
1024    {
1025        self.route(path, Method::Get, handler)
1026    }
1027
1028    /// Adds a POST route.
1029    #[must_use]
1030    pub fn post<H, Fut>(self, path: impl Into<String>, handler: H) -> Self
1031    where
1032        H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
1033        Fut: Future<Output = Response> + Send + 'static,
1034    {
1035        self.route(path, Method::Post, handler)
1036    }
1037
1038    /// Adds a PUT route.
1039    #[must_use]
1040    pub fn put<H, Fut>(self, path: impl Into<String>, handler: H) -> Self
1041    where
1042        H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
1043        Fut: Future<Output = Response> + Send + 'static,
1044    {
1045        self.route(path, Method::Put, handler)
1046    }
1047
1048    /// Adds a DELETE route.
1049    #[must_use]
1050    pub fn delete<H, Fut>(self, path: impl Into<String>, handler: H) -> Self
1051    where
1052        H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
1053        Fut: Future<Output = Response> + Send + 'static,
1054    {
1055        self.route(path, Method::Delete, handler)
1056    }
1057
1058    /// Adds a PATCH route.
1059    #[must_use]
1060    pub fn patch<H, Fut>(self, path: impl Into<String>, handler: H) -> Self
1061    where
1062        H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
1063        Fut: Future<Output = Response> + Send + 'static,
1064    {
1065        self.route(path, Method::Patch, handler)
1066    }
1067
1068    /// Adds middleware to the application.
1069    ///
1070    /// Middleware is executed in the order it is added:
1071    /// - `before` hooks run first-to-last
1072    /// - `after` hooks run last-to-first
1073    #[must_use]
1074    pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> Self {
1075        self.middleware.push(Arc::new(middleware));
1076        self
1077    }
1078
1079    /// Adds shared state to the application.
1080    ///
1081    /// State can be accessed by handlers through the `State<T>` extractor.
1082    #[must_use]
1083    pub fn state<T: Send + Sync + 'static>(mut self, state: T) -> Self {
1084        self.state.insert(state);
1085        self
1086    }
1087
1088    /// Registers a custom exception handler for a specific error type.
1089    ///
1090    /// When an error of type `E` occurs during request handling, the registered
1091    /// handler will be called to convert it into a response.
1092    ///
1093    /// # Example
1094    ///
1095    /// ```ignore
1096    /// #[derive(Debug)]
1097    /// struct AuthError(String);
1098    ///
1099    /// impl std::fmt::Display for AuthError {
1100    ///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1101    ///         write!(f, "Auth error: {}", self.0)
1102    ///     }
1103    /// }
1104    ///
1105    /// impl std::error::Error for AuthError {}
1106    ///
1107    /// let app = App::builder()
1108    ///     .exception_handler(|_ctx, err: AuthError| {
1109    ///         Response::with_status(StatusCode::UNAUTHORIZED)
1110    ///             .header("www-authenticate", b"Bearer".to_vec())
1111    ///             .body_json(&json!({"error": err.0}))
1112    ///     })
1113    ///     .build();
1114    /// ```
1115    #[must_use]
1116    pub fn exception_handler<E, H>(mut self, handler: H) -> Self
1117    where
1118        E: std::error::Error + Send + Sync + 'static,
1119        H: Fn(&RequestContext, E) -> Response + Send + Sync + 'static,
1120    {
1121        self.exception_handlers.register::<E>(handler);
1122        self
1123    }
1124
1125    /// Sets the exception handlers registry.
1126    ///
1127    /// This replaces any previously registered handlers.
1128    #[must_use]
1129    pub fn exception_handlers(mut self, handlers: ExceptionHandlers) -> Self {
1130        self.exception_handlers = handlers;
1131        self
1132    }
1133
1134    /// Uses default exception handlers for common error types.
1135    ///
1136    /// This registers handlers for:
1137    /// - [`HttpError`](crate::HttpError) → JSON response with status/detail
1138    /// - [`ValidationErrors`](crate::ValidationErrors) → 422 with error list
1139    #[must_use]
1140    pub fn with_default_exception_handlers(mut self) -> Self {
1141        self.exception_handlers = ExceptionHandlers::with_defaults();
1142        self
1143    }
1144
1145    // =========================================================================
1146    // Lifecycle Hooks
1147    // =========================================================================
1148
1149    /// Registers a synchronous startup hook.
1150    ///
1151    /// Startup hooks run before the server starts accepting connections,
1152    /// in the order they are registered (FIFO).
1153    ///
1154    /// # Example
1155    ///
1156    /// ```ignore
1157    /// let app = App::builder()
1158    ///     .on_startup(|| {
1159    ///         println!("Connecting to database...");
1160    ///         Ok(())
1161    ///     })
1162    ///     .on_startup(|| {
1163    ///         println!("Loading configuration...");
1164    ///         Ok(())
1165    ///     })
1166    ///     .build();
1167    /// ```
1168    #[must_use]
1169    pub fn on_startup<F>(mut self, hook: F) -> Self
1170    where
1171        F: FnOnce() -> Result<(), StartupHookError> + Send + 'static,
1172    {
1173        self.startup_hooks.push(StartupHook::Sync(Box::new(hook)));
1174        self
1175    }
1176
1177    /// Registers an async startup hook.
1178    ///
1179    /// Async startup hooks are awaited in registration order.
1180    ///
1181    /// # Example
1182    ///
1183    /// ```ignore
1184    /// let app = App::builder()
1185    ///     .on_startup_async(|| async {
1186    ///         let pool = connect_to_database().await?;
1187    ///         Ok(())
1188    ///     })
1189    ///     .build();
1190    /// ```
1191    #[must_use]
1192    pub fn on_startup_async<F, Fut>(mut self, hook: F) -> Self
1193    where
1194        F: FnOnce() -> Fut + Send + 'static,
1195        Fut: Future<Output = Result<(), StartupHookError>> + Send + 'static,
1196    {
1197        self.startup_hooks.push(StartupHook::AsyncFactory(Box::new(
1198            move || Box::pin(hook()),
1199        )));
1200        self
1201    }
1202
1203    /// Registers a synchronous shutdown hook.
1204    ///
1205    /// Shutdown hooks run after the server stops accepting connections
1206    /// and all in-flight requests complete (or are cancelled).
1207    ///
1208    /// Shutdown hooks run in reverse registration order (LIFO), matching
1209    /// typical resource cleanup patterns (last acquired, first released).
1210    ///
1211    /// # Example
1212    ///
1213    /// ```ignore
1214    /// let app = App::builder()
1215    ///     .on_shutdown(|| {
1216    ///         println!("Closing database connections...");
1217    ///     })
1218    ///     .build();
1219    /// ```
1220    #[must_use]
1221    pub fn on_shutdown<F>(mut self, hook: F) -> Self
1222    where
1223        F: FnOnce() + Send + 'static,
1224    {
1225        self.shutdown_hooks.push(Box::new(hook));
1226        self
1227    }
1228
1229    /// Registers an async shutdown hook.
1230    ///
1231    /// Async shutdown hooks are awaited in reverse registration order (LIFO).
1232    ///
1233    /// # Example
1234    ///
1235    /// ```ignore
1236    /// let app = App::builder()
1237    ///     .on_shutdown_async(|| async {
1238    ///         flush_metrics().await;
1239    ///     })
1240    ///     .build();
1241    /// ```
1242    #[must_use]
1243    pub fn on_shutdown_async<F, Fut>(mut self, hook: F) -> Self
1244    where
1245        F: FnOnce() -> Fut + Send + 'static,
1246        Fut: Future<Output = ()> + Send + 'static,
1247    {
1248        self.async_shutdown_hooks
1249            .push(Box::new(move || Box::pin(hook())));
1250        self
1251    }
1252
1253    /// Returns the number of registered startup hooks.
1254    #[must_use]
1255    pub fn startup_hook_count(&self) -> usize {
1256        self.startup_hooks.len()
1257    }
1258
1259    /// Returns the number of registered shutdown hooks.
1260    #[must_use]
1261    pub fn shutdown_hook_count(&self) -> usize {
1262        self.shutdown_hooks.len() + self.async_shutdown_hooks.len()
1263    }
1264
1265    /// Builds the application.
1266    ///
1267    /// This consumes the builder and returns the configured [`App`].
1268    ///
1269    /// # Panics
1270    ///
1271    /// Panics if any routes conflict (same method + structurally identical path pattern).
1272    #[must_use]
1273    #[allow(clippy::too_many_lines)]
1274    pub fn build(mut self) -> App {
1275        // Builder-level API metadata wins over the OpenApiConfig's own fields.
1276        if let Some(openapi) = self.openapi_config.as_mut() {
1277            if let Some(title) = self.api_title.take() {
1278                openapi.title = title;
1279            }
1280            if let Some(version) = self.api_version.take() {
1281                openapi.version = version;
1282            }
1283            if let Some(description) = self.api_description.take() {
1284                openapi.description = Some(description);
1285            }
1286        }
1287
1288        // Generate OpenAPI spec if configured
1289        let (openapi_spec, openapi_path) = if let Some(ref openapi_config) = self.openapi_config {
1290            if openapi_config.enabled {
1291                let spec = self.generate_openapi_spec(openapi_config);
1292                let spec_json =
1293                    serde_json::to_string_pretty(&spec).unwrap_or_else(|_| "{}".to_string());
1294                (
1295                    Some(Arc::new(spec_json)),
1296                    Some(openapi_config.openapi_path.clone()),
1297                )
1298            } else {
1299                (None, None)
1300            }
1301        } else {
1302            (None, None)
1303        };
1304
1305        // Add OpenAPI endpoint if spec was generated
1306        if let (Some(spec), Some(path)) = (&openapi_spec, &openapi_path) {
1307            let spec_clone = Arc::clone(spec);
1308            self.routes.push(RouteEntry::new(
1309                Method::Get,
1310                path.clone(),
1311                move |_ctx: &RequestContext, _req: &mut Request| {
1312                    let spec = Arc::clone(&spec_clone);
1313                    async move {
1314                        Response::ok()
1315                            .header("content-type", b"application/json".to_vec())
1316                            .body(crate::response::ResponseBody::Bytes(
1317                                spec.as_bytes().to_vec(),
1318                            ))
1319                    }
1320                },
1321            ));
1322        }
1323
1324        // Add interactive docs endpoints (Swagger UI / ReDoc) if configured and OpenAPI is enabled.
1325        //
1326        // These are appended after OpenAPI generation so they do not appear in the OpenAPI spec.
1327        if let (Some(openapi_url), Some(docs_config)) = (openapi_path.clone(), self.docs_config) {
1328            let docs_config = Arc::new(docs_config);
1329            let openapi_url = Arc::new(openapi_url);
1330
1331            if let Some(docs_path) = docs_config.docs_path.clone() {
1332                let cfg = Arc::clone(&docs_config);
1333                let url = Arc::clone(&openapi_url);
1334                self.routes.push(RouteEntry::new(
1335                    Method::Get,
1336                    docs_path.clone(),
1337                    move |_ctx: &RequestContext, _req: &mut Request| {
1338                        let cfg = Arc::clone(&cfg);
1339                        let url = Arc::clone(&url);
1340                        async move { crate::docs::swagger_ui_response(&cfg, &url) }
1341                    },
1342                ));
1343
1344                // FastAPI uses `/docs/oauth2-redirect` by default, relative to docs_path.
1345                let docs_prefix = docs_path.trim_end_matches('/');
1346                let oauth2_redirect_path = if docs_prefix.is_empty() {
1347                    "/oauth2-redirect".to_string()
1348                } else {
1349                    format!("{docs_prefix}/oauth2-redirect")
1350                };
1351                self.routes.push(RouteEntry::new(
1352                    Method::Get,
1353                    oauth2_redirect_path,
1354                    |_ctx: &RequestContext, _req: &mut Request| async move {
1355                        crate::docs::oauth2_redirect_response()
1356                    },
1357                ));
1358            }
1359
1360            if let Some(redoc_path) = docs_config.redoc_path.clone() {
1361                let cfg = Arc::clone(&docs_config);
1362                let url = Arc::clone(&openapi_url);
1363                self.routes.push(RouteEntry::new(
1364                    Method::Get,
1365                    redoc_path,
1366                    move |_ctx: &RequestContext, _req: &mut Request| {
1367                        let cfg = Arc::clone(&cfg);
1368                        let url = Arc::clone(&url);
1369                        async move { crate::docs::redoc_response(&cfg, &url) }
1370                    },
1371                ));
1372            }
1373        }
1374
1375        let mut middleware_stack = MiddlewareStack::with_capacity(self.middleware.len());
1376        for mw in self.middleware {
1377            middleware_stack.push_arc(mw);
1378        }
1379
1380        // Build the trie-based router from registered routes
1381        let mut router = Router::new();
1382        for entry in &self.routes {
1383            let route = entry
1384                .route_meta()
1385                .cloned()
1386                .unwrap_or_else(|| Route::new(entry.method, &entry.path));
1387            router
1388                .add(route)
1389                .expect("route conflict during App::build()");
1390        }
1391
1392        // Build the websocket router separately (so websocket + HTTP can share the same path).
1393        let mut ws_router = Router::new();
1394        for entry in &self.ws_routes {
1395            ws_router
1396                .add(Route::new(Method::Get, &entry.path))
1397                .expect("websocket route conflict during App::build()");
1398        }
1399
1400        App {
1401            config: self.config,
1402            routes: self.routes,
1403            ws_routes: self.ws_routes,
1404            router,
1405            ws_router,
1406            middleware: middleware_stack,
1407            state: Arc::new(self.state),
1408            exception_handlers: Arc::new(self.exception_handlers),
1409            dependency_overrides: Arc::new(crate::dependency::DependencyOverrides::new()),
1410            startup_hooks: parking_lot::Mutex::new(self.startup_hooks),
1411            shutdown_hooks: parking_lot::Mutex::new(self.shutdown_hooks),
1412            async_shutdown_hooks: parking_lot::Mutex::new(self.async_shutdown_hooks),
1413            openapi_spec,
1414        }
1415    }
1416
1417    /// Generate an OpenAPI spec from registered routes.
1418    fn generate_openapi_spec(&self, config: &OpenApiConfig) -> fastapi_openapi::OpenApi {
1419        use fastapi_openapi::{OpenApiBuilder, Operation, Response as OAResponse};
1420        use std::collections::HashMap;
1421
1422        let mut builder = OpenApiBuilder::new(&config.title, &config.version);
1423
1424        // Add description if provided
1425        if let Some(ref desc) = config.description {
1426            builder = builder.description(desc);
1427        }
1428
1429        // Add servers
1430        for (url, desc) in &config.servers {
1431            builder = builder.server(url, desc.clone());
1432        }
1433
1434        // Add tags
1435        for (name, desc) in &config.tags {
1436            builder = builder.tag(name, desc.clone());
1437        }
1438
1439        // Add operations for each registered route
1440        for entry in &self.routes {
1441            if let Some(route) = entry.route_meta() {
1442                builder.add_route(route);
1443                continue;
1444            }
1445
1446            // Create a basic operation with default response
1447            let mut responses = HashMap::new();
1448            responses.insert(
1449                "200".to_string(),
1450                OAResponse {
1451                    description: "Successful response".to_string(),
1452                    content: HashMap::new(),
1453                },
1454            );
1455
1456            let operation = Operation {
1457                operation_id: Some(format!(
1458                    "{}_{}",
1459                    entry.method.as_str().to_lowercase(),
1460                    entry
1461                        .path
1462                        .replace('/', "_")
1463                        .replace(['{', '}'], "")
1464                        .trim_matches('_')
1465                )),
1466                summary: None,
1467                description: None,
1468                tags: Vec::new(),
1469                parameters: Vec::new(),
1470                request_body: None,
1471                responses,
1472                deprecated: false,
1473            };
1474
1475            builder = builder.operation(entry.method.as_str(), &entry.path, operation);
1476        }
1477
1478        builder.build()
1479    }
1480}
1481
1482impl std::fmt::Debug for AppBuilder {
1483    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1484        f.debug_struct("AppBuilder")
1485            .field("config", &self.config)
1486            .field("routes", &self.routes.len())
1487            .field("middleware", &self.middleware.len())
1488            .field("state", &self.state)
1489            .field("exception_handlers", &self.exception_handlers)
1490            .field("startup_hooks", &self.startup_hooks.len())
1491            .field("shutdown_hooks", &self.shutdown_hook_count())
1492            .finish()
1493    }
1494}
1495
1496/// A configured web application.
1497///
1498/// The `App` holds all routes, middleware, state, and lifecycle hooks,
1499/// and provides methods to handle incoming requests.
1500pub struct App {
1501    config: AppConfig,
1502    routes: Vec<RouteEntry>,
1503    ws_routes: Vec<WebSocketRouteEntry>,
1504    router: Router,
1505    ws_router: Router,
1506    middleware: MiddlewareStack,
1507    state: Arc<StateContainer>,
1508    exception_handlers: Arc<ExceptionHandlers>,
1509    dependency_overrides: Arc<crate::dependency::DependencyOverrides>,
1510    startup_hooks: parking_lot::Mutex<Vec<StartupHook>>,
1511    shutdown_hooks: parking_lot::Mutex<Vec<Box<dyn FnOnce() + Send>>>,
1512    async_shutdown_hooks: parking_lot::Mutex<
1513        Vec<Box<dyn FnOnce() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send>>,
1514    >,
1515    /// The generated OpenAPI specification (if enabled).
1516    openapi_spec: Option<Arc<String>>,
1517}
1518
1519impl App {
1520    /// Creates a new application builder.
1521    #[must_use]
1522    pub fn builder() -> AppBuilder {
1523        AppBuilder::new()
1524    }
1525
1526    /// Create an in-process test client for this app.
1527    ///
1528    /// This takes `Arc<Self>` so tests can keep using shared `Arc<App>` values
1529    /// without extra boilerplate.
1530    #[cfg(feature = "testing")]
1531    #[must_use]
1532    pub fn test_client(self: Arc<Self>) -> crate::testing::TestClient<Arc<Self>> {
1533        crate::testing::TestClient::new(self)
1534    }
1535
1536    /// Create an in-process test client with a deterministic seed.
1537    #[cfg(feature = "testing")]
1538    #[must_use]
1539    pub fn test_client_with_seed(
1540        self: Arc<Self>,
1541        seed: u64,
1542    ) -> crate::testing::TestClient<Arc<Self>> {
1543        crate::testing::TestClient::with_seed(self, seed)
1544    }
1545
1546    /// Returns the application configuration.
1547    #[must_use]
1548    pub fn config(&self) -> &AppConfig {
1549        &self.config
1550    }
1551
1552    /// Returns the number of registered routes.
1553    #[must_use]
1554    pub fn route_count(&self) -> usize {
1555        self.routes.len()
1556    }
1557
1558    /// Returns the number of registered websocket routes.
1559    #[must_use]
1560    pub fn websocket_route_count(&self) -> usize {
1561        self.ws_routes.len()
1562    }
1563
1564    /// Returns true if a websocket route matches the given path.
1565    #[must_use]
1566    pub fn has_websocket_route(&self, path: &str) -> bool {
1567        matches!(
1568            self.ws_router.lookup(path, Method::Get),
1569            RouteLookup::Match(_)
1570        )
1571    }
1572
1573    /// Returns an iterator over route metadata (method, path).
1574    ///
1575    /// This is useful for generating OpenAPI specifications or debugging.
1576    pub fn routes(&self) -> impl Iterator<Item = (Method, &str)> {
1577        self.routes.iter().map(|r| (r.method, r.path.as_str()))
1578    }
1579
1580    /// Returns the generated OpenAPI specification JSON, if OpenAPI is enabled.
1581    ///
1582    /// # Example
1583    ///
1584    /// ```ignore
1585    /// let app = App::builder()
1586    ///     .openapi(OpenApiConfig::new().title("My API"))
1587    ///     .build();
1588    ///
1589    /// if let Some(spec) = app.openapi_spec() {
1590    ///     println!("OpenAPI spec: {}", spec);
1591    /// }
1592    /// ```
1593    #[must_use]
1594    pub fn openapi_spec(&self) -> Option<&str> {
1595        self.openapi_spec.as_ref().map(|s| s.as_str())
1596    }
1597
1598    /// Returns the shared state container.
1599    #[must_use]
1600    pub fn state(&self) -> &Arc<StateContainer> {
1601        &self.state
1602    }
1603
1604    /// Gets a reference to shared state of type T.
1605    pub fn get_state<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
1606        self.state.get::<T>()
1607    }
1608
1609    /// Returns the exception handlers registry.
1610    #[must_use]
1611    pub fn exception_handlers(&self) -> &Arc<ExceptionHandlers> {
1612        &self.exception_handlers
1613    }
1614
1615    /// Register a fixed override value for a dependency type.
1616    ///
1617    /// This is a test-focused convenience API; production code typically wires
1618    /// dependencies via [`crate::dependency::FromDependency`] implementations.
1619    pub fn override_dependency_value<T>(&self, value: T)
1620    where
1621        T: crate::dependency::FromDependency,
1622    {
1623        self.dependency_overrides.insert_value(value);
1624    }
1625
1626    /// Clear all registered dependency overrides.
1627    pub fn clear_dependency_overrides(&self) {
1628        self.dependency_overrides.clear();
1629    }
1630
1631    /// Returns the shared dependency overrides registry.
1632    #[must_use]
1633    pub fn dependency_overrides(&self) -> Arc<crate::dependency::DependencyOverrides> {
1634        Arc::clone(&self.dependency_overrides)
1635    }
1636
1637    /// Take request-scoped background tasks (if any) from a request.
1638    ///
1639    /// The HTTP server calls this after writing the response so any deferred work
1640    /// runs outside the main request handler path.
1641    pub fn take_background_tasks(req: &mut Request) -> Option<crate::request::BackgroundTasks> {
1642        req.take_extension::<crate::request::BackgroundTasks>()
1643    }
1644
1645    /// Handles an error using registered exception handlers.
1646    ///
1647    /// If a handler is registered for the error type, it will be invoked.
1648    /// Otherwise, returns `None`.
1649    pub fn handle_error<E>(&self, ctx: &RequestContext, err: E) -> Option<Response>
1650    where
1651        E: std::error::Error + Send + Sync + 'static,
1652    {
1653        self.exception_handlers.handle(ctx, err)
1654    }
1655
1656    /// Handles an error, returning a 500 response if no handler is registered.
1657    pub fn handle_error_or_default<E>(&self, ctx: &RequestContext, err: E) -> Response
1658    where
1659        E: std::error::Error + Send + Sync + 'static,
1660    {
1661        self.exception_handlers.handle_or_default(ctx, err)
1662    }
1663
1664    /// Handles an incoming request.
1665    ///
1666    /// This matches the request against registered routes, runs middleware,
1667    /// and returns the response.
1668    pub async fn handle(&self, ctx: &RequestContext, req: &mut Request) -> Response {
1669        // Use the trie-based router for efficient matching with path parameter extraction
1670        match self.router.lookup(req.path(), req.method()) {
1671            RouteLookup::Match(route_match) => {
1672                // Find the handler by matching the route path
1673                let entry = self.routes.iter().find(|e| {
1674                    e.method == route_match.route.method && e.path == route_match.route.path
1675                });
1676
1677                let Some(entry) = entry else {
1678                    // This should never happen if router and routes are in sync
1679                    return Response::with_status(StatusCode::INTERNAL_SERVER_ERROR);
1680                };
1681
1682                // Store extracted path parameters in the request
1683                if !route_match.params.is_empty() {
1684                    let path_params = crate::extract::PathParams::from_pairs(
1685                        route_match
1686                            .params
1687                            .iter()
1688                            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
1689                            .collect(),
1690                    );
1691                    req.insert_extension(path_params);
1692                }
1693
1694                // Create a handler that wraps the route
1695                let handler = RouteHandler { entry };
1696                self.middleware.execute(&handler, ctx, req).await
1697            }
1698            RouteLookup::MethodNotAllowed { allowed } => {
1699                // Auto-handle `OPTIONS` by returning 204 with an `Allow` header.
1700                // For other methods, return 405 with the `Allow` header.
1701                if req.method() == Method::Options {
1702                    let mut methods = allowed.methods().to_vec();
1703                    if !methods.contains(&Method::Options) {
1704                        methods.push(Method::Options);
1705                    }
1706                    let allow = fastapi_router::AllowedMethods::new(methods);
1707                    Response::with_status(StatusCode::NO_CONTENT)
1708                        .header("allow", allow.header_value().as_bytes().to_vec())
1709                } else {
1710                    Response::with_status(StatusCode::METHOD_NOT_ALLOWED)
1711                        .header("allow", allowed.header_value().as_bytes().to_vec())
1712                }
1713            }
1714            RouteLookup::NotFound => Response::with_status(StatusCode::NOT_FOUND),
1715        }
1716    }
1717
1718    /// Handles an incoming websocket upgrade request after the handshake has been accepted.
1719    ///
1720    /// The HTTP server is responsible for validating the upgrade headers and writing the 101
1721    /// response. This function only performs path matching and calls the websocket handler.
1722    pub async fn handle_websocket(
1723        &self,
1724        ctx: &RequestContext,
1725        req: &mut Request,
1726        ws: crate::websocket::WebSocket,
1727    ) -> Result<(), crate::websocket::WebSocketError> {
1728        match self.ws_router.lookup(req.path(), Method::Get) {
1729            RouteLookup::Match(route_match) => {
1730                let entry = self
1731                    .ws_routes
1732                    .iter()
1733                    .find(|e| e.path == route_match.route.path);
1734                let Some(entry) = entry else {
1735                    return Err(crate::websocket::WebSocketError::Protocol(
1736                        "websocket route missing handler",
1737                    ));
1738                };
1739
1740                if !route_match.params.is_empty() {
1741                    let path_params = crate::extract::PathParams::from_pairs(
1742                        route_match
1743                            .params
1744                            .iter()
1745                            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
1746                            .collect(),
1747                    );
1748                    req.insert_extension(path_params);
1749                }
1750
1751                entry.call(ctx, req, ws).await
1752            }
1753            _ => Err(crate::websocket::WebSocketError::Protocol(
1754                "no websocket route matched",
1755            )),
1756        }
1757    }
1758
1759    // =========================================================================
1760    // Lifecycle Hook Execution
1761    // =========================================================================
1762
1763    /// Runs all startup hooks.
1764    ///
1765    /// Hooks run in registration order (FIFO). If a hook returns an error
1766    /// with `abort: true`, execution stops and returns `StartupOutcome::Aborted`.
1767    ///
1768    /// This consumes the startup hooks - they can only be run once.
1769    ///
1770    /// # Returns
1771    ///
1772    /// - `StartupOutcome::Success` if all hooks succeeded
1773    /// - `StartupOutcome::PartialSuccess` if some hooks had non-fatal errors
1774    /// - `StartupOutcome::Aborted` if a fatal hook error occurred
1775    pub async fn run_startup_hooks(&self) -> StartupOutcome {
1776        let hooks: Vec<StartupHook> = std::mem::take(&mut *self.startup_hooks.lock());
1777        let mut warnings = 0;
1778
1779        for hook in hooks {
1780            match hook.run() {
1781                Ok(None) => {
1782                    // Sync hook succeeded
1783                }
1784                Ok(Some(fut)) => {
1785                    // Async hook - await it
1786                    match fut.await {
1787                        Ok(()) => {}
1788                        Err(e) if e.abort => {
1789                            return StartupOutcome::Aborted(e);
1790                        }
1791                        Err(_) => {
1792                            warnings += 1;
1793                        }
1794                    }
1795                }
1796                Err(e) if e.abort => {
1797                    return StartupOutcome::Aborted(e);
1798                }
1799                Err(_) => {
1800                    warnings += 1;
1801                }
1802            }
1803        }
1804
1805        if warnings > 0 {
1806            StartupOutcome::PartialSuccess { warnings }
1807        } else {
1808            StartupOutcome::Success
1809        }
1810    }
1811
1812    /// Runs all shutdown hooks.
1813    ///
1814    /// Hooks run in reverse registration order (LIFO). Errors are logged
1815    /// but do not stop other hooks from running.
1816    ///
1817    /// This consumes the shutdown hooks - they can only be run once.
1818    pub async fn run_shutdown_hooks(&self) {
1819        // Run async hooks first (LIFO)
1820        let async_hooks: Vec<_> = std::mem::take(&mut *self.async_shutdown_hooks.lock());
1821        for hook in async_hooks.into_iter().rev() {
1822            let fut = hook();
1823            fut.await;
1824        }
1825
1826        // Run sync hooks (LIFO)
1827        let sync_hooks: Vec<_> = std::mem::take(&mut *self.shutdown_hooks.lock());
1828        for hook in sync_hooks.into_iter().rev() {
1829            hook();
1830        }
1831    }
1832
1833    /// Transfers shutdown hooks to a [`ShutdownController`].
1834    ///
1835    /// This moves all registered shutdown hooks to the controller, which
1836    /// will run them during the appropriate shutdown phase.
1837    ///
1838    /// Call this when integrating with the server's shutdown mechanism.
1839    pub fn transfer_shutdown_hooks(&self, controller: &ShutdownController) {
1840        // Transfer sync hooks (they'll run in LIFO due to how pop_hook works)
1841        let sync_hooks: Vec<_> = std::mem::take(&mut *self.shutdown_hooks.lock());
1842        for hook in sync_hooks {
1843            controller.register_hook(hook);
1844        }
1845
1846        // Transfer async hooks
1847        let async_hooks: Vec<_> = std::mem::take(&mut *self.async_shutdown_hooks.lock());
1848        for hook in async_hooks {
1849            controller.register_async_hook(move || hook());
1850        }
1851    }
1852
1853    /// Returns the number of pending startup hooks.
1854    #[must_use]
1855    pub fn pending_startup_hooks(&self) -> usize {
1856        self.startup_hooks.lock().len()
1857    }
1858
1859    /// Returns the number of pending shutdown hooks.
1860    #[must_use]
1861    pub fn pending_shutdown_hooks(&self) -> usize {
1862        self.shutdown_hooks.lock().len() + self.async_shutdown_hooks.lock().len()
1863    }
1864}
1865
1866impl std::fmt::Debug for App {
1867    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1868        f.debug_struct("App")
1869            .field("config", &self.config)
1870            .field("routes", &self.routes.len())
1871            .field("middleware", &self.middleware.len())
1872            .field("state", &self.state)
1873            .field("exception_handlers", &self.exception_handlers)
1874            .field("startup_hooks", &self.startup_hooks.lock().len())
1875            .field("shutdown_hooks", &self.pending_shutdown_hooks())
1876            .finish()
1877    }
1878}
1879
1880// Allow `App` to be used anywhere a middleware `Handler` is expected (e.g. TestClient).
1881impl Handler for App {
1882    fn call<'a>(
1883        &'a self,
1884        ctx: &'a RequestContext,
1885        req: &'a mut Request,
1886    ) -> BoxFuture<'a, Response> {
1887        Box::pin(async move { self.handle(ctx, req).await })
1888    }
1889
1890    fn dependency_overrides(&self) -> Option<Arc<crate::dependency::DependencyOverrides>> {
1891        Some(Arc::clone(&self.dependency_overrides))
1892    }
1893}
1894
1895/// Handler wrapper for a route entry.
1896struct RouteHandler<'a> {
1897    entry: &'a RouteEntry,
1898}
1899
1900impl<'a> Handler for RouteHandler<'a> {
1901    fn call<'b>(
1902        &'b self,
1903        ctx: &'b RequestContext,
1904        req: &'b mut Request,
1905    ) -> BoxFuture<'b, Response> {
1906        let handler = self.entry.handler.clone();
1907        Box::pin(async move { handler(ctx, req).await })
1908    }
1909}
1910
1911#[cfg(test)]
1912mod tests {
1913    use super::*;
1914
1915    use crate::response::ResponseBody;
1916
1917    // Test handlers that return 'static futures (no borrowing from parameters)
1918    fn test_handler(_ctx: &RequestContext, _req: &mut Request) -> std::future::Ready<Response> {
1919        std::future::ready(Response::ok().body(ResponseBody::Bytes(b"Hello, World!".to_vec())))
1920    }
1921
1922    fn health_handler(_ctx: &RequestContext, _req: &mut Request) -> std::future::Ready<Response> {
1923        std::future::ready(Response::ok().body(ResponseBody::Bytes(b"OK".to_vec())))
1924    }
1925
1926    fn test_context() -> RequestContext {
1927        let cx = asupersync::Cx::for_testing();
1928        RequestContext::new(cx, 1)
1929    }
1930
1931    #[test]
1932    fn builder_title_and_version_set_app_config() {
1933        let app = App::builder().title("My API").version("2.3.4").build();
1934        assert_eq!(app.config().name, "My API");
1935        assert_eq!(app.config().version, "2.3.4");
1936        // No OpenAPI configured: metadata alone must not enable it.
1937        assert!(app.openapi_spec().is_none());
1938    }
1939
1940    #[test]
1941    fn builder_metadata_populates_openapi_info_and_wins_over_openapi_config() {
1942        let app = App::builder()
1943            .openapi(OpenApiConfig::new().title("ignored").version("0.0.0"))
1944            .title("My API")
1945            .version("2.3.4")
1946            .description("A sample API")
1947            .build();
1948        let spec: serde_json::Value =
1949            serde_json::from_str(app.openapi_spec().expect("openapi enabled")).unwrap();
1950        assert_eq!(spec["info"]["title"], "My API");
1951        assert_eq!(spec["info"]["version"], "2.3.4");
1952        assert_eq!(spec["info"]["description"], "A sample API");
1953    }
1954
1955    #[test]
1956    fn app_builder_creates_app() {
1957        let app = App::builder()
1958            .config(AppConfig::new().name("Test App"))
1959            .get("/", test_handler)
1960            .get("/health", health_handler)
1961            .build();
1962
1963        assert_eq!(app.route_count(), 2);
1964        assert_eq!(app.config().name, "Test App");
1965    }
1966
1967    #[test]
1968    fn app_config_builder() {
1969        let config = AppConfig::new()
1970            .name("My API")
1971            .version("1.0.0")
1972            .debug(true)
1973            .max_body_size(2 * 1024 * 1024)
1974            .request_timeout_ms(60_000);
1975
1976        assert_eq!(config.name, "My API");
1977        assert_eq!(config.version, "1.0.0");
1978        assert!(config.debug);
1979        assert_eq!(config.max_body_size, 2 * 1024 * 1024);
1980        assert_eq!(config.request_timeout_ms, 60_000);
1981    }
1982
1983    #[test]
1984    fn state_container_insert_and_get() {
1985        #[derive(Debug, PartialEq)]
1986        struct MyState {
1987            value: i32,
1988        }
1989
1990        let mut container = StateContainer::new();
1991        container.insert(MyState { value: 42 });
1992
1993        let state = container.get::<MyState>();
1994        assert!(state.is_some());
1995        assert_eq!(state.unwrap().value, 42);
1996    }
1997
1998    #[test]
1999    fn state_container_multiple_types() {
2000        struct TypeA(i32);
2001        struct TypeB(String);
2002
2003        let mut container = StateContainer::new();
2004        container.insert(TypeA(1));
2005        container.insert(TypeB("hello".to_string()));
2006
2007        assert!(container.contains::<TypeA>());
2008        assert!(container.contains::<TypeB>());
2009        assert!(!container.contains::<i64>());
2010
2011        assert_eq!(container.get::<TypeA>().unwrap().0, 1);
2012        assert_eq!(container.get::<TypeB>().unwrap().0, "hello");
2013    }
2014
2015    #[test]
2016    fn app_builder_with_state() {
2017        struct DbPool {
2018            connection_count: usize,
2019        }
2020
2021        let app = App::builder()
2022            .state(DbPool {
2023                connection_count: 10,
2024            })
2025            .get("/", test_handler)
2026            .build();
2027
2028        let pool = app.get_state::<DbPool>();
2029        assert!(pool.is_some());
2030        assert_eq!(pool.unwrap().connection_count, 10);
2031    }
2032
2033    #[test]
2034    fn app_handles_get_request() {
2035        let app = App::builder().get("/", test_handler).build();
2036
2037        let ctx = test_context();
2038        let mut req = Request::new(Method::Get, "/");
2039
2040        let response = futures_executor::block_on(app.handle(&ctx, &mut req));
2041        assert_eq!(response.status().as_u16(), 200);
2042    }
2043
2044    #[test]
2045    fn app_returns_404_for_unknown_path() {
2046        let app = App::builder().get("/", test_handler).build();
2047
2048        let ctx = test_context();
2049        let mut req = Request::new(Method::Get, "/unknown");
2050
2051        let response = futures_executor::block_on(app.handle(&ctx, &mut req));
2052        assert_eq!(response.status().as_u16(), 404);
2053    }
2054
2055    #[test]
2056    fn app_returns_405_for_wrong_method() {
2057        let app = App::builder().get("/", test_handler).build();
2058
2059        let ctx = test_context();
2060        let mut req = Request::new(Method::Post, "/");
2061
2062        let response = futures_executor::block_on(app.handle(&ctx, &mut req));
2063        assert_eq!(response.status().as_u16(), 405);
2064    }
2065
2066    #[test]
2067    fn app_builder_all_methods() {
2068        let app = App::builder()
2069            .get("/get", test_handler)
2070            .post("/post", test_handler)
2071            .put("/put", test_handler)
2072            .delete("/delete", test_handler)
2073            .patch("/patch", test_handler)
2074            .build();
2075
2076        assert_eq!(app.route_count(), 5);
2077    }
2078
2079    #[test]
2080    fn route_entry_debug() {
2081        let entry = RouteEntry::new(Method::Get, "/test", test_handler);
2082        let debug = format!("{:?}", entry);
2083        assert!(debug.contains("RouteEntry"));
2084        assert!(debug.contains("Get"));
2085        assert!(debug.contains("/test"));
2086    }
2087
2088    #[test]
2089    fn app_with_middleware() {
2090        use crate::middleware::NoopMiddleware;
2091
2092        let app = App::builder()
2093            .middleware(NoopMiddleware)
2094            .middleware(NoopMiddleware)
2095            .get("/", test_handler)
2096            .build();
2097
2098        let ctx = test_context();
2099        let mut req = Request::new(Method::Get, "/");
2100
2101        let response = futures_executor::block_on(app.handle(&ctx, &mut req));
2102        assert_eq!(response.status().as_u16(), 200);
2103    }
2104
2105    // =========================================================================
2106    // Exception Handlers Tests
2107    // =========================================================================
2108
2109    // Custom error type for testing
2110    #[derive(Debug)]
2111    struct TestError {
2112        message: String,
2113        code: u32,
2114    }
2115
2116    impl std::fmt::Display for TestError {
2117        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2118            write!(f, "TestError({}): {}", self.code, self.message)
2119        }
2120    }
2121
2122    impl std::error::Error for TestError {}
2123
2124    // Another custom error type
2125    #[derive(Debug)]
2126    struct AnotherError(String);
2127
2128    impl std::fmt::Display for AnotherError {
2129        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2130            write!(f, "AnotherError: {}", self.0)
2131        }
2132    }
2133
2134    impl std::error::Error for AnotherError {}
2135
2136    // --- Unit Tests: Handler Registration ---
2137
2138    #[test]
2139    fn exception_handlers_new_is_empty() {
2140        let handlers = ExceptionHandlers::new();
2141        assert!(handlers.is_empty());
2142        assert_eq!(handlers.len(), 0);
2143    }
2144
2145    #[test]
2146    fn exception_handlers_register_single() {
2147        let mut handlers = ExceptionHandlers::new();
2148        handlers.register::<TestError>(|_ctx, err| {
2149            Response::with_status(StatusCode::BAD_REQUEST)
2150                .body(ResponseBody::Bytes(err.message.as_bytes().to_vec()))
2151        });
2152
2153        assert!(handlers.has_handler::<TestError>());
2154        assert!(!handlers.has_handler::<AnotherError>());
2155        assert_eq!(handlers.len(), 1);
2156    }
2157
2158    #[test]
2159    fn exception_handlers_register_multiple() {
2160        let mut handlers = ExceptionHandlers::new();
2161        handlers.register::<TestError>(|_ctx, _err| Response::with_status(StatusCode::BAD_REQUEST));
2162        handlers.register::<AnotherError>(|_ctx, _err| {
2163            Response::with_status(StatusCode::INTERNAL_SERVER_ERROR)
2164        });
2165
2166        assert!(handlers.has_handler::<TestError>());
2167        assert!(handlers.has_handler::<AnotherError>());
2168        assert_eq!(handlers.len(), 2);
2169    }
2170
2171    #[test]
2172    fn exception_handlers_builder_pattern() {
2173        let handlers = ExceptionHandlers::new()
2174            .handler::<TestError>(|_ctx, _err| Response::with_status(StatusCode::BAD_REQUEST))
2175            .handler::<AnotherError>(|_ctx, _err| {
2176                Response::with_status(StatusCode::INTERNAL_SERVER_ERROR)
2177            });
2178
2179        assert!(handlers.has_handler::<TestError>());
2180        assert!(handlers.has_handler::<AnotherError>());
2181        assert_eq!(handlers.len(), 2);
2182    }
2183
2184    #[test]
2185    fn exception_handlers_with_defaults() {
2186        let handlers = ExceptionHandlers::with_defaults();
2187
2188        assert!(handlers.has_handler::<crate::HttpError>());
2189        assert!(handlers.has_handler::<crate::ValidationErrors>());
2190        assert_eq!(handlers.len(), 2);
2191    }
2192
2193    #[test]
2194    fn exception_handlers_merge() {
2195        let mut handlers1 = ExceptionHandlers::new()
2196            .handler::<TestError>(|_ctx, _err| Response::with_status(StatusCode::BAD_REQUEST));
2197
2198        let handlers2 = ExceptionHandlers::new().handler::<AnotherError>(|_ctx, _err| {
2199            Response::with_status(StatusCode::INTERNAL_SERVER_ERROR)
2200        });
2201
2202        handlers1.merge(handlers2);
2203
2204        assert!(handlers1.has_handler::<TestError>());
2205        assert!(handlers1.has_handler::<AnotherError>());
2206        assert_eq!(handlers1.len(), 2);
2207    }
2208
2209    // --- Unit Tests: Handler Invocation ---
2210
2211    #[test]
2212    fn exception_handlers_handle_registered_error() {
2213        let handlers = ExceptionHandlers::new().handler::<TestError>(|_ctx, err| {
2214            Response::with_status(StatusCode::BAD_REQUEST)
2215                .body(ResponseBody::Bytes(err.message.as_bytes().to_vec()))
2216        });
2217
2218        let ctx = test_context();
2219        let err = TestError {
2220            message: "test error".into(),
2221            code: 42,
2222        };
2223
2224        let response = handlers.handle(&ctx, err);
2225        assert!(response.is_some());
2226
2227        let response = response.unwrap();
2228        assert_eq!(response.status().as_u16(), 400);
2229    }
2230
2231    #[test]
2232    fn exception_handlers_handle_unregistered_error() {
2233        let handlers = ExceptionHandlers::new()
2234            .handler::<TestError>(|_ctx, _err| Response::with_status(StatusCode::BAD_REQUEST));
2235
2236        let ctx = test_context();
2237        let err = AnotherError("unhandled".into());
2238
2239        let response = handlers.handle(&ctx, err);
2240        assert!(response.is_none());
2241    }
2242
2243    #[test]
2244    fn exception_handlers_handle_or_default_registered() {
2245        let handlers = ExceptionHandlers::new()
2246            .handler::<TestError>(|_ctx, _err| Response::with_status(StatusCode::BAD_REQUEST));
2247
2248        let ctx = test_context();
2249        let err = TestError {
2250            message: "test".into(),
2251            code: 1,
2252        };
2253
2254        let response = handlers.handle_or_default(&ctx, err);
2255        assert_eq!(response.status().as_u16(), 400);
2256    }
2257
2258    #[test]
2259    fn exception_handlers_handle_or_default_unregistered() {
2260        let handlers = ExceptionHandlers::new();
2261
2262        let ctx = test_context();
2263        let err = TestError {
2264            message: "test".into(),
2265            code: 1,
2266        };
2267
2268        let response = handlers.handle_or_default(&ctx, err);
2269        assert_eq!(response.status().as_u16(), 500);
2270    }
2271
2272    #[test]
2273    fn exception_handlers_error_values_passed_to_handler() {
2274        use std::sync::atomic::{AtomicU32, Ordering};
2275
2276        let captured_code = Arc::new(AtomicU32::new(0));
2277        let captured_code_clone = captured_code.clone();
2278
2279        let handlers = ExceptionHandlers::new().handler::<TestError>(move |_ctx, err| {
2280            captured_code_clone.store(err.code, Ordering::SeqCst);
2281            Response::with_status(StatusCode::BAD_REQUEST)
2282        });
2283
2284        let ctx = test_context();
2285        let err = TestError {
2286            message: "test".into(),
2287            code: 12345,
2288        };
2289
2290        let _ = handlers.handle(&ctx, err);
2291        assert_eq!(captured_code.load(Ordering::SeqCst), 12345);
2292    }
2293
2294    // --- Integration Tests: Custom Error Type Handling ---
2295
2296    #[test]
2297    fn app_builder_exception_handler_single() {
2298        let app = App::builder()
2299            .exception_handler::<TestError, _>(|_ctx, err| {
2300                Response::with_status(StatusCode::BAD_REQUEST)
2301                    .body(ResponseBody::Bytes(err.message.as_bytes().to_vec()))
2302            })
2303            .get("/", test_handler)
2304            .build();
2305
2306        assert!(app.exception_handlers().has_handler::<TestError>());
2307    }
2308
2309    #[test]
2310    fn app_builder_exception_handler_multiple() {
2311        let app = App::builder()
2312            .exception_handler::<TestError, _>(|_ctx, _err| {
2313                Response::with_status(StatusCode::BAD_REQUEST)
2314            })
2315            .exception_handler::<AnotherError, _>(|_ctx, _err| {
2316                Response::with_status(StatusCode::INTERNAL_SERVER_ERROR)
2317            })
2318            .get("/", test_handler)
2319            .build();
2320
2321        assert!(app.exception_handlers().has_handler::<TestError>());
2322        assert!(app.exception_handlers().has_handler::<AnotherError>());
2323    }
2324
2325    #[test]
2326    fn app_builder_with_default_exception_handlers() {
2327        let app = App::builder()
2328            .with_default_exception_handlers()
2329            .get("/", test_handler)
2330            .build();
2331
2332        assert!(app.exception_handlers().has_handler::<crate::HttpError>());
2333        assert!(
2334            app.exception_handlers()
2335                .has_handler::<crate::ValidationErrors>()
2336        );
2337    }
2338
2339    #[test]
2340    fn app_builder_exception_handlers_registry() {
2341        let handlers = ExceptionHandlers::new()
2342            .handler::<TestError>(|_ctx, _err| Response::with_status(StatusCode::BAD_REQUEST))
2343            .handler::<AnotherError>(|_ctx, _err| {
2344                Response::with_status(StatusCode::INTERNAL_SERVER_ERROR)
2345            });
2346
2347        let app = App::builder()
2348            .exception_handlers(handlers)
2349            .get("/", test_handler)
2350            .build();
2351
2352        assert!(app.exception_handlers().has_handler::<TestError>());
2353        assert!(app.exception_handlers().has_handler::<AnotherError>());
2354    }
2355
2356    #[test]
2357    fn app_handle_error_registered() {
2358        let app = App::builder()
2359            .exception_handler::<TestError, _>(|_ctx, _err| {
2360                Response::with_status(StatusCode::BAD_REQUEST)
2361            })
2362            .get("/", test_handler)
2363            .build();
2364
2365        let ctx = test_context();
2366        let err = TestError {
2367            message: "test".into(),
2368            code: 1,
2369        };
2370
2371        let response = app.handle_error(&ctx, err);
2372        assert!(response.is_some());
2373        assert_eq!(response.unwrap().status().as_u16(), 400);
2374    }
2375
2376    #[test]
2377    fn app_handle_error_unregistered() {
2378        let app = App::builder().get("/", test_handler).build();
2379
2380        let ctx = test_context();
2381        let err = TestError {
2382            message: "test".into(),
2383            code: 1,
2384        };
2385
2386        let response = app.handle_error(&ctx, err);
2387        assert!(response.is_none());
2388    }
2389
2390    #[test]
2391    fn app_handle_error_or_default() {
2392        let app = App::builder().get("/", test_handler).build();
2393
2394        let ctx = test_context();
2395        let err = TestError {
2396            message: "test".into(),
2397            code: 1,
2398        };
2399
2400        let response = app.handle_error_or_default(&ctx, err);
2401        assert_eq!(response.status().as_u16(), 500);
2402    }
2403
2404    // --- Integration Tests: Override Default Handler ---
2405
2406    #[test]
2407    fn exception_handlers_override_on_register() {
2408        let handlers = ExceptionHandlers::new()
2409            .handler::<TestError>(|_ctx, _err| Response::with_status(StatusCode::BAD_REQUEST))
2410            .handler::<TestError>(|_ctx, _err| {
2411                Response::with_status(StatusCode::UNPROCESSABLE_ENTITY)
2412            });
2413
2414        // Only one handler for TestError
2415        assert_eq!(handlers.len(), 1);
2416
2417        let ctx = test_context();
2418        let err = TestError {
2419            message: "test".into(),
2420            code: 1,
2421        };
2422
2423        // Should use the second (overriding) handler
2424        let response = handlers.handle(&ctx, err);
2425        assert!(response.is_some());
2426        assert_eq!(response.unwrap().status().as_u16(), 422);
2427    }
2428
2429    #[test]
2430    fn exception_handlers_merge_overrides() {
2431        let mut handlers1 = ExceptionHandlers::new()
2432            .handler::<TestError>(|_ctx, _err| Response::with_status(StatusCode::BAD_REQUEST));
2433
2434        let handlers2 = ExceptionHandlers::new().handler::<TestError>(|_ctx, _err| {
2435            Response::with_status(StatusCode::UNPROCESSABLE_ENTITY)
2436        });
2437
2438        handlers1.merge(handlers2);
2439
2440        // Only one handler for TestError after merge
2441        assert_eq!(handlers1.len(), 1);
2442
2443        let ctx = test_context();
2444        let err = TestError {
2445            message: "test".into(),
2446            code: 1,
2447        };
2448
2449        // Merged handlers should override
2450        let response = handlers1.handle(&ctx, err);
2451        assert!(response.is_some());
2452        assert_eq!(response.unwrap().status().as_u16(), 422);
2453    }
2454
2455    #[test]
2456    fn exception_handlers_override_default_http_error() {
2457        // Start with default handlers
2458        let mut handlers = ExceptionHandlers::with_defaults();
2459
2460        // Override HttpError handler
2461        handlers.register::<crate::HttpError>(|_ctx, err| {
2462            // Custom handler that adds extra header
2463            let detail = err.detail.as_deref().unwrap_or("Unknown error");
2464            Response::with_status(err.status)
2465                .header("x-custom-error", b"true".to_vec())
2466                .body(ResponseBody::Bytes(detail.as_bytes().to_vec()))
2467        });
2468
2469        // Still has 2 handlers (HttpError and ValidationErrors)
2470        assert_eq!(handlers.len(), 2);
2471
2472        let ctx = test_context();
2473        let err = crate::HttpError::bad_request().with_detail("test error");
2474
2475        let response = handlers.handle(&ctx, err);
2476        assert!(response.is_some());
2477
2478        let response = response.unwrap();
2479        assert_eq!(response.status().as_u16(), 400);
2480
2481        // Check custom header was added
2482        let custom_header = response
2483            .headers()
2484            .iter()
2485            .find(|(name, _)| name.eq_ignore_ascii_case("x-custom-error"))
2486            .map(|(_, v)| v.as_slice());
2487        assert_eq!(custom_header, Some(b"true".as_slice()));
2488    }
2489
2490    #[test]
2491    fn exception_handlers_override_default_validation_errors() {
2492        // Start with default handlers
2493        let mut handlers = ExceptionHandlers::with_defaults();
2494
2495        // Override ValidationErrors handler
2496        handlers.register::<crate::ValidationErrors>(|_ctx, errs| {
2497            // Custom handler that returns 400 instead of 422
2498            Response::with_status(StatusCode::BAD_REQUEST)
2499                .header("x-error-count", errs.len().to_string().as_bytes().to_vec())
2500        });
2501
2502        let ctx = test_context();
2503        let mut errs = crate::ValidationErrors::new();
2504        errs.push(crate::ValidationError::missing(
2505            crate::error::loc::body_field("name"),
2506        ));
2507        errs.push(crate::ValidationError::missing(
2508            crate::error::loc::body_field("email"),
2509        ));
2510
2511        let response = handlers.handle(&ctx, errs);
2512        assert!(response.is_some());
2513
2514        let response = response.unwrap();
2515        // Custom handler returns 400 instead of 422
2516        assert_eq!(response.status().as_u16(), 400);
2517
2518        // Check custom header
2519        let count_header = response
2520            .headers()
2521            .iter()
2522            .find(|(name, _)| name.eq_ignore_ascii_case("x-error-count"))
2523            .map(|(_, v)| v.as_slice());
2524        assert_eq!(count_header, Some(b"2".as_slice()));
2525    }
2526
2527    #[test]
2528    fn exception_handlers_debug_format() {
2529        let handlers = ExceptionHandlers::new()
2530            .handler::<TestError>(|_ctx, _err| Response::with_status(StatusCode::BAD_REQUEST));
2531
2532        let debug = format!("{:?}", handlers);
2533        assert!(debug.contains("ExceptionHandlers"));
2534        assert!(debug.contains("count"));
2535        assert!(debug.contains("1"));
2536    }
2537
2538    #[test]
2539    fn app_debug_includes_exception_handlers() {
2540        let app = App::builder()
2541            .exception_handler::<TestError, _>(|_ctx, _err| {
2542                Response::with_status(StatusCode::BAD_REQUEST)
2543            })
2544            .get("/", test_handler)
2545            .build();
2546
2547        let debug = format!("{:?}", app);
2548        assert!(debug.contains("exception_handlers"));
2549    }
2550
2551    #[test]
2552    fn app_builder_debug_includes_exception_handlers() {
2553        let builder = App::builder().exception_handler::<TestError, _>(|_ctx, _err| {
2554            Response::with_status(StatusCode::BAD_REQUEST)
2555        });
2556
2557        let debug = format!("{:?}", builder);
2558        assert!(debug.contains("exception_handlers"));
2559    }
2560
2561    // =========================================================================
2562    // Lifecycle Hooks Tests
2563    // =========================================================================
2564
2565    // --- Startup Hooks: Registration ---
2566
2567    #[test]
2568    fn app_builder_startup_hook_registration() {
2569        let builder = App::builder().on_startup(|| Ok(())).on_startup(|| Ok(()));
2570
2571        assert_eq!(builder.startup_hook_count(), 2);
2572    }
2573
2574    #[test]
2575    fn app_builder_shutdown_hook_registration() {
2576        let builder = App::builder().on_shutdown(|| {}).on_shutdown(|| {});
2577
2578        assert_eq!(builder.shutdown_hook_count(), 2);
2579    }
2580
2581    #[test]
2582    fn app_builder_mixed_hooks() {
2583        let builder = App::builder()
2584            .on_startup(|| Ok(()))
2585            .on_shutdown(|| {})
2586            .on_startup(|| Ok(()))
2587            .on_shutdown(|| {});
2588
2589        assert_eq!(builder.startup_hook_count(), 2);
2590        assert_eq!(builder.shutdown_hook_count(), 2);
2591    }
2592
2593    #[test]
2594    fn app_pending_hooks_count() {
2595        let app = App::builder()
2596            .on_startup(|| Ok(()))
2597            .on_startup(|| Ok(()))
2598            .on_shutdown(|| {})
2599            .get("/", test_handler)
2600            .build();
2601
2602        assert_eq!(app.pending_startup_hooks(), 2);
2603        assert_eq!(app.pending_shutdown_hooks(), 1);
2604    }
2605
2606    // --- Startup Hooks: Execution Order (FIFO) ---
2607
2608    #[test]
2609    fn startup_hooks_run_in_fifo_order() {
2610        let order = Arc::new(parking_lot::Mutex::new(Vec::new()));
2611
2612        let order1 = Arc::clone(&order);
2613        let order2 = Arc::clone(&order);
2614        let order3 = Arc::clone(&order);
2615
2616        let app = App::builder()
2617            .on_startup(move || {
2618                order1.lock().push(1);
2619                Ok(())
2620            })
2621            .on_startup(move || {
2622                order2.lock().push(2);
2623                Ok(())
2624            })
2625            .on_startup(move || {
2626                order3.lock().push(3);
2627                Ok(())
2628            })
2629            .get("/", test_handler)
2630            .build();
2631
2632        let outcome = futures_executor::block_on(app.run_startup_hooks());
2633        assert!(outcome.can_proceed());
2634
2635        // FIFO: 1, 2, 3
2636        assert_eq!(*order.lock(), vec![1, 2, 3]);
2637
2638        // Hooks consumed
2639        assert_eq!(app.pending_startup_hooks(), 0);
2640    }
2641
2642    // --- Shutdown Hooks: Execution Order (LIFO) ---
2643
2644    #[test]
2645    fn shutdown_hooks_run_in_lifo_order() {
2646        let order = Arc::new(parking_lot::Mutex::new(Vec::new()));
2647
2648        let order1 = Arc::clone(&order);
2649        let order2 = Arc::clone(&order);
2650        let order3 = Arc::clone(&order);
2651
2652        let app = App::builder()
2653            .on_shutdown(move || {
2654                order1.lock().push(1);
2655            })
2656            .on_shutdown(move || {
2657                order2.lock().push(2);
2658            })
2659            .on_shutdown(move || {
2660                order3.lock().push(3);
2661            })
2662            .get("/", test_handler)
2663            .build();
2664
2665        futures_executor::block_on(app.run_shutdown_hooks());
2666
2667        // LIFO: 3, 2, 1
2668        assert_eq!(*order.lock(), vec![3, 2, 1]);
2669
2670        // Hooks consumed
2671        assert_eq!(app.pending_shutdown_hooks(), 0);
2672    }
2673
2674    // --- Startup Hooks: Success Outcome ---
2675
2676    #[test]
2677    fn startup_hooks_success_outcome() {
2678        let app = App::builder()
2679            .on_startup(|| Ok(()))
2680            .on_startup(|| Ok(()))
2681            .get("/", test_handler)
2682            .build();
2683
2684        let outcome = futures_executor::block_on(app.run_startup_hooks());
2685        assert!(matches!(outcome, StartupOutcome::Success));
2686        assert!(outcome.can_proceed());
2687    }
2688
2689    // --- Startup Hooks: Fatal Error Aborts ---
2690
2691    #[test]
2692    fn startup_hooks_fatal_error_aborts() {
2693        let app = App::builder()
2694            .on_startup(|| Ok(()))
2695            .on_startup(|| Err(StartupHookError::new("database connection failed")))
2696            .on_startup(|| Ok(())) // Should not run
2697            .get("/", test_handler)
2698            .build();
2699
2700        let outcome = futures_executor::block_on(app.run_startup_hooks());
2701        assert!(!outcome.can_proceed());
2702
2703        if let StartupOutcome::Aborted(err) = outcome {
2704            assert!(err.message.contains("database connection failed"));
2705            assert!(err.abort);
2706        } else {
2707            panic!("Expected Aborted outcome");
2708        }
2709    }
2710
2711    // --- Startup Hooks: Non-Fatal Error Continues ---
2712
2713    #[test]
2714    fn startup_hooks_non_fatal_error_continues() {
2715        let app = App::builder()
2716            .on_startup(|| Ok(()))
2717            .on_startup(|| Err(StartupHookError::non_fatal("optional feature unavailable")))
2718            .on_startup(|| Ok(())) // Should still run
2719            .get("/", test_handler)
2720            .build();
2721
2722        let outcome = futures_executor::block_on(app.run_startup_hooks());
2723        assert!(outcome.can_proceed());
2724
2725        if let StartupOutcome::PartialSuccess { warnings } = outcome {
2726            assert_eq!(warnings, 1);
2727        } else {
2728            panic!("Expected PartialSuccess outcome");
2729        }
2730    }
2731
2732    // --- Startup Hook Error Types ---
2733
2734    #[test]
2735    fn startup_hook_error_builder() {
2736        let err = StartupHookError::new("test error")
2737            .with_hook_name("database_init")
2738            .with_abort(false);
2739
2740        assert_eq!(err.hook_name.as_deref(), Some("database_init"));
2741        assert_eq!(err.message, "test error");
2742        assert!(!err.abort);
2743    }
2744
2745    #[test]
2746    fn startup_hook_error_display() {
2747        let err = StartupHookError::new("connection failed").with_hook_name("redis_init");
2748
2749        let display = format!("{}", err);
2750        assert!(display.contains("redis_init"));
2751        assert!(display.contains("connection failed"));
2752    }
2753
2754    #[test]
2755    fn startup_hook_error_non_fatal() {
2756        let err = StartupHookError::non_fatal("optional feature");
2757        assert!(!err.abort);
2758    }
2759
2760    // --- Transfer Shutdown Hooks to Controller ---
2761
2762    #[test]
2763    fn transfer_shutdown_hooks_to_controller() {
2764        let order = Arc::new(parking_lot::Mutex::new(Vec::new()));
2765
2766        let order1 = Arc::clone(&order);
2767        let order2 = Arc::clone(&order);
2768
2769        let app = App::builder()
2770            .on_shutdown(move || {
2771                order1.lock().push(1);
2772            })
2773            .on_shutdown(move || {
2774                order2.lock().push(2);
2775            })
2776            .get("/", test_handler)
2777            .build();
2778
2779        let controller = ShutdownController::new();
2780        app.transfer_shutdown_hooks(&controller);
2781
2782        // App hooks consumed
2783        assert_eq!(app.pending_shutdown_hooks(), 0);
2784
2785        // Controller has the hooks
2786        assert_eq!(controller.hook_count(), 2);
2787
2788        // Run via controller (LIFO order)
2789        while let Some(hook) = controller.pop_hook() {
2790            hook.run();
2791        }
2792
2793        // LIFO order via controller
2794        assert_eq!(*order.lock(), vec![2, 1]);
2795    }
2796
2797    // --- Debug Format Includes Hooks ---
2798
2799    #[test]
2800    fn app_debug_includes_hooks() {
2801        let app = App::builder()
2802            .on_startup(|| Ok(()))
2803            .on_shutdown(|| {})
2804            .get("/", test_handler)
2805            .build();
2806
2807        let debug = format!("{:?}", app);
2808        assert!(debug.contains("startup_hooks"));
2809        assert!(debug.contains("shutdown_hooks"));
2810    }
2811
2812    #[test]
2813    fn app_builder_debug_includes_hooks() {
2814        let builder = App::builder().on_startup(|| Ok(())).on_shutdown(|| {});
2815
2816        let debug = format!("{:?}", builder);
2817        assert!(debug.contains("startup_hooks"));
2818        assert!(debug.contains("shutdown_hooks"));
2819    }
2820
2821    // --- Startup Outcome Accessors ---
2822
2823    #[test]
2824    fn startup_outcome_success() {
2825        let outcome = StartupOutcome::Success;
2826        assert!(outcome.can_proceed());
2827        assert!(outcome.into_error().is_none());
2828    }
2829
2830    #[test]
2831    fn startup_outcome_partial_success() {
2832        let outcome = StartupOutcome::PartialSuccess { warnings: 2 };
2833        assert!(outcome.can_proceed());
2834        assert!(outcome.into_error().is_none());
2835    }
2836
2837    #[test]
2838    fn startup_outcome_aborted() {
2839        let err = StartupHookError::new("fatal");
2840        let outcome = StartupOutcome::Aborted(err);
2841        assert!(!outcome.can_proceed());
2842
2843        let err = outcome.into_error();
2844        assert!(err.is_some());
2845        assert_eq!(err.unwrap().message, "fatal");
2846    }
2847
2848    // --- Multiple Non-Fatal Errors ---
2849
2850    #[test]
2851    fn startup_hooks_multiple_non_fatal_errors() {
2852        let app = App::builder()
2853            .on_startup(|| Err(StartupHookError::non_fatal("warning 1")))
2854            .on_startup(|| Ok(()))
2855            .on_startup(|| Err(StartupHookError::non_fatal("warning 2")))
2856            .on_startup(|| Err(StartupHookError::non_fatal("warning 3")))
2857            .get("/", test_handler)
2858            .build();
2859
2860        let outcome = futures_executor::block_on(app.run_startup_hooks());
2861        assert!(outcome.can_proceed());
2862
2863        if let StartupOutcome::PartialSuccess { warnings } = outcome {
2864            assert_eq!(warnings, 3);
2865        } else {
2866            panic!("Expected PartialSuccess");
2867        }
2868    }
2869
2870    // --- Empty Hooks ---
2871
2872    #[test]
2873    fn empty_startup_hooks() {
2874        let app = App::builder().get("/", test_handler).build();
2875
2876        let outcome = futures_executor::block_on(app.run_startup_hooks());
2877        assert!(matches!(outcome, StartupOutcome::Success));
2878    }
2879
2880    #[test]
2881    fn empty_shutdown_hooks() {
2882        let app = App::builder().get("/", test_handler).build();
2883
2884        // Should not panic with empty hooks
2885        futures_executor::block_on(app.run_shutdown_hooks());
2886    }
2887
2888    // --- Hooks Can Only Run Once ---
2889
2890    #[test]
2891    fn startup_hooks_consumed_after_run() {
2892        let counter = Arc::new(std::sync::atomic::AtomicU32::new(0));
2893        let counter_clone = Arc::clone(&counter);
2894
2895        let app = App::builder()
2896            .on_startup(move || {
2897                counter_clone.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2898                Ok(())
2899            })
2900            .get("/", test_handler)
2901            .build();
2902
2903        // First run
2904        futures_executor::block_on(app.run_startup_hooks());
2905        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
2906
2907        // Second run - no hooks left
2908        futures_executor::block_on(app.run_startup_hooks());
2909        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
2910    }
2911
2912    #[test]
2913    fn shutdown_hooks_consumed_after_run() {
2914        let counter = Arc::new(std::sync::atomic::AtomicU32::new(0));
2915        let counter_clone = Arc::clone(&counter);
2916
2917        let app = App::builder()
2918            .on_shutdown(move || {
2919                counter_clone.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2920            })
2921            .get("/", test_handler)
2922            .build();
2923
2924        // First run
2925        futures_executor::block_on(app.run_shutdown_hooks());
2926        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
2927
2928        // Second run - no hooks left
2929        futures_executor::block_on(app.run_shutdown_hooks());
2930        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
2931    }
2932
2933    #[test]
2934    fn root_path_strips_trailing_slashes() {
2935        let config = AppConfig::new().root_path("/api/");
2936        assert_eq!(config.root_path, "/api");
2937
2938        let config = AppConfig::new().root_path("/api///");
2939        assert_eq!(config.root_path, "/api");
2940
2941        let config = AppConfig::new().root_path("/api");
2942        assert_eq!(config.root_path, "/api");
2943
2944        // Empty root_path stays empty
2945        let config = AppConfig::new().root_path("");
2946        assert_eq!(config.root_path, "");
2947    }
2948}