rovo/
lib.rs

1pub use rovo_macros::rovo;
2
3// Re-export aide for convenience
4pub use aide;
5
6use ::axum::Extension;
7use aide::axum::ApiRouter as AideApiRouter;
8
9/// A drop-in replacement for axum::Router that adds OpenAPI documentation support.
10///
11/// This Router works seamlessly with handlers decorated with `#[rovo]` and provides
12/// a fluent API for building documented APIs with Swagger UI integration.
13///
14/// # Example
15/// ```ignore
16/// use rovo::{Router, rovo, routing::get};
17/// use aide::openapi::OpenApi;
18///
19/// #[rovo]
20/// async fn documented_handler() -> impl IntoApiResponse { /* ... */ }
21///
22/// let mut api = OpenApi::default();
23/// api.info.title = "My API".to_string();
24///
25/// let app = Router::new()
26///     .route("/documented", get(documented_handler))
27///     .with_swagger("/", "/api.json")
28///     .with_api_json("/api.json", serve_api)
29///     .with_state(state)
30///     .finish_api_with_extension(api);
31/// ```
32pub struct Router<S = ()> {
33    inner: AideApiRouter<S>,
34}
35
36impl<S> Router<S>
37where
38    S: Clone + Send + Sync + 'static,
39{
40    /// Create a new Router
41    pub fn new() -> Self {
42        Self {
43            inner: AideApiRouter::new(),
44        }
45    }
46
47    /// Add a route to the router
48    pub fn route<M>(mut self, path: &str, method_router: M) -> Self
49    where
50        M: Into<aide::axum::routing::ApiMethodRouter<S>>,
51    {
52        self.inner = self.inner.api_route(path, method_router.into());
53        self
54    }
55
56    /// Nest another router at the given path
57    pub fn nest(mut self, path: &str, router: Router<S>) -> Self {
58        self.inner = self.inner.nest(path, router.inner);
59        self
60    }
61
62    /// Add Swagger UI route at the specified path
63    #[cfg(feature = "swagger")]
64    pub fn with_swagger(mut self, swagger_path: &str, api_json_path: &str) -> Self
65    where
66        S: Clone + Send + Sync + 'static,
67    {
68        self.inner = self.inner.route(
69            swagger_path,
70            aide::swagger::Swagger::new(api_json_path).axum_route(),
71        );
72        self
73    }
74
75    /// Add Redoc UI route at the specified path
76    #[cfg(feature = "redoc")]
77    pub fn with_redoc(mut self, redoc_path: &str, api_json_path: &str) -> Self
78    where
79        S: Clone + Send + Sync + 'static,
80    {
81        self.inner = self.inner.route(
82            redoc_path,
83            aide::redoc::Redoc::new(api_json_path).axum_route(),
84        );
85        self
86    }
87
88    /// Add Scalar UI route at the specified path
89    #[cfg(feature = "scalar")]
90    pub fn with_scalar(mut self, scalar_path: &str, api_json_path: &str) -> Self
91    where
92        S: Clone + Send + Sync + 'static,
93    {
94        self.inner = self.inner.route(
95            scalar_path,
96            aide::scalar::Scalar::new(api_json_path).axum_route(),
97        );
98        self
99    }
100
101    /// Add the OpenAPI JSON endpoint
102    pub fn with_api_json<H, T>(mut self, path: &str, handler: H) -> Self
103    where
104        H: ::axum::handler::Handler<T, S>,
105        S: Clone + Send + Sync + 'static,
106        T: 'static,
107    {
108        self.inner = self.inner.route(path, ::axum::routing::get(handler));
109        self
110    }
111
112    /// Add state to the router
113    pub fn with_state<S2>(self, state: S) -> Router<S2>
114    where
115        S2: Clone + Send + Sync + 'static,
116    {
117        Router {
118            inner: self.inner.with_state(state),
119        }
120    }
121
122    /// Finish building the API and return an axum Router for further configuration
123    pub fn finish_api(self, api: &mut aide::openapi::OpenApi) -> ::axum::Router<S> {
124        self.inner.finish_api(api)
125    }
126
127    /// Finish the API with OpenAPI spec embedded via Extension layer
128    pub fn finish_api_with_extension(self, api: aide::openapi::OpenApi) -> ::axum::Router<S>
129    where
130        S: Clone + Send + Sync + 'static,
131    {
132        let mut api_mut = api;
133        self.inner
134            .finish_api(&mut api_mut)
135            .layer(Extension(api_mut))
136    }
137
138    /// Convert into the underlying aide ApiRouter
139    pub fn into_inner(self) -> AideApiRouter<S> {
140        self.inner
141    }
142}
143
144impl<S> Default for Router<S>
145where
146    S: Clone + Send + Sync + 'static,
147{
148    fn default() -> Self {
149        Self::new()
150    }
151}
152
153/// Trait for handlers that carry their own documentation.
154///
155/// This trait is automatically implemented by the `#[rovo]` macro for decorated handlers.
156/// It provides methods to convert the handler into documented route handlers for each HTTP method.
157///
158/// You typically won't implement this trait manually - instead, use the `#[rovo]` macro
159/// on your handler functions.
160pub trait IntoApiMethodRouter<S = ()> {
161    /// Convert into a GET route with documentation
162    fn into_get_route(self) -> aide::axum::routing::ApiMethodRouter<S>;
163    /// Convert into a POST route with documentation
164    fn into_post_route(self) -> aide::axum::routing::ApiMethodRouter<S>;
165    /// Convert into a PATCH route with documentation
166    fn into_patch_route(self) -> aide::axum::routing::ApiMethodRouter<S>;
167    /// Convert into a DELETE route with documentation
168    fn into_delete_route(self) -> aide::axum::routing::ApiMethodRouter<S>;
169    /// Convert into a PUT route with documentation
170    fn into_put_route(self) -> aide::axum::routing::ApiMethodRouter<S>;
171}
172
173/// Wrapper around `ApiMethodRouter` that provides method chaining for documented handlers.
174///
175/// This type is returned by routing functions like `get()`, `post()`, etc. and allows
176/// chaining methods with the exact same names as axum (`.post()`, `.patch()`, etc.) while
177/// accepting documented handlers decorated with `#[rovo]`.
178///
179/// # Example
180/// ```ignore
181/// use rovo::routing::get;
182///
183/// Router::new()
184///     .route("/items", get(list_items).post(create_item))
185///     .route("/items/{id}", get(get_item).patch(update_item).delete(delete_item))
186/// ```
187pub struct ApiMethodRouter<S = ()> {
188    inner: aide::axum::routing::ApiMethodRouter<S>,
189}
190
191impl<S> ApiMethodRouter<S>
192where
193    S: Clone + Send + Sync + 'static,
194{
195    /// Create a new ApiMethodRouter from aide's ApiMethodRouter
196    pub fn new(inner: aide::axum::routing::ApiMethodRouter<S>) -> Self {
197        Self { inner }
198    }
199
200    /// Chain a POST handler
201    pub fn post<H>(self, handler: H) -> Self
202    where
203        H: IntoApiMethodRouter<S>,
204    {
205        Self {
206            inner: self.inner.merge(handler.into_post_route()),
207        }
208    }
209
210    /// Chain a GET handler
211    pub fn get<H>(self, handler: H) -> Self
212    where
213        H: IntoApiMethodRouter<S>,
214    {
215        Self {
216            inner: self.inner.merge(handler.into_get_route()),
217        }
218    }
219
220    /// Chain a PATCH handler
221    pub fn patch<H>(self, handler: H) -> Self
222    where
223        H: IntoApiMethodRouter<S>,
224    {
225        Self {
226            inner: self.inner.merge(handler.into_patch_route()),
227        }
228    }
229
230    /// Chain a DELETE handler
231    pub fn delete<H>(self, handler: H) -> Self
232    where
233        H: IntoApiMethodRouter<S>,
234    {
235        Self {
236            inner: self.inner.merge(handler.into_delete_route()),
237        }
238    }
239
240    /// Chain a PUT handler
241    pub fn put<H>(self, handler: H) -> Self
242    where
243        H: IntoApiMethodRouter<S>,
244    {
245        Self {
246            inner: self.inner.merge(handler.into_put_route()),
247        }
248    }
249}
250
251impl<S> From<ApiMethodRouter<S>> for aide::axum::routing::ApiMethodRouter<S> {
252    fn from(router: ApiMethodRouter<S>) -> Self {
253        router.inner
254    }
255}
256
257/// Drop-in replacement routing functions that work with `#[rovo]` decorated handlers.
258///
259/// These functions provide the same API as axum's routing functions but accept
260/// handlers decorated with `#[rovo]` and automatically include their documentation.
261///
262/// # Example
263/// ```ignore
264/// use rovo::routing::{get, post};
265///
266/// Router::new()
267///     .route("/items", get(list_items).post(create_item))
268///     .route("/items/{id}", get(get_item).patch(update_item).delete(delete_item))
269/// ```
270pub mod routing {
271    use super::*;
272
273    /// Create a GET route with documentation from a `#[rovo]` decorated handler.
274    pub fn get<S, H>(handler: H) -> ApiMethodRouter<S>
275    where
276        H: IntoApiMethodRouter<S>,
277        S: Clone + Send + Sync + 'static,
278    {
279        ApiMethodRouter::new(handler.into_get_route())
280    }
281
282    /// Create a POST route with documentation from a `#[rovo]` decorated handler.
283    pub fn post<S, H>(handler: H) -> ApiMethodRouter<S>
284    where
285        H: IntoApiMethodRouter<S>,
286        S: Clone + Send + Sync + 'static,
287    {
288        ApiMethodRouter::new(handler.into_post_route())
289    }
290
291    /// Create a PATCH route with documentation from a `#[rovo]` decorated handler.
292    pub fn patch<S, H>(handler: H) -> ApiMethodRouter<S>
293    where
294        H: IntoApiMethodRouter<S>,
295        S: Clone + Send + Sync + 'static,
296    {
297        ApiMethodRouter::new(handler.into_patch_route())
298    }
299
300    /// Create a DELETE route with documentation from a `#[rovo]` decorated handler.
301    pub fn delete<S, H>(handler: H) -> ApiMethodRouter<S>
302    where
303        H: IntoApiMethodRouter<S>,
304        S: Clone + Send + Sync + 'static,
305    {
306        ApiMethodRouter::new(handler.into_delete_route())
307    }
308
309    /// Create a PUT route with documentation from a `#[rovo]` decorated handler.
310    pub fn put<S, H>(handler: H) -> ApiMethodRouter<S>
311    where
312        H: IntoApiMethodRouter<S>,
313        S: Clone + Send + Sync + 'static,
314    {
315        ApiMethodRouter::new(handler.into_put_route())
316    }
317}
318
319pub mod axum {
320    pub use aide::axum::{ApiRouter, IntoApiResponse};
321}