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}