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. The dependency matrix in `src/depend/matrix.rs` has
11//! the same shape over an arbitrary source, and its reasons are about
12//! that source, so nothing is imported from it.
13
14use serde::Serialize;
15
16use super::Observed;
17use super::manager::Manager;
18use super::venue::Venue;
19use crate::error::RkError;
20
21/// Whether a pair renders, and why not where it does not.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum Support {
24    /// The pair renders fragments, and a seed where the file is absent.
25    Fragment,
26    /// The pair needs knowledge the binary does not have offline.
27    Manual(&'static str),
28}
29
30/// How a pair lands in one target: the closed `support` vocabulary of
31/// the add report.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
33#[serde(rename_all = "kebab-case")]
34pub enum Mode {
35    /// The manager file is present: the fragments are applied by hand.
36    Fragment,
37    /// The manager file is absent: `--apply` seeds it.
38    Seed,
39    /// A hand edit the report describes; nothing is written.
40    Manual,
41}
42
43/// One manager and one venue.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
45pub struct Pair {
46    /// The manager.
47    pub manager: Manager,
48    /// The venue.
49    pub venue: Venue,
50}
51
52/// Every manual reason the matrix can name, closed: a test holds each
53/// pair's reason to this list.
54pub const REASONS: [&str; 4] = [
55    "nixpkgs-attribute-unknown",
56    "network-hash-needed",
57    "no-mise-flake-backend",
58    "asdf-plugin-unknown",
59];
60
61/// The support of one pair.
62#[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/// The venues a manager prefers, first first.
75#[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/// The first venue a manager renders a fragment for, or its first
84/// preference where every venue is manual.
85#[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
94/// The pair one `add` or `sync` serves, from the flags and the target.
95///
96/// A manager the flag names wins. Otherwise the one present manager is
97/// chosen, and a target with no manager file takes the flake, the pair
98/// the seeds were first written for. Two present managers need the flag.
99///
100/// # Errors
101///
102/// Returns [`RkError::Usage`] where the manager is ambiguous.
103pub 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
116/// The one manager the target carries, the flake where it carries none,
117/// or the usage error naming why `--manager` is needed.
118fn 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    /// SATISFIES packaging:the-venue-and-the-manager-cross-in-one-matrix
143    #[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    /// SATISFIES packaging:the-venue-and-the-manager-cross-in-one-matrix
173    #[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}