1use serde::{Deserialize, Serialize};
54use std::cmp::Ordering;
55use std::fmt;
56use std::str::FromStr;
57
58#[derive(Debug, Clone)]
60pub struct VersionManager {
61 supported_versions: Vec<Version>,
63 current_version: Version,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69pub struct Version {
70 pub year: u16,
72 pub month: u8,
74 pub day: u8,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum VersionCompatibility {
81 Compatible,
83 CompatibleWithWarnings(Vec<String>),
85 Incompatible(String),
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
91pub enum VersionRequirement {
92 Exact(Version),
94 Minimum(Version),
96 Maximum(Version),
98 Range(Version, Version),
100 Any(Vec<Version>),
102}
103
104impl Version {
105 pub fn new(year: u16, month: u8, day: u8) -> Result<Self, VersionError> {
112 if !(1..=12).contains(&month) {
113 return Err(VersionError::InvalidMonth(month.to_string()));
114 }
115
116 if !(1..=31).contains(&day) {
117 return Err(VersionError::InvalidDay(day.to_string()));
118 }
119
120 if month == 2 && day > 29 {
122 return Err(VersionError::InvalidDay(format!(
123 "{} (invalid for February)",
124 day
125 )));
126 }
127
128 if matches!(month, 4 | 6 | 9 | 11) && day > 30 {
129 return Err(VersionError::InvalidDay(format!(
130 "{} (month {} only has 30 days)",
131 day, month
132 )));
133 }
134
135 Ok(Self { year, month, day })
136 }
137
138 pub fn current() -> Self {
140 Self {
141 year: 2025,
142 month: 6,
143 day: 18,
144 }
145 }
146
147 pub fn draft() -> Self {
151 Self {
152 year: 2025,
153 month: 11,
154 day: 25,
155 }
156 }
157
158 pub fn is_newer_than(&self, other: &Version) -> bool {
160 self > other
161 }
162
163 pub fn is_older_than(&self, other: &Version) -> bool {
165 self < other
166 }
167
168 pub fn is_compatible_with(&self, other: &Version) -> bool {
170 self.year == other.year
173 }
174
175 pub fn to_date_string(&self) -> String {
177 format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
178 }
179
180 pub fn from_date_string(s: &str) -> Result<Self, VersionError> {
187 s.parse()
188 }
189
190 pub fn known_versions() -> Vec<Version> {
192 vec![
193 Version::new(2025, 11, 25).unwrap(), Version::new(2025, 6, 18).unwrap(), Version::new(2024, 11, 5).unwrap(), Version::new(2024, 6, 25).unwrap(), ]
198 }
199}
200
201impl fmt::Display for Version {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 write!(f, "{}", self.to_date_string())
204 }
205}
206
207impl FromStr for Version {
208 type Err = VersionError;
209
210 fn from_str(s: &str) -> Result<Self, Self::Err> {
211 let parts: Vec<&str> = s.split('-').collect();
212
213 if parts.len() != 3 {
214 return Err(VersionError::InvalidFormat(s.to_string()));
215 }
216
217 let year = parts[0]
218 .parse::<u16>()
219 .map_err(|_| VersionError::InvalidYear(parts[0].to_string()))?;
220
221 let month = parts[1]
222 .parse::<u8>()
223 .map_err(|_| VersionError::InvalidMonth(parts[1].to_string()))?;
224
225 let day = parts[2]
226 .parse::<u8>()
227 .map_err(|_| VersionError::InvalidDay(parts[2].to_string()))?;
228
229 Self::new(year, month, day)
230 }
231}
232
233impl PartialOrd for Version {
234 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
235 Some(self.cmp(other))
236 }
237}
238
239impl Ord for Version {
240 fn cmp(&self, other: &Self) -> Ordering {
241 (self.year, self.month, self.day).cmp(&(other.year, other.month, other.day))
242 }
243}
244
245impl VersionManager {
246 pub fn new(supported_versions: Vec<Version>) -> Result<Self, VersionError> {
252 if supported_versions.is_empty() {
253 return Err(VersionError::NoSupportedVersions);
254 }
255
256 let mut versions = supported_versions;
257 versions.sort_by(|a, b| b.cmp(a)); let current_version = versions[0].clone();
260
261 Ok(Self {
262 supported_versions: versions,
263 current_version,
264 })
265 }
266
267 pub fn with_default_versions() -> Self {
269 Self::new(Version::known_versions()).unwrap()
270 }
271 pub fn current_version(&self) -> &Version {
273 &self.current_version
274 }
275
276 pub fn supported_versions(&self) -> &[Version] {
278 &self.supported_versions
279 }
280
281 pub fn is_version_supported(&self, version: &Version) -> bool {
283 self.supported_versions.contains(version)
284 }
285
286 pub fn negotiate_version(&self, client_versions: &[Version]) -> Option<Version> {
288 for server_version in &self.supported_versions {
290 if client_versions.contains(server_version) {
291 return Some(server_version.clone());
292 }
293 }
294
295 None
296 }
297
298 pub fn check_compatibility(
300 &self,
301 client_version: &Version,
302 server_version: &Version,
303 ) -> VersionCompatibility {
304 if client_version == server_version {
305 return VersionCompatibility::Compatible;
306 }
307
308 if client_version.year == server_version.year {
310 let warning = format!(
311 "Version mismatch but compatible: client={client_version}, server={server_version}"
312 );
313 return VersionCompatibility::CompatibleWithWarnings(vec![warning]);
314 }
315
316 let reason =
318 format!("Incompatible versions: client={client_version}, server={server_version}");
319 VersionCompatibility::Incompatible(reason)
320 }
321
322 pub fn minimum_version(&self) -> &Version {
324 self.supported_versions
326 .last()
327 .expect("BUG: VersionManager has no versions (constructor should prevent this)")
328 }
329
330 pub fn maximum_version(&self) -> &Version {
332 &self.supported_versions[0] }
334
335 pub fn satisfies_requirement(
337 &self,
338 version: &Version,
339 requirement: &VersionRequirement,
340 ) -> bool {
341 match requirement {
342 VersionRequirement::Exact(required) => version == required,
343 VersionRequirement::Minimum(min) => version >= min,
344 VersionRequirement::Maximum(max) => version <= max,
345 VersionRequirement::Range(min, max) => version >= min && version <= max,
346 VersionRequirement::Any(versions) => versions.contains(version),
347 }
348 }
349}
350
351impl Default for VersionManager {
352 fn default() -> Self {
353 Self::with_default_versions()
354 }
355}
356
357impl VersionRequirement {
358 pub fn exact(version: Version) -> Self {
360 Self::Exact(version)
361 }
362
363 pub fn minimum(version: Version) -> Self {
365 Self::Minimum(version)
366 }
367
368 pub fn maximum(version: Version) -> Self {
370 Self::Maximum(version)
371 }
372
373 pub fn range(min: Version, max: Version) -> Result<Self, VersionError> {
379 if min > max {
380 return Err(VersionError::InvalidRange(min, max));
381 }
382 Ok(Self::Range(min, max))
383 }
384
385 pub fn any(versions: Vec<Version>) -> Result<Self, VersionError> {
391 if versions.is_empty() {
392 return Err(VersionError::EmptyVersionList);
393 }
394 Ok(Self::Any(versions))
395 }
396
397 pub fn is_satisfied_by(&self, version: &Version) -> bool {
399 match self {
400 Self::Exact(required) => version == required,
401 Self::Minimum(min) => version >= min,
402 Self::Maximum(max) => version <= max,
403 Self::Range(min, max) => version >= min && version <= max,
404 Self::Any(versions) => versions.contains(version),
405 }
406 }
407}
408
409#[derive(Debug, Clone, thiserror::Error)]
411pub enum VersionError {
412 #[error("Invalid version format: {0}")]
414 InvalidFormat(String),
415 #[error("Invalid year: {0}")]
417 InvalidYear(String),
418 #[error("Invalid month: {0} (must be 1-12)")]
420 InvalidMonth(String),
421 #[error("Invalid day: {0} (must be 1-31)")]
423 InvalidDay(String),
424 #[error("No supported versions provided")]
426 NoSupportedVersions,
427 #[error("Invalid version range: {0} > {1}")]
429 InvalidRange(Version, Version),
430 #[error("Empty version list")]
432 EmptyVersionList,
433}
434
435pub mod utils {
437 use super::*;
438
439 pub fn parse_versions(version_strings: &[&str]) -> Result<Vec<Version>, VersionError> {
445 version_strings.iter().map(|s| s.parse()).collect()
446 }
447
448 pub fn newest_version(versions: &[Version]) -> Option<&Version> {
450 versions.iter().max()
451 }
452
453 pub fn oldest_version(versions: &[Version]) -> Option<&Version> {
455 versions.iter().min()
456 }
457
458 pub fn are_all_compatible(versions: &[Version]) -> bool {
460 if versions.len() < 2 {
461 return true;
462 }
463
464 let first = &versions[0];
465 versions.iter().all(|v| first.is_compatible_with(v))
466 }
467
468 pub fn compatibility_description(compatibility: &VersionCompatibility) -> String {
470 match compatibility {
471 VersionCompatibility::Compatible => "Fully compatible".to_string(),
472 VersionCompatibility::CompatibleWithWarnings(warnings) => {
473 format!("Compatible with warnings: {}", warnings.join(", "))
474 }
475 VersionCompatibility::Incompatible(reason) => {
476 format!("Incompatible: {reason}")
477 }
478 }
479 }
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485 use proptest::prelude::*;
486
487 #[test]
488 fn test_version_creation() {
489 let version = Version::new(2025, 6, 18).unwrap();
490 assert_eq!(version.year, 2025);
491 assert_eq!(version.month, 6);
492 assert_eq!(version.day, 18);
493
494 assert!(Version::new(2025, 13, 18).is_err());
496
497 assert!(Version::new(2025, 6, 32).is_err());
499 }
500
501 #[test]
502 fn test_version_parsing() {
503 let version: Version = "2025-06-18".parse().unwrap();
504 assert_eq!(version, Version::new(2025, 6, 18).unwrap());
505
506 assert!("2025/06/18".parse::<Version>().is_err());
508 assert!("invalid".parse::<Version>().is_err());
509 }
510
511 #[test]
512 fn test_version_comparison() {
513 let v1 = Version::new(2025, 6, 18).unwrap();
514 let v2 = Version::new(2024, 11, 5).unwrap();
515 let v3 = Version::new(2025, 6, 18).unwrap();
516
517 assert!(v1 > v2);
518 assert!(v1.is_newer_than(&v2));
519 assert!(v2.is_older_than(&v1));
520 assert_eq!(v1, v3);
521 }
522
523 #[test]
524 fn test_version_compatibility() {
525 let v1 = Version::new(2025, 6, 18).unwrap();
526 let v2 = Version::new(2025, 12, 1).unwrap(); let v3 = Version::new(2024, 6, 18).unwrap(); assert!(v1.is_compatible_with(&v2));
530 assert!(!v1.is_compatible_with(&v3));
531 }
532
533 #[test]
534 fn test_version_manager() {
535 let versions = vec![
536 Version::new(2025, 6, 18).unwrap(),
537 Version::new(2024, 11, 5).unwrap(),
538 ];
539
540 let manager = VersionManager::new(versions).unwrap();
541
542 assert_eq!(
543 manager.current_version(),
544 &Version::new(2025, 6, 18).unwrap()
545 );
546 assert!(manager.is_version_supported(&Version::new(2024, 11, 5).unwrap()));
547 assert!(!manager.is_version_supported(&Version::new(2023, 1, 1).unwrap()));
548 }
549
550 #[test]
551 fn test_version_negotiation() {
552 let manager = VersionManager::default();
553
554 let client_versions = vec![
555 Version::new(2024, 11, 5).unwrap(),
556 Version::new(2025, 6, 18).unwrap(),
557 ];
558
559 let negotiated = manager.negotiate_version(&client_versions);
560 assert_eq!(negotiated, Some(Version::new(2025, 6, 18).unwrap()));
561 }
562
563 #[test]
564 fn test_version_requirements() {
565 let version = Version::new(2025, 6, 18).unwrap();
566
567 let exact_req = VersionRequirement::exact(version.clone());
568 assert!(exact_req.is_satisfied_by(&version));
569
570 let min_req = VersionRequirement::minimum(Version::new(2024, 1, 1).unwrap());
571 assert!(min_req.is_satisfied_by(&version));
572
573 let max_req = VersionRequirement::maximum(Version::new(2024, 1, 1).unwrap());
574 assert!(!max_req.is_satisfied_by(&version));
575 }
576
577 #[test]
578 fn test_compatibility_checking() {
579 let manager = VersionManager::default();
580
581 let v1 = Version::new(2025, 6, 18).unwrap();
582 let v2 = Version::new(2025, 12, 1).unwrap();
583 let v3 = Version::new(2024, 1, 1).unwrap();
584
585 let compat = manager.check_compatibility(&v1, &v2);
587 assert!(matches!(
588 compat,
589 VersionCompatibility::CompatibleWithWarnings(_)
590 ));
591
592 let compat = manager.check_compatibility(&v1, &v3);
594 assert!(matches!(compat, VersionCompatibility::Incompatible(_)));
595
596 let compat = manager.check_compatibility(&v1, &v1);
598 assert_eq!(compat, VersionCompatibility::Compatible);
599 }
600
601 #[test]
602 fn test_utils() {
603 let versions = utils::parse_versions(&["2025-06-18", "2024-11-05"]).unwrap();
604 assert_eq!(versions.len(), 2);
605
606 let newest = utils::newest_version(&versions);
607 assert_eq!(newest, Some(&Version::new(2025, 6, 18).unwrap()));
608
609 let oldest = utils::oldest_version(&versions);
610 assert_eq!(oldest, Some(&Version::new(2024, 11, 5).unwrap()));
611 }
612
613 proptest! {
615 #[test]
616 fn test_version_parse_roundtrip(
617 year in 2020u16..2030u16,
618 month in 1u8..=12u8,
619 day in 1u8..=28u8, ) {
621 let version = Version::new(year, month, day)?;
622 let string = version.to_date_string();
623 let parsed = Version::from_date_string(&string)?;
624 prop_assert_eq!(version, parsed);
625 }
626
627 #[test]
628 fn test_version_comparison_transitive(
629 y1 in 2020u16..2030u16,
630 m1 in 1u8..=12u8,
631 d1 in 1u8..=28u8,
632 y2 in 2020u16..2030u16,
633 m2 in 1u8..=12u8,
634 d2 in 1u8..=28u8,
635 y3 in 2020u16..2030u16,
636 m3 in 1u8..=12u8,
637 d3 in 1u8..=28u8,
638 ) {
639 let v1 = Version::new(y1, m1, d1)?;
640 let v2 = Version::new(y2, m2, d2)?;
641 let v3 = Version::new(y3, m3, d3)?;
642
643 if v1 < v2 && v2 < v3 {
645 prop_assert!(v1 < v3);
646 }
647 }
648
649 #[test]
650 fn test_version_compatibility_symmetric(
651 year in 2020u16..2030u16,
652 m1 in 1u8..=12u8,
653 d1 in 1u8..=28u8,
654 m2 in 1u8..=12u8,
655 d2 in 1u8..=28u8,
656 ) {
657 let v1 = Version::new(year, m1, d1)?;
658 let v2 = Version::new(year, m2, d2)?;
659
660 prop_assert_eq!(v1.is_compatible_with(&v2), v2.is_compatible_with(&v1));
662 }
663
664 #[test]
665 fn test_invalid_month_rejected(
666 year in 2020u16..2030u16,
667 month in 13u8..=255u8,
668 day in 1u8..=28u8,
669 ) {
670 prop_assert!(Version::new(year, month, day).is_err());
671 }
672
673 #[test]
674 fn test_invalid_day_rejected(
675 year in 2020u16..2030u16,
676 month in 1u8..=12u8,
677 day in 32u8..=255u8,
678 ) {
679 prop_assert!(Version::new(year, month, day).is_err());
680 }
681 }
682}