1use serde::{Deserialize, Serialize};
85use std::cmp::Ordering;
86use std::fmt;
87use std::str::FromStr;
88
89#[derive(Debug, Clone)]
91pub struct VersionManager {
92 supported_versions: Vec<Version>,
94 current_version: Version,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100pub struct Version {
101 pub year: u16,
103 pub month: u8,
105 pub day: u8,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
111pub enum VersionCompatibility {
112 Compatible,
114 CompatibleWithWarnings(Vec<String>),
116 Incompatible(String),
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
122pub enum VersionRequirement {
123 Exact(Version),
125 Minimum(Version),
127 Maximum(Version),
129 Range(Version, Version),
131 Any(Vec<Version>),
133}
134
135impl Version {
136 pub fn new(year: u16, month: u8, day: u8) -> Result<Self, VersionError> {
143 if !(1..=12).contains(&month) {
144 return Err(VersionError::InvalidMonth(month.to_string()));
145 }
146
147 if !(1..=31).contains(&day) {
148 return Err(VersionError::InvalidDay(day.to_string()));
149 }
150
151 if month == 2 && day > 29 {
153 return Err(VersionError::InvalidDay(format!(
154 "{} (invalid for February)",
155 day
156 )));
157 }
158
159 if matches!(month, 4 | 6 | 9 | 11) && day > 30 {
160 return Err(VersionError::InvalidDay(format!(
161 "{} (month {} only has 30 days)",
162 day, month
163 )));
164 }
165
166 Ok(Self { year, month, day })
167 }
168
169 pub fn stable() -> Self {
174 Self {
175 year: 2025,
176 month: 6,
177 day: 18,
178 }
179 }
180
181 #[deprecated(since = "2.4.0", note = "Use `stable()` or `latest()` instead")]
183 pub fn current() -> Self {
184 Self::stable()
185 }
186
187 pub fn latest() -> Self {
192 Self {
193 year: 2025,
194 month: 11,
195 day: 25,
196 }
197 }
198
199 #[deprecated(
201 since = "2.4.0",
202 note = "Use `latest()` instead - 2025-11-25 is now official"
203 )]
204 pub fn draft() -> Self {
205 Self::latest()
206 }
207
208 pub fn is_newer_than(&self, other: &Version) -> bool {
210 self > other
211 }
212
213 pub fn is_older_than(&self, other: &Version) -> bool {
215 self < other
216 }
217
218 pub fn is_compatible_with(&self, other: &Version) -> bool {
220 self.year == other.year
223 }
224
225 pub fn to_date_string(&self) -> String {
227 format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
228 }
229
230 pub fn from_date_string(s: &str) -> Result<Self, VersionError> {
237 s.parse()
238 }
239
240 pub fn known_versions() -> Vec<Version> {
242 vec![
243 Version::new(2025, 11, 25).unwrap(), Version::new(2025, 6, 18).unwrap(), Version::new(2025, 3, 26).unwrap(), Version::new(2024, 11, 5).unwrap(), ]
248 }
249}
250
251impl fmt::Display for Version {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253 write!(f, "{}", self.to_date_string())
254 }
255}
256
257impl FromStr for Version {
258 type Err = VersionError;
259
260 fn from_str(s: &str) -> Result<Self, Self::Err> {
261 let parts: Vec<&str> = s.split('-').collect();
262
263 if parts.len() != 3 {
264 return Err(VersionError::InvalidFormat(s.to_string()));
265 }
266
267 let year = parts[0]
268 .parse::<u16>()
269 .map_err(|_| VersionError::InvalidYear(parts[0].to_string()))?;
270
271 let month = parts[1]
272 .parse::<u8>()
273 .map_err(|_| VersionError::InvalidMonth(parts[1].to_string()))?;
274
275 let day = parts[2]
276 .parse::<u8>()
277 .map_err(|_| VersionError::InvalidDay(parts[2].to_string()))?;
278
279 Self::new(year, month, day)
280 }
281}
282
283impl PartialOrd for Version {
284 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
285 Some(self.cmp(other))
286 }
287}
288
289impl Ord for Version {
290 fn cmp(&self, other: &Self) -> Ordering {
291 (self.year, self.month, self.day).cmp(&(other.year, other.month, other.day))
292 }
293}
294
295impl VersionManager {
296 pub fn new(supported_versions: Vec<Version>) -> Result<Self, VersionError> {
302 if supported_versions.is_empty() {
303 return Err(VersionError::NoSupportedVersions);
304 }
305
306 let mut versions = supported_versions;
307 versions.sort_by(|a, b| b.cmp(a)); let current_version = versions[0].clone();
310
311 Ok(Self {
312 supported_versions: versions,
313 current_version,
314 })
315 }
316
317 pub fn with_default_versions() -> Self {
319 Self::new(Version::known_versions()).unwrap()
320 }
321 pub fn current_version(&self) -> &Version {
323 &self.current_version
324 }
325
326 pub fn supported_versions(&self) -> &[Version] {
328 &self.supported_versions
329 }
330
331 pub fn is_version_supported(&self, version: &Version) -> bool {
333 self.supported_versions.contains(version)
334 }
335
336 pub fn negotiate_version(&self, client_versions: &[Version]) -> Option<Version> {
338 for server_version in &self.supported_versions {
340 if client_versions.contains(server_version) {
341 return Some(server_version.clone());
342 }
343 }
344
345 None
346 }
347
348 pub fn check_compatibility(
350 &self,
351 client_version: &Version,
352 server_version: &Version,
353 ) -> VersionCompatibility {
354 if client_version == server_version {
355 return VersionCompatibility::Compatible;
356 }
357
358 if client_version.year == server_version.year {
360 let warning = format!(
361 "Version mismatch but compatible: client={client_version}, server={server_version}"
362 );
363 return VersionCompatibility::CompatibleWithWarnings(vec![warning]);
364 }
365
366 let reason =
368 format!("Incompatible versions: client={client_version}, server={server_version}");
369 VersionCompatibility::Incompatible(reason)
370 }
371
372 pub fn minimum_version(&self) -> &Version {
374 self.supported_versions
376 .last()
377 .expect("BUG: VersionManager has no versions (constructor should prevent this)")
378 }
379
380 pub fn maximum_version(&self) -> &Version {
382 &self.supported_versions[0] }
384
385 pub fn satisfies_requirement(
387 &self,
388 version: &Version,
389 requirement: &VersionRequirement,
390 ) -> bool {
391 match requirement {
392 VersionRequirement::Exact(required) => version == required,
393 VersionRequirement::Minimum(min) => version >= min,
394 VersionRequirement::Maximum(max) => version <= max,
395 VersionRequirement::Range(min, max) => version >= min && version <= max,
396 VersionRequirement::Any(versions) => versions.contains(version),
397 }
398 }
399}
400
401impl Default for VersionManager {
402 fn default() -> Self {
403 Self::with_default_versions()
404 }
405}
406
407impl VersionRequirement {
408 pub fn exact(version: Version) -> Self {
410 Self::Exact(version)
411 }
412
413 pub fn minimum(version: Version) -> Self {
415 Self::Minimum(version)
416 }
417
418 pub fn maximum(version: Version) -> Self {
420 Self::Maximum(version)
421 }
422
423 pub fn range(min: Version, max: Version) -> Result<Self, VersionError> {
429 if min > max {
430 return Err(VersionError::InvalidRange(min, max));
431 }
432 Ok(Self::Range(min, max))
433 }
434
435 pub fn any(versions: Vec<Version>) -> Result<Self, VersionError> {
441 if versions.is_empty() {
442 return Err(VersionError::EmptyVersionList);
443 }
444 Ok(Self::Any(versions))
445 }
446
447 pub fn is_satisfied_by(&self, version: &Version) -> bool {
449 match self {
450 Self::Exact(required) => version == required,
451 Self::Minimum(min) => version >= min,
452 Self::Maximum(max) => version <= max,
453 Self::Range(min, max) => version >= min && version <= max,
454 Self::Any(versions) => versions.contains(version),
455 }
456 }
457}
458
459#[derive(Debug, Clone, thiserror::Error)]
461pub enum VersionError {
462 #[error("Invalid version format: {0}")]
464 InvalidFormat(String),
465 #[error("Invalid year: {0}")]
467 InvalidYear(String),
468 #[error("Invalid month: {0} (must be 1-12)")]
470 InvalidMonth(String),
471 #[error("Invalid day: {0} (must be 1-31)")]
473 InvalidDay(String),
474 #[error("No supported versions provided")]
476 NoSupportedVersions,
477 #[error("Invalid version range: {0} > {1}")]
479 InvalidRange(Version, Version),
480 #[error("Empty version list")]
482 EmptyVersionList,
483}
484
485pub mod utils {
487 use super::*;
488
489 pub fn parse_versions(version_strings: &[&str]) -> Result<Vec<Version>, VersionError> {
495 version_strings.iter().map(|s| s.parse()).collect()
496 }
497
498 pub fn newest_version(versions: &[Version]) -> Option<&Version> {
500 versions.iter().max()
501 }
502
503 pub fn oldest_version(versions: &[Version]) -> Option<&Version> {
505 versions.iter().min()
506 }
507
508 pub fn are_all_compatible(versions: &[Version]) -> bool {
510 if versions.len() < 2 {
511 return true;
512 }
513
514 let first = &versions[0];
515 versions.iter().all(|v| first.is_compatible_with(v))
516 }
517
518 pub fn compatibility_description(compatibility: &VersionCompatibility) -> String {
520 match compatibility {
521 VersionCompatibility::Compatible => "Fully compatible".to_string(),
522 VersionCompatibility::CompatibleWithWarnings(warnings) => {
523 format!("Compatible with warnings: {}", warnings.join(", "))
524 }
525 VersionCompatibility::Incompatible(reason) => {
526 format!("Incompatible: {reason}")
527 }
528 }
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535 use proptest::prelude::*;
536
537 #[test]
538 fn test_version_creation() {
539 let version = Version::new(2025, 6, 18).unwrap();
540 assert_eq!(version.year, 2025);
541 assert_eq!(version.month, 6);
542 assert_eq!(version.day, 18);
543
544 assert!(Version::new(2025, 13, 18).is_err());
546
547 assert!(Version::new(2025, 6, 32).is_err());
549 }
550
551 #[test]
552 fn test_version_parsing() {
553 let version: Version = "2025-06-18".parse().unwrap();
554 assert_eq!(version, Version::new(2025, 6, 18).unwrap());
555
556 assert!("2025/06/18".parse::<Version>().is_err());
558 assert!("invalid".parse::<Version>().is_err());
559 }
560
561 #[test]
562 fn test_version_comparison() {
563 let v1 = Version::new(2025, 6, 18).unwrap();
564 let v2 = Version::new(2024, 11, 5).unwrap();
565 let v3 = Version::new(2025, 6, 18).unwrap();
566
567 assert!(v1 > v2);
568 assert!(v1.is_newer_than(&v2));
569 assert!(v2.is_older_than(&v1));
570 assert_eq!(v1, v3);
571 }
572
573 #[test]
574 fn test_version_compatibility() {
575 let v1 = Version::new(2025, 6, 18).unwrap();
576 let v2 = Version::new(2025, 12, 1).unwrap(); let v3 = Version::new(2024, 6, 18).unwrap(); assert!(v1.is_compatible_with(&v2));
580 assert!(!v1.is_compatible_with(&v3));
581 }
582
583 #[test]
584 fn test_version_manager() {
585 let versions = vec![
586 Version::new(2025, 6, 18).unwrap(),
587 Version::new(2024, 11, 5).unwrap(),
588 ];
589
590 let manager = VersionManager::new(versions).unwrap();
591
592 assert_eq!(
593 manager.current_version(),
594 &Version::new(2025, 6, 18).unwrap()
595 );
596 assert!(manager.is_version_supported(&Version::new(2024, 11, 5).unwrap()));
597 assert!(!manager.is_version_supported(&Version::new(2023, 1, 1).unwrap()));
598 }
599
600 #[test]
601 fn test_version_negotiation() {
602 let manager = VersionManager::default();
603
604 let client_versions = vec![
605 Version::new(2024, 11, 5).unwrap(),
606 Version::new(2025, 6, 18).unwrap(),
607 ];
608
609 let negotiated = manager.negotiate_version(&client_versions);
610 assert_eq!(negotiated, Some(Version::new(2025, 6, 18).unwrap()));
611 }
612
613 #[test]
614 fn test_version_requirements() {
615 let version = Version::new(2025, 6, 18).unwrap();
616
617 let exact_req = VersionRequirement::exact(version.clone());
618 assert!(exact_req.is_satisfied_by(&version));
619
620 let min_req = VersionRequirement::minimum(Version::new(2024, 1, 1).unwrap());
621 assert!(min_req.is_satisfied_by(&version));
622
623 let max_req = VersionRequirement::maximum(Version::new(2024, 1, 1).unwrap());
624 assert!(!max_req.is_satisfied_by(&version));
625 }
626
627 #[test]
628 fn test_compatibility_checking() {
629 let manager = VersionManager::default();
630
631 let v1 = Version::new(2025, 6, 18).unwrap();
632 let v2 = Version::new(2025, 12, 1).unwrap();
633 let v3 = Version::new(2024, 1, 1).unwrap();
634
635 let compat = manager.check_compatibility(&v1, &v2);
637 assert!(matches!(
638 compat,
639 VersionCompatibility::CompatibleWithWarnings(_)
640 ));
641
642 let compat = manager.check_compatibility(&v1, &v3);
644 assert!(matches!(compat, VersionCompatibility::Incompatible(_)));
645
646 let compat = manager.check_compatibility(&v1, &v1);
648 assert_eq!(compat, VersionCompatibility::Compatible);
649 }
650
651 #[test]
652 fn test_utils() {
653 let versions = utils::parse_versions(&["2025-06-18", "2024-11-05"]).unwrap();
654 assert_eq!(versions.len(), 2);
655
656 let newest = utils::newest_version(&versions);
657 assert_eq!(newest, Some(&Version::new(2025, 6, 18).unwrap()));
658
659 let oldest = utils::oldest_version(&versions);
660 assert_eq!(oldest, Some(&Version::new(2024, 11, 5).unwrap()));
661 }
662
663 proptest! {
665 #[test]
666 fn test_version_parse_roundtrip(
667 year in 2020u16..2030u16,
668 month in 1u8..=12u8,
669 day in 1u8..=28u8, ) {
671 let version = Version::new(year, month, day)?;
672 let string = version.to_date_string();
673 let parsed = Version::from_date_string(&string)?;
674 prop_assert_eq!(version, parsed);
675 }
676
677 #[test]
678 fn test_version_comparison_transitive(
679 y1 in 2020u16..2030u16,
680 m1 in 1u8..=12u8,
681 d1 in 1u8..=28u8,
682 y2 in 2020u16..2030u16,
683 m2 in 1u8..=12u8,
684 d2 in 1u8..=28u8,
685 y3 in 2020u16..2030u16,
686 m3 in 1u8..=12u8,
687 d3 in 1u8..=28u8,
688 ) {
689 let v1 = Version::new(y1, m1, d1)?;
690 let v2 = Version::new(y2, m2, d2)?;
691 let v3 = Version::new(y3, m3, d3)?;
692
693 if v1 < v2 && v2 < v3 {
695 prop_assert!(v1 < v3);
696 }
697 }
698
699 #[test]
700 fn test_version_compatibility_symmetric(
701 year in 2020u16..2030u16,
702 m1 in 1u8..=12u8,
703 d1 in 1u8..=28u8,
704 m2 in 1u8..=12u8,
705 d2 in 1u8..=28u8,
706 ) {
707 let v1 = Version::new(year, m1, d1)?;
708 let v2 = Version::new(year, m2, d2)?;
709
710 prop_assert_eq!(v1.is_compatible_with(&v2), v2.is_compatible_with(&v1));
712 }
713
714 #[test]
715 fn test_invalid_month_rejected(
716 year in 2020u16..2030u16,
717 month in 13u8..=255u8,
718 day in 1u8..=28u8,
719 ) {
720 prop_assert!(Version::new(year, month, day).is_err());
721 }
722
723 #[test]
724 fn test_invalid_day_rejected(
725 year in 2020u16..2030u16,
726 month in 1u8..=12u8,
727 day in 32u8..=255u8,
728 ) {
729 prop_assert!(Version::new(year, month, day).is_err());
730 }
731 }
732}