1use std::cmp::Ordering;
2use std::error::Error;
3
4mod parser;
5pub use parser::*;
6
7#[derive(Debug, PartialEq)]
8pub enum VersionLabel {
9 Major,
10 Minor,
11 Patch,
12 NumericVersion(String),
13}
14
15pub fn parse_string_to_version_label(
16 string: &str,
17) -> Result<VersionLabel, Box<dyn Error>> {
18 match string {
19 "patch" => Ok(VersionLabel::Patch),
20 "minor" => Ok(VersionLabel::Minor),
21 "major" => Ok(VersionLabel::Major),
22 string => {
23 if is_valid_numeric_version(string) {
24 return Ok(VersionLabel::NumericVersion(String::from(string)));
25 }
26
27 Err("invalid string version")?
28 }
29 }
30}
31
32fn is_valid_numeric_version(string: &str) -> bool {
33 string
34 .split('.')
35 .take(3)
36 .all(|item| item.parse::<u32>().is_ok())
37}
38
39pub fn get_updated_version(
40 cargo_toml_content: &str,
41 label: &VersionLabel,
42) -> Result<String, Box<dyn Error>> {
43 let current_version =
44 parser::get_version_from_cargo_toml(cargo_toml_content)?;
45 let current_version = string_version_to_tuple(¤t_version)?;
46 let (major, minor, patch) = current_version;
47 let new_version = match label {
48 VersionLabel::Major => format!("{}.0.0", major + 1),
49 VersionLabel::Minor => format!("{}.{}.0", major, minor + 1),
50 VersionLabel::Patch => format!("{}.{}.{}", major, minor, patch + 1),
51 VersionLabel::NumericVersion(v) => {
52 parse_numeric_version(¤t_version, v)?
53 }
54 };
55
56 Ok(new_version)
57}
58
59pub fn tuple_version_to_string(tuple_version: &(u32, u32, u32)) -> String {
60 format!(
61 "{}.{}.{}",
62 tuple_version.0, tuple_version.1, tuple_version.2,
63 )
64}
65
66fn get_padded_version(numeric_version: &str) -> Result<String, Box<dyn Error>> {
67 let (major, minor, patch) = string_version_to_tuple(numeric_version)?;
68 let new_version = format!("{}.{}.{}", major, minor, patch);
69 Ok(new_version)
70}
71
72fn string_version_to_tuple(
73 version: &str,
74) -> Result<(u32, u32, u32), Box<dyn Error>> {
75 let version = version.replace('v', "");
76 let mut version_split = version.split('.');
77 let major = version_split.next().unwrap_or("0").parse::<u32>()?;
78 let minor = version_split.next().unwrap_or("0").parse::<u32>()?;
79 let patch = version_split.next().unwrap_or("0").parse::<u32>()?;
80 Ok((major, minor, patch))
81}
82
83fn parse_numeric_version(
84 current_version_tuple: &(u32, u32, u32),
85 numeric_version: &str,
86) -> Result<String, Box<dyn Error>> {
87 let new_version = get_padded_version(numeric_version)?;
88 let current_version_string = tuple_version_to_string(current_version_tuple);
89 let current_version_number =
90 string_version_to_number(¤t_version_string)?;
91 let new_version_number = string_version_to_number(&new_version)?;
92
93 match new_version_number.cmp(¤t_version_number) {
94 Ordering::Less => {
95 Err("you can not set a version lower than the current version")?
96 }
97 Ordering::Equal => {
98 Err("new version should not be the same as current version")?
99 }
100 Ordering::Greater => Ok(new_version),
101 }
102}
103
104fn string_version_to_number(version: &str) -> Result<u32, Box<dyn Error>> {
105 Ok(version.replace('.', "").parse()?)
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 fn get_cargo_toml(version: &str) -> String {
113 format!("\
114[dependencies]
115tokio = {{ version = \"1.1.1\" }}
116
117[package]
118name = \"cargo-v\"
119version = \"{}\"
120edition = \"2021\"
121description = \"Update the version of your package easily\"
122license = \"MIT\"
123
124# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
125[dependencies.dev]
126other = {{ version = \"1.1.8\" }}
127 ",
128 version)
129 }
130
131 #[test]
132 fn should_parse_string_patch_to_version_label() {
133 let actual = parse_string_to_version_label("patch").unwrap();
134 let expected = VersionLabel::Patch;
135 assert_eq!(actual, expected);
136 }
137
138 #[test]
139 fn should_parse_string_minor_to_version_label() {
140 let actual = parse_string_to_version_label("minor").unwrap();
141 let expected = VersionLabel::Minor;
142 assert_eq!(actual, expected);
143 }
144
145 #[test]
146 fn should_parse_string_major_to_version_label() {
147 let actual = parse_string_to_version_label("major").unwrap();
148 let expected = VersionLabel::Major;
149 assert_eq!(actual, expected);
150 }
151
152 #[test]
153 fn should_parse_numeric_string_to_version_label() {
154 let actual = parse_string_to_version_label("1.0.0").unwrap();
155 let expected = VersionLabel::NumericVersion(String::from("1.0.0"));
156 assert_eq!(actual, expected);
157 }
158
159 #[test]
160 fn should_fail_to_parse_string_to_version_label() {
161 let actual = parse_string_to_version_label("rice");
162 match actual {
163 Err(e) => assert!(e.to_string().contains("invalid string version")),
164 Ok(_) => unreachable!(),
165 };
166 }
167
168 #[test]
169 fn should_update_version_by_patch_label() {
170 let cargo_toml = get_cargo_toml("0.0.1");
171 let new_version =
172 get_updated_version(&cargo_toml, &VersionLabel::Patch).unwrap();
173 let actual = set_version_in_cargo_toml(&cargo_toml, &new_version);
174 let expected = get_cargo_toml("0.0.2");
175 assert_eq!(actual, expected);
176 }
177
178 #[test]
179 fn should_update_version_by_minor_label() {
180 let cargo_toml = get_cargo_toml("0.0.2");
181 let new_version =
182 get_updated_version(&cargo_toml, &VersionLabel::Minor).unwrap();
183 let actual = set_version_in_cargo_toml(&cargo_toml, &new_version);
184 let expected = get_cargo_toml("0.1.0");
185 assert_eq!(actual, expected);
186 }
187
188 #[test]
189 fn should_update_version_by_major_label() {
190 let cargo_toml = get_cargo_toml("0.1.8");
191 let new_version =
192 get_updated_version(&cargo_toml, &VersionLabel::Major).unwrap();
193 let actual = set_version_in_cargo_toml(&cargo_toml, &new_version);
194 let expected = get_cargo_toml("1.0.0");
195 assert_eq!(actual, expected);
196 }
197
198 #[test]
199 fn should_update_patch_version_by_hand() {
200 let cargo_toml = get_cargo_toml("0.0.1");
201 let new_version = get_updated_version(
202 &cargo_toml,
203 &VersionLabel::NumericVersion(String::from("0.0.2")),
204 )
205 .unwrap();
206 let actual = set_version_in_cargo_toml(&cargo_toml, &new_version);
207 let expected = get_cargo_toml("0.0.2");
208 assert_eq!(actual, expected);
209 }
210
211 #[test]
212 fn should_update_minor_version_by_hand() {
213 let cargo_toml = get_cargo_toml("0.0.7");
214 let new_version = get_updated_version(
215 &cargo_toml,
216 &VersionLabel::NumericVersion(String::from("0.1.0")),
217 )
218 .unwrap();
219 let actual = set_version_in_cargo_toml(&cargo_toml, &new_version);
220 let expected = get_cargo_toml("0.1.0");
221 assert_eq!(actual, expected);
222 }
223
224 #[test]
225 fn should_update_major_version_by_hand() {
226 let cargo_toml = get_cargo_toml("2.8.1");
227 let new_version = get_updated_version(
228 &cargo_toml,
229 &VersionLabel::NumericVersion(String::from("3.0.0")),
230 )
231 .unwrap();
232 let actual = set_version_in_cargo_toml(&cargo_toml, &new_version);
233 let expected = get_cargo_toml("3.0.0");
234 assert_eq!(actual, expected);
235 }
236
237 #[test]
238 fn should_accept_v_char_in_front_of_version() {
239 let cargo_toml = get_cargo_toml("2.8.1");
240 let new_version = get_updated_version(
241 &cargo_toml,
242 &VersionLabel::NumericVersion(String::from("v3.0.0")),
243 )
244 .unwrap();
245 let actual = set_version_in_cargo_toml(&cargo_toml, &new_version);
246 let expected = get_cargo_toml("3.0.0");
247 assert_eq!(actual, expected);
248 }
249
250 #[test]
251 fn should_not_set_a_new_version_equal_to_the_current_version() {
252 let cargo_toml = get_cargo_toml("2.2.0");
253 match get_updated_version(
254 &cargo_toml,
255 &VersionLabel::NumericVersion(String::from("2.2.0")),
256 ) {
257 Err(e) => {
258 assert!(e
259 .to_string()
260 .contains("new version should not be the same as current version"));
261 }
262 _ => unreachable!(),
263 };
264 }
265
266 #[test]
267 fn should_not_set_a_new_version_lower_than_current_version() {
268 let cargo_toml = get_cargo_toml("2.1.2");
269 match get_updated_version(
270 &cargo_toml,
271 &VersionLabel::NumericVersion(String::from("2.1.1")),
272 ) {
273 Err(e) => {
274 assert!(e.to_string().contains(
275 "you can not set a version lower than the current version"
276 ));
277 }
278 _ => unreachable!(),
279 }
280 }
281
282 #[test]
283 fn should_pad_partial_version_with_leading_zeroes() {
284 let partial_version = "1";
285 let actual = get_padded_version(partial_version).unwrap();
286 let expected = "1.0.0";
287 assert_eq!(actual, expected);
288 }
289
290 #[test]
291 fn should_turn_string_version_in_tuple() {
292 let actual = string_version_to_tuple("1.0.0").unwrap();
293 let expected = (1, 0, 0);
294 assert_eq!(actual, expected);
295 }
296
297 #[test]
298 fn should_turn_partial_string_version_in_tuple() {
299 let actual = string_version_to_tuple("1").unwrap();
300 let expected = (1, 0, 0);
301 assert_eq!(actual, expected);
302 }
303
304 #[test]
305 fn should_turn_tuple_version_in_string() {
306 let actual = tuple_version_to_string(&(1, 2, 3));
307 let expected = "1.2.3";
308 assert_eq!(actual, expected);
309 }
310
311 #[test]
312 fn should_not_allow_set_a_negative_version() {
314 let cargo_toml = get_cargo_toml("2.2.0");
315 match get_updated_version(
316 &cargo_toml,
317 &VersionLabel::NumericVersion(String::from("-2.2.1")),
318 ) {
319 Err(e) => {
320 assert_eq!(e.to_string(), "invalid digit found in string");
321 }
322 _ => unreachable!(),
323 }
324 }
325}