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 {
364                let html = rustapi_openapi::swagger_ui_html(&url);
365                html
366            }
367        };
368
369        self.route(&openapi_path, get(spec_handler))
370            .route(path, get(docs_handler))
371    }
372
373    /// Enable Swagger UI documentation with Basic Auth protection
374    ///
375    /// When username and password are provided, the docs endpoint will require
376    /// Basic Authentication. This is useful for protecting API documentation
377    /// in production environments.
378    ///
379    /// # Example
380    ///
381    /// ```rust,ignore
382    /// RustApi::new()
383    ///     .route("/users", get(list_users))
384    ///     .docs_with_auth("/docs", "admin", "secret123")
385    ///     .run("127.0.0.1:8080")
386    ///     .await
387    /// ```
388    #[cfg(feature = "swagger-ui")]
389    pub fn docs_with_auth(self, path: &str, username: &str, password: &str) -> Self {
390        let title = self.openapi_spec.info.title.clone();
391        let version = self.openapi_spec.info.version.clone();
392        let description = self.openapi_spec.info.description.clone();
393
394        self.docs_with_auth_and_info(
395            path,
396            username,
397            password,
398            &title,
399            &version,
400            description.as_deref(),
401        )
402    }
403
404    /// Enable Swagger UI documentation with Basic Auth and custom API info
405    ///
406    /// # Example
407    ///
408    /// ```rust,ignore
409    /// RustApi::new()
410    ///     .docs_with_auth_and_info(
411    ///         "/docs",
412    ///         "admin",
413    ///         "secret",
414    ///         "My API",
415    ///         "2.0.0",
416    ///         Some("Protected API documentation")
417    ///     )
418    /// ```
419    #[cfg(feature = "swagger-ui")]
420    pub fn docs_with_auth_and_info(
421        mut self,
422        path: &str,
423        username: &str,
424        password: &str,
425        title: &str,
426        version: &str,
427        description: Option<&str>,
428    ) -> Self {
429        use crate::router::MethodRouter;
430        use base64::{engine::general_purpose::STANDARD, Engine};
431        use std::collections::HashMap;
432
433        // Update spec info
434        self.openapi_spec.info.title = title.to_string();
435        self.openapi_spec.info.version = version.to_string();
436        if let Some(desc) = description {
437            self.openapi_spec.info.description = Some(desc.to_string());
438        }
439
440        let path = path.trim_end_matches('/');
441        let openapi_path = format!("{}/openapi.json", path);
442
443        // Create expected auth header value
444        let credentials = format!("{}:{}", username, password);
445        let encoded = STANDARD.encode(credentials.as_bytes());
446        let expected_auth = format!("Basic {}", encoded);
447
448        // Clone values for closures
449        let spec_json =
450            serde_json::to_string_pretty(&self.openapi_spec.to_json()).unwrap_or_default();
451        let openapi_url = openapi_path.clone();
452        let expected_auth_spec = expected_auth.clone();
453        let expected_auth_docs = expected_auth;
454
455        // Create spec handler with auth check
456        let spec_handler: crate::handler::BoxedHandler = std::sync::Arc::new(move |req: crate::Request| {
457            let json = spec_json.clone();
458            let expected = expected_auth_spec.clone();
459            Box::pin(async move {
460                if !check_basic_auth(&req, &expected) {
461                    return unauthorized_response();
462                }
463                http::Response::builder()
464                    .status(http::StatusCode::OK)
465                    .header(http::header::CONTENT_TYPE, "application/json")
466                    .body(http_body_util::Full::new(bytes::Bytes::from(json)))
467                    .unwrap()
468            }) as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
469        });
470
471        // Create docs handler with auth check
472        let docs_handler: crate::handler::BoxedHandler = 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            }) as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
481        });
482
483        // Create method routers with boxed handlers
484        let mut spec_handlers = HashMap::new();
485        spec_handlers.insert(http::Method::GET, spec_handler);
486        let spec_router = MethodRouter::from_boxed(spec_handlers);
487
488        let mut docs_handlers = HashMap::new();
489        docs_handlers.insert(http::Method::GET, docs_handler);
490        let docs_router = MethodRouter::from_boxed(docs_handlers);
491
492        self.route(&openapi_path, spec_router)
493            .route(path, docs_router)
494    }
495
496    /// Run the server
497    ///
498    /// # Example
499    ///
500    /// ```rust,ignore
501    /// RustApi::new()
502    ///     .route("/", get(hello))
503    ///     .run("127.0.0.1:8080")
504    ///     .await
505    /// ```
506    pub async fn run(mut self, addr: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
507        // Apply body limit layer if configured (should be first in the chain)
508        if let Some(limit) = self.body_limit {
509            // Prepend body limit layer so it's the first to process requests
510            self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
511        }
512        
513        let server = Server::new(self.router, self.layers);
514        server.run(addr).await
515    }
516
517    /// Get the inner router (for testing or advanced usage)
518    pub fn into_router(self) -> Router {
519        self.router
520    }
521
522    /// Get the layer stack (for testing)
523    pub fn layers(&self) -> &LayerStack {
524        &self.layers
525    }
526}
527
528impl Default for RustApi {
529    fn default() -> Self {
530        Self::new()
531    }
532}
533
534/// Check Basic Auth header against expected credentials
535#[cfg(feature = "swagger-ui")]
536fn check_basic_auth(req: &crate::Request, expected: &str) -> bool {
537    req.headers()
538        .get(http::header::AUTHORIZATION)
539        .and_then(|v| v.to_str().ok())
540        .map(|auth| auth == expected)
541        .unwrap_or(false)
542}
543
544/// Create 401 Unauthorized response with WWW-Authenticate header
545#[cfg(feature = "swagger-ui")]
546fn unauthorized_response() -> crate::Response {
547    http::Response::builder()
548        .status(http::StatusCode::UNAUTHORIZED)
549        .header(http::header::WWW_AUTHENTICATE, "Basic realm=\"API Documentation\"")
550        .header(http::header::CONTENT_TYPE, "text/plain")
551        .body(http_body_util::Full::new(bytes::Bytes::from("Unauthorized")))
552        .unwrap()
553}