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