1use super::strategy::{VersionExtractor, VersionStrategy};
6use super::version::{ApiVersion, VersionRange};
7use crate::v31::OpenApi31Spec;
8use crate::OpenApiSpec;
9use std::collections::HashMap;
10
11#[derive(Debug, Clone)]
13pub struct VersionedRouteConfig {
14 pub matcher: VersionRange,
16 pub deprecated: bool,
18 pub deprecation_message: Option<String>,
20 pub sunset: Option<String>,
22}
23
24impl VersionedRouteConfig {
25 pub fn version(version: ApiVersion) -> Self {
27 Self {
28 matcher: VersionRange::exact(version),
29 deprecated: false,
30 deprecation_message: None,
31 sunset: None,
32 }
33 }
34
35 pub fn range(range: VersionRange) -> Self {
37 Self {
38 matcher: range,
39 deprecated: false,
40 deprecation_message: None,
41 sunset: None,
42 }
43 }
44
45 pub fn deprecated(mut self) -> Self {
47 self.deprecated = true;
48 self
49 }
50
51 pub fn with_deprecation_message(mut self, message: impl Into<String>) -> Self {
53 self.deprecated = true;
54 self.deprecation_message = Some(message.into());
55 self
56 }
57
58 pub fn with_sunset(mut self, date: impl Into<String>) -> Self {
60 self.sunset = Some(date.into());
61 self
62 }
63
64 pub fn matches(&self, version: &ApiVersion) -> bool {
66 self.matcher.contains(version)
67 }
68}
69
70#[derive(Debug, Clone)]
77pub struct VersionRouter {
78 extractor: VersionExtractor,
80 versions: HashMap<ApiVersion, VersionInfo>,
82 default_version: ApiVersion,
84 fallback: VersionFallback,
86}
87
88#[derive(Debug, Clone)]
90struct VersionInfo {
91 config: VersionedRouteConfig,
93 spec_31: Option<OpenApi31Spec>,
95 spec_30: Option<OpenApiSpec>,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
101pub enum VersionFallback {
102 #[default]
104 Default,
105 Latest,
107 Error,
109}
110
111impl VersionRouter {
112 pub fn new() -> Self {
114 Self {
115 extractor: VersionExtractor::new(),
116 versions: HashMap::new(),
117 default_version: ApiVersion::v1(),
118 fallback: VersionFallback::Default,
119 }
120 }
121
122 pub fn strategy(mut self, strategy: VersionStrategy) -> Self {
124 self.extractor = VersionExtractor::with_strategy(strategy);
125 self
126 }
127
128 pub fn strategies(mut self, strategies: Vec<VersionStrategy>) -> Self {
130 self.extractor = VersionExtractor::with_strategies(strategies);
131 self
132 }
133
134 pub fn default_version(mut self, version: ApiVersion) -> Self {
136 self.default_version = version;
137 self.extractor = self.extractor.default_version(version);
138 self
139 }
140
141 pub fn fallback(mut self, behavior: VersionFallback) -> Self {
143 self.fallback = behavior;
144 self
145 }
146
147 pub fn version(mut self, version: ApiVersion, config: VersionedRouteConfig) -> Self {
149 self.versions.insert(
150 version,
151 VersionInfo {
152 config,
153 spec_31: None,
154 spec_30: None,
155 },
156 );
157 self
158 }
159
160 pub fn version_with_spec_31(
162 mut self,
163 version: ApiVersion,
164 config: VersionedRouteConfig,
165 spec: OpenApi31Spec,
166 ) -> Self {
167 self.versions.insert(
168 version,
169 VersionInfo {
170 config,
171 spec_31: Some(spec),
172 spec_30: None,
173 },
174 );
175 self
176 }
177
178 pub fn version_with_spec_30(
180 mut self,
181 version: ApiVersion,
182 config: VersionedRouteConfig,
183 spec: OpenApiSpec,
184 ) -> Self {
185 self.versions.insert(
186 version,
187 VersionInfo {
188 config,
189 spec_31: None,
190 spec_30: Some(spec),
191 },
192 );
193 self
194 }
195
196 pub fn registered_versions(&self) -> Vec<ApiVersion> {
198 let mut versions: Vec<_> = self.versions.keys().copied().collect();
199 versions.sort();
200 versions
201 }
202
203 pub fn latest_version(&self) -> Option<ApiVersion> {
205 self.registered_versions().into_iter().max()
206 }
207
208 pub fn resolve_from_path(&self, path: &str) -> ResolvedVersion {
210 if let Some(version) = self.extractor.extract_from_path(path) {
211 self.resolve_version(version)
212 } else {
213 self.resolve_fallback()
214 }
215 }
216
217 pub fn resolve_from_headers(&self, headers: &HashMap<String, String>) -> ResolvedVersion {
219 if let Some(version) = self.extractor.extract_from_headers(headers) {
220 self.resolve_version(version)
221 } else {
222 self.resolve_fallback()
223 }
224 }
225
226 pub fn resolve_from_query(&self, query: &str) -> ResolvedVersion {
228 if let Some(version) = self.extractor.extract_from_query(query) {
229 self.resolve_version(version)
230 } else {
231 self.resolve_fallback()
232 }
233 }
234
235 fn resolve_version(&self, version: ApiVersion) -> ResolvedVersion {
237 if let Some(info) = self.versions.get(&version) {
239 return ResolvedVersion {
240 version,
241 found: true,
242 deprecated: info.config.deprecated,
243 deprecation_message: info.config.deprecation_message.clone(),
244 sunset: info.config.sunset.clone(),
245 };
246 }
247
248 for (v, info) in &self.versions {
250 if info.config.matches(&version) {
251 return ResolvedVersion {
252 version: *v,
253 found: true,
254 deprecated: info.config.deprecated,
255 deprecation_message: info.config.deprecation_message.clone(),
256 sunset: info.config.sunset.clone(),
257 };
258 }
259 }
260
261 self.resolve_fallback()
263 }
264
265 fn resolve_fallback(&self) -> ResolvedVersion {
267 match self.fallback {
268 VersionFallback::Default => {
269 let info = self.versions.get(&self.default_version);
270 ResolvedVersion {
271 version: self.default_version,
272 found: info.is_some(),
273 deprecated: info.map(|i| i.config.deprecated).unwrap_or(false),
274 deprecation_message: info.and_then(|i| i.config.deprecation_message.clone()),
275 sunset: info.and_then(|i| i.config.sunset.clone()),
276 }
277 }
278 VersionFallback::Latest => {
279 if let Some(version) = self.latest_version() {
280 let info = self.versions.get(&version);
281 ResolvedVersion {
282 version,
283 found: true,
284 deprecated: info.map(|i| i.config.deprecated).unwrap_or(false),
285 deprecation_message: info
286 .and_then(|i| i.config.deprecation_message.clone()),
287 sunset: info.and_then(|i| i.config.sunset.clone()),
288 }
289 } else {
290 ResolvedVersion {
291 version: self.default_version,
292 found: false,
293 deprecated: false,
294 deprecation_message: None,
295 sunset: None,
296 }
297 }
298 }
299 VersionFallback::Error => ResolvedVersion {
300 version: self.default_version,
301 found: false,
302 deprecated: false,
303 deprecation_message: None,
304 sunset: None,
305 },
306 }
307 }
308
309 pub fn get_spec_31(&self, version: &ApiVersion) -> Option<&OpenApi31Spec> {
311 self.versions.get(version).and_then(|v| v.spec_31.as_ref())
312 }
313
314 pub fn get_spec_30(&self, version: &ApiVersion) -> Option<&OpenApiSpec> {
316 self.versions.get(version).and_then(|v| v.spec_30.as_ref())
317 }
318
319 pub fn strip_version(&self, path: &str) -> String {
321 self.extractor.strip_version_from_path(path)
322 }
323
324 pub fn is_deprecated(&self, version: &ApiVersion) -> bool {
326 self.versions
327 .get(version)
328 .map(|v| v.config.deprecated)
329 .unwrap_or(false)
330 }
331
332 pub fn get_deprecation_info(&self, version: &ApiVersion) -> Option<DeprecationInfo> {
334 self.versions.get(version).and_then(|v| {
335 if v.config.deprecated {
336 Some(DeprecationInfo {
337 message: v.config.deprecation_message.clone(),
338 sunset: v.config.sunset.clone(),
339 })
340 } else {
341 None
342 }
343 })
344 }
345}
346
347impl Default for VersionRouter {
348 fn default() -> Self {
349 Self::new()
350 }
351}
352
353#[derive(Debug, Clone)]
355pub struct ResolvedVersion {
356 pub version: ApiVersion,
358 pub found: bool,
360 pub deprecated: bool,
362 pub deprecation_message: Option<String>,
364 pub sunset: Option<String>,
366}
367
368impl ResolvedVersion {
369 pub fn response_headers(&self) -> HashMap<String, String> {
371 let mut headers = HashMap::new();
372
373 headers.insert("API-Version".to_string(), self.version.to_string());
375
376 if self.deprecated {
378 headers.insert("Deprecation".to_string(), "true".to_string());
379
380 if let Some(sunset) = &self.sunset {
381 headers.insert("Sunset".to_string(), sunset.clone());
382 }
383
384 if let Some(message) = &self.deprecation_message {
385 headers.insert("X-Deprecation-Notice".to_string(), message.clone());
386 }
387 }
388
389 headers
390 }
391}
392
393#[derive(Debug, Clone)]
395pub struct DeprecationInfo {
396 pub message: Option<String>,
398 pub sunset: Option<String>,
400}
401
402pub struct VersionedSpecBuilder {
404 title: String,
406 description: Option<String>,
408 versions: Vec<(ApiVersion, VersionedRouteConfig)>,
410}
411
412impl VersionedSpecBuilder {
413 pub fn new(title: impl Into<String>) -> Self {
415 Self {
416 title: title.into(),
417 description: None,
418 versions: Vec::new(),
419 }
420 }
421
422 pub fn description(mut self, desc: impl Into<String>) -> Self {
424 self.description = Some(desc.into());
425 self
426 }
427
428 pub fn version(mut self, version: ApiVersion, config: VersionedRouteConfig) -> Self {
430 self.versions.push((version, config));
431 self
432 }
433
434 pub fn build_31(&self) -> HashMap<ApiVersion, OpenApi31Spec> {
436 let mut specs = HashMap::new();
437
438 for (version, config) in &self.versions {
439 let mut spec = OpenApi31Spec::new(
440 format!("{} {}", self.title, version.as_path_segment()),
441 version.to_string(),
442 );
443
444 if let Some(desc) = &self.description {
445 spec = spec.description(desc.clone());
446 }
447
448 if config.deprecated {
450 let mut info = "DEPRECATED".to_string();
451 if let Some(msg) = &config.deprecation_message {
452 info.push_str(&format!(": {}", msg));
453 }
454 if let Some(sunset) = &config.sunset {
455 info.push_str(&format!(" (Sunset: {})", sunset));
456 }
457 spec.info.summary = Some(info);
458 }
459
460 specs.insert(*version, spec);
461 }
462
463 specs
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470
471 #[test]
472 fn test_router_creation() {
473 let router = VersionRouter::new()
474 .strategy(VersionStrategy::path())
475 .default_version(ApiVersion::v1())
476 .version(
477 ApiVersion::v1(),
478 VersionedRouteConfig::version(ApiVersion::v1()),
479 )
480 .version(
481 ApiVersion::v2(),
482 VersionedRouteConfig::version(ApiVersion::v2()).deprecated(),
483 );
484
485 assert_eq!(
486 router.registered_versions(),
487 vec![ApiVersion::v1(), ApiVersion::v2()]
488 );
489 assert!(!router.is_deprecated(&ApiVersion::v1()));
490 assert!(router.is_deprecated(&ApiVersion::v2()));
491 }
492
493 #[test]
494 fn test_resolve_from_path() {
495 let router = VersionRouter::new()
496 .version(
497 ApiVersion::v1(),
498 VersionedRouteConfig::version(ApiVersion::v1()),
499 )
500 .version(
501 ApiVersion::v2(),
502 VersionedRouteConfig::version(ApiVersion::v2()),
503 );
504
505 let resolved = router.resolve_from_path("/v1/users");
506 assert!(resolved.found);
507 assert_eq!(resolved.version, ApiVersion::v1());
508
509 let resolved = router.resolve_from_path("/v2/products");
510 assert!(resolved.found);
511 assert_eq!(resolved.version, ApiVersion::v2());
512 }
513
514 #[test]
515 fn test_resolve_fallback() {
516 let router = VersionRouter::new()
517 .default_version(ApiVersion::v1())
518 .fallback(VersionFallback::Default)
519 .version(
520 ApiVersion::v1(),
521 VersionedRouteConfig::version(ApiVersion::v1()),
522 );
523
524 let resolved = router.resolve_from_path("/v3/test");
526 assert_eq!(resolved.version, ApiVersion::v1());
527 }
528
529 #[test]
530 fn test_deprecation_info() {
531 let router = VersionRouter::new().version(
532 ApiVersion::v1(),
533 VersionedRouteConfig::version(ApiVersion::v1())
534 .with_deprecation_message("Use v2 instead")
535 .with_sunset("2024-12-31T23:59:59Z"),
536 );
537
538 let info = router.get_deprecation_info(&ApiVersion::v1()).unwrap();
539 assert_eq!(info.message, Some("Use v2 instead".to_string()));
540 assert_eq!(info.sunset, Some("2024-12-31T23:59:59Z".to_string()));
541 }
542
543 #[test]
544 fn test_response_headers() {
545 let resolved = ResolvedVersion {
546 version: ApiVersion::v1(),
547 found: true,
548 deprecated: true,
549 deprecation_message: Some("Legacy version".to_string()),
550 sunset: Some("2024-12-31".to_string()),
551 };
552
553 let headers = resolved.response_headers();
554 assert_eq!(headers.get("API-Version"), Some(&"1.0.0".to_string()));
555 assert_eq!(headers.get("Deprecation"), Some(&"true".to_string()));
556 assert_eq!(headers.get("Sunset"), Some(&"2024-12-31".to_string()));
557 }
558
559 #[test]
560 fn test_versioned_spec_builder() {
561 let specs = VersionedSpecBuilder::new("My API")
562 .description("API description")
563 .version(
564 ApiVersion::v1(),
565 VersionedRouteConfig::version(ApiVersion::v1()),
566 )
567 .version(
568 ApiVersion::v2(),
569 VersionedRouteConfig::version(ApiVersion::v2()),
570 )
571 .build_31();
572
573 assert_eq!(specs.len(), 2);
574 assert!(specs.contains_key(&ApiVersion::v1()));
575 assert!(specs.contains_key(&ApiVersion::v2()));
576 }
577}