1use std::collections::HashSet;
11
12use crate::SoukError;
13
14pub fn bump_major(version: &str) -> Result<String, SoukError> {
32 let v = semver::Version::parse(version)?;
33 let bumped = semver::Version::new(v.major + 1, 0, 0);
34 Ok(bumped.to_string())
35}
36
37pub fn bump_minor(version: &str) -> Result<String, SoukError> {
55 let v = semver::Version::parse(version)?;
56 let bumped = semver::Version::new(v.major, v.minor + 1, 0);
57 Ok(bumped.to_string())
58}
59
60pub fn bump_patch(version: &str) -> Result<String, SoukError> {
77 let v = semver::Version::parse(version)?;
78 let bumped = semver::Version::new(v.major, v.minor, v.patch + 1);
79 Ok(bumped.to_string())
80}
81
82pub fn generate_unique_name(base: &str, existing: &HashSet<String>) -> String {
102 if !existing.contains(base) {
103 return base.to_string();
104 }
105
106 let mut counter = 2u64;
107 loop {
108 let candidate = format!("{base}-{counter}");
109 if !existing.contains(&candidate) {
110 return candidate;
111 }
112 counter += 1;
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
125 fn bump_major_standard() {
126 assert_eq!(bump_major("1.2.3").unwrap(), "2.0.0");
127 }
128
129 #[test]
130 fn bump_major_from_zero() {
131 assert_eq!(bump_major("0.1.0").unwrap(), "1.0.0");
132 }
133
134 #[test]
135 fn bump_major_strips_prerelease() {
136 assert_eq!(bump_major("1.2.3-beta.1").unwrap(), "2.0.0");
137 }
138
139 #[test]
140 fn bump_major_strips_build_metadata() {
141 assert_eq!(bump_major("1.2.3+build.42").unwrap(), "2.0.0");
142 }
143
144 #[test]
145 fn bump_major_resets_minor_and_patch() {
146 assert_eq!(bump_major("3.9.27").unwrap(), "4.0.0");
147 }
148
149 #[test]
150 fn bump_major_invalid_version() {
151 assert!(bump_major("not-a-version").is_err());
152 }
153
154 #[test]
155 fn bump_major_incomplete_version() {
156 assert!(bump_major("1.2").is_err());
158 }
159
160 #[test]
165 fn bump_minor_standard() {
166 assert_eq!(bump_minor("1.2.3").unwrap(), "1.3.0");
167 }
168
169 #[test]
170 fn bump_minor_from_zero() {
171 assert_eq!(bump_minor("0.0.0").unwrap(), "0.1.0");
172 }
173
174 #[test]
175 fn bump_minor_strips_prerelease() {
176 assert_eq!(bump_minor("2.0.0-rc.1").unwrap(), "2.1.0");
177 }
178
179 #[test]
180 fn bump_minor_resets_patch() {
181 assert_eq!(bump_minor("1.5.99").unwrap(), "1.6.0");
182 }
183
184 #[test]
185 fn bump_minor_zero_x_version() {
186 assert_eq!(bump_minor("0.9.1").unwrap(), "0.10.0");
187 }
188
189 #[test]
190 fn bump_minor_invalid_version() {
191 assert!(bump_minor("abc").is_err());
192 }
193
194 #[test]
199 fn bump_patch_standard() {
200 assert_eq!(bump_patch("1.2.3").unwrap(), "1.2.4");
201 }
202
203 #[test]
204 fn bump_patch_from_zero() {
205 assert_eq!(bump_patch("0.0.0").unwrap(), "0.0.1");
206 }
207
208 #[test]
209 fn bump_patch_strips_prerelease() {
210 assert_eq!(bump_patch("3.1.4-alpha").unwrap(), "3.1.5");
211 }
212
213 #[test]
214 fn bump_patch_large_numbers() {
215 assert_eq!(bump_patch("999.999.999").unwrap(), "999.999.1000");
216 }
217
218 #[test]
219 fn bump_patch_invalid_version() {
220 assert!(bump_patch("").is_err());
221 }
222
223 #[test]
224 fn bump_patch_with_build_and_prerelease() {
225 assert_eq!(bump_patch("1.0.0-alpha+build.1").unwrap(), "1.0.1");
226 }
227
228 #[test]
233 fn unique_name_no_conflict() {
234 let existing: HashSet<String> = HashSet::new();
235 assert_eq!(generate_unique_name("my-plugin", &existing), "my-plugin");
236 }
237
238 #[test]
239 fn unique_name_base_conflict() {
240 let existing: HashSet<String> = ["my-plugin".into()].into();
241 assert_eq!(generate_unique_name("my-plugin", &existing), "my-plugin-2");
242 }
243
244 #[test]
245 fn unique_name_multiple_conflicts() {
246 let existing: HashSet<String> = ["foo".into(), "foo-2".into(), "foo-3".into()].into();
247 assert_eq!(generate_unique_name("foo", &existing), "foo-4");
248 }
249
250 #[test]
251 fn unique_name_gap_in_numbers() {
252 let existing: HashSet<String> = ["foo".into(), "foo-3".into()].into();
254 assert_eq!(generate_unique_name("foo", &existing), "foo-2");
255 }
256
257 #[test]
258 fn unique_name_with_existing_suffix() {
259 let existing: HashSet<String> = ["plugin-2".into()].into();
261 assert_eq!(generate_unique_name("plugin-2", &existing), "plugin-2-2");
262 }
263
264 #[test]
265 fn unique_name_empty_base() {
266 let existing: HashSet<String> = ["".into()].into();
267 assert_eq!(generate_unique_name("", &existing), "-2");
268 }
269}