kit_rs/routing/macros.rs
1//! Route definition macros and helpers for Laravel-like routing syntax
2//!
3//! This module provides a clean, declarative way to define routes:
4//!
5//! ```rust,ignore
6//! use kit::{routes, get, post, put, delete, group};
7//!
8//! routes! {
9//! get!("/", controllers::home::index).name("home"),
10//! get!("/users", controllers::user::index).name("users.index"),
11//! post!("/users", controllers::user::store).name("users.store"),
12//! get!("/protected", controllers::home::index).middleware(AuthMiddleware),
13//!
14//! // Route groups with prefix and middleware
15//! group!("/api", {
16//! get!("/users", controllers::api::user::index).name("api.users.index"),
17//! post!("/users", controllers::api::user::store).name("api.users.store"),
18//! }).middleware(AuthMiddleware),
19//! }
20//! ```
21
22use crate::http::{Request, Response};
23
24/// Const function to validate route paths start with '/'
25///
26/// This provides compile-time validation that all route paths begin with '/'.
27/// If a path doesn't start with '/', compilation will fail with a clear error.
28///
29/// # Panics
30///
31/// Panics at compile time if the path is empty or doesn't start with '/'.
32pub const fn validate_route_path(path: &'static str) -> &'static str {
33 let bytes = path.as_bytes();
34 if bytes.is_empty() || bytes[0] != b'/' {
35 panic!("Route path must start with '/'")
36 }
37 path
38}
39use crate::middleware::{into_boxed, BoxedMiddleware, Middleware};
40use crate::routing::router::{register_route_name, BoxedHandler, Router};
41use std::future::Future;
42use std::sync::Arc;
43
44/// HTTP method for route definitions
45#[derive(Clone, Copy)]
46pub enum HttpMethod {
47 Get,
48 Post,
49 Put,
50 Delete,
51}
52
53/// Builder for route definitions that supports `.name()` and `.middleware()` chaining
54pub struct RouteDefBuilder<H> {
55 method: HttpMethod,
56 path: &'static str,
57 handler: H,
58 name: Option<&'static str>,
59 middlewares: Vec<BoxedMiddleware>,
60}
61
62impl<H, Fut> RouteDefBuilder<H>
63where
64 H: Fn(Request) -> Fut + Send + Sync + 'static,
65 Fut: Future<Output = Response> + Send + 'static,
66{
67 /// Create a new route definition builder
68 pub fn new(method: HttpMethod, path: &'static str, handler: H) -> Self {
69 Self {
70 method,
71 path,
72 handler,
73 name: None,
74 middlewares: Vec::new(),
75 }
76 }
77
78 /// Name this route for URL generation
79 pub fn name(mut self, name: &'static str) -> Self {
80 self.name = Some(name);
81 self
82 }
83
84 /// Add middleware to this route
85 pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> Self {
86 self.middlewares.push(into_boxed(middleware));
87 self
88 }
89
90 /// Register this route definition with a router
91 pub fn register(self, router: Router) -> Router {
92 // First, register the route based on method
93 let builder = match self.method {
94 HttpMethod::Get => router.get(self.path, self.handler),
95 HttpMethod::Post => router.post(self.path, self.handler),
96 HttpMethod::Put => router.put(self.path, self.handler),
97 HttpMethod::Delete => router.delete(self.path, self.handler),
98 };
99
100 // Apply any middleware
101 let builder = self
102 .middlewares
103 .into_iter()
104 .fold(builder, |b, m| b.middleware_boxed(m));
105
106 // Apply name if present, otherwise convert to Router
107 if let Some(name) = self.name {
108 builder.name(name)
109 } else {
110 builder.into()
111 }
112 }
113}
114
115/// Create a GET route definition with compile-time path validation
116///
117/// # Example
118/// ```rust,ignore
119/// get!("/users", controllers::user::index).name("users.index")
120/// ```
121///
122/// # Compile Error
123///
124/// Fails to compile if path doesn't start with '/'.
125#[macro_export]
126macro_rules! get {
127 ($path:expr, $handler:expr) => {{
128 const _: &str = $crate::validate_route_path($path);
129 $crate::__get_impl($path, $handler)
130 }};
131}
132
133/// Internal implementation for GET routes (used by the get! macro)
134#[doc(hidden)]
135pub fn __get_impl<H, Fut>(path: &'static str, handler: H) -> RouteDefBuilder<H>
136where
137 H: Fn(Request) -> Fut + Send + Sync + 'static,
138 Fut: Future<Output = Response> + Send + 'static,
139{
140 RouteDefBuilder::new(HttpMethod::Get, path, handler)
141}
142
143/// Create a POST route definition with compile-time path validation
144///
145/// # Example
146/// ```rust,ignore
147/// post!("/users", controllers::user::store).name("users.store")
148/// ```
149///
150/// # Compile Error
151///
152/// Fails to compile if path doesn't start with '/'.
153#[macro_export]
154macro_rules! post {
155 ($path:expr, $handler:expr) => {{
156 const _: &str = $crate::validate_route_path($path);
157 $crate::__post_impl($path, $handler)
158 }};
159}
160
161/// Internal implementation for POST routes (used by the post! macro)
162#[doc(hidden)]
163pub fn __post_impl<H, Fut>(path: &'static str, handler: H) -> RouteDefBuilder<H>
164where
165 H: Fn(Request) -> Fut + Send + Sync + 'static,
166 Fut: Future<Output = Response> + Send + 'static,
167{
168 RouteDefBuilder::new(HttpMethod::Post, path, handler)
169}
170
171/// Create a PUT route definition with compile-time path validation
172///
173/// # Example
174/// ```rust,ignore
175/// put!("/users/{id}", controllers::user::update).name("users.update")
176/// ```
177///
178/// # Compile Error
179///
180/// Fails to compile if path doesn't start with '/'.
181#[macro_export]
182macro_rules! put {
183 ($path:expr, $handler:expr) => {{
184 const _: &str = $crate::validate_route_path($path);
185 $crate::__put_impl($path, $handler)
186 }};
187}
188
189/// Internal implementation for PUT routes (used by the put! macro)
190#[doc(hidden)]
191pub fn __put_impl<H, Fut>(path: &'static str, handler: H) -> RouteDefBuilder<H>
192where
193 H: Fn(Request) -> Fut + Send + Sync + 'static,
194 Fut: Future<Output = Response> + Send + 'static,
195{
196 RouteDefBuilder::new(HttpMethod::Put, path, handler)
197}
198
199/// Create a DELETE route definition with compile-time path validation
200///
201/// # Example
202/// ```rust,ignore
203/// delete!("/users/{id}", controllers::user::destroy).name("users.destroy")
204/// ```
205///
206/// # Compile Error
207///
208/// Fails to compile if path doesn't start with '/'.
209#[macro_export]
210macro_rules! delete {
211 ($path:expr, $handler:expr) => {{
212 const _: &str = $crate::validate_route_path($path);
213 $crate::__delete_impl($path, $handler)
214 }};
215}
216
217/// Internal implementation for DELETE routes (used by the delete! macro)
218#[doc(hidden)]
219pub fn __delete_impl<H, Fut>(path: &'static str, handler: H) -> RouteDefBuilder<H>
220where
221 H: Fn(Request) -> Fut + Send + Sync + 'static,
222 Fut: Future<Output = Response> + Send + 'static,
223{
224 RouteDefBuilder::new(HttpMethod::Delete, path, handler)
225}
226
227// ============================================================================
228// Route Grouping Support
229// ============================================================================
230
231/// A route stored within a group (type-erased handler)
232pub struct GroupRoute {
233 method: HttpMethod,
234 path: &'static str,
235 handler: Arc<BoxedHandler>,
236 name: Option<&'static str>,
237 middlewares: Vec<BoxedMiddleware>,
238}
239
240/// Group definition that collects routes and applies prefix/middleware
241///
242/// # Example
243///
244/// ```rust,ignore
245/// routes! {
246/// group!("/api", {
247/// get!("/users", controllers::user::index).name("api.users"),
248/// post!("/users", controllers::user::store),
249/// }).middleware(AuthMiddleware),
250/// }
251/// ```
252pub struct GroupDef {
253 prefix: &'static str,
254 routes: Vec<GroupRoute>,
255 group_middlewares: Vec<BoxedMiddleware>,
256}
257
258impl GroupDef {
259 /// Create a new route group with the given prefix (internal use)
260 ///
261 /// Use the `group!` macro instead for compile-time validation.
262 #[doc(hidden)]
263 pub fn __new_unchecked(prefix: &'static str) -> Self {
264 Self {
265 prefix,
266 routes: Vec::new(),
267 group_middlewares: Vec::new(),
268 }
269 }
270
271 /// Add a route to this group
272 pub fn route<H, Fut>(mut self, route: RouteDefBuilder<H>) -> Self
273 where
274 H: Fn(Request) -> Fut + Send + Sync + 'static,
275 Fut: Future<Output = Response> + Send + 'static,
276 {
277 self.routes.push(route.into_group_route());
278 self
279 }
280
281 /// Add middleware to all routes in this group
282 ///
283 /// Middleware is applied in the order it's added.
284 ///
285 /// # Example
286 ///
287 /// ```rust,ignore
288 /// group!("/api", {
289 /// get!("/users", handler),
290 /// }).middleware(AuthMiddleware).middleware(RateLimitMiddleware)
291 /// ```
292 pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> Self {
293 self.group_middlewares.push(into_boxed(middleware));
294 self
295 }
296
297 /// Register all routes in this group with the router
298 ///
299 /// This prepends the group prefix to each route path and applies
300 /// group middleware to all routes.
301 ///
302 /// # Path Combination
303 ///
304 /// - If route path is "/" (root), the full path is just the group prefix
305 /// - Otherwise, prefix and route path are concatenated
306 pub fn register(self, mut router: Router) -> Router {
307 for route in self.routes {
308 // Build full path with prefix
309 // If route path is "/" (root), just use the prefix without trailing slash
310 let full_path = if route.path == "/" {
311 self.prefix.to_string()
312 } else {
313 format!("{}{}", self.prefix, route.path)
314 };
315 // We need to leak the string to get a 'static str for the router
316 let full_path: &'static str = Box::leak(full_path.into_boxed_str());
317
318 // Register the route with the router
319 match route.method {
320 HttpMethod::Get => {
321 router.insert_get(full_path, route.handler);
322 }
323 HttpMethod::Post => {
324 router.insert_post(full_path, route.handler);
325 }
326 HttpMethod::Put => {
327 router.insert_put(full_path, route.handler);
328 }
329 HttpMethod::Delete => {
330 router.insert_delete(full_path, route.handler);
331 }
332 }
333
334 // Register route name if present
335 if let Some(name) = route.name {
336 register_route_name(name, full_path);
337 }
338
339 // Apply group middleware first (outer), then route-specific middleware (inner)
340 for mw in &self.group_middlewares {
341 router.add_middleware(full_path, mw.clone());
342 }
343 for mw in route.middlewares {
344 router.add_middleware(full_path, mw);
345 }
346 }
347
348 router
349 }
350}
351
352impl<H, Fut> RouteDefBuilder<H>
353where
354 H: Fn(Request) -> Fut + Send + Sync + 'static,
355 Fut: Future<Output = Response> + Send + 'static,
356{
357 /// Convert this route definition to a type-erased GroupRoute
358 ///
359 /// This is used internally when adding routes to a group.
360 pub fn into_group_route(self) -> GroupRoute {
361 let handler = self.handler;
362 let boxed: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
363 GroupRoute {
364 method: self.method,
365 path: self.path,
366 handler: Arc::new(boxed),
367 name: self.name,
368 middlewares: self.middlewares,
369 }
370 }
371}
372
373/// Define a route group with a shared prefix
374///
375/// Routes within a group will have the prefix prepended to their paths.
376/// Middleware can be applied to the entire group using `.middleware()`.
377///
378/// # Example
379///
380/// ```rust,ignore
381/// use kit::{routes, get, post, group};
382///
383/// routes! {
384/// get!("/", controllers::home::index),
385///
386/// // All routes in this group start with /api
387/// group!("/api", {
388/// get!("/users", controllers::user::index), // -> GET /api/users
389/// post!("/users", controllers::user::store), // -> POST /api/users
390/// }).middleware(AuthMiddleware),
391/// }
392/// ```
393/// Define a route group with a shared prefix and compile-time validation
394///
395/// Routes within a group will have the prefix prepended to their paths.
396/// Middleware can be applied to the entire group using `.middleware()`.
397///
398/// # Compile Error
399///
400/// Fails to compile if prefix doesn't start with '/'.
401#[macro_export]
402macro_rules! group {
403 ($prefix:expr, { $( $route:expr ),* $(,)? }) => {{
404 const _: &str = $crate::validate_route_path($prefix);
405 let mut group = $crate::GroupDef::__new_unchecked($prefix);
406 $(
407 group = group.route($route);
408 )*
409 group
410 }};
411}
412
413/// Define routes with a clean, Laravel-like syntax
414///
415/// This macro generates a `pub fn register() -> Router` function automatically.
416/// Place it at the top level of your `routes.rs` file.
417///
418/// # Example
419/// ```rust,ignore
420/// use kit::{routes, get, post, put, delete};
421/// use crate::controllers;
422/// use crate::middleware::AuthMiddleware;
423///
424/// routes! {
425/// get!("/", controllers::home::index).name("home"),
426/// get!("/users", controllers::user::index).name("users.index"),
427/// get!("/users/{id}", controllers::user::show).name("users.show"),
428/// post!("/users", controllers::user::store).name("users.store"),
429/// put!("/users/{id}", controllers::user::update).name("users.update"),
430/// delete!("/users/{id}", controllers::user::destroy).name("users.destroy"),
431/// get!("/protected", controllers::home::index).middleware(AuthMiddleware),
432/// }
433/// ```
434#[macro_export]
435macro_rules! routes {
436 ( $( $route:expr ),* $(,)? ) => {
437 pub fn register() -> $crate::Router {
438 let mut router = $crate::Router::new();
439 $(
440 router = $route.register(router);
441 )*
442 router
443 }
444 };
445}