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