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