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