1use serde::{Deserialize, Serialize};
86use std::cmp::Ordering;
87use std::fmt;
88use std::str::FromStr;
89
90#[derive(Debug, Clone)]
92pub struct VersionManager {
93 supported_versions: Vec<Version>,
95 current_version: Version,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101pub struct Version {
102 pub year: u16,
104 pub month: u8,
106 pub day: u8,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
112pub enum VersionCompatibility {
113 Compatible,
115 CompatibleWithWarnings(Vec<String>),
117 Incompatible(String),
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
123pub enum VersionRequirement {
124 Exact(Version),
126 Minimum(Version),
128 Maximum(Version),
130 Range(Version, Version),
132 Any(Vec<Version>),
134}
135
136impl Version {
137 pub fn new(year: u16, month: u8, day: u8) -> Result<Self, VersionError> {
144 if !(1..=12).contains(&month) {
145 return Err(VersionError::InvalidMonth(month.to_string()));
146 }
147
148 if !(1..=31).contains(&day) {
149 return Err(VersionError::InvalidDay(day.to_string()));
150 }
151
152 if month == 2 && day > 29 {
154 return Err(VersionError::InvalidDay(format!(
155 "{} (invalid for February)",
156 day
157 )));
158 }
159
160 if matches!(month, 4 | 6 | 9 | 11) && day > 30 {
161 return Err(VersionError::InvalidDay(format!(
162 "{} (month {} only has 30 days)",
163 day, month
164 )));
165 }
166
167 Ok(Self { year, month, day })
168 }
169
170 pub fn stable() -> Self {
175 Self {
176 year: 2025,
177 month: 6,
178 day: 18,
179 }
180 }
181
182 pub fn latest() -> Self {
187 Self {
188 year: 2025,
189 month: 11,
190 day: 25,
191 }
192 }
193
194 pub fn is_newer_than(&self, other: &Version) -> bool {
196 self > other
197 }
198
199 pub fn is_older_than(&self, other: &Version) -> bool {
201 self < other
202 }
203
204 pub fn is_compatible_with(&self, other: &Version) -> bool {
206 self.year == other.year
209 }
210
211 pub fn to_date_string(&self) -> String {
213 format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
214 }
215
216 pub fn from_date_string(s: &str) -> Result<Self, VersionError> {
223 s.parse()
224 }
225
226 pub fn known_versions() -> Vec<Version> {
228 vec![
229 Version::new(2025, 11, 25).unwrap(), Version::new(2025, 6, 18).unwrap(), Version::new(2025, 3, 26).unwrap(), Version::new(2024, 11, 5).unwrap(), ]
234 }
235}
236
237impl fmt::Display for Version {
238 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239 write!(f, "{}", self.to_date_string())
240 }
241}
242
243impl FromStr for Version {
244 type Err = VersionError;
245
246 fn from_str(s: &str) -> Result<Self, Self::Err> {
247 let parts: Vec<&str> = s.split('-').collect();
248
249 if parts.len() != 3 {
250 return Err(VersionError::InvalidFormat(s.to_string()));
251 }
252
253 let year = parts[0]
254 .parse::<u16>()
255 .map_err(|_| VersionError::InvalidYear(parts[0].to_string()))?;
256
257 let month = parts[1]
258 .parse::<u8>()
259 .map_err(|_| VersionError::InvalidMonth(parts[1].to_string()))?;
260
261 let day = parts[2]
262 .parse::<u8>()
263 .map_err(|_| VersionError::InvalidDay(parts[2].to_string()))?;
264
265 Self::new(year, month, day)
266 }
267}
268
269impl PartialOrd for Version {
270 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
271 Some(self.cmp(other))
272 }
273}
274
275impl Ord for Version {
276 fn cmp(&self, other: &Self) -> Ordering {
277 (self.year, self.month, self.day).cmp(&(other.year, other.month, other.day))
278 }
279}
280
281impl VersionManager {
282 pub fn new(supported_versions: Vec<Version>) -> Result<Self, VersionError> {
288 if supported_versions.is_empty() {
289 return Err(VersionError::NoSupportedVersions);
290 }
291
292 let mut versions = supported_versions;
293 versions.sort_by(|a, b| b.cmp(a)); let current_version = versions[0].clone();
296
297 Ok(Self {
298 supported_versions: versions,
299 current_version,
300 })
301 }
302
303 pub fn with_default_versions() -> Self {
305 Self::new(Version::known_versions()).unwrap()
306 }
307 pub fn current_version(&self) -> &Version {
309 &self.current_version
310 }
311
312 pub fn supported_versions(&self) -> &[Version] {
314 &self.supported_versions
315 }
316
317 pub fn is_version_supported(&self, version: &Version) -> bool {
319 self.supported_versions.contains(version)
320 }
321
322 pub fn negotiate_version(&self, client_versions: &[Version]) -> Option<Version> {
324 for server_version in &self.supported_versions {
326 if client_versions.contains(server_version) {
327 return Some(server_version.clone());
328 }
329 }
330
331 None
332 }
333
334 pub fn check_compatibility(
336 &self,
337 client_version: &Version,
338 server_version: &Version,
339 ) -> VersionCompatibility {
340 if client_version == server_version {
341 return VersionCompatibility::Compatible;
342 }
343
344 if client_version.year == server_version.year {
346 let warning = format!(
347 "Version mismatch but compatible: client={client_version}, server={server_version}"
348 );
349 return VersionCompatibility::CompatibleWithWarnings(vec![warning]);
350 }
351
352 let reason =
354 format!("Incompatible versions: client={client_version}, server={server_version}");
355 VersionCompatibility::Incompatible(reason)
356 }
357
358 pub fn minimum_version(&self) -> &Version {
360 self.supported_versions
362 .last()
363 .expect("BUG: VersionManager has no versions (constructor should prevent this)")
364 }
365
366 pub fn maximum_version(&self) -> &Version {
368 &self.supported_versions[0] }
370
371 pub fn satisfies_requirement(
373 &self,
374 version: &Version,
375 requirement: &VersionRequirement,
376 ) -> bool {
377 match requirement {
378 VersionRequirement::Exact(required) => version == required,
379 VersionRequirement::Minimum(min) => version >= min,
380 VersionRequirement::Maximum(max) => version <= max,
381 VersionRequirement::Range(min, max) => version >= min && version <= max,
382 VersionRequirement::Any(versions) => versions.contains(version),
383 }
384 }
385}
386
387impl Default for VersionManager {
388 fn default() -> Self {
389 Self::with_default_versions()
390 }
391}
392
393impl VersionRequirement {
394 pub fn exact(version: Version) -> Self {
396 Self::Exact(version)
397 }
398
399 pub fn minimum(version: Version) -> Self {
401 Self::Minimum(version)
402 }
403
404 pub fn maximum(version: Version) -> Self {
406 Self::Maximum(version)
407 }
408
409 pub fn range(min: Version, max: Version) -> Result<Self, VersionError> {
415 if min > max {
416 return Err(VersionError::InvalidRange(min, max));
417 }
418 Ok(Self::Range(min, max))
419 }
420
421 pub fn any(versions: Vec<Version>) -> Result<Self, VersionError> {
427 if versions.is_empty() {
428 return Err(VersionError::EmptyVersionList);
429 }
430 Ok(Self::Any(versions))
431 }
432
433 pub fn is_satisfied_by(&self, version: &Version) -> bool {
435 match self {
436 Self::Exact(required) => version == required,
437 Self::Minimum(min) => version >= min,
438 Self::Maximum(max) => version <= max,
439 Self::Range(min, max) => version >= min && version <= max,
440 Self::Any(versions) => versions.contains(version),
441 }
442 }
443}
444
445#[derive(Debug, Clone, thiserror::Error)]
447pub enum VersionError {
448 #[error("Invalid version format: {0}")]
450 InvalidFormat(String),
451 #[error("Invalid year: {0}")]
453 InvalidYear(String),
454 #[error("Invalid month: {0} (must be 1-12)")]
456 InvalidMonth(String),
457 #[error("Invalid day: {0} (must be 1-31)")]
459 InvalidDay(String),
460 #[error("No supported versions provided")]
462 NoSupportedVersions,
463 #[error("Invalid version range: {0} > {1}")]
465 InvalidRange(Version, Version),
466 #[error("Empty version list")]
468 EmptyVersionList,
469}
470
471pub mod utils {
473 use super::*;
474
475 pub fn parse_versions(version_strings: &[&str]) -> Result<Vec<Version>, VersionError> {
481 version_strings.iter().map(|s| s.parse()).collect()
482 }
483
484 pub fn newest_version(versions: &[Version]) -> Option<&Version> {
486 versions.iter().max()
487 }
488
489 pub fn oldest_version(versions: &[Version]) -> Option<&Version> {
491 versions.iter().min()
492 }
493
494 pub fn are_all_compatible(versions: &[Version]) -> bool {
496 if versions.len() < 2 {
497 return true;
498 }
499
500 let first = &versions[0];
501 versions.iter().all(|v| first.is_compatible_with(v))
502 }
503
504 pub fn compatibility_description(compatibility: &VersionCompatibility) -> String {
506 match compatibility {
507 VersionCompatibility::Compatible => "Fully compatible".to_string(),
508 VersionCompatibility::CompatibleWithWarnings(warnings) => {
509 format!("Compatible with warnings: {}", warnings.join(", "))
510 }
511 VersionCompatibility::Incompatible(reason) => {
512 format!("Incompatible: {reason}")
513 }
514 }
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521 use proptest::prelude::*;
522
523 #[test]
524 fn test_version_creation() {
525 let version = Version::new(2025, 6, 18).unwrap();
526 assert_eq!(version.year, 2025);
527 assert_eq!(version.month, 6);
528 assert_eq!(version.day, 18);
529
530 assert!(Version::new(2025, 13, 18).is_err());
532
533 assert!(Version::new(2025, 6, 32).is_err());
535 }
536
537 #[test]
538 fn test_version_parsing() {
539 let version: Version = "2025-06-18".parse().unwrap();
540 assert_eq!(version, Version::new(2025, 6, 18).unwrap());
541
542 assert!("2025/06/18".parse::<Version>().is_err());
544 assert!("invalid".parse::<Version>().is_err());
545 }
546
547 #[test]
548 fn test_version_comparison() {
549 let v1 = Version::new(2025, 6, 18).unwrap();
550 let v2 = Version::new(2024, 11, 5).unwrap();
551 let v3 = Version::new(2025, 6, 18).unwrap();
552
553 assert!(v1 > v2);
554 assert!(v1.is_newer_than(&v2));
555 assert!(v2.is_older_than(&v1));
556 assert_eq!(v1, v3);
557 }
558
559 #[test]
560 fn test_version_compatibility() {
561 let v1 = Version::new(2025, 6, 18).unwrap();
562 let v2 = Version::new(2025, 12, 1).unwrap(); let v3 = Version::new(2024, 6, 18).unwrap(); assert!(v1.is_compatible_with(&v2));
566 assert!(!v1.is_compatible_with(&v3));
567 }
568
569 #[test]
570 fn test_version_manager() {
571 let versions = vec![
572 Version::new(2025, 6, 18).unwrap(),
573 Version::new(2024, 11, 5).unwrap(),
574 ];
575
576 let manager = VersionManager::new(versions).unwrap();
577
578 assert_eq!(
579 manager.current_version(),
580 &Version::new(2025, 6, 18).unwrap()
581 );
582 assert!(manager.is_version_supported(&Version::new(2024, 11, 5).unwrap()));
583 assert!(!manager.is_version_supported(&Version::new(2023, 1, 1).unwrap()));
584 }
585
586 #[test]
587 fn test_version_negotiation() {
588 let manager = VersionManager::default();
589
590 let client_versions = vec![
591 Version::new(2024, 11, 5).unwrap(),
592 Version::new(2025, 6, 18).unwrap(),
593 ];
594
595 let negotiated = manager.negotiate_version(&client_versions);
596 assert_eq!(negotiated, Some(Version::new(2025, 6, 18).unwrap()));
597 }
598
599 #[test]
600 fn test_version_requirements() {
601 let version = Version::new(2025, 6, 18).unwrap();
602
603 let exact_req = VersionRequirement::exact(version.clone());
604 assert!(exact_req.is_satisfied_by(&version));
605
606 let min_req = VersionRequirement::minimum(Version::new(2024, 1, 1).unwrap());
607 assert!(min_req.is_satisfied_by(&version));
608
609 let max_req = VersionRequirement::maximum(Version::new(2024, 1, 1).unwrap());
610 assert!(!max_req.is_satisfied_by(&version));
611 }
612
613 #[test]
614 fn test_compatibility_checking() {
615 let manager = VersionManager::default();
616
617 let v1 = Version::new(2025, 6, 18).unwrap();
618 let v2 = Version::new(2025, 12, 1).unwrap();
619 let v3 = Version::new(2024, 1, 1).unwrap();
620
621 let compat = manager.check_compatibility(&v1, &v2);
623 assert!(matches!(
624 compat,
625 VersionCompatibility::CompatibleWithWarnings(_)
626 ));
627
628 let compat = manager.check_compatibility(&v1, &v3);
630 assert!(matches!(compat, VersionCompatibility::Incompatible(_)));
631
632 let compat = manager.check_compatibility(&v1, &v1);
634 assert_eq!(compat, VersionCompatibility::Compatible);
635 }
636
637 #[test]
638 fn test_utils() {
639 let versions = utils::parse_versions(&["2025-06-18", "2024-11-05"]).unwrap();
640 assert_eq!(versions.len(), 2);
641
642 let newest = utils::newest_version(&versions);
643 assert_eq!(newest, Some(&Version::new(2025, 6, 18).unwrap()));
644
645 let oldest = utils::oldest_version(&versions);
646 assert_eq!(oldest, Some(&Version::new(2024, 11, 5).unwrap()));
647 }
648
649 proptest! {
651 #[test]
652 fn test_version_parse_roundtrip(
653 year in 2020u16..2030u16,
654 month in 1u8..=12u8,
655 day in 1u8..=28u8, ) {
657 let version = Version::new(year, month, day)?;
658 let string = version.to_date_string();
659 let parsed = Version::from_date_string(&string)?;
660 prop_assert_eq!(version, parsed);
661 }
662
663 #[test]
664 fn test_version_comparison_transitive(
665 y1 in 2020u16..2030u16,
666 m1 in 1u8..=12u8,
667 d1 in 1u8..=28u8,
668 y2 in 2020u16..2030u16,
669 m2 in 1u8..=12u8,
670 d2 in 1u8..=28u8,
671 y3 in 2020u16..2030u16,
672 m3 in 1u8..=12u8,
673 d3 in 1u8..=28u8,
674 ) {
675 let v1 = Version::new(y1, m1, d1)?;
676 let v2 = Version::new(y2, m2, d2)?;
677 let v3 = Version::new(y3, m3, d3)?;
678
679 if v1 < v2 && v2 < v3 {
681 prop_assert!(v1 < v3);
682 }
683 }
684
685 #[test]
686 fn test_version_compatibility_symmetric(
687 year in 2020u16..2030u16,
688 m1 in 1u8..=12u8,
689 d1 in 1u8..=28u8,
690 m2 in 1u8..=12u8,
691 d2 in 1u8..=28u8,
692 ) {
693 let v1 = Version::new(year, m1, d1)?;
694 let v2 = Version::new(year, m2, d2)?;
695
696 prop_assert_eq!(v1.is_compatible_with(&v2), v2.is_compatible_with(&v1));
698 }
699
700 #[test]
701 fn test_invalid_month_rejected(
702 year in 2020u16..2030u16,
703 month in 13u8..=255u8,
704 day in 1u8..=28u8,
705 ) {
706 prop_assert!(Version::new(year, month, day).is_err());
707 }
708
709 #[test]
710 fn test_invalid_day_rejected(
711 year in 2020u16..2030u16,
712 month in 1u8..=12u8,
713 day in 32u8..=255u8,
714 ) {
715 prop_assert!(Version::new(year, month, day).is_err());
716 }
717 }
718}