1use serde::{Deserialize, Serialize};
7use std::cmp::Ordering;
8use std::fmt;
9use std::str::FromStr;
10
11#[derive(Debug, Clone)]
13pub struct VersionManager {
14 supported_versions: Vec<Version>,
16 current_version: Version,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct Version {
23 pub year: u16,
25 pub month: u8,
27 pub day: u8,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum VersionCompatibility {
34 Compatible,
36 CompatibleWithWarnings(Vec<String>),
38 Incompatible(String),
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum VersionRequirement {
45 Exact(Version),
47 Minimum(Version),
49 Maximum(Version),
51 Range(Version, Version),
53 Any(Vec<Version>),
55}
56
57impl Version {
58 pub fn new(year: u16, month: u8, day: u8) -> Result<Self, VersionError> {
65 if !(1..=12).contains(&month) {
66 return Err(VersionError::InvalidMonth(month.to_string()));
67 }
68
69 if !(1..=31).contains(&day) {
70 return Err(VersionError::InvalidDay(day.to_string()));
71 }
72
73 if month == 2 && day > 29 {
75 return Err(VersionError::InvalidDay(format!(
76 "{} (invalid for February)",
77 day
78 )));
79 }
80
81 if matches!(month, 4 | 6 | 9 | 11) && day > 30 {
82 return Err(VersionError::InvalidDay(format!(
83 "{} (month {} only has 30 days)",
84 day, month
85 )));
86 }
87
88 Ok(Self { year, month, day })
89 }
90
91 pub fn current() -> Self {
93 Self {
94 year: 2025,
95 month: 6,
96 day: 18,
97 }
98 }
99
100 pub fn is_newer_than(&self, other: &Version) -> bool {
102 self > other
103 }
104
105 pub fn is_older_than(&self, other: &Version) -> bool {
107 self < other
108 }
109
110 pub fn is_compatible_with(&self, other: &Version) -> bool {
112 self.year == other.year
115 }
116
117 pub fn to_date_string(&self) -> String {
119 format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
120 }
121
122 pub fn from_date_string(s: &str) -> Result<Self, VersionError> {
129 s.parse()
130 }
131
132 pub fn known_versions() -> Vec<Version> {
134 vec![
135 Version::new(2025, 6, 18).unwrap(), Version::new(2024, 11, 5).unwrap(), Version::new(2024, 6, 25).unwrap(), ]
139 }
140}
141
142impl fmt::Display for Version {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 write!(f, "{}", self.to_date_string())
145 }
146}
147
148impl FromStr for Version {
149 type Err = VersionError;
150
151 fn from_str(s: &str) -> Result<Self, Self::Err> {
152 let parts: Vec<&str> = s.split('-').collect();
153
154 if parts.len() != 3 {
155 return Err(VersionError::InvalidFormat(s.to_string()));
156 }
157
158 let year = parts[0]
159 .parse::<u16>()
160 .map_err(|_| VersionError::InvalidYear(parts[0].to_string()))?;
161
162 let month = parts[1]
163 .parse::<u8>()
164 .map_err(|_| VersionError::InvalidMonth(parts[1].to_string()))?;
165
166 let day = parts[2]
167 .parse::<u8>()
168 .map_err(|_| VersionError::InvalidDay(parts[2].to_string()))?;
169
170 Self::new(year, month, day)
171 }
172}
173
174impl PartialOrd for Version {
175 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
176 Some(self.cmp(other))
177 }
178}
179
180impl Ord for Version {
181 fn cmp(&self, other: &Self) -> Ordering {
182 (self.year, self.month, self.day).cmp(&(other.year, other.month, other.day))
183 }
184}
185
186impl VersionManager {
187 pub fn new(supported_versions: Vec<Version>) -> Result<Self, VersionError> {
193 if supported_versions.is_empty() {
194 return Err(VersionError::NoSupportedVersions);
195 }
196
197 let mut versions = supported_versions;
198 versions.sort_by(|a, b| b.cmp(a)); let current_version = versions[0].clone();
201
202 Ok(Self {
203 supported_versions: versions,
204 current_version,
205 })
206 }
207
208 pub fn with_default_versions() -> Self {
210 Self::new(Version::known_versions()).unwrap()
211 }
212 pub fn current_version(&self) -> &Version {
214 &self.current_version
215 }
216
217 pub fn supported_versions(&self) -> &[Version] {
219 &self.supported_versions
220 }
221
222 pub fn is_version_supported(&self, version: &Version) -> bool {
224 self.supported_versions.contains(version)
225 }
226
227 pub fn negotiate_version(&self, client_versions: &[Version]) -> Option<Version> {
229 for server_version in &self.supported_versions {
231 if client_versions.contains(server_version) {
232 return Some(server_version.clone());
233 }
234 }
235
236 None
237 }
238
239 pub fn check_compatibility(
241 &self,
242 client_version: &Version,
243 server_version: &Version,
244 ) -> VersionCompatibility {
245 if client_version == server_version {
246 return VersionCompatibility::Compatible;
247 }
248
249 if client_version.year == server_version.year {
251 let warning = format!(
252 "Version mismatch but compatible: client={client_version}, server={server_version}"
253 );
254 return VersionCompatibility::CompatibleWithWarnings(vec![warning]);
255 }
256
257 let reason =
259 format!("Incompatible versions: client={client_version}, server={server_version}");
260 VersionCompatibility::Incompatible(reason)
261 }
262
263 pub fn minimum_version(&self) -> &Version {
265 self.supported_versions
267 .last()
268 .expect("BUG: VersionManager has no versions (constructor should prevent this)")
269 }
270
271 pub fn maximum_version(&self) -> &Version {
273 &self.supported_versions[0] }
275
276 pub fn satisfies_requirement(
278 &self,
279 version: &Version,
280 requirement: &VersionRequirement,
281 ) -> bool {
282 match requirement {
283 VersionRequirement::Exact(required) => version == required,
284 VersionRequirement::Minimum(min) => version >= min,
285 VersionRequirement::Maximum(max) => version <= max,
286 VersionRequirement::Range(min, max) => version >= min && version <= max,
287 VersionRequirement::Any(versions) => versions.contains(version),
288 }
289 }
290}
291
292impl Default for VersionManager {
293 fn default() -> Self {
294 Self::with_default_versions()
295 }
296}
297
298impl VersionRequirement {
299 pub fn exact(version: Version) -> Self {
301 Self::Exact(version)
302 }
303
304 pub fn minimum(version: Version) -> Self {
306 Self::Minimum(version)
307 }
308
309 pub fn maximum(version: Version) -> Self {
311 Self::Maximum(version)
312 }
313
314 pub fn range(min: Version, max: Version) -> Result<Self, VersionError> {
320 if min > max {
321 return Err(VersionError::InvalidRange(min, max));
322 }
323 Ok(Self::Range(min, max))
324 }
325
326 pub fn any(versions: Vec<Version>) -> Result<Self, VersionError> {
332 if versions.is_empty() {
333 return Err(VersionError::EmptyVersionList);
334 }
335 Ok(Self::Any(versions))
336 }
337
338 pub fn is_satisfied_by(&self, version: &Version) -> bool {
340 match self {
341 Self::Exact(required) => version == required,
342 Self::Minimum(min) => version >= min,
343 Self::Maximum(max) => version <= max,
344 Self::Range(min, max) => version >= min && version <= max,
345 Self::Any(versions) => versions.contains(version),
346 }
347 }
348}
349
350#[derive(Debug, Clone, thiserror::Error)]
352pub enum VersionError {
353 #[error("Invalid version format: {0}")]
355 InvalidFormat(String),
356 #[error("Invalid year: {0}")]
358 InvalidYear(String),
359 #[error("Invalid month: {0} (must be 1-12)")]
361 InvalidMonth(String),
362 #[error("Invalid day: {0} (must be 1-31)")]
364 InvalidDay(String),
365 #[error("No supported versions provided")]
367 NoSupportedVersions,
368 #[error("Invalid version range: {0} > {1}")]
370 InvalidRange(Version, Version),
371 #[error("Empty version list")]
373 EmptyVersionList,
374}
375
376pub mod utils {
378 use super::*;
379
380 pub fn parse_versions(version_strings: &[&str]) -> Result<Vec<Version>, VersionError> {
386 version_strings.iter().map(|s| s.parse()).collect()
387 }
388
389 pub fn newest_version(versions: &[Version]) -> Option<&Version> {
391 versions.iter().max()
392 }
393
394 pub fn oldest_version(versions: &[Version]) -> Option<&Version> {
396 versions.iter().min()
397 }
398
399 pub fn are_all_compatible(versions: &[Version]) -> bool {
401 if versions.len() < 2 {
402 return true;
403 }
404
405 let first = &versions[0];
406 versions.iter().all(|v| first.is_compatible_with(v))
407 }
408
409 pub fn compatibility_description(compatibility: &VersionCompatibility) -> String {
411 match compatibility {
412 VersionCompatibility::Compatible => "Fully compatible".to_string(),
413 VersionCompatibility::CompatibleWithWarnings(warnings) => {
414 format!("Compatible with warnings: {}", warnings.join(", "))
415 }
416 VersionCompatibility::Incompatible(reason) => {
417 format!("Incompatible: {reason}")
418 }
419 }
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426 use proptest::prelude::*;
427
428 #[test]
429 fn test_version_creation() {
430 let version = Version::new(2025, 6, 18).unwrap();
431 assert_eq!(version.year, 2025);
432 assert_eq!(version.month, 6);
433 assert_eq!(version.day, 18);
434
435 assert!(Version::new(2025, 13, 18).is_err());
437
438 assert!(Version::new(2025, 6, 32).is_err());
440 }
441
442 #[test]
443 fn test_version_parsing() {
444 let version: Version = "2025-06-18".parse().unwrap();
445 assert_eq!(version, Version::new(2025, 6, 18).unwrap());
446
447 assert!("2025/06/18".parse::<Version>().is_err());
449 assert!("invalid".parse::<Version>().is_err());
450 }
451
452 #[test]
453 fn test_version_comparison() {
454 let v1 = Version::new(2025, 6, 18).unwrap();
455 let v2 = Version::new(2024, 11, 5).unwrap();
456 let v3 = Version::new(2025, 6, 18).unwrap();
457
458 assert!(v1 > v2);
459 assert!(v1.is_newer_than(&v2));
460 assert!(v2.is_older_than(&v1));
461 assert_eq!(v1, v3);
462 }
463
464 #[test]
465 fn test_version_compatibility() {
466 let v1 = Version::new(2025, 6, 18).unwrap();
467 let v2 = Version::new(2025, 12, 1).unwrap(); let v3 = Version::new(2024, 6, 18).unwrap(); assert!(v1.is_compatible_with(&v2));
471 assert!(!v1.is_compatible_with(&v3));
472 }
473
474 #[test]
475 fn test_version_manager() {
476 let versions = vec![
477 Version::new(2025, 6, 18).unwrap(),
478 Version::new(2024, 11, 5).unwrap(),
479 ];
480
481 let manager = VersionManager::new(versions).unwrap();
482
483 assert_eq!(
484 manager.current_version(),
485 &Version::new(2025, 6, 18).unwrap()
486 );
487 assert!(manager.is_version_supported(&Version::new(2024, 11, 5).unwrap()));
488 assert!(!manager.is_version_supported(&Version::new(2023, 1, 1).unwrap()));
489 }
490
491 #[test]
492 fn test_version_negotiation() {
493 let manager = VersionManager::default();
494
495 let client_versions = vec![
496 Version::new(2024, 11, 5).unwrap(),
497 Version::new(2025, 6, 18).unwrap(),
498 ];
499
500 let negotiated = manager.negotiate_version(&client_versions);
501 assert_eq!(negotiated, Some(Version::new(2025, 6, 18).unwrap()));
502 }
503
504 #[test]
505 fn test_version_requirements() {
506 let version = Version::new(2025, 6, 18).unwrap();
507
508 let exact_req = VersionRequirement::exact(version.clone());
509 assert!(exact_req.is_satisfied_by(&version));
510
511 let min_req = VersionRequirement::minimum(Version::new(2024, 1, 1).unwrap());
512 assert!(min_req.is_satisfied_by(&version));
513
514 let max_req = VersionRequirement::maximum(Version::new(2024, 1, 1).unwrap());
515 assert!(!max_req.is_satisfied_by(&version));
516 }
517
518 #[test]
519 fn test_compatibility_checking() {
520 let manager = VersionManager::default();
521
522 let v1 = Version::new(2025, 6, 18).unwrap();
523 let v2 = Version::new(2025, 12, 1).unwrap();
524 let v3 = Version::new(2024, 1, 1).unwrap();
525
526 let compat = manager.check_compatibility(&v1, &v2);
528 assert!(matches!(
529 compat,
530 VersionCompatibility::CompatibleWithWarnings(_)
531 ));
532
533 let compat = manager.check_compatibility(&v1, &v3);
535 assert!(matches!(compat, VersionCompatibility::Incompatible(_)));
536
537 let compat = manager.check_compatibility(&v1, &v1);
539 assert_eq!(compat, VersionCompatibility::Compatible);
540 }
541
542 #[test]
543 fn test_utils() {
544 let versions = utils::parse_versions(&["2025-06-18", "2024-11-05"]).unwrap();
545 assert_eq!(versions.len(), 2);
546
547 let newest = utils::newest_version(&versions);
548 assert_eq!(newest, Some(&Version::new(2025, 6, 18).unwrap()));
549
550 let oldest = utils::oldest_version(&versions);
551 assert_eq!(oldest, Some(&Version::new(2024, 11, 5).unwrap()));
552 }
553
554 proptest! {
556 #[test]
557 fn test_version_parse_roundtrip(
558 year in 2020u16..2030u16,
559 month in 1u8..=12u8,
560 day in 1u8..=28u8, ) {
562 let version = Version::new(year, month, day)?;
563 let string = version.to_date_string();
564 let parsed = Version::from_date_string(&string)?;
565 prop_assert_eq!(version, parsed);
566 }
567
568 #[test]
569 fn test_version_comparison_transitive(
570 y1 in 2020u16..2030u16,
571 m1 in 1u8..=12u8,
572 d1 in 1u8..=28u8,
573 y2 in 2020u16..2030u16,
574 m2 in 1u8..=12u8,
575 d2 in 1u8..=28u8,
576 y3 in 2020u16..2030u16,
577 m3 in 1u8..=12u8,
578 d3 in 1u8..=28u8,
579 ) {
580 let v1 = Version::new(y1, m1, d1)?;
581 let v2 = Version::new(y2, m2, d2)?;
582 let v3 = Version::new(y3, m3, d3)?;
583
584 if v1 < v2 && v2 < v3 {
586 prop_assert!(v1 < v3);
587 }
588 }
589
590 #[test]
591 fn test_version_compatibility_symmetric(
592 year in 2020u16..2030u16,
593 m1 in 1u8..=12u8,
594 d1 in 1u8..=28u8,
595 m2 in 1u8..=12u8,
596 d2 in 1u8..=28u8,
597 ) {
598 let v1 = Version::new(year, m1, d1)?;
599 let v2 = Version::new(year, m2, d2)?;
600
601 prop_assert_eq!(v1.is_compatible_with(&v2), v2.is_compatible_with(&v1));
603 }
604
605 #[test]
606 fn test_invalid_month_rejected(
607 year in 2020u16..2030u16,
608 month in 13u8..=255u8,
609 day in 1u8..=28u8,
610 ) {
611 prop_assert!(Version::new(year, month, day).is_err());
612 }
613
614 #[test]
615 fn test_invalid_day_rejected(
616 year in 2020u16..2030u16,
617 month in 1u8..=12u8,
618 day in 32u8..=255u8,
619 ) {
620 prop_assert!(Version::new(year, month, day).is_err());
621 }
622 }
623}