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}