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