elif_http/routing/
versioned.rs

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