1pub mod adapter;
33
34use serde::{Deserialize, Serialize};
35use std::cmp::Ordering;
36use std::fmt;
37use std::str::FromStr;
38
39#[derive(Debug, Clone)]
41pub struct VersionManager {
42 supported_versions: Vec<Version>,
44 current_version: Version,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct Version {
51 pub year: u16,
53 pub month: u8,
55 pub day: u8,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
61pub enum VersionCompatibility {
62 Compatible,
64 Incompatible(String),
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70pub enum VersionRequirement {
71 Exact(Version),
73 Minimum(Version),
75 Maximum(Version),
77 Range(Version, Version),
79 Any(Vec<Version>),
81}
82
83impl Version {
84 pub fn new(year: u16, month: u8, day: u8) -> Result<Self, VersionError> {
91 if !(1..=12).contains(&month) {
92 return Err(VersionError::InvalidMonth(month.to_string()));
93 }
94
95 if !(1..=31).contains(&day) {
96 return Err(VersionError::InvalidDay(day.to_string()));
97 }
98
99 if month == 2 && day > 29 {
101 return Err(VersionError::InvalidDay(format!(
102 "{} (invalid for February)",
103 day
104 )));
105 }
106
107 if matches!(month, 4 | 6 | 9 | 11) && day > 30 {
108 return Err(VersionError::InvalidDay(format!(
109 "{} (month {} only has 30 days)",
110 day, month
111 )));
112 }
113
114 Ok(Self { year, month, day })
115 }
116
117 pub fn latest() -> Self {
119 Self {
120 year: 2025,
121 month: 11,
122 day: 25,
123 }
124 }
125
126 pub fn current() -> Self {
128 Self::latest()
129 }
130
131 pub fn is_newer_than(&self, other: &Version) -> bool {
133 self > other
134 }
135
136 pub fn is_older_than(&self, other: &Version) -> bool {
138 self < other
139 }
140
141 pub fn is_compatible_with(&self, other: &Version) -> bool {
143 self == other
144 }
145
146 pub fn to_date_string(&self) -> String {
148 format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
149 }
150
151 pub fn from_date_string(s: &str) -> Result<Self, VersionError> {
158 s.parse()
159 }
160
161 pub fn known_versions() -> Vec<Version> {
163 vec![Version::latest()]
164 }
165}
166
167impl fmt::Display for Version {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 write!(f, "{}", self.to_date_string())
170 }
171}
172
173impl FromStr for Version {
174 type Err = VersionError;
175
176 fn from_str(s: &str) -> Result<Self, Self::Err> {
177 let parts: Vec<&str> = s.split('-').collect();
178
179 if parts.len() != 3 {
180 return Err(VersionError::InvalidFormat(s.to_string()));
181 }
182
183 let year = parts[0]
184 .parse::<u16>()
185 .map_err(|_| VersionError::InvalidYear(parts[0].to_string()))?;
186
187 let month = parts[1]
188 .parse::<u8>()
189 .map_err(|_| VersionError::InvalidMonth(parts[1].to_string()))?;
190
191 let day = parts[2]
192 .parse::<u8>()
193 .map_err(|_| VersionError::InvalidDay(parts[2].to_string()))?;
194
195 Self::new(year, month, day)
196 }
197}
198
199impl PartialOrd for Version {
200 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
201 Some(self.cmp(other))
202 }
203}
204
205impl Ord for Version {
206 fn cmp(&self, other: &Self) -> Ordering {
207 (self.year, self.month, self.day).cmp(&(other.year, other.month, other.day))
208 }
209}
210
211impl VersionManager {
212 pub fn new(supported_versions: Vec<Version>) -> Result<Self, VersionError> {
218 if supported_versions.is_empty() {
219 return Err(VersionError::NoSupportedVersions);
220 }
221
222 let mut versions = supported_versions;
223 versions.sort_by(|a, b| b.cmp(a)); let current_version = versions[0].clone();
226
227 Ok(Self {
228 supported_versions: versions,
229 current_version,
230 })
231 }
232
233 pub fn with_default_versions() -> Self {
235 Self::new(Version::known_versions()).unwrap()
236 }
237 pub fn current_version(&self) -> &Version {
239 &self.current_version
240 }
241
242 pub fn supported_versions(&self) -> &[Version] {
244 &self.supported_versions
245 }
246
247 pub fn is_version_supported(&self, version: &Version) -> bool {
249 self.supported_versions.contains(version)
250 }
251
252 pub fn negotiate_version(&self, client_versions: &[Version]) -> Option<Version> {
254 for server_version in &self.supported_versions {
256 if client_versions.contains(server_version) {
257 return Some(server_version.clone());
258 }
259 }
260
261 None
262 }
263
264 pub fn check_compatibility(
266 &self,
267 client_version: &Version,
268 server_version: &Version,
269 ) -> VersionCompatibility {
270 if client_version == server_version {
271 return VersionCompatibility::Compatible;
272 }
273
274 let reason =
275 format!("Incompatible versions: client={client_version}, server={server_version}");
276 VersionCompatibility::Incompatible(reason)
277 }
278
279 pub fn minimum_version(&self) -> &Version {
281 self.supported_versions
283 .last()
284 .expect("BUG: VersionManager has no versions (constructor should prevent this)")
285 }
286
287 pub fn maximum_version(&self) -> &Version {
289 &self.supported_versions[0] }
291
292 pub fn satisfies_requirement(
294 &self,
295 version: &Version,
296 requirement: &VersionRequirement,
297 ) -> bool {
298 match requirement {
299 VersionRequirement::Exact(required) => version == required,
300 VersionRequirement::Minimum(min) => version >= min,
301 VersionRequirement::Maximum(max) => version <= max,
302 VersionRequirement::Range(min, max) => version >= min && version <= max,
303 VersionRequirement::Any(versions) => versions.contains(version),
304 }
305 }
306}
307
308impl Default for VersionManager {
309 fn default() -> Self {
310 Self::with_default_versions()
311 }
312}
313
314impl VersionRequirement {
315 pub fn exact(version: Version) -> Self {
317 Self::Exact(version)
318 }
319
320 pub fn minimum(version: Version) -> Self {
322 Self::Minimum(version)
323 }
324
325 pub fn maximum(version: Version) -> Self {
327 Self::Maximum(version)
328 }
329
330 pub fn range(min: Version, max: Version) -> Result<Self, VersionError> {
336 if min > max {
337 return Err(VersionError::InvalidRange(min, max));
338 }
339 Ok(Self::Range(min, max))
340 }
341
342 pub fn any(versions: Vec<Version>) -> Result<Self, VersionError> {
348 if versions.is_empty() {
349 return Err(VersionError::EmptyVersionList);
350 }
351 Ok(Self::Any(versions))
352 }
353
354 pub fn is_satisfied_by(&self, version: &Version) -> bool {
356 match self {
357 Self::Exact(required) => version == required,
358 Self::Minimum(min) => version >= min,
359 Self::Maximum(max) => version <= max,
360 Self::Range(min, max) => version >= min && version <= max,
361 Self::Any(versions) => versions.contains(version),
362 }
363 }
364}
365
366#[derive(Debug, Clone, thiserror::Error)]
368pub enum VersionError {
369 #[error("Invalid version format: {0}")]
371 InvalidFormat(String),
372 #[error("Invalid year: {0}")]
374 InvalidYear(String),
375 #[error("Invalid month: {0} (must be 1-12)")]
377 InvalidMonth(String),
378 #[error("Invalid day: {0} (must be 1-31)")]
380 InvalidDay(String),
381 #[error("No supported versions provided")]
383 NoSupportedVersions,
384 #[error("Invalid version range: {0} > {1}")]
386 InvalidRange(Version, Version),
387 #[error("Empty version list")]
389 EmptyVersionList,
390}
391
392pub mod utils {
394 use super::*;
395
396 pub fn parse_versions(version_strings: &[&str]) -> Result<Vec<Version>, VersionError> {
402 version_strings.iter().map(|s| s.parse()).collect()
403 }
404
405 pub fn newest_version(versions: &[Version]) -> Option<&Version> {
407 versions.iter().max()
408 }
409
410 pub fn oldest_version(versions: &[Version]) -> Option<&Version> {
412 versions.iter().min()
413 }
414
415 pub fn are_all_compatible(versions: &[Version]) -> bool {
417 if versions.len() < 2 {
418 return true;
419 }
420
421 let first = &versions[0];
422 versions.iter().all(|v| first.is_compatible_with(v))
423 }
424
425 pub fn compatibility_description(compatibility: &VersionCompatibility) -> String {
427 match compatibility {
428 VersionCompatibility::Compatible => "Fully compatible".to_string(),
429 VersionCompatibility::Incompatible(reason) => {
430 format!("Incompatible: {reason}")
431 }
432 }
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use proptest::prelude::*;
440
441 #[test]
442 fn test_version_creation() {
443 let version = Version::new(2025, 6, 18).unwrap();
444 assert_eq!(version.year, 2025);
445 assert_eq!(version.month, 6);
446 assert_eq!(version.day, 18);
447
448 assert!(Version::new(2025, 13, 18).is_err());
450
451 assert!(Version::new(2025, 6, 32).is_err());
453 }
454
455 #[test]
456 fn test_version_parsing() {
457 let version: Version = "2025-11-25".parse().unwrap();
458 assert_eq!(version, Version::new(2025, 11, 25).unwrap());
459
460 assert!("2025/06/18".parse::<Version>().is_err());
462 assert!("invalid".parse::<Version>().is_err());
463 }
464
465 #[test]
466 fn test_version_comparison() {
467 let v1 = Version::new(2025, 11, 25).unwrap();
468 let v2 = Version::new(2024, 11, 5).unwrap();
469 let v3 = Version::new(2025, 11, 25).unwrap();
470
471 assert!(v1 > v2);
472 assert!(v1.is_newer_than(&v2));
473 assert!(v2.is_older_than(&v1));
474 assert_eq!(v1, v3);
475 }
476
477 #[test]
478 fn test_version_compatibility() {
479 let v1 = Version::new(2025, 11, 25).unwrap();
480 let v2 = Version::new(2025, 12, 1).unwrap();
481 let v3 = Version::new(2024, 6, 18).unwrap(); assert!(!v1.is_compatible_with(&v2));
484 assert!(!v1.is_compatible_with(&v3));
485 }
486
487 #[test]
488 fn test_version_manager() {
489 let versions = vec![
490 Version::new(2025, 11, 25).unwrap(),
491 Version::new(2024, 11, 5).unwrap(),
492 ];
493
494 let manager = VersionManager::new(versions).unwrap();
495
496 assert_eq!(
497 manager.current_version(),
498 &Version::new(2025, 11, 25).unwrap()
499 );
500 assert!(manager.is_version_supported(&Version::new(2024, 11, 5).unwrap()));
501 assert!(!manager.is_version_supported(&Version::new(2023, 1, 1).unwrap()));
502 }
503
504 #[test]
505 fn test_version_negotiation() {
506 let manager = VersionManager::default();
507
508 let client_versions = vec![
509 Version::new(2024, 11, 5).unwrap(),
510 Version::new(2025, 11, 25).unwrap(),
511 ];
512
513 let negotiated = manager.negotiate_version(&client_versions);
514 assert_eq!(negotiated, Some(Version::new(2025, 11, 25).unwrap()));
515 }
516
517 #[test]
518 fn test_version_requirements() {
519 let version = Version::new(2025, 11, 25).unwrap();
520
521 let exact_req = VersionRequirement::exact(version.clone());
522 assert!(exact_req.is_satisfied_by(&version));
523
524 let min_req = VersionRequirement::minimum(Version::new(2024, 1, 1).unwrap());
525 assert!(min_req.is_satisfied_by(&version));
526
527 let max_req = VersionRequirement::maximum(Version::new(2024, 1, 1).unwrap());
528 assert!(!max_req.is_satisfied_by(&version));
529 }
530
531 #[test]
532 fn test_compatibility_checking() {
533 let manager = VersionManager::default();
534
535 let v1 = Version::new(2025, 11, 25).unwrap();
536 let v2 = Version::new(2025, 12, 1).unwrap();
537 let v3 = Version::new(2024, 1, 1).unwrap();
538
539 let compat = manager.check_compatibility(&v1, &v2);
541 assert!(matches!(compat, VersionCompatibility::Incompatible(_)));
542
543 let compat = manager.check_compatibility(&v1, &v3);
545 assert!(matches!(compat, VersionCompatibility::Incompatible(_)));
546
547 let compat = manager.check_compatibility(&v1, &v1);
549 assert_eq!(compat, VersionCompatibility::Compatible);
550 }
551
552 #[test]
553 fn test_utils() {
554 let versions = utils::parse_versions(&["2025-11-25", "2024-11-05"]).unwrap();
555 assert_eq!(versions.len(), 2);
556
557 let newest = utils::newest_version(&versions);
558 assert_eq!(newest, Some(&Version::new(2025, 11, 25).unwrap()));
559
560 let oldest = utils::oldest_version(&versions);
561 assert_eq!(oldest, Some(&Version::new(2024, 11, 5).unwrap()));
562 }
563
564 proptest! {
566 #[test]
567 fn test_version_parse_roundtrip(
568 year in 2020u16..2030u16,
569 month in 1u8..=12u8,
570 day in 1u8..=28u8, ) {
572 let version = Version::new(year, month, day)?;
573 let string = version.to_date_string();
574 let parsed = Version::from_date_string(&string)?;
575 prop_assert_eq!(version, parsed);
576 }
577
578 #[test]
579 fn test_version_comparison_transitive(
580 y1 in 2020u16..2030u16,
581 m1 in 1u8..=12u8,
582 d1 in 1u8..=28u8,
583 y2 in 2020u16..2030u16,
584 m2 in 1u8..=12u8,
585 d2 in 1u8..=28u8,
586 y3 in 2020u16..2030u16,
587 m3 in 1u8..=12u8,
588 d3 in 1u8..=28u8,
589 ) {
590 let v1 = Version::new(y1, m1, d1)?;
591 let v2 = Version::new(y2, m2, d2)?;
592 let v3 = Version::new(y3, m3, d3)?;
593
594 if v1 < v2 && v2 < v3 {
596 prop_assert!(v1 < v3);
597 }
598 }
599
600 #[test]
601 fn test_version_compatibility_symmetric(
602 year in 2020u16..2030u16,
603 m1 in 1u8..=12u8,
604 d1 in 1u8..=28u8,
605 m2 in 1u8..=12u8,
606 d2 in 1u8..=28u8,
607 ) {
608 let v1 = Version::new(year, m1, d1)?;
609 let v2 = Version::new(year, m2, d2)?;
610
611 prop_assert_eq!(v1.is_compatible_with(&v2), v2.is_compatible_with(&v1));
613 }
614
615 #[test]
616 fn test_invalid_month_rejected(
617 year in 2020u16..2030u16,
618 month in 13u8..=255u8,
619 day in 1u8..=28u8,
620 ) {
621 prop_assert!(Version::new(year, month, day).is_err());
622 }
623
624 #[test]
625 fn test_invalid_day_rejected(
626 year in 2020u16..2030u16,
627 month in 1u8..=12u8,
628 day in 32u8..=255u8,
629 ) {
630 prop_assert!(Version::new(year, month, day).is_err());
631 }
632 }
633}