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