Skip to main content

rustapi_core/
app.rs

1//! RustApi application builder
2
3use crate::error::Result;
4use crate::events::LifecycleHooks;
5use crate::interceptor::{InterceptorChain, RequestInterceptor, ResponseInterceptor};
6use crate::middleware::{
7    BodyLimitLayer, BoxedNext, LayerStack, MiddlewareLayer, DEFAULT_BODY_LIMIT,
8};
9use crate::response::IntoResponse;
10use crate::router::{MethodRouter, Router};
11use crate::server::Server;
12use crate::{Request, Response};
13use http::Extensions;
14use std::collections::BTreeMap;
15#[cfg(feature = "dashboard")]
16use std::collections::BTreeSet;
17use std::future::Future;
18use std::sync::Arc;
19use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
20
21/// A dispatcher that can drive requests through the RustAPI pipeline
22/// (interceptors + layers + router) without any network or serialization overhead.
23///
24/// Obtained via [`RustApi::request_dispatcher`].
25#[derive(Clone)]
26pub struct RequestDispatcher {
27    router: Arc<Router>,
28    layers: LayerStack,
29    interceptors: InterceptorChain,
30}
31
32impl RequestDispatcher {
33    /// Returns the shared state Extensions from the underlying router.
34    /// Useful for in-process request construction to preserve `State<T>` etc.
35    pub fn state_ref(&self) -> Arc<Extensions> {
36        self.router.state_ref()
37    }
38
39    /// Dispatch a request through the full stack (interceptors → middleware layers
40    /// → route handler → response interceptors).
41    ///
42    /// This replicates the logic used by the normal HTTP server.
43    pub async fn dispatch(&self, request: Request) -> Response {
44        let req = self.interceptors.intercept_request(request);
45
46        let path = req.path().to_owned();
47        let method = req.method().clone();
48
49        let response = if self.layers.is_empty() {
50            crate::server::route_request_direct(&self.router, req, &path, &method).await
51        } else {
52            let router = self.router.clone();
53            let p = path.clone();
54            let m = method.clone();
55
56            let routing_handler: BoxedNext = Arc::new(move |r: Request| {
57                let router = router.clone();
58                let pp = p.clone();
59                let mm = m.clone();
60                Box::pin(async move { crate::server::route_request(&router, r, &pp, &mm).await })
61            });
62
63            self.layers.execute(req, routing_handler).await
64        };
65
66        self.interceptors.intercept_response(response)
67    }
68}
69
70/// Main application builder for RustAPI
71///
72/// # Example
73///
74/// ```rust,ignore
75/// use rustapi_rs::prelude::*;
76///
77/// #[tokio::main]
78/// async fn main() -> Result<()> {
79///     RustApi::new()
80///         .state(AppState::new())
81///         .route("/", get(hello))
82///         .route("/users/{id}", get(get_user))
83///         .run("127.0.0.1:8080")
84///         .await
85/// }
86/// ```
87pub struct RustApi {
88    router: Router,
89    openapi_spec: rustapi_openapi::OpenApiSpec,
90    layers: LayerStack,
91    body_limit: Option<usize>,
92    interceptors: InterceptorChain,
93    lifecycle_hooks: LifecycleHooks,
94    hot_reload: bool,
95    #[cfg(feature = "http3")]
96    http3_config: Option<crate::http3::Http3Config>,
97    health_check: Option<crate::health::HealthCheck>,
98    health_endpoint_config: Option<crate::health::HealthEndpointConfig>,
99    status_config: Option<crate::status::StatusConfig>,
100    #[cfg(feature = "dashboard")]
101    dashboard_config: Option<crate::dashboard::DashboardConfig>,
102}
103
104/// Configuration for RustAPI's built-in production baseline preset.
105///
106/// This preset bundles together the most common foundation pieces for a
107/// production HTTP service:
108/// - request IDs on every response
109/// - structured tracing spans with service metadata
110/// - standard `/health`, `/ready`, and `/live` probes
111#[derive(Debug, Clone)]
112pub struct ProductionDefaultsConfig {
113    service_name: String,
114    version: Option<String>,
115    tracing_level: tracing::Level,
116    health_endpoint_config: Option<crate::health::HealthEndpointConfig>,
117    enable_request_id: bool,
118    enable_tracing: bool,
119    enable_health_endpoints: bool,
120}
121
122impl ProductionDefaultsConfig {
123    /// Create a new production baseline configuration.
124    pub fn new(service_name: impl Into<String>) -> Self {
125        Self {
126            service_name: service_name.into(),
127            version: None,
128            tracing_level: tracing::Level::INFO,
129            health_endpoint_config: None,
130            enable_request_id: true,
131            enable_tracing: true,
132            enable_health_endpoints: true,
133        }
134    }
135
136    /// Annotate tracing spans and default health payloads with an application version.
137    pub fn version(mut self, version: impl Into<String>) -> Self {
138        self.version = Some(version.into());
139        self
140    }
141
142    /// Set the tracing log level used by the preset tracing layer.
143    pub fn tracing_level(mut self, level: tracing::Level) -> Self {
144        self.tracing_level = level;
145        self
146    }
147
148    /// Override the default health endpoint paths.
149    pub fn health_endpoint_config(mut self, config: crate::health::HealthEndpointConfig) -> Self {
150        self.health_endpoint_config = Some(config);
151        self
152    }
153
154    /// Enable or disable request ID propagation.
155    pub fn request_id(mut self, enabled: bool) -> Self {
156        self.enable_request_id = enabled;
157        self
158    }
159
160    /// Enable or disable structured tracing middleware.
161    pub fn tracing(mut self, enabled: bool) -> Self {
162        self.enable_tracing = enabled;
163        self
164    }
165
166    /// Enable or disable built-in health endpoints.
167    pub fn health_endpoints(mut self, enabled: bool) -> Self {
168        self.enable_health_endpoints = enabled;
169        self
170    }
171}
172
173impl RustApi {
174    /// Create a new RustAPI application
175    pub fn new() -> Self {
176        // Initialize tracing if not already done
177        let _ = tracing_subscriber::registry()
178            .with(
179                EnvFilter::try_from_default_env()
180                    .unwrap_or_else(|_| EnvFilter::new("info,rustapi=debug")),
181            )
182            .with(tracing_subscriber::fmt::layer())
183            .try_init();
184
185        Self {
186            router: Router::new(),
187            openapi_spec: rustapi_openapi::OpenApiSpec::new("RustAPI Application", "1.0.0")
188                .register::<rustapi_openapi::ErrorSchema>()
189                .register::<rustapi_openapi::ErrorBodySchema>()
190                .register::<rustapi_openapi::ValidationErrorSchema>()
191                .register::<rustapi_openapi::ValidationErrorBodySchema>()
192                .register::<rustapi_openapi::FieldErrorSchema>(),
193            layers: LayerStack::new(),
194            body_limit: Some(DEFAULT_BODY_LIMIT), // Default 1MB limit
195            interceptors: InterceptorChain::new(),
196            lifecycle_hooks: LifecycleHooks::new(),
197            hot_reload: false,
198            #[cfg(feature = "http3")]
199            http3_config: None,
200            health_check: None,
201            health_endpoint_config: None,
202            status_config: None,
203            #[cfg(feature = "dashboard")]
204            dashboard_config: None,
205        }
206    }
207
208    /// The primary way to build a RustAPI application.
209    ///
210    /// Collects all routes decorated with `#[rustapi_rs::get]`, `#[rustapi_rs::post]`, etc.
211    /// at link time via `linkme` and registers them automatically — no manual `.route()`
212    /// or `.mount_route()` calls needed. This is baked into the core and requires no
213    /// feature flags.
214    ///
215    /// When the `swagger-ui` feature is enabled (included in the default `core` feature),
216    /// Swagger UI is served at `/docs`. Without it, only the auto-discovered routes are
217    /// registered.
218    ///
219    /// Use [`RustApi::new()`] when handlers are plain `async fn` not annotated with
220    /// the route macros, or when you need full manual control over route registration.
221    ///
222    /// # Example
223    ///
224    /// ```rust,ignore
225    /// use rustapi_rs::prelude::*;
226    ///
227    /// #[rustapi_rs::get("/users")]
228    /// async fn list_users() -> Json<Vec<User>> {
229    ///     Json(vec![])
230    /// }
231    ///
232    /// #[rustapi_rs::main]
233    /// async fn main() -> Result<()> {
234    ///     RustApi::auto().run("0.0.0.0:8080").await
235    /// }
236    /// ```
237    #[cfg(feature = "swagger-ui")]
238    pub fn auto() -> Self {
239        Self::new().mount_auto_routes_grouped().docs("/docs")
240    }
241
242    #[cfg(not(feature = "swagger-ui"))]
243    pub fn auto() -> Self {
244        Self::new().mount_auto_routes_grouped()
245    }
246
247    /// Create a configurable RustAPI application with auto-routes.
248    ///
249    /// Provides builder methods for customization while still
250    /// auto-registering all decorated routes.
251    ///
252    /// # Example
253    ///
254    /// ```rust,ignore
255    /// use rustapi_rs::prelude::*;
256    ///
257    /// RustApi::config()
258    ///     .docs_path("/api-docs")
259    ///     .body_limit(5 * 1024 * 1024)  // 5MB
260    ///     .openapi_info("My API", "2.0.0", Some("API Description"))
261    ///     .run("0.0.0.0:8080")
262    ///     .await?;
263    /// ```
264    pub fn config() -> RustApiConfig {
265        RustApiConfig::new()
266    }
267
268    /// Set the global body size limit for request bodies
269    ///
270    /// This protects against denial-of-service attacks via large payloads.
271    /// The default limit is 1MB (1024 * 1024 bytes).
272    ///
273    /// # Arguments
274    ///
275    /// * `limit` - Maximum body size in bytes
276    ///
277    /// # Example
278    ///
279    /// ```rust,ignore
280    /// use rustapi_rs::prelude::*;
281    ///
282    /// RustApi::new()
283    ///     .body_limit(5 * 1024 * 1024)  // 5MB limit
284    ///     .route("/upload", post(upload_handler))
285    ///     .run("127.0.0.1:8080")
286    ///     .await
287    /// ```
288    pub fn body_limit(mut self, limit: usize) -> Self {
289        self.body_limit = Some(limit);
290        self
291    }
292
293    /// Disable the body size limit
294    ///
295    /// Warning: This removes protection against large payload attacks.
296    /// Only use this if you have other mechanisms to limit request sizes.
297    ///
298    /// # Example
299    ///
300    /// ```rust,ignore
301    /// RustApi::new()
302    ///     .no_body_limit()  // Disable body size limit
303    ///     .route("/upload", post(upload_handler))
304    /// ```
305    pub fn no_body_limit(mut self) -> Self {
306        self.body_limit = None;
307        self
308    }
309
310    /// Add a middleware layer to the application
311    ///
312    /// Layers are executed in the order they are added (outermost first).
313    /// The first layer added will be the first to process the request and
314    /// the last to process the response.
315    ///
316    /// # Example
317    ///
318    /// ```rust,ignore
319    /// use rustapi_rs::prelude::*;
320    /// use rustapi_core::middleware::{RequestIdLayer, TracingLayer};
321    ///
322    /// RustApi::new()
323    ///     .layer(RequestIdLayer::new())  // First to process request
324    ///     .layer(TracingLayer::new())    // Second to process request
325    ///     .route("/", get(handler))
326    ///     .run("127.0.0.1:8080")
327    ///     .await
328    /// ```
329    pub fn layer<L>(mut self, layer: L) -> Self
330    where
331        L: MiddlewareLayer,
332    {
333        self.layers.push(Box::new(layer));
334        self
335    }
336
337    /// Add a request interceptor to the application
338    ///
339    /// Request interceptors are executed in registration order before the route handler.
340    /// Each interceptor can modify the request before passing it to the next interceptor
341    /// or handler.
342    ///
343    /// # Example
344    ///
345    /// ```rust,ignore
346    /// use rustapi_core::{RustApi, interceptor::RequestInterceptor, Request};
347    ///
348    /// #[derive(Clone)]
349    /// struct AddRequestId;
350    ///
351    /// impl RequestInterceptor for AddRequestId {
352    ///     fn intercept(&self, mut req: Request) -> Request {
353    ///         req.extensions_mut().insert(uuid::Uuid::new_v4());
354    ///         req
355    ///     }
356    ///
357    ///     fn clone_box(&self) -> Box<dyn RequestInterceptor> {
358    ///         Box::new(self.clone())
359    ///     }
360    /// }
361    ///
362    /// RustApi::new()
363    ///     .request_interceptor(AddRequestId)
364    ///     .route("/", get(handler))
365    ///     .run("127.0.0.1:8080")
366    ///     .await
367    /// ```
368    pub fn request_interceptor<I>(mut self, interceptor: I) -> Self
369    where
370        I: RequestInterceptor,
371    {
372        self.interceptors.add_request_interceptor(interceptor);
373        self
374    }
375
376    /// Add a response interceptor to the application
377    ///
378    /// Response interceptors are executed in reverse registration order after the route
379    /// handler completes. Each interceptor can modify the response before passing it
380    /// to the previous interceptor or client.
381    ///
382    /// # Example
383    ///
384    /// ```rust,ignore
385    /// use rustapi_core::{RustApi, interceptor::ResponseInterceptor, Response};
386    ///
387    /// #[derive(Clone)]
388    /// struct AddServerHeader;
389    ///
390    /// impl ResponseInterceptor for AddServerHeader {
391    ///     fn intercept(&self, mut res: Response) -> Response {
392    ///         res.headers_mut().insert("X-Server", "RustAPI".parse().unwrap());
393    ///         res
394    ///     }
395    ///
396    ///     fn clone_box(&self) -> Box<dyn ResponseInterceptor> {
397    ///         Box::new(self.clone())
398    ///     }
399    /// }
400    ///
401    /// RustApi::new()
402    ///     .response_interceptor(AddServerHeader)
403    ///     .route("/", get(handler))
404    ///     .run("127.0.0.1:8080")
405    ///     .await
406    /// ```
407    pub fn response_interceptor<I>(mut self, interceptor: I) -> Self
408    where
409        I: ResponseInterceptor,
410    {
411        self.interceptors.add_response_interceptor(interceptor);
412        self
413    }
414
415    /// Add application state
416    ///
417    /// State is shared across all handlers and can be extracted using `State<T>`.
418    ///
419    /// # Example
420    ///
421    /// ```rust,ignore
422    /// #[derive(Clone)]
423    /// struct AppState {
424    ///     db: DbPool,
425    /// }
426    ///
427    /// RustApi::new()
428    ///     .state(AppState::new())
429    /// ```
430    pub fn state<S>(self, _state: S) -> Self
431    where
432        S: Clone + Send + Sync + 'static,
433    {
434        // Store state in the router's shared Extensions so `State<T>` extractor can retrieve it.
435        let state = _state;
436        let mut app = self;
437        let r = std::mem::take(&mut app.router);
438        app.router = r.state(state);
439        app
440    }
441
442    /// Register an `on_start` lifecycle hook
443    ///
444    /// The callback runs **after** route registration and **before** the server
445    /// begins accepting connections. Multiple hooks execute in registration order.
446    ///
447    /// # Example
448    ///
449    /// ```rust,ignore
450    /// RustApi::new()
451    ///     .on_start(|| async {
452    ///         println!("🚀 Server starting...");
453    ///         // e.g. run DB migrations, warm caches
454    ///     })
455    ///     .run("127.0.0.1:8080")
456    ///     .await
457    /// ```
458    pub fn on_start<F, Fut>(mut self, hook: F) -> Self
459    where
460        F: FnOnce() -> Fut + Send + 'static,
461        Fut: Future<Output = ()> + Send + 'static,
462    {
463        self.lifecycle_hooks
464            .on_start
465            .push(Box::new(move || Box::pin(hook())));
466        self
467    }
468
469    /// Register an `on_shutdown` lifecycle hook
470    ///
471    /// The callback runs **after** the shutdown signal is received and the server
472    /// stops accepting new connections. Multiple hooks execute in registration order.
473    ///
474    /// # Example
475    ///
476    /// ```rust,ignore
477    /// RustApi::new()
478    ///     .on_shutdown(|| async {
479    ///         println!("👋 Server shutting down...");
480    ///         // e.g. flush logs, close DB connections
481    ///     })
482    ///     .run_with_shutdown("127.0.0.1:8080", ctrl_c())
483    ///     .await
484    /// ```
485    pub fn on_shutdown<F, Fut>(mut self, hook: F) -> Self
486    where
487        F: FnOnce() -> Fut + Send + 'static,
488        Fut: Future<Output = ()> + Send + 'static,
489    {
490        self.lifecycle_hooks
491            .on_shutdown
492            .push(Box::new(move || Box::pin(hook())));
493        self
494    }
495
496    /// Enable hot-reload mode for development
497    ///
498    /// When enabled:
499    /// - A dev-mode banner is printed at startup
500    /// - The `RUSTAPI_HOT_RELOAD` env var is set so that `cargo rustapi watch`
501    ///   can detect the server is reload-aware
502    /// - If the server is **not** already running under the CLI watcher,
503    ///   a helpful hint is printed suggesting `cargo rustapi run --watch`
504    ///
505    /// # Example
506    ///
507    /// ```rust,ignore
508    /// RustApi::new()
509    ///     .hot_reload(true)
510    ///     .route("/", get(hello))
511    ///     .run("127.0.0.1:8080")
512    ///     .await
513    /// ```
514    pub fn hot_reload(mut self, enabled: bool) -> Self {
515        self.hot_reload = enabled;
516        self
517    }
518
519    /// Register an OpenAPI schema
520    ///
521    /// # Example
522    ///
523    /// ```rust,ignore
524    /// #[derive(Schema)]
525    /// struct User { ... }
526    ///
527    /// RustApi::new()
528    ///     .register_schema::<User>()
529    /// ```
530    pub fn register_schema<T: rustapi_openapi::schema::RustApiSchema>(mut self) -> Self {
531        self.openapi_spec = self.openapi_spec.register::<T>();
532        self
533    }
534
535    /// Configure OpenAPI info (title, version, description)
536    pub fn openapi_info(mut self, title: &str, version: &str, description: Option<&str>) -> Self {
537        // NOTE: Do not reset the spec here; doing so would drop collected paths/schemas.
538        // This is especially important for `RustApi::auto()` and `RustApi::config()`.
539        self.openapi_spec.info.title = title.to_string();
540        self.openapi_spec.info.version = version.to_string();
541        self.openapi_spec.info.description = description.map(|d| d.to_string());
542        self
543    }
544
545    /// Get the current OpenAPI spec (for advanced usage/testing).
546    pub fn openapi_spec(&self) -> &rustapi_openapi::OpenApiSpec {
547        &self.openapi_spec
548    }
549
550    fn mount_auto_routes_grouped(mut self) -> Self {
551        let routes = crate::auto_route::collect_auto_routes();
552
553        if routes.is_empty() {
554            // This is a common source of confusion with linkme-based auto registration.
555            // We emit a clear warning so users know their annotated handlers were not linked.
556            tracing::warn!(
557                target: "rustapi::auto",
558                count = 0,
559                "RustApi::auto() collected 0 routes. \
560                 This usually means either:\n\
561                 - No handlers were annotated with #[rustapi_rs::get], #[post], etc.\n\
562                 - The binary/test was not linked with the annotated modules (common in some test setups).\n\
563                 - You are building a library (cdylib/rlib) where linkme distributed slices may not be populated.\n\n\
564                 You can still register routes manually with .route() or check with rustapi_rs::auto_route_count()."
565            );
566        } else {
567            #[cfg(feature = "tracing")]
568            tracing::debug!(
569                target: "rustapi::auto",
570                count = routes.len(),
571                "Auto route collection found handlers"
572            );
573        }
574
575        // Use BTreeMap for deterministic route registration order
576        let mut by_path: BTreeMap<String, MethodRouter> = BTreeMap::new();
577
578        for route in routes {
579            let crate::handler::Route {
580                path: route_path,
581                method,
582                handler,
583                operation,
584                component_registrar,
585                ..
586            } = route;
587
588            let method_enum = match method {
589                "GET" => http::Method::GET,
590                "POST" => http::Method::POST,
591                "PUT" => http::Method::PUT,
592                "DELETE" => http::Method::DELETE,
593                "PATCH" => http::Method::PATCH,
594                _ => http::Method::GET,
595            };
596
597            let path = if route_path.starts_with('/') {
598                route_path.to_string()
599            } else {
600                format!("/{}", route_path)
601            };
602
603            let entry = by_path.entry(path).or_default();
604            entry.insert_boxed_with_operation(method_enum, handler, operation, component_registrar);
605        }
606
607        #[cfg(feature = "tracing")]
608        let route_count: usize = by_path.values().map(|mr| mr.allowed_methods().len()).sum();
609        #[cfg(feature = "tracing")]
610        let path_count = by_path.len();
611
612        for (path, method_router) in by_path {
613            self = self.route(&path, method_router);
614        }
615
616        crate::trace_info!(
617            paths = path_count,
618            routes = route_count,
619            "Auto-registered routes"
620        );
621
622        // Apply any auto-registered schemas.
623        crate::auto_schema::apply_auto_schemas(&mut self.openapi_spec);
624
625        self
626    }
627
628    /// Add a route
629    ///
630    /// # Example
631    ///
632    /// ```rust,ignore
633    /// RustApi::new()
634    ///     .route("/", get(index))
635    ///     .route("/users", get(list_users).post(create_user))
636    ///     .route("/users/{id}", get(get_user).delete(delete_user))
637    /// ```
638    pub fn route(mut self, path: &str, method_router: MethodRouter) -> Self {
639        for register_components in &method_router.component_registrars {
640            register_components(&mut self.openapi_spec);
641        }
642
643        // Register operations in OpenAPI spec
644        for (method, op) in &method_router.operations {
645            let mut op = op.clone();
646            add_path_params_to_operation(path, &mut op, &BTreeMap::new());
647            self.openapi_spec = self.openapi_spec.path(path, method.as_str(), op);
648        }
649
650        self.router = self.router.route(path, method_router);
651        self
652    }
653
654    /// Add a typed route
655    pub fn typed<P: crate::typed_path::TypedPath>(self, method_router: MethodRouter) -> Self {
656        self.route(P::PATH, method_router)
657    }
658
659    /// Mount a handler (convenience method)
660    ///
661    /// Alias for `.route(path, method_router)` for a single handler.
662    #[deprecated(note = "Use route() directly or mount_route() for macro-based routing")]
663    pub fn mount(self, path: &str, method_router: MethodRouter) -> Self {
664        self.route(path, method_router)
665    }
666
667    /// Mount a route created with `#[rustapi::get]`, `#[rustapi::post]`, etc.
668    ///
669    /// # Example
670    ///
671    /// ```rust,ignore
672    /// use rustapi_rs::prelude::*;
673    ///
674    /// #[rustapi::get("/users")]
675    /// async fn list_users() -> Json<Vec<User>> {
676    ///     Json(vec![])
677    /// }
678    ///
679    /// RustApi::new()
680    ///     .mount_route(route!(list_users))
681    ///     .run("127.0.0.1:8080")
682    ///     .await
683    /// ```
684    pub fn mount_route(mut self, route: crate::handler::Route) -> Self {
685        let method_enum = match route.method {
686            "GET" => http::Method::GET,
687            "POST" => http::Method::POST,
688            "PUT" => http::Method::PUT,
689            "DELETE" => http::Method::DELETE,
690            "PATCH" => http::Method::PATCH,
691            _ => http::Method::GET,
692        };
693
694        (route.component_registrar)(&mut self.openapi_spec);
695
696        // Register operation in OpenAPI spec
697        let mut op = route.operation;
698        add_path_params_to_operation(route.path, &mut op, &route.param_schemas);
699        self.openapi_spec = self.openapi_spec.path(route.path, route.method, op);
700
701        self.route_with_method(route.path, method_enum, route.handler)
702    }
703
704    /// Helper to mount a single method handler
705    fn route_with_method(
706        self,
707        path: &str,
708        method: http::Method,
709        handler: crate::handler::BoxedHandler,
710    ) -> Self {
711        use crate::router::MethodRouter;
712        // use http::Method; // Removed
713
714        // This is simplified. In a real implementation we'd merge with existing router at this path
715        // For now we assume one handler per path or we simply allow overwriting for this MVP step
716        // (matchit router doesn't allow easy merging/updating existing entries without rebuilding)
717        //
718        // TOOD: Enhance Router to support method merging
719
720        let path = if !path.starts_with('/') {
721            format!("/{}", path)
722        } else {
723            path.to_string()
724        };
725
726        // Check if we already have this path?
727        // For MVP, valid assumption: user calls .route() or .mount() once per path-method-combo
728        // But we need to handle multiple methods on same path.
729        // Our Router wrapper currently just inserts.
730
731        // Since we can't easily query matchit, we'll just insert.
732        // Limitations: strictly sequential mounting for now.
733
734        let mut handlers = std::collections::HashMap::new();
735        handlers.insert(method, handler);
736
737        let method_router = MethodRouter::from_boxed(handlers);
738        self.route(&path, method_router)
739    }
740
741    /// Nest a router under a prefix
742    ///
743    /// All routes from the nested router will be registered with the prefix
744    /// prepended to their paths. OpenAPI operations from the nested router
745    /// are also propagated to the parent's OpenAPI spec with prefixed paths.
746    ///
747    /// # Example
748    ///
749    /// ```rust,ignore
750    /// let api_v1 = Router::new()
751    ///     .route("/users", get(list_users));
752    ///
753    /// RustApi::new()
754    ///     .nest("/api/v1", api_v1)
755    /// ```
756    pub fn nest(mut self, prefix: &str, router: Router) -> Self {
757        // Normalize the prefix for OpenAPI paths
758        let normalized_prefix = normalize_prefix_for_openapi(prefix);
759
760        // Propagate OpenAPI operations from nested router with prefixed paths
761        // We need to do this before calling router.nest() because it consumes the router
762        for (matchit_path, method_router) in router.method_routers() {
763            for register_components in &method_router.component_registrars {
764                register_components(&mut self.openapi_spec);
765            }
766
767            // Get the display path from registered_routes (has {param} format)
768            let display_path = router
769                .registered_routes()
770                .get(matchit_path)
771                .map(|info| info.path.clone())
772                .unwrap_or_else(|| matchit_path.clone());
773
774            // Build the prefixed display path for OpenAPI
775            let prefixed_path = if display_path == "/" {
776                normalized_prefix.clone()
777            } else {
778                format!("{}{}", normalized_prefix, display_path)
779            };
780
781            // Register each operation in the OpenAPI spec
782            for (method, op) in &method_router.operations {
783                let mut op = op.clone();
784                add_path_params_to_operation(&prefixed_path, &mut op, &BTreeMap::new());
785                self.openapi_spec = self.openapi_spec.path(&prefixed_path, method.as_str(), op);
786            }
787        }
788
789        // Delegate to Router::nest for actual route registration
790        self.router = self.router.nest(prefix, router);
791        self
792    }
793
794    /// Serve static files from a directory
795    ///
796    /// Maps a URL path prefix to a filesystem directory. Requests to paths under
797    /// the prefix will serve files from the corresponding location in the directory.
798    ///
799    /// # Arguments
800    ///
801    /// * `prefix` - URL path prefix (e.g., "/static", "/assets")
802    /// * `root` - Filesystem directory path
803    ///
804    /// # Features
805    ///
806    /// - Automatic MIME type detection
807    /// - ETag and Last-Modified headers for caching
808    /// - Index file serving for directories
809    /// - Path traversal prevention
810    ///
811    /// # Example
812    ///
813    /// ```rust,ignore
814    /// use rustapi_rs::prelude::*;
815    ///
816    /// RustApi::new()
817    ///     .serve_static("/assets", "./public")
818    ///     .serve_static("/uploads", "./uploads")
819    ///     .run("127.0.0.1:8080")
820    ///     .await
821    /// ```
822    pub fn serve_static(self, prefix: &str, root: impl Into<std::path::PathBuf>) -> Self {
823        self.serve_static_with_config(crate::static_files::StaticFileConfig::new(root, prefix))
824    }
825
826    /// Serve static files with custom configuration
827    ///
828    /// # Example
829    ///
830    /// ```rust,ignore
831    /// use rustapi_core::static_files::StaticFileConfig;
832    ///
833    /// let config = StaticFileConfig::new("./public", "/assets")
834    ///     .max_age(86400)  // Cache for 1 day
835    ///     .fallback("index.html");  // SPA fallback
836    ///
837    /// RustApi::new()
838    ///     .serve_static_with_config(config)
839    ///     .run("127.0.0.1:8080")
840    ///     .await
841    /// ```
842    pub fn serve_static_with_config(self, config: crate::static_files::StaticFileConfig) -> Self {
843        use crate::router::MethodRouter;
844        use std::collections::HashMap;
845
846        let prefix = config.prefix.clone();
847        let catch_all_path = format!("{}/*path", prefix.trim_end_matches('/'));
848
849        // Create the static file handler
850        let handler: crate::handler::BoxedHandler =
851            std::sync::Arc::new(move |req: crate::Request| {
852                let config = config.clone();
853                let path = req.uri().path().to_string();
854
855                Box::pin(async move {
856                    let relative_path = path.strip_prefix(&config.prefix).unwrap_or(&path);
857
858                    match crate::static_files::StaticFile::serve(relative_path, &config).await {
859                        Ok(response) => response,
860                        Err(err) => err.into_response(),
861                    }
862                })
863                    as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
864            });
865
866        let mut handlers = HashMap::new();
867        handlers.insert(http::Method::GET, handler);
868        let method_router = MethodRouter::from_boxed(handlers);
869
870        self.route(&catch_all_path, method_router)
871    }
872
873    /// Enable response compression
874    ///
875    /// Adds gzip/deflate compression for response bodies. The compression
876    /// is based on the client's Accept-Encoding header.
877    ///
878    /// # Example
879    ///
880    /// ```rust,ignore
881    /// use rustapi_rs::prelude::*;
882    ///
883    /// RustApi::new()
884    ///     .compression()
885    ///     .route("/", get(handler))
886    ///     .run("127.0.0.1:8080")
887    ///     .await
888    /// ```
889    #[cfg(feature = "compression")]
890    pub fn compression(self) -> Self {
891        self.layer(crate::middleware::CompressionLayer::new())
892    }
893
894    /// Enable response compression with custom configuration
895    ///
896    /// # Example
897    ///
898    /// ```rust,ignore
899    /// use rustapi_core::middleware::CompressionConfig;
900    ///
901    /// RustApi::new()
902    ///     .compression_with_config(
903    ///         CompressionConfig::new()
904    ///             .min_size(512)
905    ///             .level(9)
906    ///     )
907    ///     .route("/", get(handler))
908    /// ```
909    #[cfg(feature = "compression")]
910    pub fn compression_with_config(self, config: crate::middleware::CompressionConfig) -> Self {
911        self.layer(crate::middleware::CompressionLayer::with_config(config))
912    }
913
914    /// Enable Swagger UI documentation
915    ///
916    /// This adds two endpoints:
917    /// - `{path}` - Swagger UI interface
918    /// - `{path}/openapi.json` - OpenAPI JSON specification
919    ///
920    /// **Important:** Call `.docs()` AFTER registering all routes. The OpenAPI
921    /// specification is captured at the time `.docs()` is called, so routes
922    /// added afterwards will not appear in the documentation.
923    ///
924    /// # Example
925    ///
926    /// ```text
927    /// RustApi::new()
928    ///     .route("/users", get(list_users))     // Add routes first
929    ///     .route("/posts", get(list_posts))     // Add more routes
930    ///     .docs("/docs")  // Then enable docs - captures all routes above
931    ///     .run("127.0.0.1:8080")
932    ///     .await
933    /// ```
934    ///
935    /// For `RustApi::auto()`, routes are collected before `.docs()` is called,
936    /// so this is handled automatically.
937    #[cfg(feature = "swagger-ui")]
938    pub fn docs(self, path: &str) -> Self {
939        let title = self.openapi_spec.info.title.clone();
940        let version = self.openapi_spec.info.version.clone();
941        let description = self.openapi_spec.info.description.clone();
942
943        self.docs_with_info(path, &title, &version, description.as_deref())
944    }
945
946    /// Enable Swagger UI documentation with custom API info
947    ///
948    /// # Example
949    ///
950    /// ```rust,ignore
951    /// RustApi::new()
952    ///     .docs_with_info("/docs", "My API", "2.0.0", Some("API for managing users"))
953    /// ```
954    #[cfg(feature = "swagger-ui")]
955    pub fn docs_with_info(
956        mut self,
957        path: &str,
958        title: &str,
959        version: &str,
960        description: Option<&str>,
961    ) -> Self {
962        use crate::router::get;
963        // Update spec info
964        self.openapi_spec.info.title = title.to_string();
965        self.openapi_spec.info.version = version.to_string();
966        if let Some(desc) = description {
967            self.openapi_spec.info.description = Some(desc.to_string());
968        }
969
970        let path = path.trim_end_matches('/');
971        let openapi_path = format!("{}/openapi.json", path);
972
973        // Clone values for closures
974        let spec_value = self.openapi_spec.to_json();
975        let spec_json = serde_json::to_string_pretty(&spec_value).unwrap_or_else(|e| {
976            // Safe fallback if JSON serialization fails (though unlikely for Value)
977            tracing::error!("Failed to serialize OpenAPI spec: {}", e);
978            "{}".to_string()
979        });
980        let openapi_url = openapi_path.clone();
981
982        // Add OpenAPI JSON endpoint
983        let spec_handler = move || {
984            let json = spec_json.clone();
985            async move {
986                http::Response::builder()
987                    .status(http::StatusCode::OK)
988                    .header(http::header::CONTENT_TYPE, "application/json")
989                    .body(crate::response::Body::from(json))
990                    .unwrap_or_else(|e| {
991                        tracing::error!("Failed to build response: {}", e);
992                        http::Response::builder()
993                            .status(http::StatusCode::INTERNAL_SERVER_ERROR)
994                            .body(crate::response::Body::from("Internal Server Error"))
995                            .unwrap()
996                    })
997            }
998        };
999
1000        // Add Swagger UI endpoint
1001        let docs_handler = move || {
1002            let url = openapi_url.clone();
1003            async move {
1004                let response = rustapi_openapi::swagger_ui_html(&url);
1005                response.map(crate::response::Body::Full)
1006            }
1007        };
1008
1009        self.route(&openapi_path, get(spec_handler))
1010            .route(path, get(docs_handler))
1011    }
1012
1013    /// Enable Swagger UI documentation with Basic Auth protection
1014    ///
1015    /// When username and password are provided, the docs endpoint will require
1016    /// Basic Authentication. This is useful for protecting API documentation
1017    /// in production environments.
1018    ///
1019    /// # Example
1020    ///
1021    /// ```rust,ignore
1022    /// RustApi::new()
1023    ///     .route("/users", get(list_users))
1024    ///     .docs_with_auth("/docs", "admin", "secret123")
1025    ///     .run("127.0.0.1:8080")
1026    ///     .await
1027    /// ```
1028    #[cfg(feature = "swagger-ui")]
1029    pub fn docs_with_auth(self, path: &str, username: &str, password: &str) -> Self {
1030        let title = self.openapi_spec.info.title.clone();
1031        let version = self.openapi_spec.info.version.clone();
1032        let description = self.openapi_spec.info.description.clone();
1033
1034        self.docs_with_auth_and_info(
1035            path,
1036            username,
1037            password,
1038            &title,
1039            &version,
1040            description.as_deref(),
1041        )
1042    }
1043
1044    /// Enable Swagger UI documentation with Basic Auth and custom API info
1045    ///
1046    /// # Example
1047    ///
1048    /// ```rust,ignore
1049    /// RustApi::new()
1050    ///     .docs_with_auth_and_info(
1051    ///         "/docs",
1052    ///         "admin",
1053    ///         "secret",
1054    ///         "My API",
1055    ///         "2.0.0",
1056    ///         Some("Protected API documentation")
1057    ///     )
1058    /// ```
1059    #[cfg(feature = "swagger-ui")]
1060    pub fn docs_with_auth_and_info(
1061        mut self,
1062        path: &str,
1063        username: &str,
1064        password: &str,
1065        title: &str,
1066        version: &str,
1067        description: Option<&str>,
1068    ) -> Self {
1069        use crate::router::MethodRouter;
1070        use std::collections::HashMap;
1071
1072        #[inline]
1073        fn base64_encode(input: &[u8]) -> String {
1074            const ALPHA: &[u8; 64] =
1075                b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1076            let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
1077            for chunk in input.chunks(3) {
1078                let b0 = chunk[0] as usize;
1079                let b1 = if chunk.len() > 1 {
1080                    chunk[1] as usize
1081                } else {
1082                    0
1083                };
1084                let b2 = if chunk.len() > 2 {
1085                    chunk[2] as usize
1086                } else {
1087                    0
1088                };
1089                out.push(ALPHA[b0 >> 2] as char);
1090                out.push(ALPHA[((b0 & 3) << 4) | (b1 >> 4)] as char);
1091                out.push(if chunk.len() > 1 {
1092                    ALPHA[((b1 & 0xf) << 2) | (b2 >> 6)] as char
1093                } else {
1094                    '='
1095                });
1096                out.push(if chunk.len() > 2 {
1097                    ALPHA[b2 & 63] as char
1098                } else {
1099                    '='
1100                });
1101            }
1102            out
1103        }
1104
1105        // Update spec info
1106        self.openapi_spec.info.title = title.to_string();
1107        self.openapi_spec.info.version = version.to_string();
1108        if let Some(desc) = description {
1109            self.openapi_spec.info.description = Some(desc.to_string());
1110        }
1111
1112        let path = path.trim_end_matches('/');
1113        let openapi_path = format!("{}/openapi.json", path);
1114
1115        // Create expected auth header value
1116        let credentials = format!("{}:{}", username, password);
1117        let encoded = base64_encode(credentials.as_bytes());
1118        let expected_auth = format!("Basic {}", encoded);
1119
1120        // Clone values for closures
1121        let spec_value = self.openapi_spec.to_json();
1122        let spec_json = serde_json::to_string_pretty(&spec_value).unwrap_or_else(|e| {
1123            tracing::error!("Failed to serialize OpenAPI spec: {}", e);
1124            "{}".to_string()
1125        });
1126        let openapi_url = openapi_path.clone();
1127        let expected_auth_spec = expected_auth.clone();
1128        let expected_auth_docs = expected_auth;
1129
1130        // Create spec handler with auth check
1131        let spec_handler: crate::handler::BoxedHandler =
1132            std::sync::Arc::new(move |req: crate::Request| {
1133                let json = spec_json.clone();
1134                let expected = expected_auth_spec.clone();
1135                Box::pin(async move {
1136                    if !check_basic_auth(&req, &expected) {
1137                        return unauthorized_response();
1138                    }
1139                    http::Response::builder()
1140                        .status(http::StatusCode::OK)
1141                        .header(http::header::CONTENT_TYPE, "application/json")
1142                        .body(crate::response::Body::from(json))
1143                        .unwrap_or_else(|e| {
1144                            tracing::error!("Failed to build response: {}", e);
1145                            http::Response::builder()
1146                                .status(http::StatusCode::INTERNAL_SERVER_ERROR)
1147                                .body(crate::response::Body::from("Internal Server Error"))
1148                                .unwrap()
1149                        })
1150                })
1151                    as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
1152            });
1153
1154        // Create docs handler with auth check
1155        let docs_handler: crate::handler::BoxedHandler =
1156            std::sync::Arc::new(move |req: crate::Request| {
1157                let url = openapi_url.clone();
1158                let expected = expected_auth_docs.clone();
1159                Box::pin(async move {
1160                    if !check_basic_auth(&req, &expected) {
1161                        return unauthorized_response();
1162                    }
1163                    let response = rustapi_openapi::swagger_ui_html(&url);
1164                    response.map(crate::response::Body::Full)
1165                })
1166                    as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
1167            });
1168
1169        // Create method routers with boxed handlers
1170        let mut spec_handlers = HashMap::new();
1171        spec_handlers.insert(http::Method::GET, spec_handler);
1172        let spec_router = MethodRouter::from_boxed(spec_handlers);
1173
1174        let mut docs_handlers = HashMap::new();
1175        docs_handlers.insert(http::Method::GET, docs_handler);
1176        let docs_router = MethodRouter::from_boxed(docs_handlers);
1177
1178        self.route(&openapi_path, spec_router)
1179            .route(path, docs_router)
1180    }
1181
1182    /// Enable automatic status page with default configuration
1183    pub fn status_page(self) -> Self {
1184        self.status_page_with_config(crate::status::StatusConfig::default())
1185    }
1186
1187    /// Enable automatic status page with custom configuration
1188    pub fn status_page_with_config(mut self, config: crate::status::StatusConfig) -> Self {
1189        self.status_config = Some(config);
1190        self
1191    }
1192
1193    /// Enable built-in `/health`, `/ready`, and `/live` endpoints with default paths.
1194    ///
1195    /// The default health check includes a lightweight `self` probe so the
1196    /// endpoints are immediately useful even before dependency checks are added.
1197    pub fn health_endpoints(mut self) -> Self {
1198        self.health_endpoint_config = Some(crate::health::HealthEndpointConfig::default());
1199        if self.health_check.is_none() {
1200            self.health_check = Some(crate::health::HealthCheckBuilder::default().build());
1201        }
1202        self
1203    }
1204
1205    /// Enable built-in health endpoints with custom paths.
1206    pub fn health_endpoints_with_config(
1207        mut self,
1208        config: crate::health::HealthEndpointConfig,
1209    ) -> Self {
1210        self.health_endpoint_config = Some(config);
1211        if self.health_check.is_none() {
1212            self.health_check = Some(crate::health::HealthCheckBuilder::default().build());
1213        }
1214        self
1215    }
1216
1217    /// Register a custom health check and enable built-in health endpoints.
1218    ///
1219    /// The configured check is used by `/health` and `/ready`, while `/live`
1220    /// remains a lightweight process-level probe.
1221    pub fn with_health_check(mut self, health_check: crate::health::HealthCheck) -> Self {
1222        self.health_check = Some(health_check);
1223        if self.health_endpoint_config.is_none() {
1224            self.health_endpoint_config = Some(crate::health::HealthEndpointConfig::default());
1225        }
1226        self
1227    }
1228
1229    /// Apply a one-call production baseline preset.
1230    ///
1231    /// This enables:
1232    /// - `RequestIdLayer`
1233    /// - `TracingLayer` with `service` and `environment` fields
1234    /// - built-in `/health`, `/ready`, and `/live` probes
1235    pub fn production_defaults(self, service_name: impl Into<String>) -> Self {
1236        self.production_defaults_with_config(ProductionDefaultsConfig::new(service_name))
1237    }
1238
1239    /// Apply the production baseline preset with custom configuration.
1240    pub fn production_defaults_with_config(mut self, config: ProductionDefaultsConfig) -> Self {
1241        if config.enable_request_id {
1242            self = self.layer(crate::middleware::RequestIdLayer::new());
1243        }
1244
1245        if config.enable_tracing {
1246            let mut tracing_layer =
1247                crate::middleware::TracingLayer::with_level(config.tracing_level)
1248                    .with_field("service", config.service_name.clone())
1249                    .with_field("environment", crate::error::get_environment().to_string());
1250
1251            if let Some(version) = &config.version {
1252                tracing_layer = tracing_layer.with_field("version", version.clone());
1253            }
1254
1255            self = self.layer(tracing_layer);
1256        }
1257
1258        if config.enable_health_endpoints {
1259            if self.health_check.is_none() {
1260                let mut builder = crate::health::HealthCheckBuilder::default();
1261                if let Some(version) = &config.version {
1262                    builder = builder.version(version.clone());
1263                }
1264                self.health_check = Some(builder.build());
1265            }
1266
1267            if self.health_endpoint_config.is_none() {
1268                self.health_endpoint_config =
1269                    Some(config.health_endpoint_config.unwrap_or_default());
1270            }
1271        }
1272
1273        self
1274    }
1275
1276    /// Print a hot-reload dev banner if `.hot_reload(true)` is set
1277    fn print_hot_reload_banner(&self, addr: &str) {
1278        if !self.hot_reload {
1279            return;
1280        }
1281
1282        // Set the env var so the CLI watcher can detect it
1283        std::env::set_var("RUSTAPI_HOT_RELOAD", "1");
1284
1285        let is_under_watcher = std::env::var("RUSTAPI_HOT_RELOAD")
1286            .map(|v| v == "1")
1287            .unwrap_or(false);
1288
1289        tracing::info!("🔄 Hot-reload mode enabled");
1290
1291        if is_under_watcher {
1292            tracing::info!("   File watcher active — changes will trigger rebuild + restart");
1293        } else {
1294            tracing::info!("   Tip: Run with `cargo rustapi run --watch` for automatic hot-reload");
1295        }
1296
1297        tracing::info!("   Listening on http://{addr}");
1298    }
1299
1300    // Helper to apply status page logic (monitor, layer, route)
1301    fn apply_health_endpoints(&mut self) {
1302        if let Some(config) = &self.health_endpoint_config {
1303            use crate::router::get;
1304
1305            let health_check = self
1306                .health_check
1307                .clone()
1308                .unwrap_or_else(|| crate::health::HealthCheckBuilder::default().build());
1309
1310            let health_path = config.health_path.clone();
1311            let readiness_path = config.readiness_path.clone();
1312            let liveness_path = config.liveness_path.clone();
1313
1314            let health_handler = {
1315                let health_check = health_check.clone();
1316                move || {
1317                    let health_check = health_check.clone();
1318                    async move { crate::health::health_response(health_check).await }
1319                }
1320            };
1321
1322            let readiness_handler = {
1323                let health_check = health_check.clone();
1324                move || {
1325                    let health_check = health_check.clone();
1326                    async move { crate::health::readiness_response(health_check).await }
1327                }
1328            };
1329
1330            let liveness_handler = || async { crate::health::liveness_response().await };
1331
1332            let router = std::mem::take(&mut self.router);
1333            self.router = router
1334                .route(&health_path, get(health_handler))
1335                .route(&readiness_path, get(readiness_handler))
1336                .route(&liveness_path, get(liveness_handler));
1337        }
1338    }
1339
1340    fn apply_status_page(&mut self) {
1341        if let Some(config) = &self.status_config {
1342            let monitor = std::sync::Arc::new(crate::status::StatusMonitor::new());
1343
1344            // 1. Add middleware layer
1345            self.layers
1346                .push(Box::new(crate::status::StatusLayer::new(monitor.clone())));
1347
1348            // 2. Add status route
1349            use crate::router::MethodRouter;
1350            use std::collections::HashMap;
1351
1352            let monitor = monitor.clone();
1353            let config = config.clone();
1354            let path = config.path.clone(); // Clone path before moving config
1355
1356            let handler: crate::handler::BoxedHandler = std::sync::Arc::new(move |_| {
1357                let monitor = monitor.clone();
1358                let config = config.clone();
1359                Box::pin(async move {
1360                    crate::status::status_handler(monitor, config)
1361                        .await
1362                        .into_response()
1363                })
1364            });
1365
1366            let mut handlers = HashMap::new();
1367            handlers.insert(http::Method::GET, handler);
1368            let method_router = MethodRouter::from_boxed(handlers);
1369
1370            // We need to take the router out to call route() which consumes it
1371            let router = std::mem::take(&mut self.router);
1372            self.router = router.route(&path, method_router);
1373        }
1374    }
1375
1376    #[cfg(feature = "dashboard")]
1377    fn apply_dashboard(&mut self) {
1378        use crate::dashboard::{DashboardMetrics, RouteInventoryItem};
1379        use crate::handler::BoxedHandler;
1380        use crate::response::Body;
1381        use crate::router::MethodRouter;
1382        use std::collections::HashMap;
1383        use std::sync::Arc;
1384
1385        let mut config = match self.dashboard_config.take() {
1386            Some(c) => c,
1387            None => return,
1388        };
1389        config.normalize_paths();
1390
1391        // Build route inventory from currently registered routes. This snapshot
1392        // intentionally happens before dashboard routes are mounted so the UI
1393        // represents application endpoints rather than the dashboard itself.
1394        let mut inventory: Vec<RouteInventoryItem> = self
1395            .router
1396            .registered_routes()
1397            .values()
1398            .map(|info| {
1399                let methods: Vec<String> = info.methods.iter().map(|m| m.to_string()).collect();
1400                let health_eligible = self
1401                    .health_endpoint_config
1402                    .as_ref()
1403                    .map(|health| {
1404                        info.path == health.health_path
1405                            || info.path == health.readiness_path
1406                            || info.path == health.liveness_path
1407                    })
1408                    .unwrap_or(false);
1409
1410                RouteInventoryItem::new(info.path.clone(), methods)
1411                    .with_tags(openapi_tags_for_route(
1412                        &self.openapi_spec,
1413                        &info.path,
1414                        &info.methods,
1415                    ))
1416                    .with_feature_gates(infer_route_feature_gates(&info.path))
1417                    .health_eligible(health_eligible)
1418                    .replay_eligible(is_dashboard_replay_eligible(&info.path, health_eligible))
1419            })
1420            .collect();
1421        inventory.sort_by(|a, b| a.path.cmp(&b.path));
1422
1423        let metrics = Arc::new(DashboardMetrics::new_with_replay_admin_path(
1424            inventory,
1425            config.replay_api_path.clone(),
1426        ));
1427
1428        // Insert metrics into router state using the public .state() API
1429        let router = std::mem::take(&mut self.router);
1430        self.router = router.state(Arc::clone(&metrics));
1431
1432        // Register dashboard routes
1433        let prefix = config.path.trim_end_matches('/').to_owned();
1434
1435        fn not_found() -> crate::response::Response {
1436            http::Response::builder()
1437                .status(404)
1438                .body(Body::Full(http_body_util::Full::new(bytes::Bytes::from(
1439                    "Not Found",
1440                ))))
1441                .unwrap()
1442        }
1443
1444        // Route 1: GET /__rustapi/dashboard  (the SPA page)
1445        {
1446            let metrics_c = Arc::clone(&metrics);
1447            let config_c = config.clone();
1448            let handler: BoxedHandler = Arc::new(move |req| {
1449                let metrics = Arc::clone(&metrics_c);
1450                let cfg = config_c.clone();
1451                Box::pin(async move {
1452                    let headers = req.headers().clone();
1453                    let method = req.method().to_string();
1454                    let path = req.uri().path().to_owned();
1455                    crate::dashboard::routes::dispatch(&headers, &method, &path, &metrics, &cfg)
1456                        .await
1457                        .unwrap_or_else(not_found)
1458                })
1459            });
1460            let mut h = HashMap::new();
1461            h.insert(http::Method::GET, handler);
1462            let router = std::mem::take(&mut self.router);
1463            self.router = router.route(&prefix, MethodRouter::from_boxed(h));
1464        }
1465
1466        // Route 2: GET /__rustapi/dashboard/*path  (API sub-paths)
1467        {
1468            let metrics_c = Arc::clone(&metrics);
1469            let config_c = config.clone();
1470            let wildcard_path = format!("{}/*path", prefix);
1471            let handler: BoxedHandler = Arc::new(move |req| {
1472                let metrics = Arc::clone(&metrics_c);
1473                let cfg = config_c.clone();
1474                Box::pin(async move {
1475                    let headers = req.headers().clone();
1476                    let method = req.method().to_string();
1477                    let path = req.uri().path().to_owned();
1478                    crate::dashboard::routes::dispatch(&headers, &method, &path, &metrics, &cfg)
1479                        .await
1480                        .unwrap_or_else(not_found)
1481                })
1482            });
1483            let mut h = HashMap::new();
1484            h.insert(http::Method::GET, handler);
1485            let router = std::mem::take(&mut self.router);
1486            self.router = router.route(&wildcard_path, MethodRouter::from_boxed(h));
1487        }
1488    }
1489
1490    /// Enable the embedded isometric system dashboard.
1491    ///
1492    /// Registers a self-contained admin surface at the configured path
1493    /// (default: `/__rustapi/dashboard`).
1494    ///
1495    /// # Example
1496    ///
1497    /// ```rust,ignore
1498    /// use rustapi_core::dashboard::DashboardConfig;
1499    ///
1500    /// RustApi::new()
1501    ///     .route("/api/users", get(list_users))
1502    ///     .dashboard(
1503    ///         DashboardConfig::new()
1504    ///             .admin_token("my-secret")
1505    ///     )
1506    ///     .run("127.0.0.1:8080")
1507    ///     .await
1508    /// ```
1509    #[cfg(feature = "dashboard")]
1510    pub fn dashboard(mut self, config: crate::dashboard::DashboardConfig) -> Self {
1511        self.dashboard_config = Some(config);
1512        self
1513    }
1514
1515    /// Run the server
1516    ///
1517    /// # Example
1518    ///
1519    /// ```rust,ignore
1520    /// RustApi::new()
1521    ///     .route("/", get(hello))
1522    ///     .run("127.0.0.1:8080")
1523    ///     .await
1524    /// ```
1525    pub async fn run(mut self, addr: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1526        // Hot-reload mode banner
1527        self.print_hot_reload_banner(addr);
1528
1529        // Apply health endpoints if configured
1530        self.apply_health_endpoints();
1531
1532        // Apply status page if configured
1533        self.apply_status_page();
1534
1535        // Apply embedded dashboard if configured
1536        #[cfg(feature = "dashboard")]
1537        self.apply_dashboard();
1538
1539        // Apply body limit layer if configured (should be first in the chain)
1540        if let Some(limit) = self.body_limit {
1541            // Prepend body limit layer so it's the first to process requests
1542            self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
1543        }
1544
1545        // Run on_start lifecycle hooks before accepting connections
1546        for hook in self.lifecycle_hooks.on_start {
1547            hook().await;
1548        }
1549
1550        let server = Server::new(self.router, self.layers, self.interceptors);
1551        server.run(addr).await
1552    }
1553
1554    /// Run the server with graceful shutdown signal
1555    pub async fn run_with_shutdown<F>(
1556        mut self,
1557        addr: impl AsRef<str>,
1558        signal: F,
1559    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
1560    where
1561        F: std::future::Future<Output = ()> + Send + 'static,
1562    {
1563        // Hot-reload mode banner
1564        self.print_hot_reload_banner(addr.as_ref());
1565
1566        // Apply health endpoints if configured
1567        self.apply_health_endpoints();
1568
1569        // Apply status page if configured
1570        self.apply_status_page();
1571
1572        // Apply embedded dashboard if configured
1573        #[cfg(feature = "dashboard")]
1574        self.apply_dashboard();
1575
1576        if let Some(limit) = self.body_limit {
1577            self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
1578        }
1579
1580        // Run on_start lifecycle hooks before accepting connections
1581        for hook in self.lifecycle_hooks.on_start {
1582            hook().await;
1583        }
1584
1585        // Wrap the shutdown signal to run on_shutdown hooks after signal fires
1586        let shutdown_hooks = self.lifecycle_hooks.on_shutdown;
1587        let wrapped_signal = async move {
1588            signal.await;
1589            // Run on_shutdown hooks after the shutdown signal fires
1590            for hook in shutdown_hooks {
1591                hook().await;
1592            }
1593        };
1594
1595        let server = Server::new(self.router, self.layers, self.interceptors);
1596        server
1597            .run_with_shutdown(addr.as_ref(), wrapped_signal)
1598            .await
1599    }
1600
1601    /// Get the inner router (for testing or advanced usage)
1602    pub fn into_router(self) -> Router {
1603        self.router
1604    }
1605
1606    /// Get a reference to the inner router (for advanced usage, e.g. in-process MCP dispatch).
1607    pub fn router(&self) -> &Router {
1608        &self.router
1609    }
1610
1611    /// Get the layer stack (for testing)
1612    pub fn layers(&self) -> &LayerStack {
1613        &self.layers
1614    }
1615
1616    /// Get the interceptor chain (for testing)
1617    pub fn interceptors(&self) -> &InterceptorChain {
1618        &self.interceptors
1619    }
1620
1621    /// Returns a dispatcher that can execute requests directly through this
1622    /// app's router + layers + interceptors, with zero network overhead.
1623    ///
1624    /// This is intended for in-process protocol integrations (e.g. MCP tool calls
1625    /// when running side-by-side with the main HTTP server).
1626    pub fn request_dispatcher(&self) -> RequestDispatcher {
1627        RequestDispatcher {
1628            router: Arc::new(self.router.clone()),
1629            layers: self.layers().clone(),
1630            interceptors: self.interceptors().clone(),
1631        }
1632    }
1633
1634    /// Enable HTTP/3 support with TLS certificates
1635    ///
1636    /// HTTP/3 requires TLS certificates. For development, you can use
1637    /// self-signed certificates with `run_http3_dev`.
1638    ///
1639    /// # Example
1640    ///
1641    /// ```rust,ignore
1642    /// RustApi::new()
1643    ///     .route("/", get(hello))
1644    ///     .run_http3("0.0.0.0:443", "cert.pem", "key.pem")
1645    ///     .await
1646    /// ```
1647    #[cfg(feature = "http3")]
1648    pub async fn run_http3(
1649        mut self,
1650        config: crate::http3::Http3Config,
1651    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1652        use std::sync::Arc;
1653
1654        // Apply health endpoints if configured
1655        self.apply_health_endpoints();
1656
1657        // Apply status page if configured
1658        self.apply_status_page();
1659
1660        // Apply body limit layer if configured
1661        if let Some(limit) = self.body_limit {
1662            self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
1663        }
1664
1665        let server = crate::http3::Http3Server::new(
1666            &config,
1667            Arc::new(self.router.clone()),
1668            Arc::new(self.layers.clone()),
1669            Arc::new(self.interceptors.clone()),
1670        )
1671        .await?;
1672
1673        server.run().await
1674    }
1675
1676    /// Run HTTP/3 server with self-signed certificate (development only)
1677    ///
1678    /// This is useful for local development and testing.
1679    /// **Do not use in production!**
1680    ///
1681    /// # Example
1682    ///
1683    /// ```rust,ignore
1684    /// RustApi::new()
1685    ///     .route("/", get(hello))
1686    ///     .run_http3_dev("0.0.0.0:8443")
1687    ///     .await
1688    /// ```
1689    #[cfg(feature = "http3-dev")]
1690    pub async fn run_http3_dev(
1691        mut self,
1692        addr: &str,
1693    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1694        use std::sync::Arc;
1695
1696        // Apply health endpoints if configured
1697        self.apply_health_endpoints();
1698
1699        // Apply status page if configured
1700        self.apply_status_page();
1701
1702        // Apply body limit layer if configured
1703        if let Some(limit) = self.body_limit {
1704            self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
1705        }
1706
1707        let server = crate::http3::Http3Server::new_with_self_signed(
1708            addr,
1709            Arc::new(self.router.clone()),
1710            Arc::new(self.layers.clone()),
1711            Arc::new(self.interceptors.clone()),
1712        )
1713        .await?;
1714
1715        server.run().await
1716    }
1717
1718    /// Configure HTTP/3 support for `run_http3` and `run_dual_stack`.
1719    ///
1720    /// # Example
1721    ///
1722    /// ```rust,ignore
1723    /// RustApi::new()
1724    ///     .with_http3("cert.pem", "key.pem")
1725    ///     .run_dual_stack("127.0.0.1:8080")
1726    ///     .await
1727    /// ```
1728    #[cfg(feature = "http3")]
1729    pub fn with_http3(mut self, cert_path: impl Into<String>, key_path: impl Into<String>) -> Self {
1730        self.http3_config = Some(crate::http3::Http3Config::new(cert_path, key_path));
1731        self
1732    }
1733
1734    /// Run both HTTP/1.1 (TCP) and HTTP/3 (QUIC/UDP) simultaneously.
1735    ///
1736    /// The HTTP/3 listener is bound to the same host and port as `http_addr`
1737    /// so clients can upgrade to either protocol on one endpoint.
1738    ///
1739    /// # Example
1740    ///
1741    /// ```rust,ignore
1742    /// RustApi::new()
1743    ///     .route("/", get(hello))
1744    ///     .with_http3("cert.pem", "key.pem")
1745    ///     .run_dual_stack("0.0.0.0:8080")
1746    ///     .await
1747    /// ```
1748    #[cfg(feature = "http3")]
1749    pub async fn run_dual_stack(
1750        mut self,
1751        http_addr: &str,
1752    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1753        use std::sync::Arc;
1754
1755        let mut config = self
1756            .http3_config
1757            .take()
1758            .ok_or("HTTP/3 config not set. Use .with_http3(...)")?;
1759
1760        let http_socket: std::net::SocketAddr = http_addr.parse()?;
1761        config.bind_addr = if http_socket.ip().is_ipv6() {
1762            format!("[{}]", http_socket.ip())
1763        } else {
1764            http_socket.ip().to_string()
1765        };
1766        config.port = http_socket.port();
1767        let http_addr = http_socket.to_string();
1768
1769        // Apply health endpoints if configured
1770        self.apply_health_endpoints();
1771
1772        // Apply status page if configured
1773        self.apply_status_page();
1774
1775        // Apply body limit layer if configured
1776        if let Some(limit) = self.body_limit {
1777            self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
1778        }
1779
1780        let router = Arc::new(self.router);
1781        let layers = Arc::new(self.layers);
1782        let interceptors = Arc::new(self.interceptors);
1783
1784        let http1_server =
1785            Server::from_shared(router.clone(), layers.clone(), interceptors.clone());
1786        let http3_server =
1787            crate::http3::Http3Server::new(&config, router, layers, interceptors).await?;
1788
1789        tracing::info!(
1790            http1_addr = %http_addr,
1791            http3_addr = %config.socket_addr(),
1792            "Starting dual-stack HTTP/1.1 + HTTP/3 servers"
1793        );
1794
1795        tokio::try_join!(
1796            http1_server.run_with_shutdown(&http_addr, std::future::pending::<()>()),
1797            http3_server.run_with_shutdown(std::future::pending::<()>()),
1798        )?;
1799
1800        Ok(())
1801    }
1802}
1803
1804#[cfg(feature = "dashboard")]
1805fn openapi_tags_for_route(
1806    spec: &rustapi_openapi::OpenApiSpec,
1807    path: &str,
1808    methods: &[http::Method],
1809) -> Vec<String> {
1810    let Some(path_item) = spec.paths.get(path) else {
1811        return Vec::new();
1812    };
1813
1814    let mut tags = BTreeSet::new();
1815    for method in methods {
1816        if let Some(operation) = operation_for_method(path_item, method) {
1817            tags.extend(operation.tags.iter().cloned());
1818        }
1819    }
1820
1821    tags.into_iter().collect()
1822}
1823
1824#[cfg(feature = "dashboard")]
1825fn operation_for_method<'a>(
1826    path_item: &'a rustapi_openapi::PathItem,
1827    method: &http::Method,
1828) -> Option<&'a rustapi_openapi::Operation> {
1829    match *method {
1830        http::Method::GET => path_item.get.as_ref(),
1831        http::Method::POST => path_item.post.as_ref(),
1832        http::Method::PUT => path_item.put.as_ref(),
1833        http::Method::PATCH => path_item.patch.as_ref(),
1834        http::Method::DELETE => path_item.delete.as_ref(),
1835        http::Method::HEAD => path_item.head.as_ref(),
1836        http::Method::OPTIONS => path_item.options.as_ref(),
1837        http::Method::TRACE => path_item.trace.as_ref(),
1838        _ => None,
1839    }
1840}
1841
1842#[cfg(feature = "dashboard")]
1843fn infer_route_feature_gates(path: &str) -> Vec<String> {
1844    if path.contains("openapi") || path.contains("docs") {
1845        vec!["core-openapi".to_string()]
1846    } else if path.starts_with("/__rustapi/replays") {
1847        vec!["extras-replay".to_string()]
1848    } else {
1849        Vec::new()
1850    }
1851}
1852
1853#[cfg(feature = "dashboard")]
1854fn is_dashboard_replay_eligible(path: &str, health_eligible: bool) -> bool {
1855    !health_eligible && !path.starts_with("/__rustapi/")
1856}
1857
1858fn add_path_params_to_operation(
1859    path: &str,
1860    op: &mut rustapi_openapi::Operation,
1861    param_schemas: &BTreeMap<String, String>,
1862) {
1863    let mut params: Vec<String> = Vec::new();
1864    let mut in_brace = false;
1865    let mut current = String::new();
1866
1867    for ch in path.chars() {
1868        match ch {
1869            '{' => {
1870                in_brace = true;
1871                current.clear();
1872            }
1873            '}' => {
1874                if in_brace {
1875                    in_brace = false;
1876                    if !current.is_empty() {
1877                        params.push(current.clone());
1878                    }
1879                }
1880            }
1881            _ => {
1882                if in_brace {
1883                    current.push(ch);
1884                }
1885            }
1886        }
1887    }
1888
1889    if params.is_empty() {
1890        return;
1891    }
1892
1893    let op_params = &mut op.parameters;
1894
1895    for name in params {
1896        let already = op_params
1897            .iter()
1898            .any(|p| p.location == "path" && p.name == name);
1899        if already {
1900            continue;
1901        }
1902
1903        // Use custom schema if provided, otherwise infer from name
1904        let schema = if let Some(schema_type) = param_schemas.get(&name) {
1905            schema_type_to_openapi_schema(schema_type)
1906        } else {
1907            infer_path_param_schema(&name)
1908        };
1909
1910        op_params.push(rustapi_openapi::Parameter {
1911            name,
1912            location: "path".to_string(),
1913            required: true,
1914            description: None,
1915            deprecated: None,
1916            schema: Some(schema),
1917        });
1918    }
1919}
1920
1921/// Convert a schema type string to an OpenAPI schema reference
1922fn schema_type_to_openapi_schema(schema_type: &str) -> rustapi_openapi::SchemaRef {
1923    match schema_type.to_lowercase().as_str() {
1924        "uuid" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1925            "type": "string",
1926            "format": "uuid"
1927        })),
1928        "integer" | "int" | "int64" | "i64" => {
1929            rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1930                "type": "integer",
1931                "format": "int64"
1932            }))
1933        }
1934        "int32" | "i32" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1935            "type": "integer",
1936            "format": "int32"
1937        })),
1938        "number" | "float" | "f64" | "f32" => {
1939            rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1940                "type": "number"
1941            }))
1942        }
1943        "boolean" | "bool" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1944            "type": "boolean"
1945        })),
1946        _ => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1947            "type": "string"
1948        })),
1949    }
1950}
1951
1952/// Infer the OpenAPI schema type for a path parameter based on naming conventions.
1953///
1954/// Common patterns:
1955/// - `*_id`, `*Id`, `id` → integer (but NOT *uuid)
1956/// - `*_count`, `*_num`, `page`, `limit`, `offset` → integer  
1957/// - `*_uuid`, `uuid` → string with uuid format
1958/// - `year`, `month`, `day` → integer
1959/// - Everything else → string
1960fn infer_path_param_schema(name: &str) -> rustapi_openapi::SchemaRef {
1961    let lower = name.to_lowercase();
1962
1963    // UUID patterns (check first to avoid false positive from "id" suffix)
1964    let is_uuid = lower == "uuid" || lower.ends_with("_uuid") || lower.ends_with("uuid");
1965
1966    if is_uuid {
1967        return rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1968            "type": "string",
1969            "format": "uuid"
1970        }));
1971    }
1972
1973    // Integer patterns
1974    // Integer patterns
1975    let is_integer = lower == "page"
1976        || lower == "limit"
1977        || lower == "offset"
1978        || lower == "count"
1979        || lower.ends_with("_count")
1980        || lower.ends_with("_num")
1981        || lower == "year"
1982        || lower == "month"
1983        || lower == "day"
1984        || lower == "index"
1985        || lower == "position";
1986
1987    if is_integer {
1988        rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1989            "type": "integer",
1990            "format": "int64"
1991        }))
1992    } else {
1993        rustapi_openapi::SchemaRef::Inline(serde_json::json!({ "type": "string" }))
1994    }
1995}
1996
1997/// Normalize a prefix for OpenAPI paths.
1998///
1999/// Ensures the prefix:
2000/// - Starts with exactly one leading slash
2001/// - Has no trailing slash (unless it's just "/")
2002/// - Has no double slashes
2003fn normalize_prefix_for_openapi(prefix: &str) -> String {
2004    // Handle empty string
2005    if prefix.is_empty() {
2006        return "/".to_string();
2007    }
2008
2009    // Split by slashes and filter out empty segments (handles multiple slashes)
2010    let segments: Vec<&str> = prefix.split('/').filter(|s| !s.is_empty()).collect();
2011
2012    // If no segments after filtering, return root
2013    if segments.is_empty() {
2014        return "/".to_string();
2015    }
2016
2017    // Build the normalized prefix with leading slash
2018    let mut result = String::with_capacity(prefix.len() + 1);
2019    for segment in segments {
2020        result.push('/');
2021        result.push_str(segment);
2022    }
2023
2024    result
2025}
2026
2027impl Default for RustApi {
2028    fn default() -> Self {
2029        Self::new()
2030    }
2031}
2032
2033/// Check Basic Auth header against expected credentials
2034#[cfg(feature = "swagger-ui")]
2035fn check_basic_auth(req: &crate::Request, expected: &str) -> bool {
2036    req.headers()
2037        .get(http::header::AUTHORIZATION)
2038        .and_then(|v| v.to_str().ok())
2039        .map(|auth| auth == expected)
2040        .unwrap_or(false)
2041}
2042
2043/// Create 401 Unauthorized response with WWW-Authenticate header
2044#[cfg(feature = "swagger-ui")]
2045fn unauthorized_response() -> crate::Response {
2046    http::Response::builder()
2047        .status(http::StatusCode::UNAUTHORIZED)
2048        .header(
2049            http::header::WWW_AUTHENTICATE,
2050            "Basic realm=\"API Documentation\"",
2051        )
2052        .header(http::header::CONTENT_TYPE, "text/plain")
2053        .body(crate::response::Body::from("Unauthorized"))
2054        .unwrap()
2055}
2056
2057/// Configuration builder for RustAPI with auto-routes
2058pub struct RustApiConfig {
2059    docs_path: Option<String>,
2060    docs_enabled: bool,
2061    api_title: String,
2062    api_version: String,
2063    api_description: Option<String>,
2064    body_limit: Option<usize>,
2065    layers: LayerStack,
2066}
2067
2068impl Default for RustApiConfig {
2069    fn default() -> Self {
2070        Self::new()
2071    }
2072}
2073
2074impl RustApiConfig {
2075    pub fn new() -> Self {
2076        Self {
2077            docs_path: Some("/docs".to_string()),
2078            docs_enabled: true,
2079            api_title: "RustAPI".to_string(),
2080            api_version: "1.0.0".to_string(),
2081            api_description: None,
2082            body_limit: None,
2083            layers: LayerStack::new(),
2084        }
2085    }
2086
2087    /// Set the docs path (default: "/docs")
2088    pub fn docs_path(mut self, path: impl Into<String>) -> Self {
2089        self.docs_path = Some(path.into());
2090        self
2091    }
2092
2093    /// Enable or disable docs (default: true)
2094    pub fn docs_enabled(mut self, enabled: bool) -> Self {
2095        self.docs_enabled = enabled;
2096        self
2097    }
2098
2099    /// Set OpenAPI info
2100    pub fn openapi_info(
2101        mut self,
2102        title: impl Into<String>,
2103        version: impl Into<String>,
2104        description: Option<impl Into<String>>,
2105    ) -> Self {
2106        self.api_title = title.into();
2107        self.api_version = version.into();
2108        self.api_description = description.map(|d| d.into());
2109        self
2110    }
2111
2112    /// Set body size limit
2113    pub fn body_limit(mut self, limit: usize) -> Self {
2114        self.body_limit = Some(limit);
2115        self
2116    }
2117
2118    /// Add a middleware layer
2119    pub fn layer<L>(mut self, layer: L) -> Self
2120    where
2121        L: MiddlewareLayer,
2122    {
2123        self.layers.push(Box::new(layer));
2124        self
2125    }
2126
2127    /// Build the RustApi instance
2128    pub fn build(self) -> RustApi {
2129        let mut app = RustApi::new().mount_auto_routes_grouped();
2130
2131        // Apply configuration
2132        if let Some(limit) = self.body_limit {
2133            app = app.body_limit(limit);
2134        }
2135
2136        app = app.openapi_info(
2137            &self.api_title,
2138            &self.api_version,
2139            self.api_description.as_deref(),
2140        );
2141
2142        #[cfg(feature = "swagger-ui")]
2143        if self.docs_enabled {
2144            if let Some(path) = self.docs_path {
2145                app = app.docs(&path);
2146            }
2147        }
2148
2149        // Apply layers
2150        // Note: layers are applied in reverse order in RustApi::layer logic (pushing to vec)
2151        app.layers.extend(self.layers);
2152
2153        app
2154    }
2155
2156    /// Build and run the server
2157    pub async fn run(
2158        self,
2159        addr: impl AsRef<str>,
2160    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
2161        self.build().run(addr.as_ref()).await
2162    }
2163}
2164
2165#[cfg(test)]
2166mod tests {
2167    use super::RustApi;
2168    use crate::extract::{FromRequestParts, State};
2169    use crate::path_params::PathParams;
2170    use crate::request::Request;
2171    use crate::router::{get, post, Router};
2172    use bytes::Bytes;
2173    use http::Method;
2174    use proptest::prelude::*;
2175
2176    #[test]
2177    fn state_is_available_via_extractor() {
2178        let app = RustApi::new().state(123u32);
2179        let router = app.into_router();
2180
2181        let req = http::Request::builder()
2182            .method(Method::GET)
2183            .uri("/test")
2184            .body(())
2185            .unwrap();
2186        let (parts, _) = req.into_parts();
2187
2188        let request = Request::new(
2189            parts,
2190            crate::request::BodyVariant::Buffered(Bytes::new()),
2191            router.state_ref(),
2192            PathParams::new(),
2193        );
2194        let State(value) = State::<u32>::from_request_parts(&request).unwrap();
2195        assert_eq!(value, 123u32);
2196    }
2197
2198    #[test]
2199    fn test_path_param_type_inference_integer() {
2200        use super::infer_path_param_schema;
2201
2202        // Test common integer patterns
2203        let int_params = [
2204            "page",
2205            "limit",
2206            "offset",
2207            "count",
2208            "item_count",
2209            "year",
2210            "month",
2211            "day",
2212            "index",
2213            "position",
2214        ];
2215
2216        for name in int_params {
2217            let schema = infer_path_param_schema(name);
2218            match schema {
2219                rustapi_openapi::SchemaRef::Inline(v) => {
2220                    assert_eq!(
2221                        v.get("type").and_then(|v| v.as_str()),
2222                        Some("integer"),
2223                        "Expected '{}' to be inferred as integer",
2224                        name
2225                    );
2226                }
2227                _ => panic!("Expected inline schema for '{}'", name),
2228            }
2229        }
2230    }
2231
2232    #[test]
2233    fn test_path_param_type_inference_uuid() {
2234        use super::infer_path_param_schema;
2235
2236        // Test UUID patterns
2237        let uuid_params = ["uuid", "user_uuid", "sessionUuid"];
2238
2239        for name in uuid_params {
2240            let schema = infer_path_param_schema(name);
2241            match schema {
2242                rustapi_openapi::SchemaRef::Inline(v) => {
2243                    assert_eq!(
2244                        v.get("type").and_then(|v| v.as_str()),
2245                        Some("string"),
2246                        "Expected '{}' to be inferred as string",
2247                        name
2248                    );
2249                    assert_eq!(
2250                        v.get("format").and_then(|v| v.as_str()),
2251                        Some("uuid"),
2252                        "Expected '{}' to have uuid format",
2253                        name
2254                    );
2255                }
2256                _ => panic!("Expected inline schema for '{}'", name),
2257            }
2258        }
2259    }
2260
2261    #[test]
2262    fn test_path_param_type_inference_string() {
2263        use super::infer_path_param_schema;
2264
2265        // Test string (default) patterns
2266        let string_params = [
2267            "name", "slug", "code", "token", "username", "id", "user_id", "userId", "postId",
2268        ];
2269
2270        for name in string_params {
2271            let schema = infer_path_param_schema(name);
2272            match schema {
2273                rustapi_openapi::SchemaRef::Inline(v) => {
2274                    assert_eq!(
2275                        v.get("type").and_then(|v| v.as_str()),
2276                        Some("string"),
2277                        "Expected '{}' to be inferred as string",
2278                        name
2279                    );
2280                    assert!(
2281                        v.get("format").is_none()
2282                            || v.get("format").and_then(|v| v.as_str()) != Some("uuid"),
2283                        "Expected '{}' to NOT have uuid format",
2284                        name
2285                    );
2286                }
2287                _ => panic!("Expected inline schema for '{}'", name),
2288            }
2289        }
2290    }
2291
2292    #[test]
2293    fn test_schema_type_to_openapi_schema() {
2294        use super::schema_type_to_openapi_schema;
2295
2296        // Test UUID schema
2297        let uuid_schema = schema_type_to_openapi_schema("uuid");
2298        match uuid_schema {
2299            rustapi_openapi::SchemaRef::Inline(v) => {
2300                assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("string"));
2301                assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("uuid"));
2302            }
2303            _ => panic!("Expected inline schema for uuid"),
2304        }
2305
2306        // Test integer schemas
2307        for schema_type in ["integer", "int", "int64", "i64"] {
2308            let schema = schema_type_to_openapi_schema(schema_type);
2309            match schema {
2310                rustapi_openapi::SchemaRef::Inline(v) => {
2311                    assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("integer"));
2312                    assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("int64"));
2313                }
2314                _ => panic!("Expected inline schema for {}", schema_type),
2315            }
2316        }
2317
2318        // Test int32 schema
2319        let int32_schema = schema_type_to_openapi_schema("int32");
2320        match int32_schema {
2321            rustapi_openapi::SchemaRef::Inline(v) => {
2322                assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("integer"));
2323                assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("int32"));
2324            }
2325            _ => panic!("Expected inline schema for int32"),
2326        }
2327
2328        // Test number/float schema
2329        for schema_type in ["number", "float"] {
2330            let schema = schema_type_to_openapi_schema(schema_type);
2331            match schema {
2332                rustapi_openapi::SchemaRef::Inline(v) => {
2333                    assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("number"));
2334                }
2335                _ => panic!("Expected inline schema for {}", schema_type),
2336            }
2337        }
2338
2339        // Test boolean schema
2340        for schema_type in ["boolean", "bool"] {
2341            let schema = schema_type_to_openapi_schema(schema_type);
2342            match schema {
2343                rustapi_openapi::SchemaRef::Inline(v) => {
2344                    assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("boolean"));
2345                }
2346                _ => panic!("Expected inline schema for {}", schema_type),
2347            }
2348        }
2349
2350        // Test string schema (default)
2351        let string_schema = schema_type_to_openapi_schema("string");
2352        match string_schema {
2353            rustapi_openapi::SchemaRef::Inline(v) => {
2354                assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("string"));
2355            }
2356            _ => panic!("Expected inline schema for string"),
2357        }
2358    }
2359
2360    // **Feature: router-nesting, Property 11: OpenAPI Integration**
2361    //
2362    // For any nested routes with OpenAPI operations, the operations should appear
2363    // in the parent's OpenAPI spec with prefixed paths and preserved metadata.
2364    //
2365    // **Validates: Requirements 4.1, 4.2**
2366    proptest! {
2367        #![proptest_config(ProptestConfig::with_cases(100))]
2368
2369        /// Property: Nested routes appear in OpenAPI spec with prefixed paths
2370        ///
2371        /// For any router with routes nested under a prefix, all routes should
2372        /// appear in the OpenAPI spec with the prefix prepended to their paths.
2373        #[test]
2374        fn prop_nested_routes_in_openapi_spec(
2375            // Generate prefix segments
2376            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2377            // Generate route path segments
2378            route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2379            has_param in any::<bool>(),
2380        ) {
2381            async fn handler() -> &'static str { "handler" }
2382
2383            // Build the prefix
2384            let prefix = format!("/{}", prefix_segments.join("/"));
2385
2386            // Build the route path
2387            let mut route_path = format!("/{}", route_segments.join("/"));
2388            if has_param {
2389                route_path.push_str("/{id}");
2390            }
2391
2392            // Create nested router and nest it through RustApi
2393            let nested_router = Router::new().route(&route_path, get(handler));
2394            let app = RustApi::new().nest(&prefix, nested_router);
2395
2396            // Build expected prefixed path for OpenAPI (uses {param} format)
2397            let expected_openapi_path = format!("{}{}", prefix, route_path);
2398
2399            // Get the OpenAPI spec
2400            let spec = app.openapi_spec();
2401
2402            // Property: The prefixed route should exist in OpenAPI paths
2403            prop_assert!(
2404                spec.paths.contains_key(&expected_openapi_path),
2405                "Expected OpenAPI path '{}' not found. Available paths: {:?}",
2406                expected_openapi_path,
2407                spec.paths.keys().collect::<Vec<_>>()
2408            );
2409
2410            // Property: The path item should have a GET operation
2411            let path_item = spec.paths.get(&expected_openapi_path).unwrap();
2412            prop_assert!(
2413                path_item.get.is_some(),
2414                "GET operation should exist for path '{}'",
2415                expected_openapi_path
2416            );
2417        }
2418
2419        /// Property: Multiple HTTP methods are preserved in OpenAPI spec after nesting
2420        ///
2421        /// For any router with routes having multiple HTTP methods, nesting should
2422        /// preserve all method operations in the OpenAPI spec.
2423        #[test]
2424        fn prop_multiple_methods_preserved_in_openapi(
2425            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2426            route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2427        ) {
2428            async fn get_handler() -> &'static str { "get" }
2429            async fn post_handler() -> &'static str { "post" }
2430
2431            // Build the prefix and route path
2432            let prefix = format!("/{}", prefix_segments.join("/"));
2433            let route_path = format!("/{}", route_segments.join("/"));
2434
2435            // Create nested router with both GET and POST using separate routes
2436            // Since MethodRouter doesn't have chaining methods, we create two routes
2437            let get_route_path = format!("{}/get", route_path);
2438            let post_route_path = format!("{}/post", route_path);
2439            let nested_router = Router::new()
2440                .route(&get_route_path, get(get_handler))
2441                .route(&post_route_path, post(post_handler));
2442            let app = RustApi::new().nest(&prefix, nested_router);
2443
2444            // Build expected prefixed paths for OpenAPI
2445            let expected_get_path = format!("{}{}", prefix, get_route_path);
2446            let expected_post_path = format!("{}{}", prefix, post_route_path);
2447
2448            // Get the OpenAPI spec
2449            let spec = app.openapi_spec();
2450
2451            // Property: Both paths should exist
2452            prop_assert!(
2453                spec.paths.contains_key(&expected_get_path),
2454                "Expected OpenAPI path '{}' not found",
2455                expected_get_path
2456            );
2457            prop_assert!(
2458                spec.paths.contains_key(&expected_post_path),
2459                "Expected OpenAPI path '{}' not found",
2460                expected_post_path
2461            );
2462
2463            // Property: GET operation should exist on get path
2464            let get_path_item = spec.paths.get(&expected_get_path).unwrap();
2465            prop_assert!(
2466                get_path_item.get.is_some(),
2467                "GET operation should exist for path '{}'",
2468                expected_get_path
2469            );
2470
2471            // Property: POST operation should exist on post path
2472            let post_path_item = spec.paths.get(&expected_post_path).unwrap();
2473            prop_assert!(
2474                post_path_item.post.is_some(),
2475                "POST operation should exist for path '{}'",
2476                expected_post_path
2477            );
2478        }
2479
2480        /// Property: Path parameters are added to OpenAPI operations after nesting
2481        ///
2482        /// For any nested route with path parameters, the OpenAPI operation should
2483        /// include the path parameters.
2484        #[test]
2485        fn prop_path_params_in_openapi_after_nesting(
2486            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2487            param_name in "[a-z][a-z0-9]{0,5}",
2488        ) {
2489            async fn handler() -> &'static str { "handler" }
2490
2491            // Build the prefix and route path with parameter
2492            let prefix = format!("/{}", prefix_segments.join("/"));
2493            let route_path = format!("/{{{}}}", param_name);
2494
2495            // Create nested router
2496            let nested_router = Router::new().route(&route_path, get(handler));
2497            let app = RustApi::new().nest(&prefix, nested_router);
2498
2499            // Build expected prefixed path for OpenAPI
2500            let expected_openapi_path = format!("{}{}", prefix, route_path);
2501
2502            // Get the OpenAPI spec
2503            let spec = app.openapi_spec();
2504
2505            // Property: The path should exist
2506            prop_assert!(
2507                spec.paths.contains_key(&expected_openapi_path),
2508                "Expected OpenAPI path '{}' not found",
2509                expected_openapi_path
2510            );
2511
2512            // Property: The GET operation should have the path parameter
2513            let path_item = spec.paths.get(&expected_openapi_path).unwrap();
2514            let get_op = path_item.get.as_ref().unwrap();
2515
2516            prop_assert!(
2517                !get_op.parameters.is_empty(),
2518                "Operation should have parameters for path '{}'",
2519                expected_openapi_path
2520            );
2521
2522            let params = &get_op.parameters;
2523            let has_param = params.iter().any(|p| p.name == param_name && p.location == "path");
2524            prop_assert!(
2525                has_param,
2526                "Path parameter '{}' should exist in operation parameters. Found: {:?}",
2527                param_name,
2528                params.iter().map(|p| &p.name).collect::<Vec<_>>()
2529            );
2530        }
2531    }
2532
2533    // **Feature: router-nesting, Property 13: RustApi Integration**
2534    //
2535    // For any router nested through `RustApi::new().nest()`, the behavior should be
2536    // identical to nesting through `Router::new().nest()`, and routes should appear
2537    // in the OpenAPI spec.
2538    //
2539    // **Validates: Requirements 6.1, 6.2**
2540    proptest! {
2541        #![proptest_config(ProptestConfig::with_cases(100))]
2542
2543        /// Property: RustApi::nest delegates to Router::nest and produces identical route registration
2544        ///
2545        /// For any router with routes nested under a prefix, nesting through RustApi
2546        /// should produce the same route registration as nesting through Router directly.
2547        #[test]
2548        fn prop_rustapi_nest_delegates_to_router_nest(
2549            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2550            route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2551            has_param in any::<bool>(),
2552        ) {
2553            async fn handler() -> &'static str { "handler" }
2554
2555            // Build the prefix
2556            let prefix = format!("/{}", prefix_segments.join("/"));
2557
2558            // Build the route path
2559            let mut route_path = format!("/{}", route_segments.join("/"));
2560            if has_param {
2561                route_path.push_str("/{id}");
2562            }
2563
2564            // Create nested router
2565            let nested_router_for_rustapi = Router::new().route(&route_path, get(handler));
2566            let nested_router_for_router = Router::new().route(&route_path, get(handler));
2567
2568            // Nest through RustApi
2569            let rustapi_app = RustApi::new().nest(&prefix, nested_router_for_rustapi);
2570            let rustapi_router = rustapi_app.into_router();
2571
2572            // Nest through Router directly
2573            let router_app = Router::new().nest(&prefix, nested_router_for_router);
2574
2575            // Property: Both should have the same registered routes
2576            let rustapi_routes = rustapi_router.registered_routes();
2577            let router_routes = router_app.registered_routes();
2578
2579            prop_assert_eq!(
2580                rustapi_routes.len(),
2581                router_routes.len(),
2582                "RustApi and Router should have same number of routes"
2583            );
2584
2585            // Property: All routes from Router should exist in RustApi
2586            for (path, info) in router_routes {
2587                prop_assert!(
2588                    rustapi_routes.contains_key(path),
2589                    "Route '{}' from Router should exist in RustApi routes",
2590                    path
2591                );
2592
2593                let rustapi_info = rustapi_routes.get(path).unwrap();
2594                prop_assert_eq!(
2595                    &info.path, &rustapi_info.path,
2596                    "Display paths should match for route '{}'",
2597                    path
2598                );
2599                prop_assert_eq!(
2600                    info.methods.len(), rustapi_info.methods.len(),
2601                    "Method count should match for route '{}'",
2602                    path
2603                );
2604            }
2605        }
2606
2607        /// Property: RustApi::nest includes nested routes in OpenAPI spec
2608        ///
2609        /// For any router with routes nested through RustApi, all routes should
2610        /// appear in the OpenAPI specification with prefixed paths.
2611        #[test]
2612        fn prop_rustapi_nest_includes_routes_in_openapi(
2613            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2614            route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2615            has_param in any::<bool>(),
2616        ) {
2617            async fn handler() -> &'static str { "handler" }
2618
2619            // Build the prefix
2620            let prefix = format!("/{}", prefix_segments.join("/"));
2621
2622            // Build the route path
2623            let mut route_path = format!("/{}", route_segments.join("/"));
2624            if has_param {
2625                route_path.push_str("/{id}");
2626            }
2627
2628            // Create nested router and nest through RustApi
2629            let nested_router = Router::new().route(&route_path, get(handler));
2630            let app = RustApi::new().nest(&prefix, nested_router);
2631
2632            // Build expected prefixed path for OpenAPI
2633            let expected_openapi_path = format!("{}{}", prefix, route_path);
2634
2635            // Get the OpenAPI spec
2636            let spec = app.openapi_spec();
2637
2638            // Property: The prefixed route should exist in OpenAPI paths
2639            prop_assert!(
2640                spec.paths.contains_key(&expected_openapi_path),
2641                "Expected OpenAPI path '{}' not found. Available paths: {:?}",
2642                expected_openapi_path,
2643                spec.paths.keys().collect::<Vec<_>>()
2644            );
2645
2646            // Property: The path item should have a GET operation
2647            let path_item = spec.paths.get(&expected_openapi_path).unwrap();
2648            prop_assert!(
2649                path_item.get.is_some(),
2650                "GET operation should exist for path '{}'",
2651                expected_openapi_path
2652            );
2653        }
2654
2655        /// Property: RustApi::nest route matching is identical to Router::nest
2656        ///
2657        /// For any nested route, matching through RustApi should produce the same
2658        /// result as matching through Router directly.
2659        #[test]
2660        fn prop_rustapi_nest_route_matching_identical(
2661            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2662            route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2663            param_value in "[a-z0-9]{1,10}",
2664        ) {
2665            use crate::router::RouteMatch;
2666
2667            async fn handler() -> &'static str { "handler" }
2668
2669            // Build the prefix and route path with parameter
2670            let prefix = format!("/{}", prefix_segments.join("/"));
2671            let route_path = format!("/{}/{{id}}", route_segments.join("/"));
2672
2673            // Create nested routers
2674            let nested_router_for_rustapi = Router::new().route(&route_path, get(handler));
2675            let nested_router_for_router = Router::new().route(&route_path, get(handler));
2676
2677            // Nest through both RustApi and Router
2678            let rustapi_app = RustApi::new().nest(&prefix, nested_router_for_rustapi);
2679            let rustapi_router = rustapi_app.into_router();
2680            let router_app = Router::new().nest(&prefix, nested_router_for_router);
2681
2682            // Build the full path to match
2683            let full_path = format!("{}/{}/{}", prefix, route_segments.join("/"), param_value);
2684
2685            // Match through both
2686            let rustapi_match = rustapi_router.match_route(&full_path, &Method::GET);
2687            let router_match = router_app.match_route(&full_path, &Method::GET);
2688
2689            // Property: Both should return Found with same parameters
2690            match (rustapi_match, router_match) {
2691                (RouteMatch::Found { params: rustapi_params, .. }, RouteMatch::Found { params: router_params, .. }) => {
2692                    prop_assert_eq!(
2693                        rustapi_params.len(),
2694                        router_params.len(),
2695                        "Parameter count should match"
2696                    );
2697                    for (key, value) in &router_params {
2698                        prop_assert!(
2699                            rustapi_params.contains_key(key),
2700                            "RustApi should have parameter '{}'",
2701                            key
2702                        );
2703                        prop_assert_eq!(
2704                            rustapi_params.get(key).unwrap(),
2705                            value,
2706                            "Parameter '{}' value should match",
2707                            key
2708                        );
2709                    }
2710                }
2711                (rustapi_result, router_result) => {
2712                    prop_assert!(
2713                        false,
2714                        "Both should return Found, but RustApi returned {:?} and Router returned {:?}",
2715                        match rustapi_result {
2716                            RouteMatch::Found { .. } => "Found",
2717                            RouteMatch::NotFound => "NotFound",
2718                            RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
2719                        },
2720                        match router_result {
2721                            RouteMatch::Found { .. } => "Found",
2722                            RouteMatch::NotFound => "NotFound",
2723                            RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
2724                        }
2725                    );
2726                }
2727            }
2728        }
2729    }
2730
2731    /// Unit test: Verify OpenAPI operations are propagated during nesting
2732    #[test]
2733    fn test_openapi_operations_propagated_during_nesting() {
2734        async fn list_users() -> &'static str {
2735            "list users"
2736        }
2737        async fn get_user() -> &'static str {
2738            "get user"
2739        }
2740        async fn create_user() -> &'static str {
2741            "create user"
2742        }
2743
2744        // Create nested router with multiple routes
2745        // Note: We use separate routes since MethodRouter doesn't support chaining
2746        let users_router = Router::new()
2747            .route("/", get(list_users))
2748            .route("/create", post(create_user))
2749            .route("/{id}", get(get_user));
2750
2751        // Nest under /api/v1/users
2752        let app = RustApi::new().nest("/api/v1/users", users_router);
2753
2754        let spec = app.openapi_spec();
2755
2756        // Verify /api/v1/users path exists with GET
2757        assert!(
2758            spec.paths.contains_key("/api/v1/users"),
2759            "Should have /api/v1/users path"
2760        );
2761        let users_path = spec.paths.get("/api/v1/users").unwrap();
2762        assert!(users_path.get.is_some(), "Should have GET operation");
2763
2764        // Verify /api/v1/users/create path exists with POST
2765        assert!(
2766            spec.paths.contains_key("/api/v1/users/create"),
2767            "Should have /api/v1/users/create path"
2768        );
2769        let create_path = spec.paths.get("/api/v1/users/create").unwrap();
2770        assert!(create_path.post.is_some(), "Should have POST operation");
2771
2772        // Verify /api/v1/users/{id} path exists with GET
2773        assert!(
2774            spec.paths.contains_key("/api/v1/users/{id}"),
2775            "Should have /api/v1/users/{{id}} path"
2776        );
2777        let user_path = spec.paths.get("/api/v1/users/{id}").unwrap();
2778        assert!(
2779            user_path.get.is_some(),
2780            "Should have GET operation for user by id"
2781        );
2782
2783        // Verify path parameter is added
2784        let get_user_op = user_path.get.as_ref().unwrap();
2785        assert!(!get_user_op.parameters.is_empty(), "Should have parameters");
2786        let params = &get_user_op.parameters;
2787        assert!(
2788            params
2789                .iter()
2790                .any(|p| p.name == "id" && p.location == "path"),
2791            "Should have 'id' path parameter"
2792        );
2793    }
2794
2795    /// Unit test: Verify nested routes don't appear without nesting
2796    #[test]
2797    fn test_openapi_spec_empty_without_routes() {
2798        let app = RustApi::new();
2799        let spec = app.openapi_spec();
2800
2801        // Should have no paths (except potentially default ones)
2802        assert!(
2803            spec.paths.is_empty(),
2804            "OpenAPI spec should have no paths without routes"
2805        );
2806    }
2807
2808    /// Unit test: Verify RustApi::nest delegates correctly to Router::nest
2809    ///
2810    /// **Feature: router-nesting, Property 13: RustApi Integration**
2811    /// **Validates: Requirements 6.1, 6.2**
2812    #[test]
2813    fn test_rustapi_nest_delegates_to_router_nest() {
2814        use crate::router::RouteMatch;
2815
2816        async fn list_users() -> &'static str {
2817            "list users"
2818        }
2819        async fn get_user() -> &'static str {
2820            "get user"
2821        }
2822        async fn create_user() -> &'static str {
2823            "create user"
2824        }
2825
2826        // Create nested router with multiple routes
2827        let users_router = Router::new()
2828            .route("/", get(list_users))
2829            .route("/create", post(create_user))
2830            .route("/{id}", get(get_user));
2831
2832        // Nest through RustApi
2833        let app = RustApi::new().nest("/api/v1/users", users_router);
2834        let router = app.into_router();
2835
2836        // Verify routes are registered correctly
2837        let routes = router.registered_routes();
2838        assert_eq!(routes.len(), 3, "Should have 3 routes registered");
2839
2840        // Verify route paths
2841        assert!(
2842            routes.contains_key("/api/v1/users"),
2843            "Should have /api/v1/users route"
2844        );
2845        assert!(
2846            routes.contains_key("/api/v1/users/create"),
2847            "Should have /api/v1/users/create route"
2848        );
2849        assert!(
2850            routes.contains_key("/api/v1/users/:id"),
2851            "Should have /api/v1/users/:id route"
2852        );
2853
2854        // Verify route matching works
2855        match router.match_route("/api/v1/users", &Method::GET) {
2856            RouteMatch::Found { params, .. } => {
2857                assert!(params.is_empty(), "Root route should have no params");
2858            }
2859            _ => panic!("GET /api/v1/users should be found"),
2860        }
2861
2862        match router.match_route("/api/v1/users/create", &Method::POST) {
2863            RouteMatch::Found { params, .. } => {
2864                assert!(params.is_empty(), "Create route should have no params");
2865            }
2866            _ => panic!("POST /api/v1/users/create should be found"),
2867        }
2868
2869        match router.match_route("/api/v1/users/123", &Method::GET) {
2870            RouteMatch::Found { params, .. } => {
2871                assert_eq!(
2872                    params.get("id"),
2873                    Some(&"123".to_string()),
2874                    "Should extract id param"
2875                );
2876            }
2877            _ => panic!("GET /api/v1/users/123 should be found"),
2878        }
2879
2880        // Verify method not allowed
2881        match router.match_route("/api/v1/users", &Method::DELETE) {
2882            RouteMatch::MethodNotAllowed { allowed } => {
2883                assert!(allowed.contains(&Method::GET), "Should allow GET");
2884            }
2885            _ => panic!("DELETE /api/v1/users should return MethodNotAllowed"),
2886        }
2887    }
2888
2889    /// Unit test: Verify RustApi::nest includes routes in OpenAPI spec
2890    ///
2891    /// **Feature: router-nesting, Property 13: RustApi Integration**
2892    /// **Validates: Requirements 6.1, 6.2**
2893    #[test]
2894    fn test_rustapi_nest_includes_routes_in_openapi_spec() {
2895        async fn list_items() -> &'static str {
2896            "list items"
2897        }
2898        async fn get_item() -> &'static str {
2899            "get item"
2900        }
2901
2902        // Create nested router
2903        let items_router = Router::new()
2904            .route("/", get(list_items))
2905            .route("/{item_id}", get(get_item));
2906
2907        // Nest through RustApi
2908        let app = RustApi::new().nest("/api/items", items_router);
2909
2910        // Verify OpenAPI spec
2911        let spec = app.openapi_spec();
2912
2913        // Verify paths exist
2914        assert!(
2915            spec.paths.contains_key("/api/items"),
2916            "Should have /api/items in OpenAPI"
2917        );
2918        assert!(
2919            spec.paths.contains_key("/api/items/{item_id}"),
2920            "Should have /api/items/{{item_id}} in OpenAPI"
2921        );
2922
2923        // Verify operations
2924        let list_path = spec.paths.get("/api/items").unwrap();
2925        assert!(
2926            list_path.get.is_some(),
2927            "Should have GET operation for /api/items"
2928        );
2929
2930        let get_path = spec.paths.get("/api/items/{item_id}").unwrap();
2931        assert!(
2932            get_path.get.is_some(),
2933            "Should have GET operation for /api/items/{{item_id}}"
2934        );
2935
2936        // Verify path parameter is added
2937        let get_op = get_path.get.as_ref().unwrap();
2938        assert!(!get_op.parameters.is_empty(), "Should have parameters");
2939        let params = &get_op.parameters;
2940        assert!(
2941            params
2942                .iter()
2943                .any(|p| p.name == "item_id" && p.location == "path"),
2944            "Should have 'item_id' path parameter"
2945        );
2946    }
2947}