use camino::{Utf8Path, Utf8PathBuf};
use serde::Serialize;
use crate::self_depend::pin::{self, Pin};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, clap::ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub enum Manager {
Flake,
Mise,
Asdf,
Devbox,
}
impl Manager {
pub const ALL: [Self; 4] = [Self::Flake, Self::Mise, Self::Asdf, Self::Devbox];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Flake => "flake",
Self::Mise => "mise",
Self::Asdf => "asdf",
Self::Devbox => "devbox",
}
}
#[must_use]
pub const fn files(self) -> &'static [&'static str] {
match self {
Self::Flake => &["flake.nix"],
Self::Mise => &["mise.toml", ".mise.toml"],
Self::Asdf => &[".tool-versions"],
Self::Devbox => &["devbox.json"],
}
}
#[must_use]
pub const fn lock_file(self) -> Option<&'static str> {
match self {
Self::Flake => Some("flake.lock"),
Self::Mise | Self::Asdf | Self::Devbox => None,
}
}
}
impl std::fmt::Display for Manager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Detected {
pub manager: Manager,
pub file: Option<Utf8PathBuf>,
pub pin: Option<Pin>,
}
impl Detected {
#[must_use]
pub const fn names_this_tool(&self) -> bool {
self.pin.is_some()
}
}
#[must_use]
pub fn detect(target: &Utf8Path) -> Vec<Detected> {
Manager::ALL
.into_iter()
.map(|manager| detect_one(target, manager))
.collect()
}
fn detect_one(target: &Utf8Path, manager: Manager) -> Detected {
for file in manager.files() {
let path = target.join(file);
let Ok(text) = std::fs::read_to_string(&path) else {
continue;
};
return Detected {
manager,
file: Some(Utf8PathBuf::from(*file)),
pin: pin::read(manager, &text),
};
}
Detected {
manager,
file: None,
pin: None,
}
}
#[must_use]
pub fn wired(detected: &[Detected]) -> Vec<&Detected> {
detected
.iter()
.filter(|held| held.names_this_tool())
.collect()
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, reason = "a test panics as its failure signal")]
use super::*;
#[test]
fn every_manager_is_detected_once_in_order() {
let dir = tempfile::tempdir().unwrap();
let root = Utf8Path::from_path(dir.path()).unwrap();
std::fs::write(root.join(".tool-versions"), "nodejs 20.0.0\n").unwrap();
let held = detect(root);
let order: Vec<Manager> = held.iter().map(|d| d.manager).collect();
assert_eq!(order, Manager::ALL);
assert_eq!(
held[2].file.as_deref(),
Some(Utf8Path::new(".tool-versions"))
);
assert!(held[2].pin.is_none());
assert!(held[0].file.is_none());
assert!(wired(&held).is_empty());
}
#[test]
fn a_mise_file_is_found_under_either_name() {
let dir = tempfile::tempdir().unwrap();
let root = Utf8Path::from_path(dir.path()).unwrap();
std::fs::write(root.join(".mise.toml"), "[tools]\n").unwrap();
let held = detect_one(root, Manager::Mise);
assert_eq!(held.file.as_deref(), Some(Utf8Path::new(".mise.toml")));
}
}