use serde::Serialize;
use super::Observed;
use super::manager::Manager;
use super::venue::Venue;
use crate::error::RkError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Support {
Fragment,
Manual(&'static str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Mode {
Fragment,
Seed,
Manual,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct Pair {
pub manager: Manager,
pub venue: Venue,
}
pub const REASONS: [&str; 4] = [
"nixpkgs-attribute-unknown",
"network-hash-needed",
"no-mise-flake-backend",
"asdf-plugin-unknown",
];
#[must_use]
pub const fn support(manager: Manager, venue: Venue) -> Support {
match (manager, venue) {
(Manager::Flake | Manager::Devbox, Venue::Flake)
| (Manager::Mise, Venue::Crates | Venue::GithubRelease) => Support::Fragment,
(Manager::Flake, Venue::GithubRelease) => Support::Manual("network-hash-needed"),
(Manager::Flake | Manager::Devbox, _) => Support::Manual("nixpkgs-attribute-unknown"),
(Manager::Mise, Venue::Flake) => Support::Manual("no-mise-flake-backend"),
(Manager::Asdf, _) => Support::Manual("asdf-plugin-unknown"),
}
}
#[must_use]
pub const fn preference(manager: Manager) -> [Venue; 3] {
match manager {
Manager::Flake | Manager::Devbox => [Venue::Flake, Venue::Crates, Venue::GithubRelease],
Manager::Mise | Manager::Asdf => [Venue::Crates, Venue::GithubRelease, Venue::Flake],
}
}
#[must_use]
pub fn default_venue(manager: Manager) -> Venue {
let order = preference(manager);
order
.into_iter()
.find(|venue| support(manager, *venue) == Support::Fragment)
.unwrap_or(order[0])
}
pub fn choose(
observed: &Observed,
manager: Option<Manager>,
venue: Option<Venue>,
) -> Result<Pair, RkError> {
let manager = match manager {
Some(manager) => manager,
None => detected_manager(observed)?,
};
let venue = venue.unwrap_or_else(|| default_venue(manager));
Ok(Pair { manager, venue })
}
fn detected_manager(observed: &Observed) -> Result<Manager, RkError> {
let present: Vec<Manager> = observed
.managers
.iter()
.filter(|entry| entry.present.is_present())
.map(|entry| entry.manager)
.collect();
match present.as_slice() {
[one] => Ok(*one),
[] => Ok(Manager::Flake),
many => {
let names: Vec<&str> = many.iter().map(|m| m.as_str()).collect();
Err(RkError::Usage(format!(
"the target carries {}; pass --manager to choose",
names.join(" and ")
)))
}
}
}
#[cfg(test)]
mod tests {
use super::{Manager, Mode, REASONS, Support, Venue, default_venue, preference, support};
#[test]
fn every_pair_in_the_matrix_is_classified_once() {
let mut fragment_pairs = 0;
for manager in Manager::ALL {
let mut seen = Vec::new();
for venue in preference(manager) {
assert!(!seen.contains(&venue), "{manager:?} lists {venue:?} once");
seen.push(venue);
if support(manager, venue) == Support::Fragment {
fragment_pairs += 1;
}
}
assert_eq!(
seen.len(),
Venue::ALL.len(),
"{manager:?} covers every venue"
);
}
assert_eq!(
fragment_pairs, 4,
"flake, devbox, and the two mise pairs render"
);
assert_eq!(default_venue(Manager::Flake), Venue::Flake);
assert_eq!(default_venue(Manager::Mise), Venue::Crates);
assert_eq!(default_venue(Manager::Devbox), Venue::Flake);
assert_eq!(default_venue(Manager::Asdf), Venue::Crates);
assert_eq!(Mode::Seed, Mode::Seed);
}
#[test]
fn every_manual_pair_names_a_closed_reason() {
let mut named = Vec::new();
for manager in Manager::ALL {
for venue in Venue::ALL {
if let Support::Manual(reason) = support(manager, venue) {
assert!(
REASONS.contains(&reason),
"{manager:?}/{venue:?}: {reason} is not in the closed set"
);
named.push(reason);
}
}
}
for reason in REASONS {
assert!(named.contains(&reason), "{reason} is named by no pair");
}
}
#[test]
fn the_asdf_rows_are_manual_with_their_reason() {
for venue in Venue::ALL {
assert_eq!(
support(Manager::Asdf, venue),
Support::Manual("asdf-plugin-unknown"),
"{venue:?}"
);
}
}
}