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