Skip to main content

release_kit/self_depend/
matrix.rs

1//! The manager × venue matrix for `rk` itself: which pairs land as a
2//! fragment, seeded where the manager file is absent, and which are a
3//! hand edit with a named reason.
4//!
5//! The matrix guesses nothing it cannot read offline. release-kit's
6//! crate name, flake output, and release archives are known, so the
7//! pairs that consume one of those render; the pairs that need a nixpkgs
8//! attribute, a source hash, or an asdf plugin nobody publishes are
9//! `manual` with that reason, and the report says so instead of
10//! inventing one.
11
12use serde::Serialize;
13
14use super::Observed;
15use super::manager::Manager;
16use super::venue::Venue;
17use crate::error::RkError;
18
19/// Whether a pair renders, and why not where it does not.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Support {
22    /// The pair renders fragments, and a seed where the file is absent.
23    Fragment,
24    /// The pair needs knowledge the binary does not have offline.
25    Manual(&'static str),
26}
27
28/// How a pair lands in one target: the closed `support` vocabulary of
29/// the add report.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
31#[serde(rename_all = "kebab-case")]
32pub enum Mode {
33    /// The manager file is present: the fragments are applied by hand.
34    Fragment,
35    /// The manager file is absent: `--apply` seeds it.
36    Seed,
37    /// A hand edit the report describes; nothing is written.
38    Manual,
39}
40
41/// One manager and one venue.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
43pub struct Pair {
44    /// The manager.
45    pub manager: Manager,
46    /// The venue.
47    pub venue: Venue,
48}
49
50/// Every manual reason the matrix can name, closed: a test holds each
51/// pair's reason to this list.
52pub const REASONS: [&str; 4] = [
53    "nixpkgs-attribute-unknown",
54    "network-hash-needed",
55    "no-mise-flake-backend",
56    "asdf-plugin-unknown",
57];
58
59/// The support of one pair.
60#[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/// The venues a manager prefers, first first.
73#[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/// The first venue a manager renders a fragment for, or its first
82/// preference where every venue is manual.
83#[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
92/// The pair one `add` or `sync` serves, from the flags and the target.
93///
94/// A manager the flag names wins. Otherwise the one present manager is
95/// chosen, and a target with no manager file takes the flake, the pair
96/// the seeds were first written for. Two present managers need the flag.
97///
98/// # Errors
99///
100/// Returns [`RkError::Usage`] where the manager is ambiguous.
101pub 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
114/// The one manager the target carries, the flake where it carries none,
115/// or the usage error naming why `--manager` is needed.
116fn 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    /// SATISFIES packaging:the-venue-and-the-manager-cross-in-one-matrix
141    #[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    /// SATISFIES packaging:the-venue-and-the-manager-cross-in-one-matrix
171    #[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}