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 #[cfg(feature = "swagger-ui")]
234 pub fn docs(self, path: &str) -> Self {
235 let title = self.openapi_spec.info.title.clone();
236 let version = self.openapi_spec.info.version.clone();
237 let description = self.openapi_spec.info.description.clone();
238
239 self.docs_with_info(path, &title, &version, description.as_deref())
240 }
241
242 /// Enable Swagger UI documentation with custom API info
243 ///
244 /// # Example
245 ///
246 /// ```rust,ignore
247 /// RustApi::new()
248 /// .docs_with_info("/docs", "My API", "2.0.0", Some("API for managing users"))
249 /// ```
250 #[cfg(feature = "swagger-ui")]
251 pub fn docs_with_info(
252 mut self,
253 path: &str,
254 title: &str,
255 version: &str,
256 description: Option<&str>,
257 ) -> Self {
258 use crate::router::get;
259 // Update spec info
260 self.openapi_spec.info.title = title.to_string();
261 self.openapi_spec.info.version = version.to_string();
262 if let Some(desc) = description {
263 self.openapi_spec.info.description = Some(desc.to_string());
264 }
265
266 let path = path.trim_end_matches('/');
267 let openapi_path = format!("{}/openapi.json", path);
268
269 // Clone values for closures
270 let spec_json =
271 serde_json::to_string_pretty(&self.openapi_spec.to_json()).unwrap_or_default();
272 let openapi_url = openapi_path.clone();
273
274 // Add OpenAPI JSON endpoint
275 let spec_handler = move || {
276 let json = spec_json.clone();
277 async move {
278 http::Response::builder()
279 .status(http::StatusCode::OK)
280 .header(http::header::CONTENT_TYPE, "application/json")
281 .body(http_body_util::Full::new(bytes::Bytes::from(json)))
282 .unwrap()
283 }
284 };
285
286 // Add Swagger UI endpoint
287 let docs_handler = move || {
288 let url = openapi_url.clone();
289 async move {
290 let html = rustapi_openapi::swagger_ui_html(&url);
291 html
292 }
293 };
294
295 self.route(&openapi_path, get(spec_handler))
296 .route(path, get(docs_handler))
297 }
298
299 /// Run the server
300 ///
301 /// # Example
302 ///
303 /// ```rust,ignore
304 /// RustApi::new()
305 /// .route("/", get(hello))
306 /// .run("127.0.0.1:8080")
307 /// .await
308 /// ```
309 pub async fn run(self, addr: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
310 let server = Server::new(self.router);
311 server.run(addr).await
312 }
313
314 /// Get the inner router (for testing or advanced usage)
315 pub fn into_router(self) -> Router {
316 self.router
317 }
318}
319
320impl Default for RustApi {
321 fn default() -> Self {
322 Self::new()
323 }
324}