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