1use semver::Version;
5use serde::{Deserialize, Serialize};
6
7#[derive(
13 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, utoipa::ToSchema,
14)]
15pub enum SemverBreakingLine {
16 StableMajor(u64),
18 PreOneMinor(u64),
20 PreZeroPatch(u64),
22}
23
24#[must_use]
27pub fn is_semver_compatible_upgrade(current: &Version, candidate: &Version) -> bool {
28 if candidate <= current {
29 return false;
30 }
31 if !candidate.pre.is_empty() {
35 return false;
36 }
37
38 if current.major != 0 {
39 return candidate.major == current.major;
40 }
41 if current.minor != 0 {
42 return candidate.major == 0 && candidate.minor == current.minor;
43 }
44
45 candidate.major == 0 && candidate.minor == 0 && candidate.patch == current.patch
46}
47
48#[must_use]
50pub const fn breaking_line(version: &Version) -> SemverBreakingLine {
51 if version.major != 0 {
52 return SemverBreakingLine::StableMajor(version.major);
53 }
54 if version.minor != 0 {
55 return SemverBreakingLine::PreOneMinor(version.minor);
56 }
57
58 SemverBreakingLine::PreZeroPatch(version.patch)
59}
60
61pub fn is_within_recent_breaking_lines<'a>(
68 candidate: &Version,
69 known_versions: impl IntoIterator<Item = &'a Version>,
70 limit: usize,
71) -> bool {
72 if limit == 0 {
73 return false;
74 }
75
76 let mut versions = known_versions.into_iter().cloned().collect::<Vec<_>>();
77 versions.sort_by(|left, right| right.cmp(left));
78
79 let candidate_line = breaking_line(candidate);
80 let mut lines = Vec::<SemverBreakingLine>::new();
81 for version in versions {
82 let line = breaking_line(&version);
83 if lines.iter().any(|known| known == &line) {
84 continue;
85 }
86 lines.push(line);
87 if lines.len() == limit {
88 break;
89 }
90 }
91
92 lines.iter().any(|line| line == &candidate_line)
93}
94
95#[cfg(test)]
96mod tests {
97 use super::{
98 SemverBreakingLine, breaking_line, is_semver_compatible_upgrade,
99 is_within_recent_breaking_lines,
100 };
101
102 fn version(raw: &str) -> semver::Version {
103 semver::Version::parse(raw).unwrap()
104 }
105
106 #[test]
107 fn semver_compatibility_matches_cargo_major_rules() {
108 assert!(is_semver_compatible_upgrade(
109 &version("1.2.3"),
110 &version("1.9.0")
111 ));
112 assert!(!is_semver_compatible_upgrade(
113 &version("1.2.3"),
114 &version("2.0.0")
115 ));
116 assert!(is_semver_compatible_upgrade(
117 &version("0.9.1"),
118 &version("0.9.7")
119 ));
120 assert!(!is_semver_compatible_upgrade(
121 &version("0.9.1"),
122 &version("0.10.0")
123 ));
124 assert!(!is_semver_compatible_upgrade(
125 &version("0.0.5"),
126 &version("0.0.6")
127 ));
128 }
129
130 #[test]
131 fn pre_release_candidates_are_never_compatible_upgrades() {
132 assert!(!is_semver_compatible_upgrade(
133 &version("1.4.3"),
134 &version("1.5.0-rc.1")
135 ));
136 assert!(!is_semver_compatible_upgrade(
137 &version("0.9.1"),
138 &version("0.9.7-beta.2")
139 ));
140 }
141
142 #[test]
143 fn breaking_lines_follow_semver_boundaries() {
144 assert_eq!(
145 breaking_line(&version("3.2.1")),
146 SemverBreakingLine::StableMajor(3)
147 );
148 assert_eq!(
149 breaking_line(&version("0.9.4")),
150 SemverBreakingLine::PreOneMinor(9)
151 );
152 assert_eq!(
153 breaking_line(&version("0.0.7")),
154 SemverBreakingLine::PreZeroPatch(7)
155 );
156 }
157
158 #[test]
159 fn recent_breaking_line_window_ignores_older_lines() {
160 let known = [
161 version("3.0.2"),
162 version("2.4.1"),
163 version("1.9.9"),
164 version("0.8.7"),
165 ];
166 assert!(is_within_recent_breaking_lines(
167 &version("3.0.2"),
168 known.iter(),
169 3
170 ));
171 assert!(is_within_recent_breaking_lines(
172 &version("2.4.1"),
173 known.iter(),
174 3
175 ));
176 assert!(is_within_recent_breaking_lines(
177 &version("1.9.9"),
178 known.iter(),
179 3
180 ));
181 assert!(!is_within_recent_breaking_lines(
182 &version("0.8.7"),
183 known.iter(),
184 3
185 ));
186 }
187}