elif_http/routing/
group.rs

1//! Route groups for organizing related routes
2
3use super::{HttpMethod, RouteInfo, RouteRegistry};
4use axum::{
5    handler::Handler,
6    routing::{delete, get, patch, post, put},
7    Router as AxumRouter,
8};
9use service_builder::builder;
10use std::sync::{Arc, Mutex};
11
12/// Route group for organizing related routes with shared configuration
13#[derive(Debug)]
14pub struct RouteGroup<S = ()>
15where
16    S: Clone + Send + Sync + 'static,
17{
18    prefix: String,
19    name: String,
20    router: AxumRouter<S>,
21    registry: Arc<Mutex<RouteRegistry>>,
22    #[allow(dead_code)]
23    middleware: Vec<String>, // Placeholder for future middleware
24}
25
26impl<S> RouteGroup<S>
27where
28    S: Clone + Send + Sync + 'static,
29{
30    /// Create a new route group
31    pub fn new(name: &str, prefix: &str, registry: Arc<Mutex<RouteRegistry>>) -> Self {
32        Self {
33            prefix: prefix.trim_end_matches('/').to_string(),
34            name: name.to_string(),
35            router: AxumRouter::new(),
36            registry,
37            middleware: Vec::new(),
38        }
39    }
40
41    /// Create full path with group prefix
42    fn full_path(&self, path: &str) -> String {
43        let path = path.trim_start_matches('/');
44        if self.prefix.is_empty() {
45            format!("/{}", path)
46        } else {
47            format!("{}/{}", self.prefix, path)
48        }
49    }
50
51    /// Register a route in the group
52    fn register_route(&self, method: HttpMethod, path: &str, route_name: Option<String>) {
53        let full_path = self.full_path(path);
54        let params = self.extract_param_names(&full_path);
55
56        let final_name = route_name.or_else(|| {
57            // Generate default name: group.method.path_segments
58            let path_segments: Vec<&str> = path
59                .trim_matches('/')
60                .split('/')
61                .filter(|s| !s.is_empty() && !s.starts_with('{'))
62                .collect();
63
64            if path_segments.is_empty() {
65                Some(format!(
66                    "{}.{}",
67                    self.name,
68                    method_to_string(&method).to_lowercase()
69                ))
70            } else {
71                Some(format!(
72                    "{}.{}.{}",
73                    self.name,
74                    method_to_string(&method).to_lowercase(),
75                    path_segments.join("_")
76                ))
77            }
78        });
79
80        let route_info = RouteInfo {
81            name: final_name,
82            path: full_path,
83            method,
84            params,
85            group: Some(self.name.clone()),
86        };
87
88        let route_id = format!("{}_{}", self.name, uuid::Uuid::new_v4());
89        self.registry.lock().unwrap().register(route_id, route_info);
90    }
91
92    /// Extract parameter names from path
93    fn extract_param_names(&self, path: &str) -> Vec<String> {
94        path.split('/')
95            .filter_map(|segment| {
96                if segment.starts_with('{') && segment.ends_with('}') {
97                    Some(segment[1..segment.len() - 1].to_string())
98                } else {
99                    None
100                }
101            })
102            .collect()
103    }
104
105    /// Add a GET route to the group
106    pub fn get<H, T>(mut self, path: &str, handler: H) -> Self
107    where
108        H: Handler<T, S>,
109        T: 'static,
110    {
111        self.register_route(HttpMethod::GET, path, None);
112        self.router = self.router.route(path, get(handler));
113        self
114    }
115
116    /// Add a POST route to the group
117    pub fn post<H, T>(mut self, path: &str, handler: H) -> Self
118    where
119        H: Handler<T, S>,
120        T: 'static,
121    {
122        self.register_route(HttpMethod::POST, path, None);
123        self.router = self.router.route(path, post(handler));
124        self
125    }
126
127    /// Add a PUT route to the group
128    pub fn put<H, T>(mut self, path: &str, handler: H) -> Self
129    where
130        H: Handler<T, S>,
131        T: 'static,
132    {
133        self.register_route(HttpMethod::PUT, path, None);
134        self.router = self.router.route(path, put(handler));
135        self
136    }
137
138    /// Add a DELETE route to the group
139    pub fn delete<H, T>(mut self, path: &str, handler: H) -> Self
140    where
141        H: Handler<T, S>,
142        T: 'static,
143    {
144        self.register_route(HttpMethod::DELETE, path, None);
145        self.router = self.router.route(path, delete(handler));
146        self
147    }
148
149    /// Add a PATCH route to the group
150    pub fn patch<H, T>(mut self, path: &str, handler: H) -> Self
151    where
152        H: Handler<T, S>,
153        T: 'static,
154    {
155        self.register_route(HttpMethod::PATCH, path, None);
156        self.router = self.router.route(path, patch(handler));
157        self
158    }
159
160    /// Add a named route
161    pub fn route<H, T>(mut self, method: HttpMethod, path: &str, name: &str, handler: H) -> Self
162    where
163        H: Handler<T, S>,
164        T: 'static,
165    {
166        self.register_route(method.clone(), path, Some(name.to_string()));
167
168        let axum_method = axum::http::Method::from(method);
169        match axum_method {
170            axum::http::Method::GET => self.router = self.router.route(path, get(handler)),
171            axum::http::Method::POST => self.router = self.router.route(path, post(handler)),
172            axum::http::Method::PUT => self.router = self.router.route(path, put(handler)),
173            axum::http::Method::DELETE => self.router = self.router.route(path, delete(handler)),
174            axum::http::Method::PATCH => self.router = self.router.route(path, patch(handler)),
175            _ => {} // TODO: Handle other methods
176        }
177
178        self
179    }
180
181    /// Get the prefix for this group
182    pub fn prefix(&self) -> &str {
183        &self.prefix
184    }
185
186    /// Get the name of this group
187    pub fn name(&self) -> &str {
188        &self.name
189    }
190
191    /// Convert to Axum router for mounting
192    pub fn into_router(self) -> AxumRouter<S> {
193        self.router
194    }
195}
196
197/// Configuration for GroupBuilder
198#[derive(Debug, Clone)]
199#[builder]
200pub struct GroupBuilderConfig {
201    #[builder(getter)]
202    pub name: String,
203
204    #[builder(default, getter)]
205    pub prefix: String,
206
207    #[builder(default, getter)]
208    pub middleware: Vec<String>,
209}
210
211impl GroupBuilderConfig {
212    /// Build a RouteGroup from the config
213    pub fn build_group<S>(self, registry: Arc<Mutex<RouteRegistry>>) -> RouteGroup<S>
214    where
215        S: Clone + Send + Sync + 'static,
216    {
217        RouteGroup::new(&self.name, &self.prefix, registry)
218    }
219}
220
221// Add convenience methods to the generated builder
222impl GroupBuilderConfigBuilder {
223    /// Add middleware to the group
224    pub fn add_middleware(self, middleware_name: &str) -> Self {
225        let mut middlewares_vec = self.middleware.unwrap_or_default();
226        middlewares_vec.push(middleware_name.to_string());
227        GroupBuilderConfigBuilder {
228            name: self.name,
229            prefix: self.prefix,
230            middleware: Some(middlewares_vec),
231        }
232    }
233
234    /// Add multiple middlewares
235    pub fn add_middlewares(self, new_middlewares: Vec<String>) -> Self {
236        let mut middlewares_vec = self.middleware.unwrap_or_default();
237        middlewares_vec.extend(new_middlewares);
238        GroupBuilderConfigBuilder {
239            name: self.name,
240            prefix: self.prefix,
241            middleware: Some(middlewares_vec),
242        }
243    }
244
245    pub fn build_config(self) -> GroupBuilderConfig {
246        self.build_with_defaults().unwrap()
247    }
248}
249
250/// Builder for creating route groups with configuration
251pub struct GroupBuilder {
252    builder_config: GroupBuilderConfigBuilder,
253}
254
255impl GroupBuilder {
256    pub fn new(name: &str) -> Self {
257        Self {
258            builder_config: GroupBuilderConfig::builder().name(name.to_string()),
259        }
260    }
261
262    /// Set the URL prefix for the group
263    pub fn prefix(self, prefix: &str) -> Self {
264        Self {
265            builder_config: self.builder_config.prefix(prefix.to_string()),
266        }
267    }
268
269    /// Add middleware to the group (placeholder)
270    pub fn middleware(self, middleware_name: &str) -> Self {
271        Self {
272            builder_config: self.builder_config.add_middleware(middleware_name),
273        }
274    }
275
276    /// Add multiple middlewares
277    pub fn middlewares(self, middlewares: Vec<String>) -> Self {
278        Self {
279            builder_config: self.builder_config.add_middlewares(middlewares),
280        }
281    }
282
283    /// Build the route group
284    pub fn build<S>(self, registry: Arc<Mutex<RouteRegistry>>) -> RouteGroup<S>
285    where
286        S: Clone + Send + Sync + 'static,
287    {
288        self.builder_config.build_config().build_group(registry)
289    }
290}
291
292/// Convert HttpMethod to string for naming
293fn method_to_string(method: &HttpMethod) -> &'static str {
294    match method {
295        HttpMethod::GET => "GET",
296        HttpMethod::POST => "POST",
297        HttpMethod::PUT => "PUT",
298        HttpMethod::DELETE => "DELETE",
299        HttpMethod::PATCH => "PATCH",
300        HttpMethod::HEAD => "HEAD",
301        HttpMethod::OPTIONS => "OPTIONS",
302        HttpMethod::TRACE => "TRACE",
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use axum::response::Html;
310
311    async fn handler() -> Html<&'static str> {
312        Html("<h1>Handler</h1>")
313    }
314
315    #[test]
316    fn test_group_creation() {
317        let registry = Arc::new(Mutex::new(RouteRegistry::new()));
318        let group = RouteGroup::<()>::new("api", "/api/v1", Arc::clone(&registry));
319
320        assert_eq!(group.name(), "api");
321        assert_eq!(group.prefix(), "/api/v1");
322    }
323
324    #[test]
325    fn test_group_path_generation() {
326        let registry = Arc::new(Mutex::new(RouteRegistry::new()));
327        let group = RouteGroup::<()>::new("api", "/api/v1", registry);
328
329        assert_eq!(group.full_path("users"), "/api/v1/users");
330        assert_eq!(group.full_path("/users/{id}"), "/api/v1/users/{id}");
331    }
332
333    #[test]
334    fn test_group_builder() {
335        let registry = Arc::new(Mutex::new(RouteRegistry::new()));
336        let group = GroupBuilder::new("api")
337            .prefix("/api/v1")
338            .middleware("auth")
339            .build::<()>(registry)
340            .get("/users", handler)
341            .post("/users", handler);
342
343        assert_eq!(group.name(), "api");
344        assert_eq!(group.prefix(), "/api/v1");
345    }
346}