Skip to main content

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