elif_http/routing/
versioned.rs

1use super::router::Router;
2use crate::{
3    request::ElifRequest,
4    response::IntoElifResponse,
5    errors::HttpResult,
6    middleware::versioning::{VersioningConfig, ApiVersion},
7};
8use std::collections::HashMap;
9use std::future::Future;
10
11/// Versioned router that handles multiple API versions
12#[derive(Debug)]
13pub struct VersionedRouter<S = ()> 
14where 
15    S: Clone + Send + Sync + 'static,
16{
17    /// Version-specific routers
18    pub version_routers: HashMap<String, Router<S>>,
19    /// Versioning configuration
20    pub versioning_config: VersioningConfig,
21    /// Global router for non-versioned routes
22    pub global_router: Option<Router<S>>,
23    /// Base API path (e.g., "/api")
24    pub base_path: String,
25}
26
27impl<S> VersionedRouter<S>
28where
29    S: Clone + Send + Sync + 'static,
30{
31    /// Create a new versioned router
32    pub fn new() -> Self {
33        Self {
34            version_routers: HashMap::new(),
35            versioning_config: VersioningConfig::builder().build().unwrap(),
36            global_router: None,
37            base_path: "/api".to_string(),
38        }
39    }
40
41    /// Add a version with its router
42    pub fn version(mut self, version: &str, router: Router<S>) -> Self {
43        self.version_routers.insert(version.to_string(), router);
44        
45        // Add version to config if not exists
46        self.versioning_config.add_version(version.to_string(), ApiVersion {
47            version: version.to_string(),
48            deprecated: false,
49            deprecation_message: None,
50            sunset_date: None,
51            is_default: self.version_routers.len() == 1, // First version is default
52        });
53        
54        self
55    }
56
57    /// Mark a version as deprecated
58    pub fn deprecate_version(mut self, version: &str, message: Option<&str>, sunset_date: Option<&str>) -> Self {
59        self.versioning_config.deprecate_version(
60            version,
61            message.map(|s| s.to_string()),
62            sunset_date.map(|s| s.to_string())
63        );
64        self
65    }
66
67    /// Set default version
68    pub fn default_version(mut self, version: &str) -> Self {
69        let (versions, strategy, _, include_deprecation_headers, version_header_name, version_param_name, strict_validation) = self.versioning_config.clone_config();
70        
71        // Rebuild config with new default version
72        let mut new_config = VersioningConfig::builder()
73            .versions(versions)
74            .strategy(strategy)
75            .include_deprecation_headers(include_deprecation_headers)
76            .version_header_name(version_header_name)
77            .version_param_name(version_param_name)
78            .strict_validation(strict_validation)
79            .default_version(Some(version.to_string()))
80            .build().unwrap();
81        
82        // Add the version if it doesn't exist
83        new_config.add_version(version.to_string(), ApiVersion {
84            version: version.to_string(),
85            deprecated: false,
86            deprecation_message: None,
87            sunset_date: None,
88            is_default: true,
89        });
90        
91        self.versioning_config = new_config;
92        self
93    }
94
95    /// Set versioning strategy
96    pub fn strategy(mut self, strategy: crate::middleware::versioning::VersionStrategy) -> Self {
97        let (versions, _, default_version, include_deprecation_headers, version_header_name, version_param_name, strict_validation) = self.versioning_config.clone_config();
98        
99        // Rebuild config with new strategy
100        self.versioning_config = VersioningConfig::builder()
101            .versions(versions)
102            .strategy(strategy)
103            .include_deprecation_headers(include_deprecation_headers)
104            .version_header_name(version_header_name)
105            .version_param_name(version_param_name)
106            .strict_validation(strict_validation)
107            .default_version(default_version)
108            .build().unwrap();
109        self
110    }
111
112    /// Add global routes (not versioned)
113    pub fn global(mut self, router: Router<S>) -> Self {
114        self.global_router = Some(router);
115        self
116    }
117
118    /// Build the final router with versioning middleware
119    pub fn build(self) -> Router<S> {
120        let mut final_router = Router::new();
121        
122        // Add global routes first (if any)
123        if let Some(global_router) = self.global_router {
124            final_router = final_router.merge(global_router);
125        }
126
127        // Create versioned routes
128        for (version, version_router) in self.version_routers {
129            let version_path = match self.versioning_config.get_strategy() {
130                crate::middleware::versioning::VersionStrategy::UrlPath => {
131                    format!("{}/{}", self.base_path, version)
132                },
133                _ => {
134                    // For non-URL strategies, all versions use the same base path
135                    self.base_path.clone()
136                }
137            };
138            
139            // Nest the version router under the version path
140            final_router = final_router.nest(&version_path, version_router);
141        }
142
143        // Apply versioning middleware layer - this is critical!
144        // This ensures that version detection and response headers work for ALL strategies
145        let versioning_layer = crate::middleware::versioning::versioning_layer(self.versioning_config);
146        
147        // Convert to axum router and apply the layer
148        let axum_router = final_router.into_axum_router();
149        let layered_router = axum_router.layer(versioning_layer);
150        
151        // Convert back to elif Router
152        // Note: This creates a new Router with the layered axum router
153        Router::new().merge_axum(layered_router)
154    }
155
156    /// Create a router builder for a specific version
157    pub fn version_builder<'a>(&'a mut self, version: &str) -> VersionedRouteBuilder<'a, S> {
158        VersionedRouteBuilder::new(version, self)
159    }
160}
161
162/// Builder for adding routes to a specific version
163pub struct VersionedRouteBuilder<'a, S> 
164where 
165    S: Clone + Send + Sync + 'static,
166{
167    version: String,
168    router: &'a mut VersionedRouter<S>,
169    current_router: Router<S>,
170}
171
172impl<'a, S> VersionedRouteBuilder<'a, S>
173where
174    S: Clone + Send + Sync + 'static,
175{
176    fn new(version: &str, router: &'a mut VersionedRouter<S>) -> Self {
177        Self {
178            version: version.to_string(),
179            router,
180            current_router: Router::new(),
181        }
182    }
183
184    /// Add a GET route for this version
185    pub fn get<F, Fut, R>(mut self, path: &str, handler: F) -> Self
186    where
187        F: Fn(ElifRequest) -> Fut + Send + Clone + 'static,
188        Fut: Future<Output = HttpResult<R>> + Send + 'static,
189        R: IntoElifResponse + Send + 'static,
190    {
191        self.current_router = self.current_router.get(path, handler);
192        self
193    }
194
195    /// Add a POST route for this version
196    pub fn post<F, Fut, R>(mut self, path: &str, handler: F) -> Self
197    where
198        F: Fn(ElifRequest) -> Fut + Send + Clone + 'static,
199        Fut: Future<Output = HttpResult<R>> + Send + 'static,
200        R: IntoElifResponse + Send + 'static,
201    {
202        self.current_router = self.current_router.post(path, handler);
203        self
204    }
205
206    /// Add a PUT route for this version
207    pub fn put<F, Fut, R>(mut self, path: &str, handler: F) -> Self
208    where
209        F: Fn(ElifRequest) -> Fut + Send + Clone + 'static,
210        Fut: Future<Output = HttpResult<R>> + Send + 'static,
211        R: IntoElifResponse + Send + 'static,
212    {
213        self.current_router = self.current_router.put(path, handler);
214        self
215    }
216
217    /// Add a DELETE route for this version
218    pub fn delete<F, Fut, R>(mut self, path: &str, handler: F) -> Self
219    where
220        F: Fn(ElifRequest) -> Fut + Send + Clone + 'static,
221        Fut: Future<Output = HttpResult<R>> + Send + 'static,
222        R: IntoElifResponse + Send + 'static,
223    {
224        self.current_router = self.current_router.delete(path, handler);
225        self
226    }
227
228    /// Add a PATCH route for this version
229    pub fn patch<F, Fut, R>(mut self, path: &str, handler: F) -> Self
230    where
231        F: Fn(ElifRequest) -> Fut + Send + Clone + 'static,
232        Fut: Future<Output = HttpResult<R>> + Send + 'static,
233        R: IntoElifResponse + Send + 'static,
234    {
235        self.current_router = self.current_router.patch(path, handler);
236        self
237    }
238
239    /// Finish building routes for this version
240    pub fn finish(self) {
241        self.router.version_routers.insert(self.version.clone(), self.current_router);
242    }
243}
244
245/// Convenience functions for creating versioned routers
246pub fn versioned_router<S>() -> VersionedRouter<S> 
247where 
248    S: Clone + Send + Sync + 'static,
249{
250    VersionedRouter::<S>::new()
251}
252
253/// Create a versioned router with URL path strategy
254pub fn path_versioned_router<S>() -> VersionedRouter<S> 
255where 
256    S: Clone + Send + Sync + 'static,
257{
258    VersionedRouter::<S> {
259        version_routers: HashMap::new(),
260        versioning_config: VersioningConfig::builder()
261            .strategy(crate::middleware::versioning::VersionStrategy::UrlPath)
262            .build().unwrap(),
263        global_router: None,
264        base_path: "/api".to_string(),
265    }
266}
267
268/// Create a versioned router with header strategy
269pub fn header_versioned_router<S>(header_name: &str) -> VersionedRouter<S> 
270where 
271    S: Clone + Send + Sync + 'static,
272{
273    VersionedRouter::<S> {
274        version_routers: HashMap::new(),
275        versioning_config: VersioningConfig::builder()
276            .strategy(crate::middleware::versioning::VersionStrategy::Header(header_name.to_string()))
277            .build().unwrap(),
278        global_router: None,
279        base_path: "/api".to_string(),
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286    use crate::response::ElifJson;
287    
288    #[tokio::test]
289    async fn test_versioned_router_creation() {
290        let router = VersionedRouter::<()>::new()
291            .version("v1", Router::new())
292            .version("v2", Router::new())
293            .default_version("v1")
294            .deprecate_version("v1", Some("Please use v2"), Some("2024-12-31"));
295
296        assert_eq!(router.version_routers.len(), 2);
297        assert!(router.version_routers.contains_key("v1"));
298        assert!(router.version_routers.contains_key("v2"));
299        
300        let v1_version = router.versioning_config.get_version("v1").unwrap();
301        assert!(v1_version.deprecated);
302        assert_eq!(v1_version.deprecation_message, Some("Please use v2".to_string()));
303    }
304
305    #[tokio::test] 
306    async fn test_version_builder() {
307        let mut router = VersionedRouter::<()>::new();
308        
309        router.version_builder("v1")
310            .get("/users", |_req| async { Ok(ElifJson("users v1")) })
311            .post("/users", |_req| async { Ok(ElifJson("create user v1")) })
312            .finish();
313        
314        assert!(router.version_routers.contains_key("v1"));
315    }
316
317    #[test]
318    fn test_convenience_functions() {
319        let _path_router = path_versioned_router::<()>();
320        let _header_router = header_versioned_router::<()>("Api-Version");
321        let _versioned_router = versioned_router::<()>();
322    }
323}