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}