rustapi_core/
app.rs

1//! RustApi application builder
2
3use crate::error::Result;
4use crate::middleware::{BodyLimitLayer, LayerStack, MiddlewareLayer, DEFAULT_BODY_LIMIT};
5use crate::response::IntoResponse;
6use crate::router::{MethodRouter, Router};
7use crate::server::Server;
8use std::collections::HashMap;
9use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
10
11/// Main application builder for RustAPI
12///
13/// # Example
14///
15/// ```rust,ignore
16/// use rustapi_rs::prelude::*;
17///
18/// #[tokio::main]
19/// async fn main() -> Result<()> {
20///     RustApi::new()
21///         .state(AppState::new())
22///         .route("/", get(hello))
23///         .route("/users/{id}", get(get_user))
24///         .run("127.0.0.1:8080")
25///         .await
26/// }
27/// ```
28pub struct RustApi {
29    router: Router,
30    openapi_spec: rustapi_openapi::OpenApiSpec,
31    layers: LayerStack,
32    body_limit: Option<usize>,
33}
34
35impl RustApi {
36    /// Create a new RustAPI application
37    pub fn new() -> Self {
38        // Initialize tracing if not already done
39        let _ = tracing_subscriber::registry()
40            .with(
41                EnvFilter::try_from_default_env()
42                    .unwrap_or_else(|_| EnvFilter::new("info,rustapi=debug")),
43            )
44            .with(tracing_subscriber::fmt::layer())
45            .try_init();
46
47        Self {
48            router: Router::new(),
49            openapi_spec: rustapi_openapi::OpenApiSpec::new("RustAPI Application", "1.0.0")
50                .register::<rustapi_openapi::ErrorSchema>()
51                .register::<rustapi_openapi::ErrorBodySchema>()
52                .register::<rustapi_openapi::ValidationErrorSchema>()
53                .register::<rustapi_openapi::ValidationErrorBodySchema>()
54                .register::<rustapi_openapi::FieldErrorSchema>(),
55            layers: LayerStack::new(),
56            body_limit: Some(DEFAULT_BODY_LIMIT), // Default 1MB limit
57        }
58    }
59
60    /// Create a zero-config RustAPI application.
61    ///
62    /// All routes decorated with `#[rustapi::get]`, `#[rustapi::post]`, etc.
63    /// are automatically registered. Swagger UI is enabled at `/docs` by default.
64    ///
65    /// # Example
66    ///
67    /// ```rust,ignore
68    /// use rustapi_rs::prelude::*;
69    ///
70    /// #[rustapi::get("/users")]
71    /// async fn list_users() -> Json<Vec<User>> {
72    ///     Json(vec![])
73    /// }
74    ///
75    /// #[rustapi::main]
76    /// async fn main() -> Result<()> {
77    ///     // Zero config - routes are auto-registered!
78    ///     RustApi::auto()
79    ///         .run("0.0.0.0:8080")
80    ///         .await
81    /// }
82    /// ```
83    #[cfg(feature = "swagger-ui")]
84    pub fn auto() -> Self {
85        // Build app with grouped auto-routes and auto-schemas, then enable docs.
86        Self::new().mount_auto_routes_grouped().docs("/docs")
87    }
88
89    /// Create a zero-config RustAPI application (without swagger-ui feature).
90    ///
91    /// All routes decorated with `#[rustapi::get]`, `#[rustapi::post]`, etc.
92    /// are automatically registered.
93    #[cfg(not(feature = "swagger-ui"))]
94    pub fn auto() -> Self {
95        Self::new().mount_auto_routes_grouped()
96    }
97
98    /// Create a configurable RustAPI application with auto-routes.
99    ///
100    /// Provides builder methods for customization while still
101    /// auto-registering all decorated routes.
102    ///
103    /// # Example
104    ///
105    /// ```rust,ignore
106    /// use rustapi_rs::prelude::*;
107    ///
108    /// RustApi::config()
109    ///     .docs_path("/api-docs")
110    ///     .body_limit(5 * 1024 * 1024)  // 5MB
111    ///     .openapi_info("My API", "2.0.0", Some("API Description"))
112    ///     .run("0.0.0.0:8080")
113    ///     .await?;
114    /// ```
115    pub fn config() -> RustApiConfig {
116        RustApiConfig::new()
117    }
118
119    /// Set the global body size limit for request bodies
120    ///
121    /// This protects against denial-of-service attacks via large payloads.
122    /// The default limit is 1MB (1024 * 1024 bytes).
123    ///
124    /// # Arguments
125    ///
126    /// * `limit` - Maximum body size in bytes
127    ///
128    /// # Example
129    ///
130    /// ```rust,ignore
131    /// use rustapi_rs::prelude::*;
132    ///
133    /// RustApi::new()
134    ///     .body_limit(5 * 1024 * 1024)  // 5MB limit
135    ///     .route("/upload", post(upload_handler))
136    ///     .run("127.0.0.1:8080")
137    ///     .await
138    /// ```
139    pub fn body_limit(mut self, limit: usize) -> Self {
140        self.body_limit = Some(limit);
141        self
142    }
143
144    /// Disable the body size limit
145    ///
146    /// Warning: This removes protection against large payload attacks.
147    /// Only use this if you have other mechanisms to limit request sizes.
148    ///
149    /// # Example
150    ///
151    /// ```rust,ignore
152    /// RustApi::new()
153    ///     .no_body_limit()  // Disable body size limit
154    ///     .route("/upload", post(upload_handler))
155    /// ```
156    pub fn no_body_limit(mut self) -> Self {
157        self.body_limit = None;
158        self
159    }
160
161    /// Add a middleware layer to the application
162    ///
163    /// Layers are executed in the order they are added (outermost first).
164    /// The first layer added will be the first to process the request and
165    /// the last to process the response.
166    ///
167    /// # Example
168    ///
169    /// ```rust,ignore
170    /// use rustapi_rs::prelude::*;
171    /// use rustapi_core::middleware::{RequestIdLayer, TracingLayer};
172    ///
173    /// RustApi::new()
174    ///     .layer(RequestIdLayer::new())  // First to process request
175    ///     .layer(TracingLayer::new())    // Second to process request
176    ///     .route("/", get(handler))
177    ///     .run("127.0.0.1:8080")
178    ///     .await
179    /// ```
180    pub fn layer<L>(mut self, layer: L) -> Self
181    where
182        L: MiddlewareLayer,
183    {
184        self.layers.push(Box::new(layer));
185        self
186    }
187
188    /// Add application state
189    ///
190    /// State is shared across all handlers and can be extracted using `State<T>`.
191    ///
192    /// # Example
193    ///
194    /// ```rust,ignore
195    /// #[derive(Clone)]
196    /// struct AppState {
197    ///     db: DbPool,
198    /// }
199    ///
200    /// RustApi::new()
201    ///     .state(AppState::new())
202    /// ```
203    pub fn state<S>(self, _state: S) -> Self
204    where
205        S: Clone + Send + Sync + 'static,
206    {
207        // Store state in the router's shared Extensions so `State<T>` extractor can retrieve it.
208        let state = _state;
209        let mut app = self;
210        app.router = app.router.state(state);
211        app
212    }
213
214    /// Register an OpenAPI schema
215    ///
216    /// # Example
217    ///
218    /// ```rust,ignore
219    /// #[derive(Schema)]
220    /// struct User { ... }
221    ///
222    /// RustApi::new()
223    ///     .register_schema::<User>()
224    /// ```
225    pub fn register_schema<T: for<'a> rustapi_openapi::Schema<'a>>(mut self) -> Self {
226        self.openapi_spec = self.openapi_spec.register::<T>();
227        self
228    }
229
230    /// Configure OpenAPI info (title, version, description)
231    pub fn openapi_info(mut self, title: &str, version: &str, description: Option<&str>) -> Self {
232        // NOTE: Do not reset the spec here; doing so would drop collected paths/schemas.
233        // This is especially important for `RustApi::auto()` and `RustApi::config()`.
234        self.openapi_spec.info.title = title.to_string();
235        self.openapi_spec.info.version = version.to_string();
236        self.openapi_spec.info.description = description.map(|d| d.to_string());
237        self
238    }
239
240    /// Get the current OpenAPI spec (for advanced usage/testing).
241    pub fn openapi_spec(&self) -> &rustapi_openapi::OpenApiSpec {
242        &self.openapi_spec
243    }
244
245    fn mount_auto_routes_grouped(mut self) -> Self {
246        let routes = crate::auto_route::collect_auto_routes();
247        let mut by_path: HashMap<String, MethodRouter> = HashMap::new();
248
249        for route in routes {
250            let method_enum = match route.method {
251                "GET" => http::Method::GET,
252                "POST" => http::Method::POST,
253                "PUT" => http::Method::PUT,
254                "DELETE" => http::Method::DELETE,
255                "PATCH" => http::Method::PATCH,
256                _ => http::Method::GET,
257            };
258
259            let path = if route.path.starts_with('/') {
260                route.path.to_string()
261            } else {
262                format!("/{}", route.path)
263            };
264
265            let entry = by_path.entry(path).or_default();
266            entry.insert_boxed_with_operation(method_enum, route.handler, route.operation);
267        }
268
269        let route_count: usize = by_path.values().map(|mr| mr.allowed_methods().len()).sum();
270        let path_count = by_path.len();
271
272        for (path, method_router) in by_path {
273            self = self.route(&path, method_router);
274        }
275
276        tracing::info!(
277            paths = path_count,
278            routes = route_count,
279            "Auto-registered routes"
280        );
281
282        // Apply any auto-registered schemas.
283        crate::auto_schema::apply_auto_schemas(&mut self.openapi_spec);
284
285        self
286    }
287
288    /// Add a route
289    ///
290    /// # Example
291    ///
292    /// ```rust,ignore
293    /// RustApi::new()
294    ///     .route("/", get(index))
295    ///     .route("/users", get(list_users).post(create_user))
296    ///     .route("/users/{id}", get(get_user).delete(delete_user))
297    /// ```
298    pub fn route(mut self, path: &str, method_router: MethodRouter) -> Self {
299        // Register operations in OpenAPI spec
300        for (method, op) in &method_router.operations {
301            let mut op = op.clone();
302            add_path_params_to_operation(path, &mut op);
303            self.openapi_spec = self.openapi_spec.path(path, method.as_str(), op);
304        }
305
306        self.router = self.router.route(path, method_router);
307        self
308    }
309
310    /// Mount a handler (convenience method)
311    ///
312    /// Alias for `.route(path, method_router)` for a single handler.
313    #[deprecated(note = "Use route() directly or mount_route() for macro-based routing")]
314    pub fn mount(self, path: &str, method_router: MethodRouter) -> Self {
315        self.route(path, method_router)
316    }
317
318    /// Mount a route created with `#[rustapi::get]`, `#[rustapi::post]`, etc.
319    ///
320    /// # Example
321    ///
322    /// ```rust,ignore
323    /// use rustapi_rs::prelude::*;
324    ///
325    /// #[rustapi::get("/users")]
326    /// async fn list_users() -> Json<Vec<User>> {
327    ///     Json(vec![])
328    /// }
329    ///
330    /// RustApi::new()
331    ///     .mount_route(route!(list_users))
332    ///     .run("127.0.0.1:8080")
333    ///     .await
334    /// ```
335    pub fn mount_route(mut self, route: crate::handler::Route) -> Self {
336        let method_enum = match route.method {
337            "GET" => http::Method::GET,
338            "POST" => http::Method::POST,
339            "PUT" => http::Method::PUT,
340            "DELETE" => http::Method::DELETE,
341            "PATCH" => http::Method::PATCH,
342            _ => http::Method::GET,
343        };
344
345        // Register operation in OpenAPI spec
346        let mut op = route.operation;
347        add_path_params_to_operation(route.path, &mut op);
348        self.openapi_spec = self.openapi_spec.path(route.path, route.method, op);
349
350        self.route_with_method(route.path, method_enum, route.handler)
351    }
352
353    /// Helper to mount a single method handler
354    fn route_with_method(
355        self,
356        path: &str,
357        method: http::Method,
358        handler: crate::handler::BoxedHandler,
359    ) -> Self {
360        use crate::router::MethodRouter;
361        // use http::Method; // Removed
362
363        // This is simplified. In a real implementation we'd merge with existing router at this path
364        // For now we assume one handler per path or we simply allow overwriting for this MVP step
365        // (matchit router doesn't allow easy merging/updating existing entries without rebuilding)
366        //
367        // TOOD: Enhance Router to support method merging
368
369        let path = if !path.starts_with('/') {
370            format!("/{}", path)
371        } else {
372            path.to_string()
373        };
374
375        // Check if we already have this path?
376        // For MVP, valid assumption: user calls .route() or .mount() once per path-method-combo
377        // But we need to handle multiple methods on same path.
378        // Our Router wrapper currently just inserts.
379
380        // Since we can't easily query matchit, we'll just insert.
381        // Limitations: strictly sequential mounting for now.
382
383        let mut handlers = std::collections::HashMap::new();
384        handlers.insert(method, handler);
385
386        let method_router = MethodRouter::from_boxed(handlers);
387        self.route(&path, method_router)
388    }
389
390    /// Nest a router under a prefix
391    ///
392    /// All routes from the nested router will be registered with the prefix
393    /// prepended to their paths. OpenAPI operations from the nested router
394    /// are also propagated to the parent's OpenAPI spec with prefixed paths.
395    ///
396    /// # Example
397    ///
398    /// ```rust,ignore
399    /// let api_v1 = Router::new()
400    ///     .route("/users", get(list_users));
401    ///
402    /// RustApi::new()
403    ///     .nest("/api/v1", api_v1)
404    /// ```
405    pub fn nest(mut self, prefix: &str, router: Router) -> Self {
406        // Normalize the prefix for OpenAPI paths
407        let normalized_prefix = normalize_prefix_for_openapi(prefix);
408
409        // Propagate OpenAPI operations from nested router with prefixed paths
410        // We need to do this before calling router.nest() because it consumes the router
411        for (matchit_path, method_router) in router.method_routers() {
412            // Get the display path from registered_routes (has {param} format)
413            let display_path = router
414                .registered_routes()
415                .get(matchit_path)
416                .map(|info| info.path.clone())
417                .unwrap_or_else(|| matchit_path.clone());
418
419            // Build the prefixed display path for OpenAPI
420            let prefixed_path = if display_path == "/" {
421                normalized_prefix.clone()
422            } else {
423                format!("{}{}", normalized_prefix, display_path)
424            };
425
426            // Register each operation in the OpenAPI spec
427            for (method, op) in &method_router.operations {
428                let mut op = op.clone();
429                add_path_params_to_operation(&prefixed_path, &mut op);
430                self.openapi_spec = self.openapi_spec.path(&prefixed_path, method.as_str(), op);
431            }
432        }
433
434        // Delegate to Router::nest for actual route registration
435        self.router = self.router.nest(prefix, router);
436        self
437    }
438
439    /// Serve static files from a directory
440    ///
441    /// Maps a URL path prefix to a filesystem directory. Requests to paths under
442    /// the prefix will serve files from the corresponding location in the directory.
443    ///
444    /// # Arguments
445    ///
446    /// * `prefix` - URL path prefix (e.g., "/static", "/assets")
447    /// * `root` - Filesystem directory path
448    ///
449    /// # Features
450    ///
451    /// - Automatic MIME type detection
452    /// - ETag and Last-Modified headers for caching
453    /// - Index file serving for directories
454    /// - Path traversal prevention
455    ///
456    /// # Example
457    ///
458    /// ```rust,ignore
459    /// use rustapi_rs::prelude::*;
460    ///
461    /// RustApi::new()
462    ///     .serve_static("/assets", "./public")
463    ///     .serve_static("/uploads", "./uploads")
464    ///     .run("127.0.0.1:8080")
465    ///     .await
466    /// ```
467    pub fn serve_static(self, prefix: &str, root: impl Into<std::path::PathBuf>) -> Self {
468        self.serve_static_with_config(crate::static_files::StaticFileConfig::new(root, prefix))
469    }
470
471    /// Serve static files with custom configuration
472    ///
473    /// # Example
474    ///
475    /// ```rust,ignore
476    /// use rustapi_core::static_files::StaticFileConfig;
477    ///
478    /// let config = StaticFileConfig::new("./public", "/assets")
479    ///     .max_age(86400)  // Cache for 1 day
480    ///     .fallback("index.html");  // SPA fallback
481    ///
482    /// RustApi::new()
483    ///     .serve_static_with_config(config)
484    ///     .run("127.0.0.1:8080")
485    ///     .await
486    /// ```
487    pub fn serve_static_with_config(self, config: crate::static_files::StaticFileConfig) -> Self {
488        use crate::router::MethodRouter;
489        use std::collections::HashMap;
490
491        let prefix = config.prefix.clone();
492        let catch_all_path = format!("{}/*path", prefix.trim_end_matches('/'));
493
494        // Create the static file handler
495        let handler: crate::handler::BoxedHandler =
496            std::sync::Arc::new(move |req: crate::Request| {
497                let config = config.clone();
498                let path = req.uri().path().to_string();
499
500                Box::pin(async move {
501                    let relative_path = path.strip_prefix(&config.prefix).unwrap_or(&path);
502
503                    match crate::static_files::StaticFile::serve(relative_path, &config).await {
504                        Ok(response) => response,
505                        Err(err) => err.into_response(),
506                    }
507                })
508                    as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
509            });
510
511        let mut handlers = HashMap::new();
512        handlers.insert(http::Method::GET, handler);
513        let method_router = MethodRouter::from_boxed(handlers);
514
515        self.route(&catch_all_path, method_router)
516    }
517
518    /// Enable response compression
519    ///
520    /// Adds gzip/deflate compression for response bodies. The compression
521    /// is based on the client's Accept-Encoding header.
522    ///
523    /// # Example
524    ///
525    /// ```rust,ignore
526    /// use rustapi_rs::prelude::*;
527    ///
528    /// RustApi::new()
529    ///     .compression()
530    ///     .route("/", get(handler))
531    ///     .run("127.0.0.1:8080")
532    ///     .await
533    /// ```
534    #[cfg(feature = "compression")]
535    pub fn compression(self) -> Self {
536        self.layer(crate::middleware::CompressionLayer::new())
537    }
538
539    /// Enable response compression with custom configuration
540    ///
541    /// # Example
542    ///
543    /// ```rust,ignore
544    /// use rustapi_core::middleware::CompressionConfig;
545    ///
546    /// RustApi::new()
547    ///     .compression_with_config(
548    ///         CompressionConfig::new()
549    ///             .min_size(512)
550    ///             .level(9)
551    ///     )
552    ///     .route("/", get(handler))
553    /// ```
554    #[cfg(feature = "compression")]
555    pub fn compression_with_config(self, config: crate::middleware::CompressionConfig) -> Self {
556        self.layer(crate::middleware::CompressionLayer::with_config(config))
557    }
558
559    /// Enable Swagger UI documentation
560    ///
561    /// This adds two endpoints:
562    /// - `{path}` - Swagger UI interface
563    /// - `{path}/openapi.json` - OpenAPI JSON specification
564    ///
565    /// # Example
566    ///
567    /// ```text
568    /// RustApi::new()
569    ///     .route("/users", get(list_users))
570    ///     .docs("/docs")  // Swagger UI at /docs, spec at /docs/openapi.json
571    ///     .run("127.0.0.1:8080")
572    ///     .await
573    /// ```
574    #[cfg(feature = "swagger-ui")]
575    pub fn docs(self, path: &str) -> Self {
576        let title = self.openapi_spec.info.title.clone();
577        let version = self.openapi_spec.info.version.clone();
578        let description = self.openapi_spec.info.description.clone();
579
580        self.docs_with_info(path, &title, &version, description.as_deref())
581    }
582
583    /// Enable Swagger UI documentation with custom API info
584    ///
585    /// # Example
586    ///
587    /// ```rust,ignore
588    /// RustApi::new()
589    ///     .docs_with_info("/docs", "My API", "2.0.0", Some("API for managing users"))
590    /// ```
591    #[cfg(feature = "swagger-ui")]
592    pub fn docs_with_info(
593        mut self,
594        path: &str,
595        title: &str,
596        version: &str,
597        description: Option<&str>,
598    ) -> Self {
599        use crate::router::get;
600        // Update spec info
601        self.openapi_spec.info.title = title.to_string();
602        self.openapi_spec.info.version = version.to_string();
603        if let Some(desc) = description {
604            self.openapi_spec.info.description = Some(desc.to_string());
605        }
606
607        let path = path.trim_end_matches('/');
608        let openapi_path = format!("{}/openapi.json", path);
609
610        // Clone values for closures
611        let spec_json =
612            serde_json::to_string_pretty(&self.openapi_spec.to_json()).unwrap_or_default();
613        let openapi_url = openapi_path.clone();
614
615        // Add OpenAPI JSON endpoint
616        let spec_handler = move || {
617            let json = spec_json.clone();
618            async move {
619                http::Response::builder()
620                    .status(http::StatusCode::OK)
621                    .header(http::header::CONTENT_TYPE, "application/json")
622                    .body(http_body_util::Full::new(bytes::Bytes::from(json)))
623                    .unwrap()
624            }
625        };
626
627        // Add Swagger UI endpoint
628        let docs_handler = move || {
629            let url = openapi_url.clone();
630            async move { rustapi_openapi::swagger_ui_html(&url) }
631        };
632
633        self.route(&openapi_path, get(spec_handler))
634            .route(path, get(docs_handler))
635    }
636
637    /// Enable Swagger UI documentation with Basic Auth protection
638    ///
639    /// When username and password are provided, the docs endpoint will require
640    /// Basic Authentication. This is useful for protecting API documentation
641    /// in production environments.
642    ///
643    /// # Example
644    ///
645    /// ```rust,ignore
646    /// RustApi::new()
647    ///     .route("/users", get(list_users))
648    ///     .docs_with_auth("/docs", "admin", "secret123")
649    ///     .run("127.0.0.1:8080")
650    ///     .await
651    /// ```
652    #[cfg(feature = "swagger-ui")]
653    pub fn docs_with_auth(self, path: &str, username: &str, password: &str) -> Self {
654        let title = self.openapi_spec.info.title.clone();
655        let version = self.openapi_spec.info.version.clone();
656        let description = self.openapi_spec.info.description.clone();
657
658        self.docs_with_auth_and_info(
659            path,
660            username,
661            password,
662            &title,
663            &version,
664            description.as_deref(),
665        )
666    }
667
668    /// Enable Swagger UI documentation with Basic Auth and custom API info
669    ///
670    /// # Example
671    ///
672    /// ```rust,ignore
673    /// RustApi::new()
674    ///     .docs_with_auth_and_info(
675    ///         "/docs",
676    ///         "admin",
677    ///         "secret",
678    ///         "My API",
679    ///         "2.0.0",
680    ///         Some("Protected API documentation")
681    ///     )
682    /// ```
683    #[cfg(feature = "swagger-ui")]
684    pub fn docs_with_auth_and_info(
685        mut self,
686        path: &str,
687        username: &str,
688        password: &str,
689        title: &str,
690        version: &str,
691        description: Option<&str>,
692    ) -> Self {
693        use crate::router::MethodRouter;
694        use base64::{engine::general_purpose::STANDARD, Engine};
695        use std::collections::HashMap;
696
697        // Update spec info
698        self.openapi_spec.info.title = title.to_string();
699        self.openapi_spec.info.version = version.to_string();
700        if let Some(desc) = description {
701            self.openapi_spec.info.description = Some(desc.to_string());
702        }
703
704        let path = path.trim_end_matches('/');
705        let openapi_path = format!("{}/openapi.json", path);
706
707        // Create expected auth header value
708        let credentials = format!("{}:{}", username, password);
709        let encoded = STANDARD.encode(credentials.as_bytes());
710        let expected_auth = format!("Basic {}", encoded);
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        let expected_auth_spec = expected_auth.clone();
717        let expected_auth_docs = expected_auth;
718
719        // Create spec handler with auth check
720        let spec_handler: crate::handler::BoxedHandler =
721            std::sync::Arc::new(move |req: crate::Request| {
722                let json = spec_json.clone();
723                let expected = expected_auth_spec.clone();
724                Box::pin(async move {
725                    if !check_basic_auth(&req, &expected) {
726                        return unauthorized_response();
727                    }
728                    http::Response::builder()
729                        .status(http::StatusCode::OK)
730                        .header(http::header::CONTENT_TYPE, "application/json")
731                        .body(http_body_util::Full::new(bytes::Bytes::from(json)))
732                        .unwrap()
733                })
734                    as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
735            });
736
737        // Create docs handler with auth check
738        let docs_handler: crate::handler::BoxedHandler =
739            std::sync::Arc::new(move |req: crate::Request| {
740                let url = openapi_url.clone();
741                let expected = expected_auth_docs.clone();
742                Box::pin(async move {
743                    if !check_basic_auth(&req, &expected) {
744                        return unauthorized_response();
745                    }
746                    rustapi_openapi::swagger_ui_html(&url)
747                })
748                    as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
749            });
750
751        // Create method routers with boxed handlers
752        let mut spec_handlers = HashMap::new();
753        spec_handlers.insert(http::Method::GET, spec_handler);
754        let spec_router = MethodRouter::from_boxed(spec_handlers);
755
756        let mut docs_handlers = HashMap::new();
757        docs_handlers.insert(http::Method::GET, docs_handler);
758        let docs_router = MethodRouter::from_boxed(docs_handlers);
759
760        self.route(&openapi_path, spec_router)
761            .route(path, docs_router)
762    }
763
764    /// Run the server
765    ///
766    /// # Example
767    ///
768    /// ```rust,ignore
769    /// RustApi::new()
770    ///     .route("/", get(hello))
771    ///     .run("127.0.0.1:8080")
772    ///     .await
773    /// ```
774    pub async fn run(mut self, addr: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
775        // Apply body limit layer if configured (should be first in the chain)
776        if let Some(limit) = self.body_limit {
777            // Prepend body limit layer so it's the first to process requests
778            self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
779        }
780
781        let server = Server::new(self.router, self.layers);
782        server.run(addr).await
783    }
784
785    /// Get the inner router (for testing or advanced usage)
786    pub fn into_router(self) -> Router {
787        self.router
788    }
789
790    /// Get the layer stack (for testing)
791    pub fn layers(&self) -> &LayerStack {
792        &self.layers
793    }
794}
795
796fn add_path_params_to_operation(path: &str, op: &mut rustapi_openapi::Operation) {
797    let mut params: Vec<String> = Vec::new();
798    let mut in_brace = false;
799    let mut current = String::new();
800
801    for ch in path.chars() {
802        match ch {
803            '{' => {
804                in_brace = true;
805                current.clear();
806            }
807            '}' => {
808                if in_brace {
809                    in_brace = false;
810                    if !current.is_empty() {
811                        params.push(current.clone());
812                    }
813                }
814            }
815            _ => {
816                if in_brace {
817                    current.push(ch);
818                }
819            }
820        }
821    }
822
823    if params.is_empty() {
824        return;
825    }
826
827    let op_params = op.parameters.get_or_insert_with(Vec::new);
828
829    for name in params {
830        let already = op_params
831            .iter()
832            .any(|p| p.location == "path" && p.name == name);
833        if already {
834            continue;
835        }
836
837        op_params.push(rustapi_openapi::Parameter {
838            name,
839            location: "path".to_string(),
840            required: true,
841            description: None,
842            schema: rustapi_openapi::SchemaRef::Inline(serde_json::json!({ "type": "string" })),
843        });
844    }
845}
846
847/// Normalize a prefix for OpenAPI paths.
848///
849/// Ensures the prefix:
850/// - Starts with exactly one leading slash
851/// - Has no trailing slash (unless it's just "/")
852/// - Has no double slashes
853fn normalize_prefix_for_openapi(prefix: &str) -> String {
854    // Handle empty string
855    if prefix.is_empty() {
856        return "/".to_string();
857    }
858
859    // Split by slashes and filter out empty segments (handles multiple slashes)
860    let segments: Vec<&str> = prefix.split('/').filter(|s| !s.is_empty()).collect();
861
862    // If no segments after filtering, return root
863    if segments.is_empty() {
864        return "/".to_string();
865    }
866
867    // Build the normalized prefix with leading slash
868    let mut result = String::with_capacity(prefix.len() + 1);
869    for segment in segments {
870        result.push('/');
871        result.push_str(segment);
872    }
873
874    result
875}
876
877impl Default for RustApi {
878    fn default() -> Self {
879        Self::new()
880    }
881}
882
883#[cfg(test)]
884mod tests {
885    use super::RustApi;
886    use crate::extract::{FromRequestParts, State};
887    use crate::request::Request;
888    use crate::router::{get, post, Router};
889    use bytes::Bytes;
890    use http::Method;
891    use proptest::prelude::*;
892    use std::collections::HashMap;
893
894    #[test]
895    fn state_is_available_via_extractor() {
896        let app = RustApi::new().state(123u32);
897        let router = app.into_router();
898
899        let req = http::Request::builder()
900            .method(Method::GET)
901            .uri("/test")
902            .body(())
903            .unwrap();
904        let (parts, _) = req.into_parts();
905
906        let request = Request::new(parts, Bytes::new(), router.state_ref(), HashMap::new());
907        let State(value) = State::<u32>::from_request_parts(&request).unwrap();
908        assert_eq!(value, 123u32);
909    }
910
911    // **Feature: router-nesting, Property 11: OpenAPI Integration**
912    //
913    // For any nested routes with OpenAPI operations, the operations should appear
914    // in the parent's OpenAPI spec with prefixed paths and preserved metadata.
915    //
916    // **Validates: Requirements 4.1, 4.2**
917    proptest! {
918        #![proptest_config(ProptestConfig::with_cases(100))]
919
920        /// Property: Nested routes appear in OpenAPI spec with prefixed paths
921        ///
922        /// For any router with routes nested under a prefix, all routes should
923        /// appear in the OpenAPI spec with the prefix prepended to their paths.
924        #[test]
925        fn prop_nested_routes_in_openapi_spec(
926            // Generate prefix segments
927            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
928            // Generate route path segments
929            route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
930            has_param in any::<bool>(),
931        ) {
932            async fn handler() -> &'static str { "handler" }
933
934            // Build the prefix
935            let prefix = format!("/{}", prefix_segments.join("/"));
936
937            // Build the route path
938            let mut route_path = format!("/{}", route_segments.join("/"));
939            if has_param {
940                route_path.push_str("/{id}");
941            }
942
943            // Create nested router and nest it through RustApi
944            let nested_router = Router::new().route(&route_path, get(handler));
945            let app = RustApi::new().nest(&prefix, nested_router);
946
947            // Build expected prefixed path for OpenAPI (uses {param} format)
948            let expected_openapi_path = format!("{}{}", prefix, route_path);
949
950            // Get the OpenAPI spec
951            let spec = app.openapi_spec();
952
953            // Property: The prefixed route should exist in OpenAPI paths
954            prop_assert!(
955                spec.paths.contains_key(&expected_openapi_path),
956                "Expected OpenAPI path '{}' not found. Available paths: {:?}",
957                expected_openapi_path,
958                spec.paths.keys().collect::<Vec<_>>()
959            );
960
961            // Property: The path item should have a GET operation
962            let path_item = spec.paths.get(&expected_openapi_path).unwrap();
963            prop_assert!(
964                path_item.get.is_some(),
965                "GET operation should exist for path '{}'",
966                expected_openapi_path
967            );
968        }
969
970        /// Property: Multiple HTTP methods are preserved in OpenAPI spec after nesting
971        ///
972        /// For any router with routes having multiple HTTP methods, nesting should
973        /// preserve all method operations in the OpenAPI spec.
974        #[test]
975        fn prop_multiple_methods_preserved_in_openapi(
976            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
977            route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
978        ) {
979            async fn get_handler() -> &'static str { "get" }
980            async fn post_handler() -> &'static str { "post" }
981
982            // Build the prefix and route path
983            let prefix = format!("/{}", prefix_segments.join("/"));
984            let route_path = format!("/{}", route_segments.join("/"));
985
986            // Create nested router with both GET and POST using separate routes
987            // Since MethodRouter doesn't have chaining methods, we create two routes
988            let get_route_path = format!("{}/get", route_path);
989            let post_route_path = format!("{}/post", route_path);
990            let nested_router = Router::new()
991                .route(&get_route_path, get(get_handler))
992                .route(&post_route_path, post(post_handler));
993            let app = RustApi::new().nest(&prefix, nested_router);
994
995            // Build expected prefixed paths for OpenAPI
996            let expected_get_path = format!("{}{}", prefix, get_route_path);
997            let expected_post_path = format!("{}{}", prefix, post_route_path);
998
999            // Get the OpenAPI spec
1000            let spec = app.openapi_spec();
1001
1002            // Property: Both paths should exist
1003            prop_assert!(
1004                spec.paths.contains_key(&expected_get_path),
1005                "Expected OpenAPI path '{}' not found",
1006                expected_get_path
1007            );
1008            prop_assert!(
1009                spec.paths.contains_key(&expected_post_path),
1010                "Expected OpenAPI path '{}' not found",
1011                expected_post_path
1012            );
1013
1014            // Property: GET operation should exist on get path
1015            let get_path_item = spec.paths.get(&expected_get_path).unwrap();
1016            prop_assert!(
1017                get_path_item.get.is_some(),
1018                "GET operation should exist for path '{}'",
1019                expected_get_path
1020            );
1021
1022            // Property: POST operation should exist on post path
1023            let post_path_item = spec.paths.get(&expected_post_path).unwrap();
1024            prop_assert!(
1025                post_path_item.post.is_some(),
1026                "POST operation should exist for path '{}'",
1027                expected_post_path
1028            );
1029        }
1030
1031        /// Property: Path parameters are added to OpenAPI operations after nesting
1032        ///
1033        /// For any nested route with path parameters, the OpenAPI operation should
1034        /// include the path parameters.
1035        #[test]
1036        fn prop_path_params_in_openapi_after_nesting(
1037            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1038            param_name in "[a-z][a-z0-9]{0,5}",
1039        ) {
1040            async fn handler() -> &'static str { "handler" }
1041
1042            // Build the prefix and route path with parameter
1043            let prefix = format!("/{}", prefix_segments.join("/"));
1044            let route_path = format!("/{{{}}}", param_name);
1045
1046            // Create nested router
1047            let nested_router = Router::new().route(&route_path, get(handler));
1048            let app = RustApi::new().nest(&prefix, nested_router);
1049
1050            // Build expected prefixed path for OpenAPI
1051            let expected_openapi_path = format!("{}{}", prefix, route_path);
1052
1053            // Get the OpenAPI spec
1054            let spec = app.openapi_spec();
1055
1056            // Property: The path should exist
1057            prop_assert!(
1058                spec.paths.contains_key(&expected_openapi_path),
1059                "Expected OpenAPI path '{}' not found",
1060                expected_openapi_path
1061            );
1062
1063            // Property: The GET operation should have the path parameter
1064            let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1065            let get_op = path_item.get.as_ref().unwrap();
1066
1067            prop_assert!(
1068                get_op.parameters.is_some(),
1069                "Operation should have parameters for path '{}'",
1070                expected_openapi_path
1071            );
1072
1073            let params = get_op.parameters.as_ref().unwrap();
1074            let has_param = params.iter().any(|p| p.name == param_name && p.location == "path");
1075            prop_assert!(
1076                has_param,
1077                "Path parameter '{}' should exist in operation parameters. Found: {:?}",
1078                param_name,
1079                params.iter().map(|p| &p.name).collect::<Vec<_>>()
1080            );
1081        }
1082    }
1083
1084    // **Feature: router-nesting, Property 13: RustApi Integration**
1085    //
1086    // For any router nested through `RustApi::new().nest()`, the behavior should be
1087    // identical to nesting through `Router::new().nest()`, and routes should appear
1088    // in the OpenAPI spec.
1089    //
1090    // **Validates: Requirements 6.1, 6.2**
1091    proptest! {
1092        #![proptest_config(ProptestConfig::with_cases(100))]
1093
1094        /// Property: RustApi::nest delegates to Router::nest and produces identical route registration
1095        ///
1096        /// For any router with routes nested under a prefix, nesting through RustApi
1097        /// should produce the same route registration as nesting through Router directly.
1098        #[test]
1099        fn prop_rustapi_nest_delegates_to_router_nest(
1100            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1101            route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1102            has_param in any::<bool>(),
1103        ) {
1104            async fn handler() -> &'static str { "handler" }
1105
1106            // Build the prefix
1107            let prefix = format!("/{}", prefix_segments.join("/"));
1108
1109            // Build the route path
1110            let mut route_path = format!("/{}", route_segments.join("/"));
1111            if has_param {
1112                route_path.push_str("/{id}");
1113            }
1114
1115            // Create nested router
1116            let nested_router_for_rustapi = Router::new().route(&route_path, get(handler));
1117            let nested_router_for_router = Router::new().route(&route_path, get(handler));
1118
1119            // Nest through RustApi
1120            let rustapi_app = RustApi::new().nest(&prefix, nested_router_for_rustapi);
1121            let rustapi_router = rustapi_app.into_router();
1122
1123            // Nest through Router directly
1124            let router_app = Router::new().nest(&prefix, nested_router_for_router);
1125
1126            // Property: Both should have the same registered routes
1127            let rustapi_routes = rustapi_router.registered_routes();
1128            let router_routes = router_app.registered_routes();
1129
1130            prop_assert_eq!(
1131                rustapi_routes.len(),
1132                router_routes.len(),
1133                "RustApi and Router should have same number of routes"
1134            );
1135
1136            // Property: All routes from Router should exist in RustApi
1137            for (path, info) in router_routes {
1138                prop_assert!(
1139                    rustapi_routes.contains_key(path),
1140                    "Route '{}' from Router should exist in RustApi routes",
1141                    path
1142                );
1143
1144                let rustapi_info = rustapi_routes.get(path).unwrap();
1145                prop_assert_eq!(
1146                    &info.path, &rustapi_info.path,
1147                    "Display paths should match for route '{}'",
1148                    path
1149                );
1150                prop_assert_eq!(
1151                    info.methods.len(), rustapi_info.methods.len(),
1152                    "Method count should match for route '{}'",
1153                    path
1154                );
1155            }
1156        }
1157
1158        /// Property: RustApi::nest includes nested routes in OpenAPI spec
1159        ///
1160        /// For any router with routes nested through RustApi, all routes should
1161        /// appear in the OpenAPI specification with prefixed paths.
1162        #[test]
1163        fn prop_rustapi_nest_includes_routes_in_openapi(
1164            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1165            route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1166            has_param in any::<bool>(),
1167        ) {
1168            async fn handler() -> &'static str { "handler" }
1169
1170            // Build the prefix
1171            let prefix = format!("/{}", prefix_segments.join("/"));
1172
1173            // Build the route path
1174            let mut route_path = format!("/{}", route_segments.join("/"));
1175            if has_param {
1176                route_path.push_str("/{id}");
1177            }
1178
1179            // Create nested router and nest through RustApi
1180            let nested_router = Router::new().route(&route_path, get(handler));
1181            let app = RustApi::new().nest(&prefix, nested_router);
1182
1183            // Build expected prefixed path for OpenAPI
1184            let expected_openapi_path = format!("{}{}", prefix, route_path);
1185
1186            // Get the OpenAPI spec
1187            let spec = app.openapi_spec();
1188
1189            // Property: The prefixed route should exist in OpenAPI paths
1190            prop_assert!(
1191                spec.paths.contains_key(&expected_openapi_path),
1192                "Expected OpenAPI path '{}' not found. Available paths: {:?}",
1193                expected_openapi_path,
1194                spec.paths.keys().collect::<Vec<_>>()
1195            );
1196
1197            // Property: The path item should have a GET operation
1198            let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1199            prop_assert!(
1200                path_item.get.is_some(),
1201                "GET operation should exist for path '{}'",
1202                expected_openapi_path
1203            );
1204        }
1205
1206        /// Property: RustApi::nest route matching is identical to Router::nest
1207        ///
1208        /// For any nested route, matching through RustApi should produce the same
1209        /// result as matching through Router directly.
1210        #[test]
1211        fn prop_rustapi_nest_route_matching_identical(
1212            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
1213            route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
1214            param_value in "[a-z0-9]{1,10}",
1215        ) {
1216            use crate::router::RouteMatch;
1217
1218            async fn handler() -> &'static str { "handler" }
1219
1220            // Build the prefix and route path with parameter
1221            let prefix = format!("/{}", prefix_segments.join("/"));
1222            let route_path = format!("/{}/{{id}}", route_segments.join("/"));
1223
1224            // Create nested routers
1225            let nested_router_for_rustapi = Router::new().route(&route_path, get(handler));
1226            let nested_router_for_router = Router::new().route(&route_path, get(handler));
1227
1228            // Nest through both RustApi and Router
1229            let rustapi_app = RustApi::new().nest(&prefix, nested_router_for_rustapi);
1230            let rustapi_router = rustapi_app.into_router();
1231            let router_app = Router::new().nest(&prefix, nested_router_for_router);
1232
1233            // Build the full path to match
1234            let full_path = format!("{}/{}/{}", prefix, route_segments.join("/"), param_value);
1235
1236            // Match through both
1237            let rustapi_match = rustapi_router.match_route(&full_path, &Method::GET);
1238            let router_match = router_app.match_route(&full_path, &Method::GET);
1239
1240            // Property: Both should return Found with same parameters
1241            match (rustapi_match, router_match) {
1242                (RouteMatch::Found { params: rustapi_params, .. }, RouteMatch::Found { params: router_params, .. }) => {
1243                    prop_assert_eq!(
1244                        rustapi_params.len(),
1245                        router_params.len(),
1246                        "Parameter count should match"
1247                    );
1248                    for (key, value) in &router_params {
1249                        prop_assert!(
1250                            rustapi_params.contains_key(key),
1251                            "RustApi should have parameter '{}'",
1252                            key
1253                        );
1254                        prop_assert_eq!(
1255                            rustapi_params.get(key).unwrap(),
1256                            value,
1257                            "Parameter '{}' value should match",
1258                            key
1259                        );
1260                    }
1261                }
1262                (rustapi_result, router_result) => {
1263                    prop_assert!(
1264                        false,
1265                        "Both should return Found, but RustApi returned {:?} and Router returned {:?}",
1266                        match rustapi_result {
1267                            RouteMatch::Found { .. } => "Found",
1268                            RouteMatch::NotFound => "NotFound",
1269                            RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
1270                        },
1271                        match router_result {
1272                            RouteMatch::Found { .. } => "Found",
1273                            RouteMatch::NotFound => "NotFound",
1274                            RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
1275                        }
1276                    );
1277                }
1278            }
1279        }
1280    }
1281
1282    /// Unit test: Verify OpenAPI operations are propagated during nesting
1283    #[test]
1284    fn test_openapi_operations_propagated_during_nesting() {
1285        async fn list_users() -> &'static str {
1286            "list users"
1287        }
1288        async fn get_user() -> &'static str {
1289            "get user"
1290        }
1291        async fn create_user() -> &'static str {
1292            "create user"
1293        }
1294
1295        // Create nested router with multiple routes
1296        // Note: We use separate routes since MethodRouter doesn't support chaining
1297        let users_router = Router::new()
1298            .route("/", get(list_users))
1299            .route("/create", post(create_user))
1300            .route("/{id}", get(get_user));
1301
1302        // Nest under /api/v1/users
1303        let app = RustApi::new().nest("/api/v1/users", users_router);
1304
1305        let spec = app.openapi_spec();
1306
1307        // Verify /api/v1/users path exists with GET
1308        assert!(
1309            spec.paths.contains_key("/api/v1/users"),
1310            "Should have /api/v1/users path"
1311        );
1312        let users_path = spec.paths.get("/api/v1/users").unwrap();
1313        assert!(users_path.get.is_some(), "Should have GET operation");
1314
1315        // Verify /api/v1/users/create path exists with POST
1316        assert!(
1317            spec.paths.contains_key("/api/v1/users/create"),
1318            "Should have /api/v1/users/create path"
1319        );
1320        let create_path = spec.paths.get("/api/v1/users/create").unwrap();
1321        assert!(create_path.post.is_some(), "Should have POST operation");
1322
1323        // Verify /api/v1/users/{id} path exists with GET
1324        assert!(
1325            spec.paths.contains_key("/api/v1/users/{id}"),
1326            "Should have /api/v1/users/{{id}} path"
1327        );
1328        let user_path = spec.paths.get("/api/v1/users/{id}").unwrap();
1329        assert!(
1330            user_path.get.is_some(),
1331            "Should have GET operation for user by id"
1332        );
1333
1334        // Verify path parameter is added
1335        let get_user_op = user_path.get.as_ref().unwrap();
1336        assert!(get_user_op.parameters.is_some(), "Should have parameters");
1337        let params = get_user_op.parameters.as_ref().unwrap();
1338        assert!(
1339            params
1340                .iter()
1341                .any(|p| p.name == "id" && p.location == "path"),
1342            "Should have 'id' path parameter"
1343        );
1344    }
1345
1346    /// Unit test: Verify nested routes don't appear without nesting
1347    #[test]
1348    fn test_openapi_spec_empty_without_routes() {
1349        let app = RustApi::new();
1350        let spec = app.openapi_spec();
1351
1352        // Should have no paths (except potentially default ones)
1353        assert!(
1354            spec.paths.is_empty(),
1355            "OpenAPI spec should have no paths without routes"
1356        );
1357    }
1358
1359    /// Unit test: Verify RustApi::nest delegates correctly to Router::nest
1360    ///
1361    /// **Feature: router-nesting, Property 13: RustApi Integration**
1362    /// **Validates: Requirements 6.1, 6.2**
1363    #[test]
1364    fn test_rustapi_nest_delegates_to_router_nest() {
1365        use crate::router::RouteMatch;
1366
1367        async fn list_users() -> &'static str {
1368            "list users"
1369        }
1370        async fn get_user() -> &'static str {
1371            "get user"
1372        }
1373        async fn create_user() -> &'static str {
1374            "create user"
1375        }
1376
1377        // Create nested router with multiple routes
1378        let users_router = Router::new()
1379            .route("/", get(list_users))
1380            .route("/create", post(create_user))
1381            .route("/{id}", get(get_user));
1382
1383        // Nest through RustApi
1384        let app = RustApi::new().nest("/api/v1/users", users_router);
1385        let router = app.into_router();
1386
1387        // Verify routes are registered correctly
1388        let routes = router.registered_routes();
1389        assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1390
1391        // Verify route paths
1392        assert!(
1393            routes.contains_key("/api/v1/users"),
1394            "Should have /api/v1/users route"
1395        );
1396        assert!(
1397            routes.contains_key("/api/v1/users/create"),
1398            "Should have /api/v1/users/create route"
1399        );
1400        assert!(
1401            routes.contains_key("/api/v1/users/:id"),
1402            "Should have /api/v1/users/:id route"
1403        );
1404
1405        // Verify route matching works
1406        match router.match_route("/api/v1/users", &Method::GET) {
1407            RouteMatch::Found { params, .. } => {
1408                assert!(params.is_empty(), "Root route should have no params");
1409            }
1410            _ => panic!("GET /api/v1/users should be found"),
1411        }
1412
1413        match router.match_route("/api/v1/users/create", &Method::POST) {
1414            RouteMatch::Found { params, .. } => {
1415                assert!(params.is_empty(), "Create route should have no params");
1416            }
1417            _ => panic!("POST /api/v1/users/create should be found"),
1418        }
1419
1420        match router.match_route("/api/v1/users/123", &Method::GET) {
1421            RouteMatch::Found { params, .. } => {
1422                assert_eq!(
1423                    params.get("id"),
1424                    Some(&"123".to_string()),
1425                    "Should extract id param"
1426                );
1427            }
1428            _ => panic!("GET /api/v1/users/123 should be found"),
1429        }
1430
1431        // Verify method not allowed
1432        match router.match_route("/api/v1/users", &Method::DELETE) {
1433            RouteMatch::MethodNotAllowed { allowed } => {
1434                assert!(allowed.contains(&Method::GET), "Should allow GET");
1435            }
1436            _ => panic!("DELETE /api/v1/users should return MethodNotAllowed"),
1437        }
1438    }
1439
1440    /// Unit test: Verify RustApi::nest includes routes in OpenAPI spec
1441    ///
1442    /// **Feature: router-nesting, Property 13: RustApi Integration**
1443    /// **Validates: Requirements 6.1, 6.2**
1444    #[test]
1445    fn test_rustapi_nest_includes_routes_in_openapi_spec() {
1446        async fn list_items() -> &'static str {
1447            "list items"
1448        }
1449        async fn get_item() -> &'static str {
1450            "get item"
1451        }
1452
1453        // Create nested router
1454        let items_router = Router::new()
1455            .route("/", get(list_items))
1456            .route("/{item_id}", get(get_item));
1457
1458        // Nest through RustApi
1459        let app = RustApi::new().nest("/api/items", items_router);
1460
1461        // Verify OpenAPI spec
1462        let spec = app.openapi_spec();
1463
1464        // Verify paths exist
1465        assert!(
1466            spec.paths.contains_key("/api/items"),
1467            "Should have /api/items in OpenAPI"
1468        );
1469        assert!(
1470            spec.paths.contains_key("/api/items/{item_id}"),
1471            "Should have /api/items/{{item_id}} in OpenAPI"
1472        );
1473
1474        // Verify operations
1475        let list_path = spec.paths.get("/api/items").unwrap();
1476        assert!(
1477            list_path.get.is_some(),
1478            "Should have GET operation for /api/items"
1479        );
1480
1481        let get_path = spec.paths.get("/api/items/{item_id}").unwrap();
1482        assert!(
1483            get_path.get.is_some(),
1484            "Should have GET operation for /api/items/{{item_id}}"
1485        );
1486
1487        // Verify path parameter is added
1488        let get_op = get_path.get.as_ref().unwrap();
1489        assert!(get_op.parameters.is_some(), "Should have parameters");
1490        let params = get_op.parameters.as_ref().unwrap();
1491        assert!(
1492            params
1493                .iter()
1494                .any(|p| p.name == "item_id" && p.location == "path"),
1495            "Should have 'item_id' path parameter"
1496        );
1497    }
1498}
1499
1500/// Check Basic Auth header against expected credentials
1501#[cfg(feature = "swagger-ui")]
1502fn check_basic_auth(req: &crate::Request, expected: &str) -> bool {
1503    req.headers()
1504        .get(http::header::AUTHORIZATION)
1505        .and_then(|v| v.to_str().ok())
1506        .map(|auth| auth == expected)
1507        .unwrap_or(false)
1508}
1509
1510/// Create 401 Unauthorized response with WWW-Authenticate header
1511#[cfg(feature = "swagger-ui")]
1512fn unauthorized_response() -> crate::Response {
1513    http::Response::builder()
1514        .status(http::StatusCode::UNAUTHORIZED)
1515        .header(
1516            http::header::WWW_AUTHENTICATE,
1517            "Basic realm=\"API Documentation\"",
1518        )
1519        .header(http::header::CONTENT_TYPE, "text/plain")
1520        .body(http_body_util::Full::new(bytes::Bytes::from(
1521            "Unauthorized",
1522        )))
1523        .unwrap()
1524}
1525
1526/// Configuration builder for RustAPI with auto-routes
1527pub struct RustApiConfig {
1528    docs_path: Option<String>,
1529    docs_enabled: bool,
1530    api_title: String,
1531    api_version: String,
1532    api_description: Option<String>,
1533    body_limit: Option<usize>,
1534    layers: LayerStack,
1535}
1536
1537impl Default for RustApiConfig {
1538    fn default() -> Self {
1539        Self::new()
1540    }
1541}
1542
1543impl RustApiConfig {
1544    pub fn new() -> Self {
1545        Self {
1546            docs_path: Some("/docs".to_string()),
1547            docs_enabled: true,
1548            api_title: "RustAPI".to_string(),
1549            api_version: "1.0.0".to_string(),
1550            api_description: None,
1551            body_limit: None,
1552            layers: LayerStack::new(),
1553        }
1554    }
1555
1556    /// Set the docs path (default: "/docs")
1557    pub fn docs_path(mut self, path: impl Into<String>) -> Self {
1558        self.docs_path = Some(path.into());
1559        self
1560    }
1561
1562    /// Enable or disable docs (default: true)
1563    pub fn docs_enabled(mut self, enabled: bool) -> Self {
1564        self.docs_enabled = enabled;
1565        self
1566    }
1567
1568    /// Set OpenAPI info
1569    pub fn openapi_info(
1570        mut self,
1571        title: impl Into<String>,
1572        version: impl Into<String>,
1573        description: Option<impl Into<String>>,
1574    ) -> Self {
1575        self.api_title = title.into();
1576        self.api_version = version.into();
1577        self.api_description = description.map(|d| d.into());
1578        self
1579    }
1580
1581    /// Set body size limit
1582    pub fn body_limit(mut self, limit: usize) -> Self {
1583        self.body_limit = Some(limit);
1584        self
1585    }
1586
1587    /// Add a middleware layer
1588    pub fn layer<L>(mut self, layer: L) -> Self
1589    where
1590        L: MiddlewareLayer,
1591    {
1592        self.layers.push(Box::new(layer));
1593        self
1594    }
1595
1596    /// Build the RustApi instance
1597    pub fn build(self) -> RustApi {
1598        let mut app = RustApi::new().mount_auto_routes_grouped();
1599
1600        // Apply configuration
1601        if let Some(limit) = self.body_limit {
1602            app = app.body_limit(limit);
1603        }
1604
1605        app = app.openapi_info(
1606            &self.api_title,
1607            &self.api_version,
1608            self.api_description.as_deref(),
1609        );
1610
1611        #[cfg(feature = "swagger-ui")]
1612        if self.docs_enabled {
1613            if let Some(path) = self.docs_path {
1614                app = app.docs(&path);
1615            }
1616        }
1617
1618        // Apply layers
1619        // Note: layers are applied in reverse order in RustApi::layer logic (pushing to vec)
1620        app.layers.extend(self.layers);
1621
1622        app
1623    }
1624
1625    /// Build and run the server
1626    pub async fn run(
1627        self,
1628        addr: impl AsRef<str>,
1629    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1630        self.build().run(addr.as_ref()).await
1631    }
1632}