rustapi_core/
app.rs

1//! RustApi application builder
2
3use crate::error::Result;
4use crate::router::{MethodRouter, Router};
5use crate::server::Server;
6use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
7
8/// Main application builder for RustAPI
9///
10/// # Example
11///
12/// ```rust,ignore
13/// use rustapi_rs::prelude::*;
14///
15/// #[tokio::main]
16/// async fn main() -> Result<()> {
17///     RustApi::new()
18///         .state(AppState::new())
19///         .route("/", get(hello))
20///         .route("/users/{id}", get(get_user))
21///         .run("127.0.0.1:8080")
22///         .await
23/// }
24/// ```
25pub struct RustApi {
26    router: Router,
27    openapi_spec: rustapi_openapi::OpenApiSpec,
28}
29
30impl RustApi {
31    /// Create a new RustAPI application
32    pub fn new() -> Self {
33        // Initialize tracing if not already done
34        let _ = tracing_subscriber::registry()
35            .with(
36                EnvFilter::try_from_default_env()
37                    .unwrap_or_else(|_| EnvFilter::new("info,rustapi=debug")),
38            )
39            .with(tracing_subscriber::fmt::layer())
40            .try_init();
41
42        Self {
43            router: Router::new(),
44            openapi_spec: rustapi_openapi::OpenApiSpec::new("RustAPI Application", "1.0.0")
45                .register::<rustapi_openapi::ErrorSchema>()
46                .register::<rustapi_openapi::ValidationErrorSchema>()
47                .register::<rustapi_openapi::FieldErrorSchema>(),
48        }
49    }
50
51    /// Add application state
52    ///
53    /// State is shared across all handlers and can be extracted using `State<T>`.
54    ///
55    /// # Example
56    ///
57    /// ```rust,ignore
58    /// #[derive(Clone)]
59    /// struct AppState {
60    ///     db: DbPool,
61    /// }
62    ///
63    /// RustApi::new()
64    ///     .state(AppState::new())
65    /// ```
66    pub fn state<S>(self, _state: S) -> Self
67    where
68        S: Clone + Send + Sync + 'static,
69    {
70        // For now, state is handled by the router/handlers directly capturing it
71        // or through a middleware. The current router (matchit) implementation
72        // doesn't support state injection directly in the same way axum does.
73        // This is a placeholder for future state management.
74        self
75    }
76
77    /// Register an OpenAPI schema
78    ///
79    /// # Example
80    ///
81    /// ```rust,ignore
82    /// #[derive(Schema)]
83    /// struct User { ... }
84    ///
85    /// RustApi::new()
86    ///     .register_schema::<User>()
87    /// ```
88    pub fn register_schema<T: for<'a> rustapi_openapi::Schema<'a>>(mut self) -> Self {
89        self.openapi_spec = self.openapi_spec.register::<T>();
90        self
91    }
92
93    /// Configure OpenAPI info (title, version, description)
94    pub fn openapi_info(mut self, title: &str, version: &str, description: Option<&str>) -> Self {
95        self.openapi_spec = rustapi_openapi::OpenApiSpec::new(title, version);
96        if let Some(desc) = description {
97            self.openapi_spec = self.openapi_spec.description(desc);
98        }
99        self
100    }
101
102    /// Add a route
103    ///
104    /// # Example
105    ///
106    /// ```rust,ignore
107    /// RustApi::new()
108    ///     .route("/", get(index))
109    ///     .route("/users", get(list_users).post(create_user))
110    ///     .route("/users/{id}", get(get_user).delete(delete_user))
111    /// ```
112    pub fn route(mut self, path: &str, method_router: MethodRouter) -> Self {
113        // Register operations in OpenAPI spec
114        for (method, op) in &method_router.operations {
115            self.openapi_spec = self.openapi_spec.path(path, method.as_str(), op.clone());
116        }
117
118        self.router = self.router.route(path, method_router);
119        self
120    }
121
122    /// Mount a handler (convenience method)
123    ///
124    /// Alias for `.route(path, method_router)` for a single handler.
125    #[deprecated(note = "Use route() directly or mount_route() for macro-based routing")]
126    pub fn mount(self, path: &str, method_router: MethodRouter) -> Self {
127        self.route(path, method_router)
128    }
129
130    /// Mount a route created with #[rustapi::get], #[rustapi::post], etc.
131    ///
132    /// # Example
133    ///
134    /// ```rust,ignore
135    /// use rustapi_rs::prelude::*;
136    ///
137    /// #[rustapi::get("/users")]
138    /// async fn list_users() -> Json<Vec<User>> {
139    ///     Json(vec![])
140    /// }
141    ///
142    /// RustApi::new()
143    ///     .mount_route(route!(list_users))
144    ///     .run("127.0.0.1:8080")
145    ///     .await
146    /// ```
147    pub fn mount_route(mut self, route: crate::handler::Route) -> Self {
148        let method_enum = match route.method {
149            "GET" => http::Method::GET,
150            "POST" => http::Method::POST,
151            "PUT" => http::Method::PUT,
152            "DELETE" => http::Method::DELETE,
153            "PATCH" => http::Method::PATCH,
154            _ => http::Method::GET,
155        };
156
157        // Register operation in OpenAPI spec
158        self.openapi_spec = self
159            .openapi_spec
160            .path(route.path, route.method, route.operation);
161
162        self.route_with_method(route.path, method_enum, route.handler)
163    }
164
165    /// Helper to mount a single method handler
166    fn route_with_method(
167        self,
168        path: &str,
169        method: http::Method,
170        handler: crate::handler::BoxedHandler,
171    ) -> Self {
172        use crate::router::MethodRouter;
173        // use http::Method; // Removed
174
175        // This is simplified. In a real implementation we'd merge with existing router at this path
176        // For now we assume one handler per path or we simply allow overwriting for this MVP step
177        // (matchit router doesn't allow easy merging/updating existing entries without rebuilding)
178        //
179        // TOOD: Enhance Router to support method merging
180
181        let path = if !path.starts_with('/') {
182            format!("/{}", path)
183        } else {
184            path.to_string()
185        };
186
187        // Check if we already have this path?
188        // For MVP, valid assumption: user calls .route() or .mount() once per path-method-combo
189        // But we need to handle multiple methods on same path.
190        // Our Router wrapper currently just inserts.
191
192        // Since we can't easily query matchit, we'll just insert.
193        // Limitations: strictly sequential mounting for now.
194
195        let mut handlers = std::collections::HashMap::new();
196        handlers.insert(method, handler);
197
198        let method_router = MethodRouter::from_boxed(handlers);
199        self.route(&path, method_router)
200    }
201
202    /// Nest a router under a prefix
203    ///
204    /// # Example
205    ///
206    /// ```rust,ignore
207    /// let api_v1 = Router::new()
208    ///     .route("/users", get(list_users));
209    ///
210    /// RustApi::new()
211    ///     .nest("/api/v1", api_v1)
212    /// ```
213    pub fn nest(mut self, prefix: &str, router: Router) -> Self {
214        self.router = self.router.nest(prefix, router);
215        self
216    }
217
218    /// Enable Swagger UI documentation
219    ///
220    /// This adds two endpoints:
221    /// - `{path}` - Swagger UI interface
222    /// - `{path}/openapi.json` - OpenAPI JSON specification
223    ///
224    /// # Example
225    ///
226    /// ```rust,ignore
227    /// RustApi::new()
228    ///     .route("/users", get(list_users))
229    ///     .docs("/docs")  // Swagger UI at /docs, spec at /docs/openapi.json
230    ///     .run("127.0.0.1:8080")
231    ///     .await
232    /// ```
233    pub fn docs(self, path: &str) -> Self {
234        let title = self.openapi_spec.info.title.clone();
235        let version = self.openapi_spec.info.version.clone();
236        let description = self.openapi_spec.info.description.clone();
237
238        self.docs_with_info(path, &title, &version, description.as_deref())
239    }
240
241    /// Enable Swagger UI documentation with custom API info
242    ///
243    /// # Example
244    ///
245    /// ```rust,ignore
246    /// RustApi::new()
247    ///     .docs_with_info("/docs", "My API", "2.0.0", Some("API for managing users"))
248    /// ```
249    pub fn docs_with_info(
250        mut self,
251        path: &str,
252        title: &str,
253        version: &str,
254        description: Option<&str>,
255    ) -> Self {
256        use crate::router::get;
257        // Update spec info
258        self.openapi_spec.info.title = title.to_string();
259        self.openapi_spec.info.version = version.to_string();
260        if let Some(desc) = description {
261            self.openapi_spec.info.description = Some(desc.to_string());
262        }
263
264        let path = path.trim_end_matches('/');
265        let openapi_path = format!("{}/openapi.json", path);
266
267        // Clone values for closures
268        let spec_json =
269            serde_json::to_string_pretty(&self.openapi_spec.to_json()).unwrap_or_default();
270        let openapi_url = openapi_path.clone();
271
272        // Add OpenAPI JSON endpoint
273        let spec_handler = move || {
274            let json = spec_json.clone();
275            async move {
276                http::Response::builder()
277                    .status(http::StatusCode::OK)
278                    .header(http::header::CONTENT_TYPE, "application/json")
279                    .body(http_body_util::Full::new(bytes::Bytes::from(json)))
280                    .unwrap()
281            }
282        };
283
284        // Add Swagger UI endpoint
285        let docs_handler = move || {
286            let url = openapi_url.clone();
287            async move {
288                let html = rustapi_openapi::swagger_ui_html(&url);
289                html
290            }
291        };
292
293        self.route(&openapi_path, get(spec_handler))
294            .route(path, get(docs_handler))
295    }
296
297    /// Run the server
298    ///
299    /// # Example
300    ///
301    /// ```rust,ignore
302    /// RustApi::new()
303    ///     .route("/", get(hello))
304    ///     .run("127.0.0.1:8080")
305    ///     .await
306    /// ```
307    pub async fn run(self, addr: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
308        let server = Server::new(self.router);
309        server.run(addr).await
310    }
311
312    /// Get the inner router (for testing or advanced usage)
313    pub fn into_router(self) -> Router {
314        self.router
315    }
316}
317
318impl Default for RustApi {
319    fn default() -> Self {
320        Self::new()
321    }
322}