spec_driven_docs/self_depend/
venue.rs1use serde::Serialize;
10
11use crate::self_depend::manager::Manager;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, clap::ValueEnum)]
15#[serde(rename_all = "kebab-case")]
16pub enum Venue {
17 Crates,
19 Flake,
21 GithubRelease,
23}
24
25impl Venue {
26 pub const ALL: [Self; 3] = [Self::Crates, Self::Flake, Self::GithubRelease];
28
29 #[must_use]
31 pub const fn as_str(self) -> &'static str {
32 match self {
33 Self::Crates => "crates",
34 Self::Flake => "flake",
35 Self::GithubRelease => "github-release",
36 }
37 }
38}
39
40impl std::fmt::Display for Venue {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.write_str(self.as_str())
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
48#[serde(rename_all = "kebab-case")]
49pub enum Reason {
50 AttributeUnknown,
52 HashNeeded,
54 NoBackend,
56 PluginUnknown,
58}
59
60impl Reason {
61 #[must_use]
63 pub const fn as_str(self) -> &'static str {
64 match self {
65 Self::AttributeUnknown => "no package-set attribute is known for this manager",
66 Self::HashNeeded => "a source hash this tool cannot compute offline is required",
67 Self::NoBackend => "the manager has no backend for this venue",
68 Self::PluginUnknown => "no published plugin installs this tool",
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
75#[serde(rename_all = "kebab-case", tag = "verdict")]
76pub enum Verdict {
77 Renders,
79 Manual {
81 reason: Reason,
83 },
84}
85
86#[must_use]
88pub const fn verdict(manager: Manager, venue: Venue) -> Verdict {
89 match (manager, venue) {
90 (Manager::Flake | Manager::Devbox, Venue::Flake)
91 | (Manager::Mise, Venue::Crates | Venue::GithubRelease) => Verdict::Renders,
92 (Manager::Flake | Manager::Devbox, Venue::Crates) => Verdict::Manual {
93 reason: Reason::AttributeUnknown,
94 },
95 (Manager::Flake | Manager::Devbox, Venue::GithubRelease) => Verdict::Manual {
96 reason: Reason::HashNeeded,
97 },
98 (Manager::Mise, Venue::Flake) => Verdict::Manual {
99 reason: Reason::NoBackend,
100 },
101 (Manager::Asdf, _) => Verdict::Manual {
102 reason: Reason::PluginUnknown,
103 },
104 }
105}
106
107#[must_use]
109pub fn default_venue(manager: Manager) -> Option<Venue> {
110 Venue::ALL
111 .into_iter()
112 .find(|venue| matches!(verdict(manager, *venue), Verdict::Renders))
113}
114
115#[derive(Debug, Clone, Serialize)]
117pub struct Pair {
118 pub manager: Manager,
120 pub venue: Venue,
122 #[serde(flatten)]
124 pub verdict: Verdict,
125}
126
127#[must_use]
129pub fn matrix() -> Vec<Pair> {
130 let mut rows = Vec::new();
131 for manager in Manager::ALL {
132 for venue in Venue::ALL {
133 rows.push(Pair {
134 manager,
135 venue,
136 verdict: verdict(manager, venue),
137 });
138 }
139 }
140 rows
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
149 fn every_pair_carries_a_verdict_and_every_manual_pair_a_reason() {
150 let rows = matrix();
151 assert_eq!(rows.len(), Manager::ALL.len() * Venue::ALL.len());
152 for row in &rows {
153 match row.verdict {
154 Verdict::Renders => {}
155 Verdict::Manual { reason } => assert!(!reason.as_str().is_empty()),
156 }
157 }
158 }
159
160 #[test]
161 fn the_rendering_pairs_are_the_four_this_release_proves() {
162 let renders: Vec<(Manager, Venue)> = matrix()
163 .into_iter()
164 .filter(|row| row.verdict == Verdict::Renders)
165 .map(|row| (row.manager, row.venue))
166 .collect();
167 assert_eq!(
168 renders,
169 [
170 (Manager::Flake, Venue::Flake),
171 (Manager::Mise, Venue::Crates),
172 (Manager::Mise, Venue::GithubRelease),
173 (Manager::Devbox, Venue::Flake),
174 ]
175 );
176 }
177
178 #[test]
179 fn a_manager_defaults_to_its_first_rendering_venue() {
180 assert_eq!(default_venue(Manager::Flake), Some(Venue::Flake));
181 assert_eq!(default_venue(Manager::Mise), Some(Venue::Crates));
182 assert_eq!(default_venue(Manager::Devbox), Some(Venue::Flake));
183 assert_eq!(default_venue(Manager::Asdf), None);
184 }
185}