release_kit/self_depend/
matrix.rs1use serde::Serialize;
13
14use super::Observed;
15use super::manager::Manager;
16use super::venue::Venue;
17use crate::error::RkError;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Support {
22 Fragment,
24 Manual(&'static str),
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
31#[serde(rename_all = "kebab-case")]
32pub enum Mode {
33 Fragment,
35 Seed,
37 Manual,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
43pub struct Pair {
44 pub manager: Manager,
46 pub venue: Venue,
48}
49
50pub const REASONS: [&str; 4] = [
53 "nixpkgs-attribute-unknown",
54 "network-hash-needed",
55 "no-mise-flake-backend",
56 "asdf-plugin-unknown",
57];
58
59#[must_use]
61pub const fn support(manager: Manager, venue: Venue) -> Support {
62 match (manager, venue) {
63 (Manager::Flake | Manager::Devbox, Venue::Flake)
64 | (Manager::Mise, Venue::Crates | Venue::GithubRelease) => Support::Fragment,
65 (Manager::Flake, Venue::GithubRelease) => Support::Manual("network-hash-needed"),
66 (Manager::Flake | Manager::Devbox, _) => Support::Manual("nixpkgs-attribute-unknown"),
67 (Manager::Mise, Venue::Flake) => Support::Manual("no-mise-flake-backend"),
68 (Manager::Asdf, _) => Support::Manual("asdf-plugin-unknown"),
69 }
70}
71
72#[must_use]
74pub const fn preference(manager: Manager) -> [Venue; 3] {
75 match manager {
76 Manager::Flake | Manager::Devbox => [Venue::Flake, Venue::Crates, Venue::GithubRelease],
77 Manager::Mise | Manager::Asdf => [Venue::Crates, Venue::GithubRelease, Venue::Flake],
78 }
79}
80
81#[must_use]
84pub fn default_venue(manager: Manager) -> Venue {
85 let order = preference(manager);
86 order
87 .into_iter()
88 .find(|venue| support(manager, *venue) == Support::Fragment)
89 .unwrap_or(order[0])
90}
91
92pub fn choose(
102 observed: &Observed,
103 manager: Option<Manager>,
104 venue: Option<Venue>,
105) -> Result<Pair, RkError> {
106 let manager = match manager {
107 Some(manager) => manager,
108 None => detected_manager(observed)?,
109 };
110 let venue = venue.unwrap_or_else(|| default_venue(manager));
111 Ok(Pair { manager, venue })
112}
113
114fn detected_manager(observed: &Observed) -> Result<Manager, RkError> {
117 let present: Vec<Manager> = observed
118 .managers
119 .iter()
120 .filter(|entry| entry.present.is_present())
121 .map(|entry| entry.manager)
122 .collect();
123 match present.as_slice() {
124 [one] => Ok(*one),
125 [] => Ok(Manager::Flake),
126 many => {
127 let names: Vec<&str> = many.iter().map(|m| m.as_str()).collect();
128 Err(RkError::Usage(format!(
129 "the target carries {}; pass --manager to choose",
130 names.join(" and ")
131 )))
132 }
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::{Manager, Mode, REASONS, Support, Venue, default_venue, preference, support};
139
140 #[test]
142 fn every_pair_in_the_matrix_is_classified_once() {
143 let mut fragment_pairs = 0;
144 for manager in Manager::ALL {
145 let mut seen = Vec::new();
146 for venue in preference(manager) {
147 assert!(!seen.contains(&venue), "{manager:?} lists {venue:?} once");
148 seen.push(venue);
149 if support(manager, venue) == Support::Fragment {
150 fragment_pairs += 1;
151 }
152 }
153 assert_eq!(
154 seen.len(),
155 Venue::ALL.len(),
156 "{manager:?} covers every venue"
157 );
158 }
159 assert_eq!(
160 fragment_pairs, 4,
161 "flake, devbox, and the two mise pairs render"
162 );
163 assert_eq!(default_venue(Manager::Flake), Venue::Flake);
164 assert_eq!(default_venue(Manager::Mise), Venue::Crates);
165 assert_eq!(default_venue(Manager::Devbox), Venue::Flake);
166 assert_eq!(default_venue(Manager::Asdf), Venue::Crates);
167 assert_eq!(Mode::Seed, Mode::Seed);
168 }
169
170 #[test]
172 fn every_manual_pair_names_a_closed_reason() {
173 let mut named = Vec::new();
174 for manager in Manager::ALL {
175 for venue in Venue::ALL {
176 if let Support::Manual(reason) = support(manager, venue) {
177 assert!(
178 REASONS.contains(&reason),
179 "{manager:?}/{venue:?}: {reason} is not in the closed set"
180 );
181 named.push(reason);
182 }
183 }
184 }
185 for reason in REASONS {
186 assert!(named.contains(&reason), "{reason} is named by no pair");
187 }
188 }
189
190 #[test]
191 fn the_asdf_rows_are_manual_with_their_reason() {
192 for venue in Venue::ALL {
193 assert_eq!(
194 support(Manager::Asdf, venue),
195 Support::Manual("asdf-plugin-unknown"),
196 "{venue:?}"
197 );
198 }
199 }
200}