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    let is_integer = lower == "id"
1180        || lower.ends_with("_id")
1181        || (lower.ends_with("id") && lower.len() > 2) // e.g., "userId", but not "uuid"
1182        || lower == "page"
1183        || lower == "limit"
1184        || lower == "offset"
1185        || lower == "count"
1186        || lower.ends_with("_count")
1187        || lower.ends_with("_num")
1188        || lower == "year"
1189        || lower == "month"
1190        || lower == "day"
1191        || lower == "index"
1192        || lower == "position";
1193
1194    if is_integer {
1195        rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1196            "type": "integer",
1197            "format": "int64"
1198        }))
1199    } else {
1200        rustapi_openapi::SchemaRef::Inline(serde_json::json!({ "type": "string" }))
1201    }
1202}
1203
1204/// Normalize a prefix for OpenAPI paths.
1205///
1206/// Ensures the prefix:
1207/// - Starts with exactly one leading slash
1208/// - Has no trailing slash (unless it's just "/")
1209/// - Has no double slashes
1210fn normalize_prefix_for_openapi(prefix: &str) -> String {
1211    // Handle empty string
1212    if prefix.is_empty() {
1213        return "/".to_string();
1214    }
1215
1216    // Split by slashes and filter out empty segments (handles multiple slashes)
1217    let segments: Vec<&str> = prefix.split('/').filter(|s| !s.is_empty()).collect();
1218
1219    // If no segments after filtering, return root
1220    if segments.is_empty() {
1221        return "/".to_string();
1222    }
1223
1224    // Build the normalized prefix with leading slash
1225    let mut result = String::with_capacity(prefix.len() + 1);
1226    for segment in segments {
1227        result.push('/');
1228        result.push_str(segment);
1229    }
1230
1231    result
1232}
1233
1234impl Default for RustApi {
1235    fn default() -> Self {
1236        Self::new()
1237    }
1238}
1239
1240#[cfg(test)]
1241mod tests {
1242    use super::RustApi;
1243    use crate::extract::{FromRequestParts, State};
1244    use crate::path_params::PathParams;
1245    use crate::request::Request;
1246    use crate::router::{get, post, Router};
1247    use bytes::Bytes;
1248    use http::Method;
1249    use proptest::prelude::*;
1250
1251    #[test]
1252    fn state_is_available_via_extractor() {
1253        let app = RustApi::new().state(123u32);
1254        let router = app.into_router();
1255
1256        let req = http::Request::builder()
1257            .method(Method::GET)
1258            .uri("/test")
1259            .body(())
1260            .unwrap();
1261        let (parts, _) = req.into_parts();
1262
1263        let request = Request::new(
1264            parts,
1265            crate::request::BodyVariant::Buffered(Bytes::new()),
1266            router.state_ref(),
1267            PathParams::new(),
1268        );
1269        let State(value) = State::<u32>::from_request_parts(&request).unwrap();
1270        assert_eq!(value, 123u32);
1271    }
1272
1273    #[test]
1274    fn test_path_param_type_inference_integer() {
1275        use super::infer_path_param_schema;
1276
1277        // Test common integer patterns
1278        let int_params = [
1279            "id",
1280            "user_id",
1281            "userId",
1282            "postId",
1283            "page",
1284            "limit",
1285            "offset",
1286            "count",
1287            "item_count",
1288            "year",
1289            "month",
1290            "day",
1291            "index",
1292            "position",
1293        ];
1294
1295        for name in int_params {
1296            let schema = infer_path_param_schema(name);
1297            match schema {
1298                rustapi_openapi::SchemaRef::Inline(v) => {
1299                    assert_eq!(
1300                        v.get("type").and_then(|v| v.as_str()),
1301                        Some("integer"),
1302                        "Expected '{}' to be inferred as integer",
1303                        name
1304                    );
1305                }
1306                _ => panic!("Expected inline schema for '{}'", name),
1307            }
1308        }
1309    }
1310
1311    #[test]
1312    fn test_path_param_type_inference_uuid() {
1313        use super::infer_path_param_schema;
1314
1315        // Test UUID patterns
1316        let uuid_params = ["uuid", "user_uuid", "sessionUuid"];
1317
1318        for name in uuid_params {
1319            let schema = infer_path_param_schema(name);
1320            match schema {
1321                rustapi_openapi::SchemaRef::Inline(v) => {
1322                    assert_eq!(
1323                        v.get("type").and_then(|v| v.as_str()),
1324                        Some("string"),
1325                        "Expected '{}' to be inferred as string",
1326                        name
1327                    );
1328                    assert_eq!(
1329                        v.get("format").and_then(|v| v.as_str()),
1330                        Some("uuid"),
1331                        "Expected '{}' to have uuid format",
1332                        name
1333                    );
1334                }
1335                _ => panic!("Expected inline schema for '{}'", name),
1336            }
1337        }
1338    }
1339
1340    #[test]
1341    fn test_path_param_type_inference_string() {
1342        use super::infer_path_param_schema;
1343
1344        // Test string (default) patterns
1345        let string_params = ["name", "slug", "code", "token", "username"];
1346
1347        for name in string_params {
1348            let schema = infer_path_param_schema(name);
1349            match schema {
1350                rustapi_openapi::SchemaRef::Inline(v) => {
1351                    assert_eq!(
1352                        v.get("type").and_then(|v| v.as_str()),
1353                        Some("string"),
1354                        "Expected '{}' to be inferred as string",
1355                        name
1356                    );
1357                    assert!(
1358                        v.get("format").is_none()
1359                            || v.get("format").and_then(|v| v.as_str()) != Some("uuid"),
1360                        "Expected '{}' to NOT have uuid format",
1361                        name
1362                    );
1363                }
1364                _ => panic!("Expected inline schema for '{}'", name),
1365            }
1366        }
1367    }
1368
1369    #[test]
1370    fn test_schema_type_to_openapi_schema() {
1371        use super::schema_type_to_openapi_schema;
1372
1373        // Test UUID schema
1374        let uuid_schema = schema_type_to_openapi_schema("uuid");
1375        match uuid_schema {
1376            rustapi_openapi::SchemaRef::Inline(v) => {
1377                assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("string"));
1378                assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("uuid"));
1379            }
1380            _ => panic!("Expected inline schema for uuid"),
1381        }
1382
1383        // Test integer schemas
1384        for schema_type in ["integer", "int", "int64", "i64"] {
1385            let schema = schema_type_to_openapi_schema(schema_type);
1386            match schema {
1387                rustapi_openapi::SchemaRef::Inline(v) => {
1388                    assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("integer"));
1389                    assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("int64"));
1390                }
1391                _ => panic!("Expected inline schema for {}", schema_type),
1392            }
1393        }
1394
1395        // Test int32 schema
1396        let int32_schema = schema_type_to_openapi_schema("int32");
1397        match int32_schema {
1398            rustapi_openapi::SchemaRef::Inline(v) => {
1399                assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("integer"));
1400                assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("int32"));
1401            }
1402            _ => panic!("Expected inline schema for int32"),
1403        }
1404
1405        // Test number/float schema
1406        for schema_type in ["number", "float"] {
1407            let schema = schema_type_to_openapi_schema(schema_type);
1408            match schema {
1409                rustapi_openapi::SchemaRef::Inline(v) => {
1410                    assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("number"));
1411                }
1412                _ => panic!("Expected inline schema for {}", schema_type),
1413            }
1414        }
1415
1416        // Test boolean schema
1417        for schema_type in ["boolean", "bool"] {
1418            let schema = schema_type_to_openapi_schema(schema_type);
1419            match schema {
1420                rustapi_openapi::SchemaRef::Inline(v) => {
1421                    assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("boolean"));
1422                }
1423                _ => panic!("Expected inline schema for {}", schema_type),
1424            }
1425        }
1426
1427        // Test string schema (default)
1428        let string_schema = schema_type_to_openapi_schema("string");
1429        match string_schema {
1430            rustapi_openapi::SchemaRef::Inline(v) => {
1431                assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("string"));
1432            }
1433            _ => panic!("Expected inline schema for string"),
1434        }
1435    }
1436
1437    // **Feature: router-nesting, Property 11: OpenAPI Integration**
1438    //
1439    // For any nested routes with OpenAPI operations, the operations should appear
1440    // in the parent's OpenAPI spec with prefixed paths and preserved metadata.
1441    //
1442    // **Validates: Requirements 4.1, 4.2**
1443    proptest! {
1444        #![proptest_config(ProptestConfig::with_cases(100))]
1445
1446        /// Property: Nested routes appear in OpenAPI spec with prefixed paths
1447        ///
1448        /// For any router with routes nested under a prefix, all routes should
1449        /// appear in the OpenAPI spec with the prefix prepended to their paths.
1450        #[test]
1451        fn prop_nested_routes_in_openapi_spec(
1452            // Generate prefix segments
1453            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1454            // Generate route path segments
1455            route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1456            has_param in any::<bool>(),
1457        ) {
1458            async fn handler() -> &'static str { "handler" }
1459
1460            // Build the prefix
1461            let prefix = format!("/{}", prefix_segments.join("/"));
1462
1463            // Build the route path
1464            let mut route_path = format!("/{}", route_segments.join("/"));
1465            if has_param {
1466                route_path.push_str("/{id}");
1467            }
1468
1469            // Create nested router and nest it through RustApi
1470            let nested_router = Router::new().route(&route_path, get(handler));
1471            let app = RustApi::new().nest(&prefix, nested_router);
1472
1473            // Build expected prefixed path for OpenAPI (uses {param} format)
1474            let expected_openapi_path = format!("{}{}", prefix, route_path);
1475
1476            // Get the OpenAPI spec
1477            let spec = app.openapi_spec();
1478
1479            // Property: The prefixed route should exist in OpenAPI paths
1480            prop_assert!(
1481                spec.paths.contains_key(&expected_openapi_path),
1482                "Expected OpenAPI path '{}' not found. Available paths: {:?}",
1483                expected_openapi_path,
1484                spec.paths.keys().collect::<Vec<_>>()
1485            );
1486
1487            // Property: The path item should have a GET operation
1488            let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1489            prop_assert!(
1490                path_item.get.is_some(),
1491                "GET operation should exist for path '{}'",
1492                expected_openapi_path
1493            );
1494        }
1495
1496        /// Property: Multiple HTTP methods are preserved in OpenAPI spec after nesting
1497        ///
1498        /// For any router with routes having multiple HTTP methods, nesting should
1499        /// preserve all method operations in the OpenAPI spec.
1500        #[test]
1501        fn prop_multiple_methods_preserved_in_openapi(
1502            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1503            route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1504        ) {
1505            async fn get_handler() -> &'static str { "get" }
1506            async fn post_handler() -> &'static str { "post" }
1507
1508            // Build the prefix and route path
1509            let prefix = format!("/{}", prefix_segments.join("/"));
1510            let route_path = format!("/{}", route_segments.join("/"));
1511
1512            // Create nested router with both GET and POST using separate routes
1513            // Since MethodRouter doesn't have chaining methods, we create two routes
1514            let get_route_path = format!("{}/get", route_path);
1515            let post_route_path = format!("{}/post", route_path);
1516            let nested_router = Router::new()
1517                .route(&get_route_path, get(get_handler))
1518                .route(&post_route_path, post(post_handler));
1519            let app = RustApi::new().nest(&prefix, nested_router);
1520
1521            // Build expected prefixed paths for OpenAPI
1522            let expected_get_path = format!("{}{}", prefix, get_route_path);
1523            let expected_post_path = format!("{}{}", prefix, post_route_path);
1524
1525            // Get the OpenAPI spec
1526            let spec = app.openapi_spec();
1527
1528            // Property: Both paths should exist
1529            prop_assert!(
1530                spec.paths.contains_key(&expected_get_path),
1531                "Expected OpenAPI path '{}' not found",
1532                expected_get_path
1533            );
1534            prop_assert!(
1535                spec.paths.contains_key(&expected_post_path),
1536                "Expected OpenAPI path '{}' not found",
1537                expected_post_path
1538            );
1539
1540            // Property: GET operation should exist on get path
1541            let get_path_item = spec.paths.get(&expected_get_path).unwrap();
1542            prop_assert!(
1543                get_path_item.get.is_some(),
1544                "GET operation should exist for path '{}'",
1545                expected_get_path
1546            );
1547
1548            // Property: POST operation should exist on post path
1549            let post_path_item = spec.paths.get(&expected_post_path).unwrap();
1550            prop_assert!(
1551                post_path_item.post.is_some(),
1552                "POST operation should exist for path '{}'",
1553                expected_post_path
1554            );
1555        }
1556
1557        /// Property: Path parameters are added to OpenAPI operations after nesting
1558        ///
1559        /// For any nested route with path parameters, the OpenAPI operation should
1560        /// include the path parameters.
1561        #[test]
1562        fn prop_path_params_in_openapi_after_nesting(
1563            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1564            param_name in "[a-z][a-z0-9]{0,5}",
1565        ) {
1566            async fn handler() -> &'static str { "handler" }
1567
1568            // Build the prefix and route path with parameter
1569            let prefix = format!("/{}", prefix_segments.join("/"));
1570            let route_path = format!("/{{{}}}", param_name);
1571
1572            // Create nested router
1573            let nested_router = Router::new().route(&route_path, get(handler));
1574            let app = RustApi::new().nest(&prefix, nested_router);
1575
1576            // Build expected prefixed path for OpenAPI
1577            let expected_openapi_path = format!("{}{}", prefix, route_path);
1578
1579            // Get the OpenAPI spec
1580            let spec = app.openapi_spec();
1581
1582            // Property: The path should exist
1583            prop_assert!(
1584                spec.paths.contains_key(&expected_openapi_path),
1585                "Expected OpenAPI path '{}' not found",
1586                expected_openapi_path
1587            );
1588
1589            // Property: The GET operation should have the path parameter
1590            let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1591            let get_op = path_item.get.as_ref().unwrap();
1592
1593            prop_assert!(
1594                get_op.parameters.is_some(),
1595                "Operation should have parameters for path '{}'",
1596                expected_openapi_path
1597            );
1598
1599            let params = get_op.parameters.as_ref().unwrap();
1600            let has_param = params.iter().any(|p| p.name == param_name && p.location == "path");
1601            prop_assert!(
1602                has_param,
1603                "Path parameter '{}' should exist in operation parameters. Found: {:?}",
1604                param_name,
1605                params.iter().map(|p| &p.name).collect::<Vec<_>>()
1606            );
1607        }
1608    }
1609
1610    // **Feature: router-nesting, Property 13: RustApi Integration**
1611    //
1612    // For any router nested through `RustApi::new().nest()`, the behavior should be
1613    // identical to nesting through `Router::new().nest()`, and routes should appear
1614    // in the OpenAPI spec.
1615    //
1616    // **Validates: Requirements 6.1, 6.2**
1617    proptest! {
1618        #![proptest_config(ProptestConfig::with_cases(100))]
1619
1620        /// Property: RustApi::nest delegates to Router::nest and produces identical route registration
1621        ///
1622        /// For any router with routes nested under a prefix, nesting through RustApi
1623        /// should produce the same route registration as nesting through Router directly.
1624        #[test]
1625        fn prop_rustapi_nest_delegates_to_router_nest(
1626            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1627            route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1628            has_param in any::<bool>(),
1629        ) {
1630            async fn handler() -> &'static str { "handler" }
1631
1632            // Build the prefix
1633            let prefix = format!("/{}", prefix_segments.join("/"));
1634
1635            // Build the route path
1636            let mut route_path = format!("/{}", route_segments.join("/"));
1637            if has_param {
1638                route_path.push_str("/{id}");
1639            }
1640
1641            // Create nested router
1642            let nested_router_for_rustapi = Router::new().route(&route_path, get(handler));
1643            let nested_router_for_router = Router::new().route(&route_path, get(handler));
1644
1645            // Nest through RustApi
1646            let rustapi_app = RustApi::new().nest(&prefix, nested_router_for_rustapi);
1647            let rustapi_router = rustapi_app.into_router();
1648
1649            // Nest through Router directly
1650            let router_app = Router::new().nest(&prefix, nested_router_for_router);
1651
1652            // Property: Both should have the same registered routes
1653            let rustapi_routes = rustapi_router.registered_routes();
1654            let router_routes = router_app.registered_routes();
1655
1656            prop_assert_eq!(
1657                rustapi_routes.len(),
1658                router_routes.len(),
1659                "RustApi and Router should have same number of routes"
1660            );
1661
1662            // Property: All routes from Router should exist in RustApi
1663            for (path, info) in router_routes {
1664                prop_assert!(
1665                    rustapi_routes.contains_key(path),
1666                    "Route '{}' from Router should exist in RustApi routes",
1667                    path
1668                );
1669
1670                let rustapi_info = rustapi_routes.get(path).unwrap();
1671                prop_assert_eq!(
1672                    &info.path, &rustapi_info.path,
1673                    "Display paths should match for route '{}'",
1674                    path
1675                );
1676                prop_assert_eq!(
1677                    info.methods.len(), rustapi_info.methods.len(),
1678                    "Method count should match for route '{}'",
1679                    path
1680                );
1681            }
1682        }
1683
1684        /// Property: RustApi::nest includes nested routes in OpenAPI spec
1685        ///
1686        /// For any router with routes nested through RustApi, all routes should
1687        /// appear in the OpenAPI specification with prefixed paths.
1688        #[test]
1689        fn prop_rustapi_nest_includes_routes_in_openapi(
1690            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1691            route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1692            has_param in any::<bool>(),
1693        ) {
1694            async fn handler() -> &'static str { "handler" }
1695
1696            // Build the prefix
1697            let prefix = format!("/{}", prefix_segments.join("/"));
1698
1699            // Build the route path
1700            let mut route_path = format!("/{}", route_segments.join("/"));
1701            if has_param {
1702                route_path.push_str("/{id}");
1703            }
1704
1705            // Create nested router and nest through RustApi
1706            let nested_router = Router::new().route(&route_path, get(handler));
1707            let app = RustApi::new().nest(&prefix, nested_router);
1708
1709            // Build expected prefixed path for OpenAPI
1710            let expected_openapi_path = format!("{}{}", prefix, route_path);
1711
1712            // Get the OpenAPI spec
1713            let spec = app.openapi_spec();
1714
1715            // Property: The prefixed route should exist in OpenAPI paths
1716            prop_assert!(
1717                spec.paths.contains_key(&expected_openapi_path),
1718                "Expected OpenAPI path '{}' not found. Available paths: {:?}",
1719                expected_openapi_path,
1720                spec.paths.keys().collect::<Vec<_>>()
1721            );
1722
1723            // Property: The path item should have a GET operation
1724            let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1725            prop_assert!(
1726                path_item.get.is_some(),
1727                "GET operation should exist for path '{}'",
1728                expected_openapi_path
1729            );
1730        }
1731
1732        /// Property: RustApi::nest route matching is identical to Router::nest
1733        ///
1734        /// For any nested route, matching through RustApi should produce the same
1735        /// result as matching through Router directly.
1736        #[test]
1737        fn prop_rustapi_nest_route_matching_identical(
1738            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
1739            route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
1740            param_value in "[a-z0-9]{1,10}",
1741        ) {
1742            use crate::router::RouteMatch;
1743
1744            async fn handler() -> &'static str { "handler" }
1745
1746            // Build the prefix and route path with parameter
1747            let prefix = format!("/{}", prefix_segments.join("/"));
1748            let route_path = format!("/{}/{{id}}", route_segments.join("/"));
1749
1750            // Create nested routers
1751            let nested_router_for_rustapi = Router::new().route(&route_path, get(handler));
1752            let nested_router_for_router = Router::new().route(&route_path, get(handler));
1753
1754            // Nest through both RustApi and Router
1755            let rustapi_app = RustApi::new().nest(&prefix, nested_router_for_rustapi);
1756            let rustapi_router = rustapi_app.into_router();
1757            let router_app = Router::new().nest(&prefix, nested_router_for_router);
1758
1759            // Build the full path to match
1760            let full_path = format!("{}/{}/{}", prefix, route_segments.join("/"), param_value);
1761
1762            // Match through both
1763            let rustapi_match = rustapi_router.match_route(&full_path, &Method::GET);
1764            let router_match = router_app.match_route(&full_path, &Method::GET);
1765
1766            // Property: Both should return Found with same parameters
1767            match (rustapi_match, router_match) {
1768                (RouteMatch::Found { params: rustapi_params, .. }, RouteMatch::Found { params: router_params, .. }) => {
1769                    prop_assert_eq!(
1770                        rustapi_params.len(),
1771                        router_params.len(),
1772                        "Parameter count should match"
1773                    );
1774                    for (key, value) in &router_params {
1775                        prop_assert!(
1776                            rustapi_params.contains_key(key),
1777                            "RustApi should have parameter '{}'",
1778                            key
1779                        );
1780                        prop_assert_eq!(
1781                            rustapi_params.get(key).unwrap(),
1782                            value,
1783                            "Parameter '{}' value should match",
1784                            key
1785                        );
1786                    }
1787                }
1788                (rustapi_result, router_result) => {
1789                    prop_assert!(
1790                        false,
1791                        "Both should return Found, but RustApi returned {:?} and Router returned {:?}",
1792                        match rustapi_result {
1793                            RouteMatch::Found { .. } => "Found",
1794                            RouteMatch::NotFound => "NotFound",
1795                            RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
1796                        },
1797                        match router_result {
1798                            RouteMatch::Found { .. } => "Found",
1799                            RouteMatch::NotFound => "NotFound",
1800                            RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
1801                        }
1802                    );
1803                }
1804            }
1805        }
1806    }
1807
1808    /// Unit test: Verify OpenAPI operations are propagated during nesting
1809    #[test]
1810    fn test_openapi_operations_propagated_during_nesting() {
1811        async fn list_users() -> &'static str {
1812            "list users"
1813        }
1814        async fn get_user() -> &'static str {
1815            "get user"
1816        }
1817        async fn create_user() -> &'static str {
1818            "create user"
1819        }
1820
1821        // Create nested router with multiple routes
1822        // Note: We use separate routes since MethodRouter doesn't support chaining
1823        let users_router = Router::new()
1824            .route("/", get(list_users))
1825            .route("/create", post(create_user))
1826            .route("/{id}", get(get_user));
1827
1828        // Nest under /api/v1/users
1829        let app = RustApi::new().nest("/api/v1/users", users_router);
1830
1831        let spec = app.openapi_spec();
1832
1833        // Verify /api/v1/users path exists with GET
1834        assert!(
1835            spec.paths.contains_key("/api/v1/users"),
1836            "Should have /api/v1/users path"
1837        );
1838        let users_path = spec.paths.get("/api/v1/users").unwrap();
1839        assert!(users_path.get.is_some(), "Should have GET operation");
1840
1841        // Verify /api/v1/users/create path exists with POST
1842        assert!(
1843            spec.paths.contains_key("/api/v1/users/create"),
1844            "Should have /api/v1/users/create path"
1845        );
1846        let create_path = spec.paths.get("/api/v1/users/create").unwrap();
1847        assert!(create_path.post.is_some(), "Should have POST operation");
1848
1849        // Verify /api/v1/users/{id} path exists with GET
1850        assert!(
1851            spec.paths.contains_key("/api/v1/users/{id}"),
1852            "Should have /api/v1/users/{{id}} path"
1853        );
1854        let user_path = spec.paths.get("/api/v1/users/{id}").unwrap();
1855        assert!(
1856            user_path.get.is_some(),
1857            "Should have GET operation for user by id"
1858        );
1859
1860        // Verify path parameter is added
1861        let get_user_op = user_path.get.as_ref().unwrap();
1862        assert!(get_user_op.parameters.is_some(), "Should have parameters");
1863        let params = get_user_op.parameters.as_ref().unwrap();
1864        assert!(
1865            params
1866                .iter()
1867                .any(|p| p.name == "id" && p.location == "path"),
1868            "Should have 'id' path parameter"
1869        );
1870    }
1871
1872    /// Unit test: Verify nested routes don't appear without nesting
1873    #[test]
1874    fn test_openapi_spec_empty_without_routes() {
1875        let app = RustApi::new();
1876        let spec = app.openapi_spec();
1877
1878        // Should have no paths (except potentially default ones)
1879        assert!(
1880            spec.paths.is_empty(),
1881            "OpenAPI spec should have no paths without routes"
1882        );
1883    }
1884
1885    /// Unit test: Verify RustApi::nest delegates correctly to Router::nest
1886    ///
1887    /// **Feature: router-nesting, Property 13: RustApi Integration**
1888    /// **Validates: Requirements 6.1, 6.2**
1889    #[test]
1890    fn test_rustapi_nest_delegates_to_router_nest() {
1891        use crate::router::RouteMatch;
1892
1893        async fn list_users() -> &'static str {
1894            "list users"
1895        }
1896        async fn get_user() -> &'static str {
1897            "get user"
1898        }
1899        async fn create_user() -> &'static str {
1900            "create user"
1901        }
1902
1903        // Create nested router with multiple routes
1904        let users_router = Router::new()
1905            .route("/", get(list_users))
1906            .route("/create", post(create_user))
1907            .route("/{id}", get(get_user));
1908
1909        // Nest through RustApi
1910        let app = RustApi::new().nest("/api/v1/users", users_router);
1911        let router = app.into_router();
1912
1913        // Verify routes are registered correctly
1914        let routes = router.registered_routes();
1915        assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1916
1917        // Verify route paths
1918        assert!(
1919            routes.contains_key("/api/v1/users"),
1920            "Should have /api/v1/users route"
1921        );
1922        assert!(
1923            routes.contains_key("/api/v1/users/create"),
1924            "Should have /api/v1/users/create route"
1925        );
1926        assert!(
1927            routes.contains_key("/api/v1/users/:id"),
1928            "Should have /api/v1/users/:id route"
1929        );
1930
1931        // Verify route matching works
1932        match router.match_route("/api/v1/users", &Method::GET) {
1933            RouteMatch::Found { params, .. } => {
1934                assert!(params.is_empty(), "Root route should have no params");
1935            }
1936            _ => panic!("GET /api/v1/users should be found"),
1937        }
1938
1939        match router.match_route("/api/v1/users/create", &Method::POST) {
1940            RouteMatch::Found { params, .. } => {
1941                assert!(params.is_empty(), "Create route should have no params");
1942            }
1943            _ => panic!("POST /api/v1/users/create should be found"),
1944        }
1945
1946        match router.match_route("/api/v1/users/123", &Method::GET) {
1947            RouteMatch::Found { params, .. } => {
1948                assert_eq!(
1949                    params.get("id"),
1950                    Some(&"123".to_string()),
1951                    "Should extract id param"
1952                );
1953            }
1954            _ => panic!("GET /api/v1/users/123 should be found"),
1955        }
1956
1957        // Verify method not allowed
1958        match router.match_route("/api/v1/users", &Method::DELETE) {
1959            RouteMatch::MethodNotAllowed { allowed } => {
1960                assert!(allowed.contains(&Method::GET), "Should allow GET");
1961            }
1962            _ => panic!("DELETE /api/v1/users should return MethodNotAllowed"),
1963        }
1964    }
1965
1966    /// Unit test: Verify RustApi::nest includes routes in OpenAPI spec
1967    ///
1968    /// **Feature: router-nesting, Property 13: RustApi Integration**
1969    /// **Validates: Requirements 6.1, 6.2**
1970    #[test]
1971    fn test_rustapi_nest_includes_routes_in_openapi_spec() {
1972        async fn list_items() -> &'static str {
1973            "list items"
1974        }
1975        async fn get_item() -> &'static str {
1976            "get item"
1977        }
1978
1979        // Create nested router
1980        let items_router = Router::new()
1981            .route("/", get(list_items))
1982            .route("/{item_id}", get(get_item));
1983
1984        // Nest through RustApi
1985        let app = RustApi::new().nest("/api/items", items_router);
1986
1987        // Verify OpenAPI spec
1988        let spec = app.openapi_spec();
1989
1990        // Verify paths exist
1991        assert!(
1992            spec.paths.contains_key("/api/items"),
1993            "Should have /api/items in OpenAPI"
1994        );
1995        assert!(
1996            spec.paths.contains_key("/api/items/{item_id}"),
1997            "Should have /api/items/{{item_id}} in OpenAPI"
1998        );
1999
2000        // Verify operations
2001        let list_path = spec.paths.get("/api/items").unwrap();
2002        assert!(
2003            list_path.get.is_some(),
2004            "Should have GET operation for /api/items"
2005        );
2006
2007        let get_path = spec.paths.get("/api/items/{item_id}").unwrap();
2008        assert!(
2009            get_path.get.is_some(),
2010            "Should have GET operation for /api/items/{{item_id}}"
2011        );
2012
2013        // Verify path parameter is added
2014        let get_op = get_path.get.as_ref().unwrap();
2015        assert!(get_op.parameters.is_some(), "Should have parameters");
2016        let params = get_op.parameters.as_ref().unwrap();
2017        assert!(
2018            params
2019                .iter()
2020                .any(|p| p.name == "item_id" && p.location == "path"),
2021            "Should have 'item_id' path parameter"
2022        );
2023    }
2024}
2025
2026/// Check Basic Auth header against expected credentials
2027#[cfg(feature = "swagger-ui")]
2028fn check_basic_auth(req: &crate::Request, expected: &str) -> bool {
2029    req.headers()
2030        .get(http::header::AUTHORIZATION)
2031        .and_then(|v| v.to_str().ok())
2032        .map(|auth| auth == expected)
2033        .unwrap_or(false)
2034}
2035
2036/// Create 401 Unauthorized response with WWW-Authenticate header
2037#[cfg(feature = "swagger-ui")]
2038fn unauthorized_response() -> crate::Response {
2039    http::Response::builder()
2040        .status(http::StatusCode::UNAUTHORIZED)
2041        .header(
2042            http::header::WWW_AUTHENTICATE,
2043            "Basic realm=\"API Documentation\"",
2044        )
2045        .header(http::header::CONTENT_TYPE, "text/plain")
2046        .body(crate::response::Body::from("Unauthorized"))
2047        .unwrap()
2048}
2049
2050/// Configuration builder for RustAPI with auto-routes
2051pub struct RustApiConfig {
2052    docs_path: Option<String>,
2053    docs_enabled: bool,
2054    api_title: String,
2055    api_version: String,
2056    api_description: Option<String>,
2057    body_limit: Option<usize>,
2058    layers: LayerStack,
2059}
2060
2061impl Default for RustApiConfig {
2062    fn default() -> Self {
2063        Self::new()
2064    }
2065}
2066
2067impl RustApiConfig {
2068    pub fn new() -> Self {
2069        Self {
2070            docs_path: Some("/docs".to_string()),
2071            docs_enabled: true,
2072            api_title: "RustAPI".to_string(),
2073            api_version: "1.0.0".to_string(),
2074            api_description: None,
2075            body_limit: None,
2076            layers: LayerStack::new(),
2077        }
2078    }
2079
2080    /// Set the docs path (default: "/docs")
2081    pub fn docs_path(mut self, path: impl Into<String>) -> Self {
2082        self.docs_path = Some(path.into());
2083        self
2084    }
2085
2086    /// Enable or disable docs (default: true)
2087    pub fn docs_enabled(mut self, enabled: bool) -> Self {
2088        self.docs_enabled = enabled;
2089        self
2090    }
2091
2092    /// Set OpenAPI info
2093    pub fn openapi_info(
2094        mut self,
2095        title: impl Into<String>,
2096        version: impl Into<String>,
2097        description: Option<impl Into<String>>,
2098    ) -> Self {
2099        self.api_title = title.into();
2100        self.api_version = version.into();
2101        self.api_description = description.map(|d| d.into());
2102        self
2103    }
2104
2105    /// Set body size limit
2106    pub fn body_limit(mut self, limit: usize) -> Self {
2107        self.body_limit = Some(limit);
2108        self
2109    }
2110
2111    /// Add a middleware layer
2112    pub fn layer<L>(mut self, layer: L) -> Self
2113    where
2114        L: MiddlewareLayer,
2115    {
2116        self.layers.push(Box::new(layer));
2117        self
2118    }
2119
2120    /// Build the RustApi instance
2121    pub fn build(self) -> RustApi {
2122        let mut app = RustApi::new().mount_auto_routes_grouped();
2123
2124        // Apply configuration
2125        if let Some(limit) = self.body_limit {
2126            app = app.body_limit(limit);
2127        }
2128
2129        app = app.openapi_info(
2130            &self.api_title,
2131            &self.api_version,
2132            self.api_description.as_deref(),
2133        );
2134
2135        #[cfg(feature = "swagger-ui")]
2136        if self.docs_enabled {
2137            if let Some(path) = self.docs_path {
2138                app = app.docs(&path);
2139            }
2140        }
2141
2142        // Apply layers
2143        // Note: layers are applied in reverse order in RustApi::layer logic (pushing to vec)
2144        app.layers.extend(self.layers);
2145
2146        app
2147    }
2148
2149    /// Build and run the server
2150    pub async fn run(
2151        self,
2152        addr: impl AsRef<str>,
2153    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
2154        self.build().run(addr.as_ref()).await
2155    }
2156}