spec_driven_docs/self_depend/
manager.rs1use camino::{Utf8Path, Utf8PathBuf};
8use serde::Serialize;
9
10use crate::self_depend::pin::{self, Pin};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, clap::ValueEnum)]
14#[serde(rename_all = "kebab-case")]
15pub enum Manager {
16 Flake,
18 Mise,
20 Asdf,
22 Devbox,
24}
25
26impl Manager {
27 pub const ALL: [Self; 4] = [Self::Flake, Self::Mise, Self::Asdf, Self::Devbox];
29
30 #[must_use]
32 pub const fn as_str(self) -> &'static str {
33 match self {
34 Self::Flake => "flake",
35 Self::Mise => "mise",
36 Self::Asdf => "asdf",
37 Self::Devbox => "devbox",
38 }
39 }
40
41 #[must_use]
45 pub const fn files(self) -> &'static [&'static str] {
46 match self {
47 Self::Flake => &["flake.nix"],
48 Self::Mise => &["mise.toml", ".mise.toml"],
49 Self::Asdf => &[".tool-versions"],
50 Self::Devbox => &["devbox.json"],
51 }
52 }
53
54 #[must_use]
56 pub const fn lock_file(self) -> Option<&'static str> {
57 match self {
58 Self::Flake => Some("flake.lock"),
59 Self::Mise | Self::Asdf | Self::Devbox => None,
60 }
61 }
62}
63
64impl std::fmt::Display for Manager {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 f.write_str(self.as_str())
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct Detected {
73 pub manager: Manager,
75 pub file: Option<Utf8PathBuf>,
77 pub pin: Option<Pin>,
79}
80
81impl Detected {
82 #[must_use]
84 pub const fn names_this_tool(&self) -> bool {
85 self.pin.is_some()
86 }
87}
88
89#[must_use]
93pub fn detect(target: &Utf8Path) -> Vec<Detected> {
94 Manager::ALL
95 .into_iter()
96 .map(|manager| detect_one(target, manager))
97 .collect()
98}
99
100fn detect_one(target: &Utf8Path, manager: Manager) -> Detected {
101 for file in manager.files() {
102 let path = target.join(file);
103 let Ok(text) = std::fs::read_to_string(&path) else {
104 continue;
105 };
106 return Detected {
107 manager,
108 file: Some(Utf8PathBuf::from(*file)),
109 pin: pin::read(manager, &text),
110 };
111 }
112 Detected {
113 manager,
114 file: None,
115 pin: None,
116 }
117}
118
119#[must_use]
121pub fn wired(detected: &[Detected]) -> Vec<&Detected> {
122 detected
123 .iter()
124 .filter(|held| held.names_this_tool())
125 .collect()
126}
127
128#[cfg(test)]
129mod tests {
130 #![allow(clippy::unwrap_used, reason = "a test panics as its failure signal")]
131
132 use super::*;
133
134 #[test]
135 fn every_manager_is_detected_once_in_order() {
136 let dir = tempfile::tempdir().unwrap();
137 let root = Utf8Path::from_path(dir.path()).unwrap();
138 std::fs::write(root.join(".tool-versions"), "nodejs 20.0.0\n").unwrap();
139 let held = detect(root);
140 let order: Vec<Manager> = held.iter().map(|d| d.manager).collect();
141 assert_eq!(order, Manager::ALL);
142 assert_eq!(
143 held[2].file.as_deref(),
144 Some(Utf8Path::new(".tool-versions"))
145 );
146 assert!(held[2].pin.is_none());
147 assert!(held[0].file.is_none());
148 assert!(wired(&held).is_empty());
149 }
150
151 #[test]
152 fn a_mise_file_is_found_under_either_name() {
153 let dir = tempfile::tempdir().unwrap();
154 let root = Utf8Path::from_path(dir.path()).unwrap();
155 std::fs::write(root.join(".mise.toml"), "[tools]\n").unwrap();
156 let held = detect_one(root, Manager::Mise);
157 assert_eq!(held.file.as_deref(), Some(Utf8Path::new(".mise.toml")));
158 }
159}