1use anyhow::anyhow;
2use regex::Regex;
3use semver::{Version, VersionReq};
4use std::process::Command;
5use toml_edit::{value, DocumentMut, Item, Table};
6
7fn is_compatible(actual_version: &Version, version_requirement: &str) -> Option<bool> {
8 VersionReq::parse(version_requirement)
9 .and_then(|x| Ok(x.matches(&actual_version)))
10 .ok()
11}
12
13fn parse_semver(tool_name: &str, stdout: &str) -> Option<String> {
14 let re = Regex::new(&format!(r"{} (\d+\.\d+\.\d+)", tool_name)).unwrap();
15 re.captures(stdout).and_then(|x| Some(x[1].to_string()))
16}
17
18pub fn get_tool_version(tool_name: &str) -> anyhow::Result<Version> {
19 let mut command = Command::new(tool_name);
20 let uv_v = command.arg("-V");
21 let out = uv_v
22 .output()
23 .expect(&format!("Cannot run '{} -V' command", tool_name));
24 let out_string =
25 String::from_utf8(out.stdout).expect(&format!("Cannot fetch '{} -V' output", tool_name));
26 let out_semver =
27 parse_semver(tool_name, &out_string).ok_or(anyhow!("Cannot parse version number"))?;
28 Version::parse(&out_semver).map_err(anyhow::Error::from)
29}
30
31fn nest(item: &mut Item, old_key: &str, new_key: &str) {
32 if let Some((key, universal)) = item
33 .as_table_mut()
34 .expect("Error while parsing the item as as table")
35 .remove_entry(old_key)
36 {
37 let mut sub_table = item
38 .get_mut(new_key)
39 .and_then(|x| x.as_table_mut())
40 .unwrap_or(&mut Table::new())
41 .clone();
42
43 sub_table.insert(&key, universal);
44 item[new_key] = Item::Table(sub_table);
45 }
46}
47
48fn rename(table: &mut Table, key: &str, new_key: &str) {
49 if let Some(value) = table.remove(key) {
50 table[new_key] = value;
51 }
52}
53
54fn rename_and_invert(table: &mut Item, key: &str, new_key: &str) -> anyhow::Result<()> {
55 if let Some(key_val) = table.as_table_mut().and_then(|x| x.remove(key)) {
56 let new_val = key_val.as_bool();
57 table[new_key] = value(!new_val.ok_or(anyhow!("Value of {} is not a valid bool", key))?)
58 }
59 Ok(())
60}
61
62pub fn convert(document: &mut DocumentMut, uv: Version) -> anyhow::Result<()> {
63 let uv_ge_4 = is_compatible(&uv, ">=0.4.0");
64
65 if let Some(tool) = document.get_mut("tool") {
66 if let Some(tool) = tool.as_table_mut() {
67 rename(tool, "rye", "uv");
68
69 nest(&mut tool["uv"], "universal", "pip");
70 nest(&mut tool["uv"], "generate-hashes", "pip");
71
72 rename_and_invert(&mut tool["uv"], "lock-with-sources", "no-sources")?;
73
74 if let Some(uv_4_compatible) = uv_ge_4 {
75 if uv_4_compatible {
76 rename_and_invert(&mut tool["uv"], "virtual", "package")?;
77 } else {
78 tool["uv"].as_table_mut().and_then(|x| x.remove("virtual"));
79 }
80 }
81 }
82 }
83
84 Ok(())
85}
86
87#[cfg(test)]
88mod tests {
89 use pretty_assertions::assert_eq;
90 use std::str::FromStr;
91
92 use toml_edit::Value;
93
94 use super::*;
95
96 #[test]
97 fn test_rename() -> anyhow::Result<()> {
98 let mut table = Table::new();
99 let val = Item::Value(Value::from_str(&"'foo'")?);
100 table["test_foo"] = val.clone();
101 rename(&mut table, "test_foo", "test_bar");
102 assert!(table.contains_key("test_bar"));
103 assert!(!table.contains_key("test_foo"));
104 assert_eq!(table["test_bar"].to_string(), val.to_string());
105 Ok(())
106 }
107
108 #[test]
109 fn test_rename_and_invert() -> anyhow::Result<()> {
110 let mut table = Table::new();
111 let mut inner_table = Table::new();
112 let val = Item::Value(Value::from(true));
113 inner_table["test_foo"] = val.clone();
114 table["bool_val"] = Item::Table(inner_table);
115
116 rename_and_invert(&mut table["bool_val"], "test_foo", "test_bar")?;
117 assert_eq!(table["bool_val"]["test_bar"].as_bool().unwrap(), false);
118 Ok(())
119 }
120}