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