use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use crate::manager::{Installed, Manager, Package, Step, capture, version_in};
use crate::snapshot;
pub struct Asdf;
impl Manager for Asdf {
fn id(&self) -> &'static str {
"asdf"
}
fn name(&self) -> &'static str {
"asdf (tools)"
}
fn binary(&self) -> &'static str {
"asdf"
}
fn supported(&self) -> bool {
cfg!(unix)
}
fn version(&self, bin: &Path) -> Option<String> {
version_in(&capture(bin, ["version"]).ok()?.stdout)
}
fn outdated(&self, bin: &Path) -> Result<Vec<Package>, String> {
let global = global_versions();
if global.is_empty() {
return Ok(Vec::new());
}
let latest = parse_latest(&capture(bin, ["latest", "--all"])?.expect(&[0])?);
Ok(outdated(&global, &latest))
}
fn upgrade_steps(&self, _bin: &Path, packages: &[Package]) -> Vec<Step> {
packages
.iter()
.map(|p| Step {
commands: vec![
vec!["install".to_string(), p.name.clone(), p.latest.clone()],
vec![
"set".to_string(),
"-u".to_string(),
p.name.clone(),
p.latest.clone(),
],
],
packages: vec![p.clone()],
password: false,
})
.collect()
}
fn installed(&self, _bin: &Path) -> Result<Vec<Installed>, String> {
Ok(global_versions()
.into_iter()
.map(|(name, version)| Installed::new(name).version(version))
.collect())
}
fn install_steps(&self, _bin: &Path, packages: &[Installed]) -> Vec<Step> {
packages
.iter()
.map(|p| {
let version = p.version.clone().unwrap_or_else(|| "latest".to_string());
Step {
commands: vec![
vec!["plugin".to_string(), "add".to_string(), p.name.clone()],
vec!["install".to_string(), p.name.clone(), version.clone()],
vec!["set".to_string(), "-u".to_string(), p.name.clone(), version],
],
packages: vec![Package::from(p)],
password: false,
}
})
.collect()
}
fn config_files(&self) -> &'static [&'static str] {
&[".tool-versions", ".asdfrc"]
}
}
fn global_versions() -> BTreeMap<String, String> {
snapshot::home()
.and_then(|home| fs::read_to_string(home.join(".tool-versions")).ok())
.map(|text| parse_tool_versions(&text))
.unwrap_or_default()
}
fn parse_tool_versions(text: &str) -> BTreeMap<String, String> {
text.lines()
.filter_map(|line| {
let mut fields = line.split('#').next()?.split_whitespace();
let (name, version) = (fields.next()?, fields.next()?);
let pinned = version == "system" || version.contains(':');
(!pinned).then(|| (name.to_string(), version.to_string()))
})
.collect()
}
fn parse_latest(text: &str) -> BTreeMap<String, String> {
text.lines()
.filter_map(|line| {
let mut fields = line.split_whitespace();
Some((fields.next()?.to_string(), fields.next()?.to_string()))
})
.collect()
}
fn outdated(global: &BTreeMap<String, String>, latest: &BTreeMap<String, String>) -> Vec<Package> {
global
.iter()
.filter_map(|(name, current)| {
let latest = latest.get(name)?;
(latest != current).then(|| Package {
name: name.clone(),
current: current.clone(),
latest: latest.clone(),
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_the_global_tool_versions() {
let text = "jq 1.7 1.6\n# comment\nnodejs 22.13.1 # pinned for work\npython system\nruby ref:v3.4.0\n\n";
let global = parse_tool_versions(text);
assert_eq!(global.len(), 2);
assert_eq!(global["jq"], "1.7");
assert_eq!(global["nodejs"], "22.13.1");
}
#[test]
fn compares_against_the_latest_release() {
let global = parse_tool_versions("jq 1.7\nshellcheck 0.11.0\ngolang 1.25.0\n");
let latest = parse_latest(
"jq\t1.8.2\tmissing\nshellcheck\t0.11.0\tinstalled\nterraform\t1.13.0\tmissing\n",
);
let packages = outdated(&global, &latest);
assert_eq!(packages.len(), 1);
assert_eq!(packages[0].name, "jq");
assert_eq!(packages[0].current, "1.7");
assert_eq!(packages[0].latest, "1.8.2");
}
}