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