1use crate::error::{PachaError, Result};
6use serde::{Deserialize, Serialize};
7use std::cmp::Ordering;
8use std::fmt;
9use std::str::FromStr;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub struct DatasetVersion {
19 pub major: u32,
21 pub minor: u32,
23 pub patch: u32,
25}
26
27impl DatasetVersion {
28 #[must_use]
30 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
31 Self { major, minor, patch }
32 }
33
34 #[must_use]
36 pub fn initial() -> Self {
37 Self::new(1, 0, 0)
38 }
39
40 #[must_use]
42 pub fn bump_major(&self) -> Self {
43 Self::new(self.major + 1, 0, 0)
44 }
45
46 #[must_use]
48 pub fn bump_minor(&self) -> Self {
49 Self::new(self.major, self.minor + 1, 0)
50 }
51
52 #[must_use]
54 pub fn bump_patch(&self) -> Self {
55 Self::new(self.major, self.minor, self.patch + 1)
56 }
57
58 pub fn parse(s: &str) -> Result<Self> {
64 s.parse()
65 }
66}
67
68impl Default for DatasetVersion {
69 fn default() -> Self {
70 Self::initial()
71 }
72}
73
74impl fmt::Display for DatasetVersion {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
77 }
78}
79
80impl FromStr for DatasetVersion {
81 type Err = PachaError;
82
83 fn from_str(s: &str) -> Result<Self> {
84 let parts: Vec<&str> = s.split('.').collect();
85 if parts.len() != 3 {
86 return Err(PachaError::InvalidVersion(format!(
87 "expected MAJOR.MINOR.PATCH, got '{s}'"
88 )));
89 }
90
91 let major = parts[0]
92 .parse::<u32>()
93 .map_err(|_| PachaError::InvalidVersion(format!("invalid major version in '{s}'")))?;
94 let minor = parts[1]
95 .parse::<u32>()
96 .map_err(|_| PachaError::InvalidVersion(format!("invalid minor version in '{s}'")))?;
97 let patch = parts[2]
98 .parse::<u32>()
99 .map_err(|_| PachaError::InvalidVersion(format!("invalid patch version in '{s}'")))?;
100
101 Ok(Self { major, minor, patch })
102 }
103}
104
105impl PartialOrd for DatasetVersion {
106 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
107 Some(self.cmp(other))
108 }
109}
110
111impl Ord for DatasetVersion {
112 fn cmp(&self, other: &Self) -> Ordering {
113 match self.major.cmp(&other.major) {
114 Ordering::Equal => {}
115 ord => return ord,
116 }
117 match self.minor.cmp(&other.minor) {
118 Ordering::Equal => {}
119 ord => return ord,
120 }
121 self.patch.cmp(&other.patch)
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use proptest::prelude::*;
129
130 #[test]
131 fn test_version_new() {
132 let v = DatasetVersion::new(1, 2, 3);
133 assert_eq!(v.major, 1);
134 assert_eq!(v.minor, 2);
135 assert_eq!(v.patch, 3);
136 }
137
138 #[test]
139 fn test_version_display() {
140 assert_eq!(DatasetVersion::new(1, 2, 3).to_string(), "1.2.3");
141 }
142
143 #[test]
144 fn test_version_parse() {
145 assert_eq!("1.2.3".parse::<DatasetVersion>().unwrap(), DatasetVersion::new(1, 2, 3));
146 }
147
148 #[test]
149 fn test_version_parse_errors() {
150 assert!("1.2".parse::<DatasetVersion>().is_err());
151 assert!("a.b.c".parse::<DatasetVersion>().is_err());
152 }
153
154 #[test]
155 fn test_version_bump() {
156 let v = DatasetVersion::new(1, 2, 3);
157 assert_eq!(v.bump_major(), DatasetVersion::new(2, 0, 0));
158 assert_eq!(v.bump_minor(), DatasetVersion::new(1, 3, 0));
159 assert_eq!(v.bump_patch(), DatasetVersion::new(1, 2, 4));
160 }
161
162 #[test]
163 fn test_version_ordering() {
164 let v100 = DatasetVersion::new(1, 0, 0);
165 let v110 = DatasetVersion::new(1, 1, 0);
166 let v200 = DatasetVersion::new(2, 0, 0);
167
168 assert!(v100 < v110);
169 assert!(v110 < v200);
170 }
171
172 proptest! {
173 #[test]
174 fn prop_version_roundtrip(major: u32, minor: u32, patch: u32) {
175 let v = DatasetVersion::new(major, minor, patch);
176 let s = v.to_string();
177 let parsed: DatasetVersion = s.parse().unwrap();
178 prop_assert_eq!(v, parsed);
179 }
180 }
181}