rustapi_core/
app.rs

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