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 const fn is_prerelease(&self) -> bool {
73 self.prerelease.is_some()
74 }
75
76 #[must_use]
78 pub const fn is_initial_development(&self) -> bool {
79 self.major == 0
80 }
81
82 #[must_use]
90 pub fn adjusted_bump_type(&self, bump: BumpType) -> BumpType {
91 if self.is_initial_development() && bump == BumpType::Major {
92 BumpType::Minor
93 } else {
94 bump
95 }
96 }
97}
98
99impl Default for Version {
100 fn default() -> Self {
101 Self::new(0, 0, 0)
102 }
103}
104
105impl FromStr for Version {
106 type Err = Error;
107
108 fn from_str(s: &str) -> Result<Self> {
109 let s = s.trim();
110 let s = s.strip_prefix('v').unwrap_or(s);
112
113 let (version_pre, build) = match s.split_once('+') {
115 Some((v, b)) => (v, Some(b.to_string())),
116 None => (s, None),
117 };
118
119 let (version, prerelease) = match version_pre.split_once('-') {
121 Some((v, p)) => (v, Some(p.to_string())),
122 None => (version_pre, None),
123 };
124
125 let parts: Vec<&str> = version.split('.').collect();
127 if parts.len() != 3 {
128 return Err(Error::invalid_version(s));
129 }
130
131 let major = parts[0]
132 .parse()
133 .map_err(|_| Error::invalid_version(format!("Invalid major version: {}", parts[0])))?;
134 let minor = parts[1]
135 .parse()
136 .map_err(|_| Error::invalid_version(format!("Invalid minor version: {}", parts[1])))?;
137 let patch = parts[2]
138 .parse()
139 .map_err(|_| Error::invalid_version(format!("Invalid patch version: {}", parts[2])))?;
140
141 Ok(Self {
142 major,
143 minor,
144 patch,
145 prerelease,
146 build,
147 })
148 }
149}
150
151impl fmt::Display for Version {
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
154 if let Some(ref pre) = self.prerelease {
155 write!(f, "-{pre}")?;
156 }
157 if let Some(ref build) = self.build {
158 write!(f, "+{build}")?;
159 }
160 Ok(())
161 }
162}
163
164impl PartialOrd for Version {
165 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
166 Some(self.cmp(other))
167 }
168}
169
170impl Ord for Version {
171 fn cmp(&self, other: &Self) -> Ordering {
172 match self.major.cmp(&other.major) {
174 Ordering::Equal => {}
175 ord => return ord,
176 }
177 match self.minor.cmp(&other.minor) {
178 Ordering::Equal => {}
179 ord => return ord,
180 }
181 match self.patch.cmp(&other.patch) {
182 Ordering::Equal => {}
183 ord => return ord,
184 }
185
186 match (&self.prerelease, &other.prerelease) {
188 (None, None) => Ordering::Equal,
189 (Some(_), None) => Ordering::Less,
190 (None, Some(_)) => Ordering::Greater,
191 (Some(a), Some(b)) => a.cmp(b),
192 }
193 }
195}
196
197pub struct VersionCalculator {
199 current_versions: HashMap<String, Version>,
201 packages_config: ReleasePackagesConfig,
203}
204
205impl VersionCalculator {
206 #[must_use]
208 pub const fn new(
209 current_versions: HashMap<String, Version>,
210 packages_config: ReleasePackagesConfig,
211 ) -> Self {
212 Self {
213 current_versions,
214 packages_config,
215 }
216 }
217
218 #[must_use]
225 pub fn calculate(&self, bumps: &HashMap<String, BumpType>) -> HashMap<String, Version> {
226 let mut new_versions = HashMap::new();
227 let mut processed: std::collections::HashSet<String> = std::collections::HashSet::new();
228
229 for (package, &bump) in bumps {
231 if processed.contains(package) || bump == BumpType::None {
232 continue;
233 }
234
235 if let Some(group) = self.packages_config.get_fixed_group(package) {
237 self.process_fixed_group(group, bumps, &mut new_versions);
238 for p in group {
239 processed.insert(p.clone());
240 }
241 }
242 else if let Some(group) = self.packages_config.get_linked_group(package) {
244 self.process_linked_group(group, bumps, &mut new_versions);
245 for p in group {
246 processed.insert(p.clone());
247 }
248 }
249 else {
251 self.process_independent(package, bump, &mut new_versions);
252 processed.insert(package.clone());
253 }
254 }
255
256 new_versions
257 }
258
259 fn process_fixed_group(
261 &self,
262 group: &[String],
263 bumps: &HashMap<String, BumpType>,
264 new_versions: &mut HashMap<String, Version>,
265 ) {
266 let max_bump = group
268 .iter()
269 .filter_map(|p| bumps.get(p))
270 .fold(BumpType::None, |acc, &b| acc.max(b));
271
272 if max_bump == BumpType::None {
273 return;
274 }
275
276 let max_version = group
278 .iter()
279 .filter_map(|p| self.current_versions.get(p))
280 .max()
281 .cloned()
282 .unwrap_or_default();
283
284 let new_version = max_version.bump(max_bump);
286 for package in group {
287 new_versions.insert(package.clone(), new_version.clone());
288 }
289 }
290
291 fn process_linked_group(
293 &self,
294 group: &[String],
295 bumps: &HashMap<String, BumpType>,
296 new_versions: &mut HashMap<String, Version>,
297 ) {
298 let max_bump = group
300 .iter()
301 .filter_map(|p| bumps.get(p))
302 .fold(BumpType::None, |acc, &b| acc.max(b));
303
304 if max_bump == BumpType::None {
305 return;
306 }
307
308 for package in group {
310 let current = self
311 .current_versions
312 .get(package)
313 .cloned()
314 .unwrap_or_default();
315 new_versions.insert(package.clone(), current.bump(max_bump));
316 }
317 }
318
319 fn process_independent(
321 &self,
322 package: &str,
323 bump: BumpType,
324 new_versions: &mut HashMap<String, Version>,
325 ) {
326 let current = self
327 .current_versions
328 .get(package)
329 .cloned()
330 .unwrap_or_default();
331 new_versions.insert(package.to_string(), current.bump(bump));
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn test_version_new() {
341 let v = Version::new(1, 2, 3);
342 assert_eq!(v.major, 1);
343 assert_eq!(v.minor, 2);
344 assert_eq!(v.patch, 3);
345 assert!(v.prerelease.is_none());
346 assert!(v.build.is_none());
347 }
348
349 #[test]
350 fn test_version_with_prerelease() {
351 let v = Version::new(1, 0, 0).with_prerelease("alpha.1");
352 assert_eq!(v.prerelease, Some("alpha.1".to_string()));
353 }
354
355 #[test]
356 fn test_version_with_build() {
357 let v = Version::new(1, 0, 0).with_build("commit.abc123");
358 assert_eq!(v.build, Some("commit.abc123".to_string()));
359 }
360
361 #[test]
362 fn test_version_parse() {
363 let v: Version = "1.2.3".parse().unwrap();
364 assert_eq!(v, Version::new(1, 2, 3));
365
366 let v: Version = "v1.2.3".parse().unwrap();
367 assert_eq!(v, Version::new(1, 2, 3));
368
369 let v: Version = "1.2.3-beta.1".parse().unwrap();
370 assert_eq!(v.major, 1);
371 assert_eq!(v.prerelease, Some("beta.1".to_string()));
372
373 let v: Version = "1.2.3+build.123".parse().unwrap();
374 assert_eq!(v.build, Some("build.123".to_string()));
375
376 let v: Version = "1.2.3-rc.1+build.456".parse().unwrap();
377 assert_eq!(v.prerelease, Some("rc.1".to_string()));
378 assert_eq!(v.build, Some("build.456".to_string()));
379 }
380
381 #[test]
382 fn test_version_parse_invalid() {
383 assert!("1.2".parse::<Version>().is_err());
384 assert!("1.2.3.4".parse::<Version>().is_err());
385 assert!("a.b.c".parse::<Version>().is_err());
386 }
387
388 #[test]
389 fn test_version_display() {
390 assert_eq!(Version::new(1, 2, 3).to_string(), "1.2.3");
391 assert_eq!(
392 Version::new(1, 2, 3).with_prerelease("alpha").to_string(),
393 "1.2.3-alpha"
394 );
395 assert_eq!(
396 Version::new(1, 2, 3).with_build("123").to_string(),
397 "1.2.3+123"
398 );
399 assert_eq!(
400 Version::new(1, 2, 3)
401 .with_prerelease("beta")
402 .with_build("456")
403 .to_string(),
404 "1.2.3-beta+456"
405 );
406 }
407
408 #[test]
409 fn test_version_bump() {
410 let v = Version::new(1, 2, 3);
411 assert_eq!(v.bump(BumpType::Patch), Version::new(1, 2, 4));
412 assert_eq!(v.bump(BumpType::Minor), Version::new(1, 3, 0));
413 assert_eq!(v.bump(BumpType::Major), Version::new(2, 0, 0));
414 assert_eq!(v.bump(BumpType::None), Version::new(1, 2, 3));
415 }
416
417 #[test]
418 fn test_version_ordering() {
419 assert!(Version::new(2, 0, 0) > Version::new(1, 0, 0));
420 assert!(Version::new(1, 1, 0) > Version::new(1, 0, 0));
421 assert!(Version::new(1, 0, 1) > Version::new(1, 0, 0));
422
423 assert!(Version::new(1, 0, 0) > Version::new(1, 0, 0).with_prerelease("alpha"));
425 }
426
427 #[test]
428 fn test_version_is_prerelease() {
429 assert!(!Version::new(1, 0, 0).is_prerelease());
430 assert!(
431 Version::new(1, 0, 0)
432 .with_prerelease("alpha")
433 .is_prerelease()
434 );
435 }
436
437 #[test]
438 fn test_version_is_initial_development() {
439 assert!(Version::new(0, 1, 0).is_initial_development());
440 assert!(!Version::new(1, 0, 0).is_initial_development());
441 }
442
443 #[test]
444 fn test_adjusted_bump_type_pre_1_0() {
445 let v = Version::new(0, 16, 0);
447 assert_eq!(v.adjusted_bump_type(BumpType::Major), BumpType::Minor);
448 assert_eq!(v.adjusted_bump_type(BumpType::Minor), BumpType::Minor);
449 assert_eq!(v.adjusted_bump_type(BumpType::Patch), BumpType::Patch);
450 assert_eq!(v.adjusted_bump_type(BumpType::None), BumpType::None);
451 }
452
453 #[test]
454 fn test_adjusted_bump_type_post_1_0() {
455 let v = Version::new(1, 0, 0);
457 assert_eq!(v.adjusted_bump_type(BumpType::Major), BumpType::Major);
458 assert_eq!(v.adjusted_bump_type(BumpType::Minor), BumpType::Minor);
459 assert_eq!(v.adjusted_bump_type(BumpType::Patch), BumpType::Patch);
460 assert_eq!(v.adjusted_bump_type(BumpType::None), BumpType::None);
461
462 let v2 = Version::new(2, 5, 3);
463 assert_eq!(v2.adjusted_bump_type(BumpType::Major), BumpType::Major);
464 }
465
466 #[test]
467 fn test_version_calculator_independent() {
468 let current = HashMap::from([
469 ("pkg-a".to_string(), Version::new(1, 0, 0)),
470 ("pkg-b".to_string(), Version::new(2, 0, 0)),
471 ]);
472 let config = ReleasePackagesConfig::default();
473 let calc = VersionCalculator::new(current, config);
474
475 let bumps = HashMap::from([
476 ("pkg-a".to_string(), BumpType::Minor),
477 ("pkg-b".to_string(), BumpType::Patch),
478 ]);
479
480 let new_versions = calc.calculate(&bumps);
481 assert_eq!(new_versions.get("pkg-a"), Some(&Version::new(1, 1, 0)));
482 assert_eq!(new_versions.get("pkg-b"), Some(&Version::new(2, 0, 1)));
483 }
484
485 #[test]
486 fn test_version_calculator_fixed_group() {
487 let current = HashMap::from([
488 ("pkg-a".to_string(), Version::new(1, 0, 0)),
489 ("pkg-b".to_string(), Version::new(1, 0, 0)),
490 ]);
491 let config = ReleasePackagesConfig {
492 fixed: vec![vec!["pkg-a".to_string(), "pkg-b".to_string()]],
493 ..Default::default()
494 };
495 let calc = VersionCalculator::new(current, config);
496
497 let bumps = HashMap::from([("pkg-a".to_string(), BumpType::Minor)]);
499
500 let new_versions = calc.calculate(&bumps);
501 assert_eq!(new_versions.get("pkg-a"), Some(&Version::new(1, 1, 0)));
502 assert_eq!(new_versions.get("pkg-b"), Some(&Version::new(1, 1, 0)));
503 }
504
505 #[test]
506 fn test_version_calculator_fixed_group_max_bump() {
507 let current = HashMap::from([
508 ("pkg-a".to_string(), Version::new(1, 0, 0)),
509 ("pkg-b".to_string(), Version::new(1, 0, 0)),
510 ]);
511 let config = ReleasePackagesConfig {
512 fixed: vec![vec!["pkg-a".to_string(), "pkg-b".to_string()]],
513 ..Default::default()
514 };
515 let calc = VersionCalculator::new(current, config);
516
517 let bumps = HashMap::from([
519 ("pkg-a".to_string(), BumpType::Patch),
520 ("pkg-b".to_string(), BumpType::Minor),
521 ]);
522
523 let new_versions = calc.calculate(&bumps);
524 assert_eq!(new_versions.get("pkg-a"), Some(&Version::new(1, 1, 0)));
526 assert_eq!(new_versions.get("pkg-b"), Some(&Version::new(1, 1, 0)));
527 }
528
529 #[test]
530 fn test_version_calculator_linked_group() {
531 let current = HashMap::from([
532 ("pkg-a".to_string(), Version::new(1, 0, 0)),
533 ("pkg-b".to_string(), Version::new(2, 0, 0)),
534 ]);
535 let config = ReleasePackagesConfig {
536 linked: vec![vec!["pkg-a".to_string(), "pkg-b".to_string()]],
537 ..Default::default()
538 };
539 let calc = VersionCalculator::new(current, config);
540
541 let bumps = HashMap::from([("pkg-a".to_string(), BumpType::Minor)]);
542
543 let new_versions = calc.calculate(&bumps);
544 assert_eq!(new_versions.get("pkg-a"), Some(&Version::new(1, 1, 0)));
546 assert_eq!(new_versions.get("pkg-b"), Some(&Version::new(2, 1, 0)));
547 }
548}