1use crate::error::{PachaError, Result};
9use serde::{Deserialize, Serialize};
10use std::cmp::Ordering;
11use std::fmt;
12use std::str::FromStr;
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub struct ModelVersion {
17 pub major: u32,
19 pub minor: u32,
21 pub patch: u32,
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub prerelease: Option<String>,
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub build: Option<String>,
29}
30
31impl ModelVersion {
32 #[must_use]
34 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
35 Self { major, minor, patch, prerelease: None, build: None }
36 }
37
38 #[must_use]
40 pub fn zero() -> Self {
41 Self::new(0, 0, 0)
42 }
43
44 #[must_use]
46 pub fn initial() -> Self {
47 Self::new(1, 0, 0)
48 }
49
50 #[must_use]
52 pub fn with_prerelease(mut self, prerelease: impl Into<String>) -> Self {
53 self.prerelease = Some(prerelease.into());
54 self
55 }
56
57 #[must_use]
59 pub fn with_build(mut self, build: impl Into<String>) -> Self {
60 self.build = Some(build.into());
61 self
62 }
63
64 #[must_use]
66 pub fn bump_major(&self) -> Self {
67 Self::new(self.major + 1, 0, 0)
68 }
69
70 #[must_use]
72 pub fn bump_minor(&self) -> Self {
73 Self::new(self.major, self.minor + 1, 0)
74 }
75
76 #[must_use]
78 pub fn bump_patch(&self) -> Self {
79 Self::new(self.major, self.minor, self.patch + 1)
80 }
81
82 #[must_use]
84 pub fn is_prerelease(&self) -> bool {
85 self.prerelease.is_some()
86 }
87
88 #[must_use]
90 pub fn is_stable(&self) -> bool {
91 self.major >= 1 && self.prerelease.is_none()
92 }
93
94 pub fn parse(s: &str) -> Result<Self> {
100 s.parse()
101 }
102}
103
104impl Default for ModelVersion {
105 fn default() -> Self {
106 Self::initial()
107 }
108}
109
110impl fmt::Display for ModelVersion {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
113 if let Some(ref pre) = self.prerelease {
114 write!(f, "-{pre}")?;
115 }
116 if let Some(ref build) = self.build {
117 write!(f, "+{build}")?;
118 }
119 Ok(())
120 }
121}
122
123impl FromStr for ModelVersion {
124 type Err = PachaError;
125
126 fn from_str(s: &str) -> Result<Self> {
127 let (version_pre, build) = match s.split_once('+') {
129 Some((v, b)) => (v, Some(b.to_string())),
130 None => (s, None),
131 };
132
133 let (version, prerelease) = match version_pre.split_once('-') {
135 Some((v, p)) => (v, Some(p.to_string())),
136 None => (version_pre, None),
137 };
138
139 let parts: Vec<&str> = version.split('.').collect();
141 if parts.len() != 3 {
142 return Err(PachaError::InvalidVersion(format!(
143 "expected MAJOR.MINOR.PATCH, got '{s}'"
144 )));
145 }
146
147 let major = parts[0]
148 .parse::<u32>()
149 .map_err(|_| PachaError::InvalidVersion(format!("invalid major version in '{s}'")))?;
150 let minor = parts[1]
151 .parse::<u32>()
152 .map_err(|_| PachaError::InvalidVersion(format!("invalid minor version in '{s}'")))?;
153 let patch = parts[2]
154 .parse::<u32>()
155 .map_err(|_| PachaError::InvalidVersion(format!("invalid patch version in '{s}'")))?;
156
157 Ok(Self { major, minor, patch, prerelease, build })
158 }
159}
160
161impl PartialOrd for ModelVersion {
162 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
163 Some(self.cmp(other))
164 }
165}
166
167impl Ord for ModelVersion {
168 fn cmp(&self, other: &Self) -> Ordering {
169 match self.major.cmp(&other.major) {
171 Ordering::Equal => {}
172 ord => return ord,
173 }
174 match self.minor.cmp(&other.minor) {
175 Ordering::Equal => {}
176 ord => return ord,
177 }
178 match self.patch.cmp(&other.patch) {
179 Ordering::Equal => {}
180 ord => return ord,
181 }
182
183 match (&self.prerelease, &other.prerelease) {
185 (None, None) => Ordering::Equal,
186 (Some(_), None) => Ordering::Less,
187 (None, Some(_)) => Ordering::Greater,
188 (Some(a), Some(b)) => a.cmp(b),
189 }
190 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use proptest::prelude::*;
198
199 #[test]
200 fn test_version_new() {
201 let v = ModelVersion::new(1, 2, 3);
202 assert_eq!(v.major, 1);
203 assert_eq!(v.minor, 2);
204 assert_eq!(v.patch, 3);
205 assert!(v.prerelease.is_none());
206 assert!(v.build.is_none());
207 }
208
209 #[test]
210 fn test_version_display() {
211 assert_eq!(ModelVersion::new(1, 2, 3).to_string(), "1.2.3");
212 assert_eq!(
213 ModelVersion::new(1, 0, 0).with_prerelease("beta.1").to_string(),
214 "1.0.0-beta.1"
215 );
216 assert_eq!(ModelVersion::new(1, 0, 0).with_build("run-123").to_string(), "1.0.0+run-123");
217 assert_eq!(
218 ModelVersion::new(1, 0, 0).with_prerelease("rc.1").with_build("abc").to_string(),
219 "1.0.0-rc.1+abc"
220 );
221 }
222
223 #[test]
224 fn test_version_parse() {
225 assert_eq!("1.2.3".parse::<ModelVersion>().unwrap(), ModelVersion::new(1, 2, 3));
226 assert_eq!("0.0.0".parse::<ModelVersion>().unwrap(), ModelVersion::zero());
227
228 let with_pre: ModelVersion = "1.0.0-beta.1".parse().unwrap();
229 assert_eq!(with_pre.prerelease, Some("beta.1".to_string()));
230
231 let with_build: ModelVersion = "1.0.0+run-123".parse().unwrap();
232 assert_eq!(with_build.build, Some("run-123".to_string()));
233
234 let full: ModelVersion = "2.1.0-rc.1+build.456".parse().unwrap();
235 assert_eq!(full.major, 2);
236 assert_eq!(full.minor, 1);
237 assert_eq!(full.patch, 0);
238 assert_eq!(full.prerelease, Some("rc.1".to_string()));
239 assert_eq!(full.build, Some("build.456".to_string()));
240 }
241
242 #[test]
243 fn test_version_parse_errors() {
244 assert!("1.2".parse::<ModelVersion>().is_err());
245 assert!("1.2.3.4".parse::<ModelVersion>().is_err());
246 assert!("a.b.c".parse::<ModelVersion>().is_err());
247 assert!("1.2.three".parse::<ModelVersion>().is_err());
248 }
249
250 #[test]
251 fn test_version_bump() {
252 let v = ModelVersion::new(1, 2, 3);
253
254 assert_eq!(v.bump_major(), ModelVersion::new(2, 0, 0));
255 assert_eq!(v.bump_minor(), ModelVersion::new(1, 3, 0));
256 assert_eq!(v.bump_patch(), ModelVersion::new(1, 2, 4));
257 }
258
259 #[test]
260 fn test_version_ordering() {
261 let v100 = ModelVersion::new(1, 0, 0);
262 let v110 = ModelVersion::new(1, 1, 0);
263 let v111 = ModelVersion::new(1, 1, 1);
264 let v200 = ModelVersion::new(2, 0, 0);
265
266 assert!(v100 < v110);
267 assert!(v110 < v111);
268 assert!(v111 < v200);
269 }
270
271 #[test]
272 fn test_prerelease_ordering() {
273 let stable = ModelVersion::new(1, 0, 0);
274 let beta = ModelVersion::new(1, 0, 0).with_prerelease("beta");
275 let alpha = ModelVersion::new(1, 0, 0).with_prerelease("alpha");
276
277 assert!(beta < stable);
279 assert!(alpha < stable);
280 assert!(alpha < beta);
282 }
283
284 #[test]
285 fn test_is_stable() {
286 assert!(ModelVersion::new(1, 0, 0).is_stable());
287 assert!(ModelVersion::new(2, 5, 3).is_stable());
288 assert!(!ModelVersion::new(0, 9, 0).is_stable());
289 assert!(!ModelVersion::new(1, 0, 0).with_prerelease("beta").is_stable());
290 }
291
292 #[test]
293 fn test_serialization() {
294 let v = ModelVersion::new(1, 2, 3).with_prerelease("rc.1");
295 let json = serde_json::to_string(&v).unwrap();
296 let deserialized: ModelVersion = serde_json::from_str(&json).unwrap();
297 assert_eq!(v, deserialized);
298 }
299
300 proptest! {
302 #[test]
303 fn prop_version_roundtrip(major: u32, minor: u32, patch: u32) {
304 let v = ModelVersion::new(major, minor, patch);
305 let s = v.to_string();
306 let parsed: ModelVersion = s.parse().unwrap();
307 prop_assert_eq!(v, parsed);
308 }
309
310 #[test]
311 fn prop_bump_major_resets(major in 0u32..1000, minor in 0u32..1000, patch in 0u32..1000) {
312 let v = ModelVersion::new(major, minor, patch);
313 let bumped = v.bump_major();
314 prop_assert_eq!(bumped.major, major + 1);
315 prop_assert_eq!(bumped.minor, 0);
316 prop_assert_eq!(bumped.patch, 0);
317 }
318
319 #[test]
320 fn prop_bump_minor_resets_patch(major in 0u32..1000, minor in 0u32..1000, patch in 0u32..1000) {
321 let v = ModelVersion::new(major, minor, patch);
322 let bumped = v.bump_minor();
323 prop_assert_eq!(bumped.major, major);
324 prop_assert_eq!(bumped.minor, minor + 1);
325 prop_assert_eq!(bumped.patch, 0);
326 }
327
328 #[test]
329 fn prop_ordering_transitive(
330 a_major in 0u32..10, a_minor in 0u32..10, a_patch in 0u32..10,
331 b_major in 0u32..10, b_minor in 0u32..10, b_patch in 0u32..10,
332 c_major in 0u32..10, c_minor in 0u32..10, c_patch in 0u32..10,
333 ) {
334 let a = ModelVersion::new(a_major, a_minor, a_patch);
335 let b = ModelVersion::new(b_major, b_minor, b_patch);
336 let c = ModelVersion::new(c_major, c_minor, c_patch);
337
338 if a < b && b < c {
339 prop_assert!(a < c);
340 }
341 }
342 }
343}
344
345#[cfg(kani)]
348mod kani_proofs {
349 use super::*;
350
351 #[kani::proof]
352 fn verify_bump_major_resets() {
353 let major: u32 = kani::any();
354 let minor: u32 = kani::any();
355 let patch: u32 = kani::any();
356 kani::assume(major < u32::MAX);
357 let v = ModelVersion::new(major, minor, patch);
358 let bumped = v.bump_major();
359 assert!(bumped.major == major + 1);
360 assert!(bumped.minor == 0);
361 assert!(bumped.patch == 0);
362 }
363
364 #[kani::proof]
365 fn verify_bump_ordering() {
366 let major: u32 = kani::any();
367 let minor: u32 = kani::any();
368 let patch: u32 = kani::any();
369 kani::assume(major < 100 && minor < 100 && patch < u32::MAX);
370 let v = ModelVersion::new(major, minor, patch);
371 let bumped = v.bump_patch();
372 assert!(bumped > v);
373 }
374
375 #[kani::proof]
376 fn verify_version_equality_reflexive() {
377 let major: u32 = kani::any();
378 let minor: u32 = kani::any();
379 let patch: u32 = kani::any();
380 kani::assume(major <= 100 && minor <= 100 && patch <= 100);
381 let v = ModelVersion::new(major, minor, patch);
382 assert!(v == v.clone());
383 }
384}