cargo_v/
lib.rs

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(&current_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(&current_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(&current_version_string)?;
91  let new_version_number = string_version_to_number(&new_version)?;
92
93  match new_version_number.cmp(&current_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  // TODO: Give a more friendly error message
313  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}