Skip to main content

rustapi_core/
app.rs

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