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