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