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    /// # Example
396    ///
397    /// ```rust,ignore
398    /// let api_v1 = Router::new()
399    ///     .route("/users", get(list_users));
400    ///
401    /// RustApi::new()
402    ///     .nest("/api/v1", api_v1)
403    /// ```
404    pub fn nest(mut self, prefix: &str, router: Router) -> Self {
405        self.router = self.router.nest(prefix, router);
406        self
407    }
408
409    /// Serve static files from a directory
410    ///
411    /// Maps a URL path prefix to a filesystem directory. Requests to paths under
412    /// the prefix will serve files from the corresponding location in the directory.
413    ///
414    /// # Arguments
415    ///
416    /// * `prefix` - URL path prefix (e.g., "/static", "/assets")
417    /// * `root` - Filesystem directory path
418    ///
419    /// # Features
420    ///
421    /// - Automatic MIME type detection
422    /// - ETag and Last-Modified headers for caching
423    /// - Index file serving for directories
424    /// - Path traversal prevention
425    ///
426    /// # Example
427    ///
428    /// ```rust,ignore
429    /// use rustapi_rs::prelude::*;
430    ///
431    /// RustApi::new()
432    ///     .serve_static("/assets", "./public")
433    ///     .serve_static("/uploads", "./uploads")
434    ///     .run("127.0.0.1:8080")
435    ///     .await
436    /// ```
437    pub fn serve_static(self, prefix: &str, root: impl Into<std::path::PathBuf>) -> Self {
438        self.serve_static_with_config(crate::static_files::StaticFileConfig::new(root, prefix))
439    }
440
441    /// Serve static files with custom configuration
442    ///
443    /// # Example
444    ///
445    /// ```rust,ignore
446    /// use rustapi_core::static_files::StaticFileConfig;
447    ///
448    /// let config = StaticFileConfig::new("./public", "/assets")
449    ///     .max_age(86400)  // Cache for 1 day
450    ///     .fallback("index.html");  // SPA fallback
451    ///
452    /// RustApi::new()
453    ///     .serve_static_with_config(config)
454    ///     .run("127.0.0.1:8080")
455    ///     .await
456    /// ```
457    pub fn serve_static_with_config(self, config: crate::static_files::StaticFileConfig) -> Self {
458        use crate::router::MethodRouter;
459        use std::collections::HashMap;
460
461        let prefix = config.prefix.clone();
462        let catch_all_path = format!("{}/*path", prefix.trim_end_matches('/'));
463
464        // Create the static file handler
465        let handler: crate::handler::BoxedHandler =
466            std::sync::Arc::new(move |req: crate::Request| {
467                let config = config.clone();
468                let path = req.uri().path().to_string();
469
470                Box::pin(async move {
471                    let relative_path = path.strip_prefix(&config.prefix).unwrap_or(&path);
472
473                    match crate::static_files::StaticFile::serve(relative_path, &config).await {
474                        Ok(response) => response,
475                        Err(err) => err.into_response(),
476                    }
477                })
478                    as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
479            });
480
481        let mut handlers = HashMap::new();
482        handlers.insert(http::Method::GET, handler);
483        let method_router = MethodRouter::from_boxed(handlers);
484
485        self.route(&catch_all_path, method_router)
486    }
487
488    /// Enable response compression
489    ///
490    /// Adds gzip/deflate compression for response bodies. The compression
491    /// is based on the client's Accept-Encoding header.
492    ///
493    /// # Example
494    ///
495    /// ```rust,ignore
496    /// use rustapi_rs::prelude::*;
497    ///
498    /// RustApi::new()
499    ///     .compression()
500    ///     .route("/", get(handler))
501    ///     .run("127.0.0.1:8080")
502    ///     .await
503    /// ```
504    #[cfg(feature = "compression")]
505    pub fn compression(self) -> Self {
506        self.layer(crate::middleware::CompressionLayer::new())
507    }
508
509    /// Enable response compression with custom configuration
510    ///
511    /// # Example
512    ///
513    /// ```rust,ignore
514    /// use rustapi_core::middleware::CompressionConfig;
515    ///
516    /// RustApi::new()
517    ///     .compression_with_config(
518    ///         CompressionConfig::new()
519    ///             .min_size(512)
520    ///             .level(9)
521    ///     )
522    ///     .route("/", get(handler))
523    /// ```
524    #[cfg(feature = "compression")]
525    pub fn compression_with_config(self, config: crate::middleware::CompressionConfig) -> Self {
526        self.layer(crate::middleware::CompressionLayer::with_config(config))
527    }
528
529    /// Enable Swagger UI documentation
530    ///
531    /// This adds two endpoints:
532    /// - `{path}` - Swagger UI interface
533    /// - `{path}/openapi.json` - OpenAPI JSON specification
534    ///
535    /// # Example
536    ///
537    /// ```text
538    /// RustApi::new()
539    ///     .route("/users", get(list_users))
540    ///     .docs("/docs")  // Swagger UI at /docs, spec at /docs/openapi.json
541    ///     .run("127.0.0.1:8080")
542    ///     .await
543    /// ```
544    #[cfg(feature = "swagger-ui")]
545    pub fn docs(self, path: &str) -> Self {
546        let title = self.openapi_spec.info.title.clone();
547        let version = self.openapi_spec.info.version.clone();
548        let description = self.openapi_spec.info.description.clone();
549
550        self.docs_with_info(path, &title, &version, description.as_deref())
551    }
552
553    /// Enable Swagger UI documentation with custom API info
554    ///
555    /// # Example
556    ///
557    /// ```rust,ignore
558    /// RustApi::new()
559    ///     .docs_with_info("/docs", "My API", "2.0.0", Some("API for managing users"))
560    /// ```
561    #[cfg(feature = "swagger-ui")]
562    pub fn docs_with_info(
563        mut self,
564        path: &str,
565        title: &str,
566        version: &str,
567        description: Option<&str>,
568    ) -> Self {
569        use crate::router::get;
570        // Update spec info
571        self.openapi_spec.info.title = title.to_string();
572        self.openapi_spec.info.version = version.to_string();
573        if let Some(desc) = description {
574            self.openapi_spec.info.description = Some(desc.to_string());
575        }
576
577        let path = path.trim_end_matches('/');
578        let openapi_path = format!("{}/openapi.json", path);
579
580        // Clone values for closures
581        let spec_json =
582            serde_json::to_string_pretty(&self.openapi_spec.to_json()).unwrap_or_default();
583        let openapi_url = openapi_path.clone();
584
585        // Add OpenAPI JSON endpoint
586        let spec_handler = move || {
587            let json = spec_json.clone();
588            async move {
589                http::Response::builder()
590                    .status(http::StatusCode::OK)
591                    .header(http::header::CONTENT_TYPE, "application/json")
592                    .body(http_body_util::Full::new(bytes::Bytes::from(json)))
593                    .unwrap()
594            }
595        };
596
597        // Add Swagger UI endpoint
598        let docs_handler = move || {
599            let url = openapi_url.clone();
600            async move { rustapi_openapi::swagger_ui_html(&url) }
601        };
602
603        self.route(&openapi_path, get(spec_handler))
604            .route(path, get(docs_handler))
605    }
606
607    /// Enable Swagger UI documentation with Basic Auth protection
608    ///
609    /// When username and password are provided, the docs endpoint will require
610    /// Basic Authentication. This is useful for protecting API documentation
611    /// in production environments.
612    ///
613    /// # Example
614    ///
615    /// ```rust,ignore
616    /// RustApi::new()
617    ///     .route("/users", get(list_users))
618    ///     .docs_with_auth("/docs", "admin", "secret123")
619    ///     .run("127.0.0.1:8080")
620    ///     .await
621    /// ```
622    #[cfg(feature = "swagger-ui")]
623    pub fn docs_with_auth(self, path: &str, username: &str, password: &str) -> Self {
624        let title = self.openapi_spec.info.title.clone();
625        let version = self.openapi_spec.info.version.clone();
626        let description = self.openapi_spec.info.description.clone();
627
628        self.docs_with_auth_and_info(
629            path,
630            username,
631            password,
632            &title,
633            &version,
634            description.as_deref(),
635        )
636    }
637
638    /// Enable Swagger UI documentation with Basic Auth and custom API info
639    ///
640    /// # Example
641    ///
642    /// ```rust,ignore
643    /// RustApi::new()
644    ///     .docs_with_auth_and_info(
645    ///         "/docs",
646    ///         "admin",
647    ///         "secret",
648    ///         "My API",
649    ///         "2.0.0",
650    ///         Some("Protected API documentation")
651    ///     )
652    /// ```
653    #[cfg(feature = "swagger-ui")]
654    pub fn docs_with_auth_and_info(
655        mut self,
656        path: &str,
657        username: &str,
658        password: &str,
659        title: &str,
660        version: &str,
661        description: Option<&str>,
662    ) -> Self {
663        use crate::router::MethodRouter;
664        use base64::{engine::general_purpose::STANDARD, Engine};
665        use std::collections::HashMap;
666
667        // Update spec info
668        self.openapi_spec.info.title = title.to_string();
669        self.openapi_spec.info.version = version.to_string();
670        if let Some(desc) = description {
671            self.openapi_spec.info.description = Some(desc.to_string());
672        }
673
674        let path = path.trim_end_matches('/');
675        let openapi_path = format!("{}/openapi.json", path);
676
677        // Create expected auth header value
678        let credentials = format!("{}:{}", username, password);
679        let encoded = STANDARD.encode(credentials.as_bytes());
680        let expected_auth = format!("Basic {}", encoded);
681
682        // Clone values for closures
683        let spec_json =
684            serde_json::to_string_pretty(&self.openapi_spec.to_json()).unwrap_or_default();
685        let openapi_url = openapi_path.clone();
686        let expected_auth_spec = expected_auth.clone();
687        let expected_auth_docs = expected_auth;
688
689        // Create spec handler with auth check
690        let spec_handler: crate::handler::BoxedHandler =
691            std::sync::Arc::new(move |req: crate::Request| {
692                let json = spec_json.clone();
693                let expected = expected_auth_spec.clone();
694                Box::pin(async move {
695                    if !check_basic_auth(&req, &expected) {
696                        return unauthorized_response();
697                    }
698                    http::Response::builder()
699                        .status(http::StatusCode::OK)
700                        .header(http::header::CONTENT_TYPE, "application/json")
701                        .body(http_body_util::Full::new(bytes::Bytes::from(json)))
702                        .unwrap()
703                })
704                    as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
705            });
706
707        // Create docs handler with auth check
708        let docs_handler: crate::handler::BoxedHandler =
709            std::sync::Arc::new(move |req: crate::Request| {
710                let url = openapi_url.clone();
711                let expected = expected_auth_docs.clone();
712                Box::pin(async move {
713                    if !check_basic_auth(&req, &expected) {
714                        return unauthorized_response();
715                    }
716                    rustapi_openapi::swagger_ui_html(&url)
717                })
718                    as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
719            });
720
721        // Create method routers with boxed handlers
722        let mut spec_handlers = HashMap::new();
723        spec_handlers.insert(http::Method::GET, spec_handler);
724        let spec_router = MethodRouter::from_boxed(spec_handlers);
725
726        let mut docs_handlers = HashMap::new();
727        docs_handlers.insert(http::Method::GET, docs_handler);
728        let docs_router = MethodRouter::from_boxed(docs_handlers);
729
730        self.route(&openapi_path, spec_router)
731            .route(path, docs_router)
732    }
733
734    /// Run the server
735    ///
736    /// # Example
737    ///
738    /// ```rust,ignore
739    /// RustApi::new()
740    ///     .route("/", get(hello))
741    ///     .run("127.0.0.1:8080")
742    ///     .await
743    /// ```
744    pub async fn run(mut self, addr: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
745        // Apply body limit layer if configured (should be first in the chain)
746        if let Some(limit) = self.body_limit {
747            // Prepend body limit layer so it's the first to process requests
748            self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
749        }
750
751        let server = Server::new(self.router, self.layers);
752        server.run(addr).await
753    }
754
755    /// Get the inner router (for testing or advanced usage)
756    pub fn into_router(self) -> Router {
757        self.router
758    }
759
760    /// Get the layer stack (for testing)
761    pub fn layers(&self) -> &LayerStack {
762        &self.layers
763    }
764}
765
766fn add_path_params_to_operation(path: &str, op: &mut rustapi_openapi::Operation) {
767    let mut params: Vec<String> = Vec::new();
768    let mut in_brace = false;
769    let mut current = String::new();
770
771    for ch in path.chars() {
772        match ch {
773            '{' => {
774                in_brace = true;
775                current.clear();
776            }
777            '}' => {
778                if in_brace {
779                    in_brace = false;
780                    if !current.is_empty() {
781                        params.push(current.clone());
782                    }
783                }
784            }
785            _ => {
786                if in_brace {
787                    current.push(ch);
788                }
789            }
790        }
791    }
792
793    if params.is_empty() {
794        return;
795    }
796
797    let op_params = op.parameters.get_or_insert_with(Vec::new);
798
799    for name in params {
800        let already = op_params
801            .iter()
802            .any(|p| p.location == "path" && p.name == name);
803        if already {
804            continue;
805        }
806
807        op_params.push(rustapi_openapi::Parameter {
808            name,
809            location: "path".to_string(),
810            required: true,
811            description: None,
812            schema: rustapi_openapi::SchemaRef::Inline(serde_json::json!({ "type": "string" })),
813        });
814    }
815}
816
817impl Default for RustApi {
818    fn default() -> Self {
819        Self::new()
820    }
821}
822
823#[cfg(test)]
824mod tests {
825    use super::RustApi;
826    use crate::extract::{FromRequestParts, State};
827    use crate::request::Request;
828    use bytes::Bytes;
829    use http::Method;
830    use std::collections::HashMap;
831
832    #[test]
833    fn state_is_available_via_extractor() {
834        let app = RustApi::new().state(123u32);
835        let router = app.into_router();
836
837        let req = http::Request::builder()
838            .method(Method::GET)
839            .uri("/test")
840            .body(())
841            .unwrap();
842        let (parts, _) = req.into_parts();
843
844        let request = Request::new(parts, Bytes::new(), router.state_ref(), HashMap::new());
845        let State(value) = State::<u32>::from_request_parts(&request).unwrap();
846        assert_eq!(value, 123u32);
847    }
848}
849
850/// Check Basic Auth header against expected credentials
851#[cfg(feature = "swagger-ui")]
852fn check_basic_auth(req: &crate::Request, expected: &str) -> bool {
853    req.headers()
854        .get(http::header::AUTHORIZATION)
855        .and_then(|v| v.to_str().ok())
856        .map(|auth| auth == expected)
857        .unwrap_or(false)
858}
859
860/// Create 401 Unauthorized response with WWW-Authenticate header
861#[cfg(feature = "swagger-ui")]
862fn unauthorized_response() -> crate::Response {
863    http::Response::builder()
864        .status(http::StatusCode::UNAUTHORIZED)
865        .header(
866            http::header::WWW_AUTHENTICATE,
867            "Basic realm=\"API Documentation\"",
868        )
869        .header(http::header::CONTENT_TYPE, "text/plain")
870        .body(http_body_util::Full::new(bytes::Bytes::from(
871            "Unauthorized",
872        )))
873        .unwrap()
874}
875
876/// Configuration builder for RustAPI with auto-routes
877pub struct RustApiConfig {
878    docs_path: Option<String>,
879    docs_enabled: bool,
880    api_title: String,
881    api_version: String,
882    api_description: Option<String>,
883    body_limit: Option<usize>,
884    layers: LayerStack,
885}
886
887impl Default for RustApiConfig {
888    fn default() -> Self {
889        Self::new()
890    }
891}
892
893impl RustApiConfig {
894    pub fn new() -> Self {
895        Self {
896            docs_path: Some("/docs".to_string()),
897            docs_enabled: true,
898            api_title: "RustAPI".to_string(),
899            api_version: "1.0.0".to_string(),
900            api_description: None,
901            body_limit: None,
902            layers: LayerStack::new(),
903        }
904    }
905
906    /// Set the docs path (default: "/docs")
907    pub fn docs_path(mut self, path: impl Into<String>) -> Self {
908        self.docs_path = Some(path.into());
909        self
910    }
911
912    /// Enable or disable docs (default: true)
913    pub fn docs_enabled(mut self, enabled: bool) -> Self {
914        self.docs_enabled = enabled;
915        self
916    }
917
918    /// Set OpenAPI info
919    pub fn openapi_info(
920        mut self,
921        title: impl Into<String>,
922        version: impl Into<String>,
923        description: Option<impl Into<String>>,
924    ) -> Self {
925        self.api_title = title.into();
926        self.api_version = version.into();
927        self.api_description = description.map(|d| d.into());
928        self
929    }
930
931    /// Set body size limit
932    pub fn body_limit(mut self, limit: usize) -> Self {
933        self.body_limit = Some(limit);
934        self
935    }
936
937    /// Add a middleware layer
938    pub fn layer<L>(mut self, layer: L) -> Self
939    where
940        L: MiddlewareLayer,
941    {
942        self.layers.push(Box::new(layer));
943        self
944    }
945
946    /// Build the RustApi instance
947    pub fn build(self) -> RustApi {
948        let mut app = RustApi::new().mount_auto_routes_grouped();
949
950        // Apply configuration
951        if let Some(limit) = self.body_limit {
952            app = app.body_limit(limit);
953        }
954
955        app = app.openapi_info(
956            &self.api_title,
957            &self.api_version,
958            self.api_description.as_deref(),
959        );
960
961        #[cfg(feature = "swagger-ui")]
962        if self.docs_enabled {
963            if let Some(path) = self.docs_path {
964                app = app.docs(&path);
965            }
966        }
967
968        // Apply layers
969        // Note: layers are applied in reverse order in RustApi::layer logic (pushing to vec)
970        app.layers.extend(self.layers);
971
972        app
973    }
974
975    /// Build and run the server
976    pub async fn run(
977        self,
978        addr: impl AsRef<str>,
979    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
980        self.build().run(addr.as_ref()).await
981    }
982}