1use crate::changeset::BumpType;
9use crate::config::ReleasePackagesConfig;
10use crate::error::{Error, Result};
11use serde::{Deserialize, Serialize};
12use std::cmp::Ordering;
13use std::collections::HashMap;
14use std::fmt;
15use std::str::FromStr;
16
17#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub struct Version {
20 pub major: u64,
22 pub minor: u64,
24 pub patch: u64,
26 pub prerelease: Option<String>,
28 pub build: Option<String>,
30}
31
32impl Version {
33 #[must_use]
35 pub const fn new(major: u64, minor: u64, patch: u64) -> Self {
36 Self {
37 major,
38 minor,
39 patch,
40 prerelease: None,
41 build: None,
42 }
43 }
44
45 #[must_use]
47 pub fn with_prerelease(mut self, prerelease: impl Into<String>) -> Self {
48 self.prerelease = Some(prerelease.into());
49 self
50 }
51
52 #[must_use]
54 pub fn with_build(mut self, build: impl Into<String>) -> Self {
55 self.build = Some(build.into());
56 self
57 }
58
59 #[must_use]
61 pub fn bump(&self, bump_type: BumpType) -> Self {
62 match bump_type {
63 BumpType::Major => Self::new(self.major + 1, 0, 0),
64 BumpType::Minor => Self::new(self.major, self.minor + 1, 0),
65 BumpType::Patch => Self::new(self.major, self.minor, self.patch + 1),
66 BumpType::None => self.clone(),
67 }
68 }
69
70 #[must_use]
72 pub fn is_prerelease(&self) -> bool {
73 self.prerelease.is_some()
74 }
75
76 #[must_use]
78 pub fn is_initial_development(&self) -> bool {
79 self.major == 0
80 }
81}
82
83impl Default for Version {
84 fn default() -> Self {
85 Self::new(0, 0, 0)
86 }
87}
88
89impl FromStr for Version {
90 type Err = Error;
91
92 fn from_str(s: &str) -> Result<Self> {
93 let s = s.trim();
94 let s = s.strip_prefix('v').unwrap_or(s);
96
97 let (version_pre, build) = match s.split_once('+') {
99 Some((v, b)) => (v, Some(b.to_string())),
100 None => (s, None),
101 };
102
103 let (version, prerelease) = match version_pre.split_once('-') {
105 Some((v, p)) => (v, Some(p.to_string())),
106 None => (version_pre, None),
107 };
108
109 let parts: Vec<&str> = version.split('.').collect();
111 if parts.len() != 3 {
112 return Err(Error::invalid_version(s));
113 }
114
115 let major = parts[0]
116 .parse()
117 .map_err(|_| Error::invalid_version(format!("Invalid major version: {}", parts[0])))?;
118 let minor = parts[1]
119 .parse()
120 .map_err(|_| Error::invalid_version(format!("Invalid minor version: {}", parts[1])))?;
121 let patch = parts[2]
122 .parse()
123 .map_err(|_| Error::invalid_version(format!("Invalid patch version: {}", parts[2])))?;
124
125 Ok(Self {
126 major,
127 minor,
128 patch,
129 prerelease,
130 build,
131 })
132 }
133}
134
135impl fmt::Display for Version {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
138 if let Some(ref pre) = self.prerelease {
139 write!(f, "-{pre}")?;
140 }
141 if let Some(ref build) = self.build {
142 write!(f, "+{build}")?;
143 }
144 Ok(())
145 }
146}
147
148impl PartialOrd for Version {
149 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
150 Some(self.cmp(other))
151 }
152}
153
154impl Ord for Version {
155 fn cmp(&self, other: &Self) -> Ordering {
156 match self.major.cmp(&other.major) {
158 Ordering::Equal => {}
159 ord => return ord,
160 }
161 match self.minor.cmp(&other.minor) {
162 Ordering::Equal => {}
163 ord => return ord,
164 }
165 match self.patch.cmp(&other.patch) {
166 Ordering::Equal => {}
167 ord => return ord,
168 }
169
170 match (&self.prerelease, &other.prerelease) {
172 (None, None) => Ordering::Equal,
173 (Some(_), None) => Ordering::Less,
174 (None, Some(_)) => Ordering::Greater,
175 (Some(a), Some(b)) => a.cmp(b),
176 }
177 }
179}
180
181pub struct VersionCalculator {
183 current_versions: HashMap<String, Version>,
185 packages_config: ReleasePackagesConfig,
187}
188
189impl VersionCalculator {
190 #[must_use]
192 pub fn new(
193 current_versions: HashMap<String, Version>,
194 packages_config: ReleasePackagesConfig,
195 ) -> Self {
196 Self {
197 current_versions,
198 packages_config,
199 }
200 }
201
202 #[must_use]
209 pub fn calculate(&self, bumps: &HashMap<String, BumpType>) -> HashMap<String, Version> {
210 let mut new_versions = HashMap::new();
211 let mut processed: std::collections::HashSet<String> = std::collections::HashSet::new();
212
213 for (package, &bump) in bumps {
215 if processed.contains(package) || bump == BumpType::None {
216 continue;
217 }
218
219 if let Some(group) = self.packages_config.get_fixed_group(package) {
221 self.process_fixed_group(group, bumps, &mut new_versions);
222 for p in group {
223 processed.insert(p.clone());
224 }
225 }
226 else if let Some(group) = self.packages_config.get_linked_group(package) {
228 self.process_linked_group(group, bumps, &mut new_versions);
229 for p in group {
230 processed.insert(p.clone());
231 }
232 }
233 else {
235 self.process_independent(package, bump, &mut new_versions);
236 processed.insert(package.clone());
237 }
238 }
239
240 new_versions
241 }
242
243 fn process_fixed_group(
245 &self,
246 group: &[String],
247 bumps: &HashMap<String, BumpType>,
248 new_versions: &mut HashMap<String, Version>,
249 ) {
250 let max_bump = group
252 .iter()
253 .filter_map(|p| bumps.get(p))
254 .fold(BumpType::None, |acc, &b| acc.max(b));
255
256 if max_bump == BumpType::None {
257 return;
258 }
259
260 let max_version = group
262 .iter()
263 .filter_map(|p| self.current_versions.get(p))
264 .max()
265 .cloned()
266 .unwrap_or_default();
267
268 let new_version = max_version.bump(max_bump);
270 for package in group {
271 new_versions.insert(package.clone(), new_version.clone());
272 }
273 }
274
275 fn process_linked_group(
277 &self,
278 group: &[String],
279 bumps: &HashMap<String, BumpType>,
280 new_versions: &mut HashMap<String, Version>,
281 ) {
282 let max_bump = group
284 .iter()
285 .filter_map(|p| bumps.get(p))
286 .fold(BumpType::None, |acc, &b| acc.max(b));
287
288 if max_bump == BumpType::None {
289 return;
290 }
291
292 for package in group {
294 let current = self
295 .current_versions
296 .get(package)
297 .cloned()
298 .unwrap_or_default();
299 new_versions.insert(package.clone(), current.bump(max_bump));
300 }
301 }
302
303 fn process_independent(
305 &self,
306 package: &str,
307 bump: BumpType,
308 new_versions: &mut HashMap<String, Version>,
309 ) {
310 let current = self
311 .current_versions
312 .get(package)
313 .cloned()
314 .unwrap_or_default();
315 new_versions.insert(package.to_string(), current.bump(bump));
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn test_version_new() {
325 let v = Version::new(1, 2, 3);
326 assert_eq!(v.major, 1);
327 assert_eq!(v.minor, 2);
328 assert_eq!(v.patch, 3);
329 assert!(v.prerelease.is_none());
330 assert!(v.build.is_none());
331 }
332
333 #[test]
334 fn test_version_with_prerelease() {
335 let v = Version::new(1, 0, 0).with_prerelease("alpha.1");
336 assert_eq!(v.prerelease, Some("alpha.1".to_string()));
337 }
338
339 #[test]
340 fn test_version_with_build() {
341 let v = Version::new(1, 0, 0).with_build("commit.abc123");
342 assert_eq!(v.build, Some("commit.abc123".to_string()));
343 }
344
345 #[test]
346 fn test_version_parse() {
347 let v: Version = "1.2.3".parse().unwrap();
348 assert_eq!(v, Version::new(1, 2, 3));
349
350 let v: Version = "v1.2.3".parse().unwrap();
351 assert_eq!(v, Version::new(1, 2, 3));
352
353 let v: Version = "1.2.3-beta.1".parse().unwrap();
354 assert_eq!(v.major, 1);
355 assert_eq!(v.prerelease, Some("beta.1".to_string()));
356
357 let v: Version = "1.2.3+build.123".parse().unwrap();
358 assert_eq!(v.build, Some("build.123".to_string()));
359
360 let v: Version = "1.2.3-rc.1+build.456".parse().unwrap();
361 assert_eq!(v.prerelease, Some("rc.1".to_string()));
362 assert_eq!(v.build, Some("build.456".to_string()));
363 }
364
365 #[test]
366 fn test_version_parse_invalid() {
367 assert!("1.2".parse::<Version>().is_err());
368 assert!("1.2.3.4".parse::<Version>().is_err());
369 assert!("a.b.c".parse::<Version>().is_err());
370 }
371
372 #[test]
373 fn test_version_display() {
374 assert_eq!(Version::new(1, 2, 3).to_string(), "1.2.3");
375 assert_eq!(
376 Version::new(1, 2, 3).with_prerelease("alpha").to_string(),
377 "1.2.3-alpha"
378 );
379 assert_eq!(
380 Version::new(1, 2, 3).with_build("123").to_string(),
381 "1.2.3+123"
382 );
383 assert_eq!(
384 Version::new(1, 2, 3)
385 .with_prerelease("beta")
386 .with_build("456")
387 .to_string(),
388 "1.2.3-beta+456"
389 );
390 }
391
392 #[test]
393 fn test_version_bump() {
394 let v = Version::new(1, 2, 3);
395 assert_eq!(v.bump(BumpType::Patch), Version::new(1, 2, 4));
396 assert_eq!(v.bump(BumpType::Minor), Version::new(1, 3, 0));
397 assert_eq!(v.bump(BumpType::Major), Version::new(2, 0, 0));
398 assert_eq!(v.bump(BumpType::None), Version::new(1, 2, 3));
399 }
400
401 #[test]
402 fn test_version_ordering() {
403 assert!(Version::new(2, 0, 0) > Version::new(1, 0, 0));
404 assert!(Version::new(1, 1, 0) > Version::new(1, 0, 0));
405 assert!(Version::new(1, 0, 1) > Version::new(1, 0, 0));
406
407 assert!(Version::new(1, 0, 0) > Version::new(1, 0, 0).with_prerelease("alpha"));
409 }
410
411 #[test]
412 fn test_version_is_prerelease() {
413 assert!(!Version::new(1, 0, 0).is_prerelease());
414 assert!(
415 Version::new(1, 0, 0)
416 .with_prerelease("alpha")
417 .is_prerelease()
418 );
419 }
420
421 #[test]
422 fn test_version_is_initial_development() {
423 assert!(Version::new(0, 1, 0).is_initial_development());
424 assert!(!Version::new(1, 0, 0).is_initial_development());
425 }
426
427 #[test]
428 fn test_version_calculator_independent() {
429 let current = HashMap::from([
430 ("pkg-a".to_string(), Version::new(1, 0, 0)),
431 ("pkg-b".to_string(), Version::new(2, 0, 0)),
432 ]);
433 let config = ReleasePackagesConfig::default();
434 let calc = VersionCalculator::new(current, config);
435
436 let bumps = HashMap::from([
437 ("pkg-a".to_string(), BumpType::Minor),
438 ("pkg-b".to_string(), BumpType::Patch),
439 ]);
440
441 let new_versions = calc.calculate(&bumps);
442 assert_eq!(new_versions.get("pkg-a"), Some(&Version::new(1, 1, 0)));
443 assert_eq!(new_versions.get("pkg-b"), Some(&Version::new(2, 0, 1)));
444 }
445
446 #[test]
447 fn test_version_calculator_fixed_group() {
448 let current = HashMap::from([
449 ("pkg-a".to_string(), Version::new(1, 0, 0)),
450 ("pkg-b".to_string(), Version::new(1, 0, 0)),
451 ]);
452 let config = ReleasePackagesConfig {
453 fixed: vec![vec!["pkg-a".to_string(), "pkg-b".to_string()]],
454 linked: vec![],
455 };
456 let calc = VersionCalculator::new(current, config);
457
458 let bumps = HashMap::from([("pkg-a".to_string(), BumpType::Minor)]);
460
461 let new_versions = calc.calculate(&bumps);
462 assert_eq!(new_versions.get("pkg-a"), Some(&Version::new(1, 1, 0)));
463 assert_eq!(new_versions.get("pkg-b"), Some(&Version::new(1, 1, 0)));
464 }
465
466 #[test]
467 fn test_version_calculator_fixed_group_max_bump() {
468 let current = HashMap::from([
469 ("pkg-a".to_string(), Version::new(1, 0, 0)),
470 ("pkg-b".to_string(), Version::new(1, 0, 0)),
471 ]);
472 let config = ReleasePackagesConfig {
473 fixed: vec![vec!["pkg-a".to_string(), "pkg-b".to_string()]],
474 linked: vec![],
475 };
476 let calc = VersionCalculator::new(current, config);
477
478 let bumps = HashMap::from([
480 ("pkg-a".to_string(), BumpType::Patch),
481 ("pkg-b".to_string(), BumpType::Minor),
482 ]);
483
484 let new_versions = calc.calculate(&bumps);
485 assert_eq!(new_versions.get("pkg-a"), Some(&Version::new(1, 1, 0)));
487 assert_eq!(new_versions.get("pkg-b"), Some(&Version::new(1, 1, 0)));
488 }
489
490 #[test]
491 fn test_version_calculator_linked_group() {
492 let current = HashMap::from([
493 ("pkg-a".to_string(), Version::new(1, 0, 0)),
494 ("pkg-b".to_string(), Version::new(2, 0, 0)),
495 ]);
496 let config = ReleasePackagesConfig {
497 fixed: vec![],
498 linked: vec![vec!["pkg-a".to_string(), "pkg-b".to_string()]],
499 };
500 let calc = VersionCalculator::new(current, config);
501
502 let bumps = HashMap::from([("pkg-a".to_string(), BumpType::Minor)]);
503
504 let new_versions = calc.calculate(&bumps);
505 assert_eq!(new_versions.get("pkg-a"), Some(&Version::new(1, 1, 0)));
507 assert_eq!(new_versions.get("pkg-b"), Some(&Version::new(2, 1, 0)));
508 }
509}