rustapi_core/
app.rs

1//! RustApi application builder
2
3use crate::error::Result;
4use crate::middleware::{BodyLimitLayer, LayerStack, MiddlewareLayer, DEFAULT_BODY_LIMIT};
5use crate::router::{MethodRouter, Router};
6use crate::server::Server;
7use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
8
9/// Main application builder for RustAPI
10///
11/// # Example
12///
13/// ```rust,ignore
14/// use rustapi_rs::prelude::*;
15///
16/// #[tokio::main]
17/// async fn main() -> Result<()> {
18///     RustApi::new()
19///         .state(AppState::new())
20///         .route("/", get(hello))
21///         .route("/users/{id}", get(get_user))
22///         .run("127.0.0.1:8080")
23///         .await
24/// }
25/// ```
26pub struct RustApi {
27    router: Router,
28    openapi_spec: rustapi_openapi::OpenApiSpec,
29    layers: LayerStack,
30    body_limit: Option<usize>,
31}
32
33impl RustApi {
34    /// Create a new RustAPI application
35    pub fn new() -> Self {
36        // Initialize tracing if not already done
37        let _ = tracing_subscriber::registry()
38            .with(
39                EnvFilter::try_from_default_env()
40                    .unwrap_or_else(|_| EnvFilter::new("info,rustapi=debug")),
41            )
42            .with(tracing_subscriber::fmt::layer())
43            .try_init();
44
45        Self {
46            router: Router::new(),
47            openapi_spec: rustapi_openapi::OpenApiSpec::new("RustAPI Application", "1.0.0")
48                .register::<rustapi_openapi::ErrorSchema>()
49                .register::<rustapi_openapi::ValidationErrorSchema>()
50                .register::<rustapi_openapi::FieldErrorSchema>(),
51            layers: LayerStack::new(),
52            body_limit: Some(DEFAULT_BODY_LIMIT), // Default 1MB limit
53        }
54    }
55
56    /// Set the global body size limit for request bodies
57    ///
58    /// This protects against denial-of-service attacks via large payloads.
59    /// The default limit is 1MB (1024 * 1024 bytes).
60    ///
61    /// # Arguments
62    ///
63    /// * `limit` - Maximum body size in bytes
64    ///
65    /// # Example
66    ///
67    /// ```rust,ignore
68    /// use rustapi_rs::prelude::*;
69    ///
70    /// RustApi::new()
71    ///     .body_limit(5 * 1024 * 1024)  // 5MB limit
72    ///     .route("/upload", post(upload_handler))
73    ///     .run("127.0.0.1:8080")
74    ///     .await
75    /// ```
76    pub fn body_limit(mut self, limit: usize) -> Self {
77        self.body_limit = Some(limit);
78        self
79    }
80
81    /// Disable the body size limit
82    ///
83    /// Warning: This removes protection against large payload attacks.
84    /// Only use this if you have other mechanisms to limit request sizes.
85    ///
86    /// # Example
87    ///
88    /// ```rust,ignore
89    /// RustApi::new()
90    ///     .no_body_limit()  // Disable body size limit
91    ///     .route("/upload", post(upload_handler))
92    /// ```
93    pub fn no_body_limit(mut self) -> Self {
94        self.body_limit = None;
95        self
96    }
97
98    /// Add a middleware layer to the application
99    ///
100    /// Layers are executed in the order they are added (outermost first).
101    /// The first layer added will be the first to process the request and
102    /// the last to process the response.
103    ///
104    /// # Example
105    ///
106    /// ```rust,ignore
107    /// use rustapi_rs::prelude::*;
108    /// use rustapi_core::middleware::{RequestIdLayer, TracingLayer};
109    ///
110    /// RustApi::new()
111    ///     .layer(RequestIdLayer::new())  // First to process request
112    ///     .layer(TracingLayer::new())    // Second to process request
113    ///     .route("/", get(handler))
114    ///     .run("127.0.0.1:8080")
115    ///     .await
116    /// ```
117    pub fn layer<L>(mut self, layer: L) -> Self
118    where
119        L: MiddlewareLayer,
120    {
121        self.layers.push(Box::new(layer));
122        self
123    }
124
125    /// Add application state
126    ///
127    /// State is shared across all handlers and can be extracted using `State<T>`.
128    ///
129    /// # Example
130    ///
131    /// ```rust,ignore
132    /// #[derive(Clone)]
133    /// struct AppState {
134    ///     db: DbPool,
135    /// }
136    ///
137    /// RustApi::new()
138    ///     .state(AppState::new())
139    /// ```
140    pub fn state<S>(self, _state: S) -> Self
141    where
142        S: Clone + Send + Sync + 'static,
143    {
144        // For now, state is handled by the router/handlers directly capturing it
145        // or through a middleware. The current router (matchit) implementation
146        // doesn't support state injection directly in the same way axum does.
147        // This is a placeholder for future state management.
148        self
149    }
150
151    /// Register an OpenAPI schema
152    ///
153    /// # Example
154    ///
155    /// ```rust,ignore
156    /// #[derive(Schema)]
157    /// struct User { ... }
158    ///
159    /// RustApi::new()
160    ///     .register_schema::<User>()
161    /// ```
162    pub fn register_schema<T: for<'a> rustapi_openapi::Schema<'a>>(mut self) -> Self {
163        self.openapi_spec = self.openapi_spec.register::<T>();
164        self
165    }
166
167    /// Configure OpenAPI info (title, version, description)
168    pub fn openapi_info(mut self, title: &str, version: &str, description: Option<&str>) -> Self {
169        self.openapi_spec = rustapi_openapi::OpenApiSpec::new(title, version);
170        if let Some(desc) = description {
171            self.openapi_spec = self.openapi_spec.description(desc);
172        }
173        self
174    }
175
176    /// Add a route
177    ///
178    /// # Example
179    ///
180    /// ```rust,ignore
181    /// RustApi::new()
182    ///     .route("/", get(index))
183    ///     .route("/users", get(list_users).post(create_user))
184    ///     .route("/users/{id}", get(get_user).delete(delete_user))
185    /// ```
186    pub fn route(mut self, path: &str, method_router: MethodRouter) -> Self {
187        // Register operations in OpenAPI spec
188        for (method, op) in &method_router.operations {
189            self.openapi_spec = self.openapi_spec.path(path, method.as_str(), op.clone());
190        }
191
192        self.router = self.router.route(path, method_router);
193        self
194    }
195
196    /// Mount a handler (convenience method)
197    ///
198    /// Alias for `.route(path, method_router)` for a single handler.
199    #[deprecated(note = "Use route() directly or mount_route() for macro-based routing")]
200    pub fn mount(self, path: &str, method_router: MethodRouter) -> Self {
201        self.route(path, method_router)
202    }
203
204    /// Mount a route created with `#[rustapi::get]`, `#[rustapi::post]`, etc.
205    ///
206    /// # Example
207    ///
208    /// ```rust,ignore
209    /// use rustapi_rs::prelude::*;
210    ///
211    /// #[rustapi::get("/users")]
212    /// async fn list_users() -> Json<Vec<User>> {
213    ///     Json(vec![])
214    /// }
215    ///
216    /// RustApi::new()
217    ///     .mount_route(route!(list_users))
218    ///     .run("127.0.0.1:8080")
219    ///     .await
220    /// ```
221    pub fn mount_route(mut self, route: crate::handler::Route) -> Self {
222        let method_enum = match route.method {
223            "GET" => http::Method::GET,
224            "POST" => http::Method::POST,
225            "PUT" => http::Method::PUT,
226            "DELETE" => http::Method::DELETE,
227            "PATCH" => http::Method::PATCH,
228            _ => http::Method::GET,
229        };
230
231        // Register operation in OpenAPI spec
232        self.openapi_spec = self
233            .openapi_spec
234            .path(route.path, route.method, route.operation);
235
236        self.route_with_method(route.path, method_enum, route.handler)
237    }
238
239    /// Helper to mount a single method handler
240    fn route_with_method(
241        self,
242        path: &str,
243        method: http::Method,
244        handler: crate::handler::BoxedHandler,
245    ) -> Self {
246        use crate::router::MethodRouter;
247        // use http::Method; // Removed
248
249        // This is simplified. In a real implementation we'd merge with existing router at this path
250        // For now we assume one handler per path or we simply allow overwriting for this MVP step
251        // (matchit router doesn't allow easy merging/updating existing entries without rebuilding)
252        //
253        // TOOD: Enhance Router to support method merging
254
255        let path = if !path.starts_with('/') {
256            format!("/{}", path)
257        } else {
258            path.to_string()
259        };
260
261        // Check if we already have this path?
262        // For MVP, valid assumption: user calls .route() or .mount() once per path-method-combo
263        // But we need to handle multiple methods on same path.
264        // Our Router wrapper currently just inserts.
265
266        // Since we can't easily query matchit, we'll just insert.
267        // Limitations: strictly sequential mounting for now.
268
269        let mut handlers = std::collections::HashMap::new();
270        handlers.insert(method, handler);
271
272        let method_router = MethodRouter::from_boxed(handlers);
273        self.route(&path, method_router)
274    }
275
276    /// Nest a router under a prefix
277    ///
278    /// # Example
279    ///
280    /// ```rust,ignore
281    /// let api_v1 = Router::new()
282    ///     .route("/users", get(list_users));
283    ///
284    /// RustApi::new()
285    ///     .nest("/api/v1", api_v1)
286    /// ```
287    pub fn nest(mut self, prefix: &str, router: Router) -> Self {
288        self.router = self.router.nest(prefix, router);
289        self
290    }
291
292    /// Enable Swagger UI documentation
293    ///
294    /// This adds two endpoints:
295    /// - `{path}` - Swagger UI interface
296    /// - `{path}/openapi.json` - OpenAPI JSON specification
297    ///
298    /// # Example
299    ///
300    /// ```text
301    /// RustApi::new()
302    ///     .route("/users", get(list_users))
303    ///     .docs("/docs")  // Swagger UI at /docs, spec at /docs/openapi.json
304    ///     .run("127.0.0.1:8080")
305    ///     .await
306    /// ```
307    #[cfg(feature = "swagger-ui")]
308    pub fn docs(self, path: &str) -> Self {
309        let title = self.openapi_spec.info.title.clone();
310        let version = self.openapi_spec.info.version.clone();
311        let description = self.openapi_spec.info.description.clone();
312
313        self.docs_with_info(path, &title, &version, description.as_deref())
314    }
315
316    /// Enable Swagger UI documentation with custom API info
317    ///
318    /// # Example
319    ///
320    /// ```rust,ignore
321    /// RustApi::new()
322    ///     .docs_with_info("/docs", "My API", "2.0.0", Some("API for managing users"))
323    /// ```
324    #[cfg(feature = "swagger-ui")]
325    pub fn docs_with_info(
326        mut self,
327        path: &str,
328        title: &str,
329        version: &str,
330        description: Option<&str>,
331    ) -> Self {
332        use crate::router::get;
333        // Update spec info
334        self.openapi_spec.info.title = title.to_string();
335        self.openapi_spec.info.version = version.to_string();
336        if let Some(desc) = description {
337            self.openapi_spec.info.description = Some(desc.to_string());
338        }
339
340        let path = path.trim_end_matches('/');
341        let openapi_path = format!("{}/openapi.json", path);
342
343        // Clone values for closures
344        let spec_json =
345            serde_json::to_string_pretty(&self.openapi_spec.to_json()).unwrap_or_default();
346        let openapi_url = openapi_path.clone();
347
348        // Add OpenAPI JSON endpoint
349        let spec_handler = move || {
350            let json = spec_json.clone();
351            async move {
352                http::Response::builder()
353                    .status(http::StatusCode::OK)
354                    .header(http::header::CONTENT_TYPE, "application/json")
355                    .body(http_body_util::Full::new(bytes::Bytes::from(json)))
356                    .unwrap()
357            }
358        };
359
360        // Add Swagger UI endpoint
361        let docs_handler = move || {
362            let url = openapi_url.clone();
363            async move { rustapi_openapi::swagger_ui_html(&url) }
364        };
365
366        self.route(&openapi_path, get(spec_handler))
367            .route(path, get(docs_handler))
368    }
369
370    /// Enable Swagger UI documentation with Basic Auth protection
371    ///
372    /// When username and password are provided, the docs endpoint will require
373    /// Basic Authentication. This is useful for protecting API documentation
374    /// in production environments.
375    ///
376    /// # Example
377    ///
378    /// ```rust,ignore
379    /// RustApi::new()
380    ///     .route("/users", get(list_users))
381    ///     .docs_with_auth("/docs", "admin", "secret123")
382    ///     .run("127.0.0.1:8080")
383    ///     .await
384    /// ```
385    #[cfg(feature = "swagger-ui")]
386    pub fn docs_with_auth(self, path: &str, username: &str, password: &str) -> Self {
387        let title = self.openapi_spec.info.title.clone();
388        let version = self.openapi_spec.info.version.clone();
389        let description = self.openapi_spec.info.description.clone();
390
391        self.docs_with_auth_and_info(
392            path,
393            username,
394            password,
395            &title,
396            &version,
397            description.as_deref(),
398        )
399    }
400
401    /// Enable Swagger UI documentation with Basic Auth and custom API info
402    ///
403    /// # Example
404    ///
405    /// ```rust,ignore
406    /// RustApi::new()
407    ///     .docs_with_auth_and_info(
408    ///         "/docs",
409    ///         "admin",
410    ///         "secret",
411    ///         "My API",
412    ///         "2.0.0",
413    ///         Some("Protected API documentation")
414    ///     )
415    /// ```
416    #[cfg(feature = "swagger-ui")]
417    pub fn docs_with_auth_and_info(
418        mut self,
419        path: &str,
420        username: &str,
421        password: &str,
422        title: &str,
423        version: &str,
424        description: Option<&str>,
425    ) -> Self {
426        use crate::router::MethodRouter;
427        use base64::{engine::general_purpose::STANDARD, Engine};
428        use std::collections::HashMap;
429
430        // Update spec info
431        self.openapi_spec.info.title = title.to_string();
432        self.openapi_spec.info.version = version.to_string();
433        if let Some(desc) = description {
434            self.openapi_spec.info.description = Some(desc.to_string());
435        }
436
437        let path = path.trim_end_matches('/');
438        let openapi_path = format!("{}/openapi.json", path);
439
440        // Create expected auth header value
441        let credentials = format!("{}:{}", username, password);
442        let encoded = STANDARD.encode(credentials.as_bytes());
443        let expected_auth = format!("Basic {}", encoded);
444
445        // Clone values for closures
446        let spec_json =
447            serde_json::to_string_pretty(&self.openapi_spec.to_json()).unwrap_or_default();
448        let openapi_url = openapi_path.clone();
449        let expected_auth_spec = expected_auth.clone();
450        let expected_auth_docs = expected_auth;
451
452        // Create spec handler with auth check
453        let spec_handler: crate::handler::BoxedHandler =
454            std::sync::Arc::new(move |req: crate::Request| {
455                let json = spec_json.clone();
456                let expected = expected_auth_spec.clone();
457                Box::pin(async move {
458                    if !check_basic_auth(&req, &expected) {
459                        return unauthorized_response();
460                    }
461                    http::Response::builder()
462                        .status(http::StatusCode::OK)
463                        .header(http::header::CONTENT_TYPE, "application/json")
464                        .body(http_body_util::Full::new(bytes::Bytes::from(json)))
465                        .unwrap()
466                })
467                    as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
468            });
469
470        // Create docs handler with auth check
471        let docs_handler: crate::handler::BoxedHandler =
472            std::sync::Arc::new(move |req: crate::Request| {
473                let url = openapi_url.clone();
474                let expected = expected_auth_docs.clone();
475                Box::pin(async move {
476                    if !check_basic_auth(&req, &expected) {
477                        return unauthorized_response();
478                    }
479                    rustapi_openapi::swagger_ui_html(&url)
480                })
481                    as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
482            });
483
484        // Create method routers with boxed handlers
485        let mut spec_handlers = HashMap::new();
486        spec_handlers.insert(http::Method::GET, spec_handler);
487        let spec_router = MethodRouter::from_boxed(spec_handlers);
488
489        let mut docs_handlers = HashMap::new();
490        docs_handlers.insert(http::Method::GET, docs_handler);
491        let docs_router = MethodRouter::from_boxed(docs_handlers);
492
493        self.route(&openapi_path, spec_router)
494            .route(path, docs_router)
495    }
496
497    /// Run the server
498    ///
499    /// # Example
500    ///
501    /// ```rust,ignore
502    /// RustApi::new()
503    ///     .route("/", get(hello))
504    ///     .run("127.0.0.1:8080")
505    ///     .await
506    /// ```
507    pub async fn run(mut self, addr: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
508        // Apply body limit layer if configured (should be first in the chain)
509        if let Some(limit) = self.body_limit {
510            // Prepend body limit layer so it's the first to process requests
511            self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
512        }
513
514        let server = Server::new(self.router, self.layers);
515        server.run(addr).await
516    }
517
518    /// Get the inner router (for testing or advanced usage)
519    pub fn into_router(self) -> Router {
520        self.router
521    }
522
523    /// Get the layer stack (for testing)
524    pub fn layers(&self) -> &LayerStack {
525        &self.layers
526    }
527}
528
529impl Default for RustApi {
530    fn default() -> Self {
531        Self::new()
532    }
533}
534
535/// Check Basic Auth header against expected credentials
536#[cfg(feature = "swagger-ui")]
537fn check_basic_auth(req: &crate::Request, expected: &str) -> bool {
538    req.headers()
539        .get(http::header::AUTHORIZATION)
540        .and_then(|v| v.to_str().ok())
541        .map(|auth| auth == expected)
542        .unwrap_or(false)
543}
544
545/// Create 401 Unauthorized response with WWW-Authenticate header
546#[cfg(feature = "swagger-ui")]
547fn unauthorized_response() -> crate::Response {
548    http::Response::builder()
549        .status(http::StatusCode::UNAUTHORIZED)
550        .header(
551            http::header::WWW_AUTHENTICATE,
552            "Basic realm=\"API Documentation\"",
553        )
554        .header(http::header::CONTENT_TYPE, "text/plain")
555        .body(http_body_util::Full::new(bytes::Bytes::from(
556            "Unauthorized",
557        )))
558        .unwrap()
559}