1use serde::Deserialize;
10
11use crate::embedded;
12
13#[derive(Debug, Clone, Deserialize)]
16pub struct Pin {
17 pub name: String,
19 pub version: String,
21 #[serde(default)]
25 pub action: Option<String>,
26 #[serde(default)]
29 pub commit: Option<String>,
30 #[serde(default)]
34 pub ref_class: Option<String>,
35 #[serde(default)]
42 pub used_by: Vec<String>,
43 #[serde(default)]
45 pub check: Option<String>,
46}
47
48#[derive(Debug, Deserialize)]
50struct Registry {
51 tool: Vec<Pin>,
53}
54
55#[must_use]
61pub fn pins() -> Vec<Pin> {
62 parse(embedded::VERSIONS)
63}
64
65#[must_use]
71pub fn pins_for(selected: &[crate::profile::catalog::Selection]) -> Vec<Pin> {
72 let keys: Vec<String> = selected
73 .iter()
74 .filter(|selection| selection.lands())
75 .flat_map(crate::profile::catalog::pin_keys)
76 .collect();
77 pins()
78 .into_iter()
79 .filter(|pin| pin.used_by.iter().any(|user| keys.contains(user)))
80 .collect()
81}
82
83#[must_use]
85pub fn version_of(name: &str) -> Option<String> {
86 pins()
87 .into_iter()
88 .find(|pin| pin.name == name)
89 .map(|pin| pin.version)
90}
91
92fn parse(text: &str) -> Vec<Pin> {
93 toml::from_str::<Registry>(text)
94 .map(|registry| registry.tool)
95 .unwrap_or_default()
96}
97
98#[cfg(test)]
99mod tests {
100 use super::{pins, pins_for};
101
102 #[test]
106 fn the_embedded_registry_parses_with_every_field() {
107 let pins = pins();
108 assert!(!pins.is_empty(), "the registry parsed to nothing");
109 for pin in &pins {
110 assert!(!pin.version.is_empty(), "{}: no version", pin.name);
111 assert!(!pin.used_by.is_empty(), "{}: no used_by", pin.name);
112 assert!(
113 pin.check.is_some() || (pin.action.is_some() && pin.commit.is_some()),
114 "{}: no check URL and no ref to resolve",
115 pin.name
116 );
117 }
118 }
119
120 #[test]
124 fn every_used_by_entry_names_a_catalogued_capability() {
125 let drivers = crate::profile::catalog::known_drivers();
126 for pin in pins() {
127 for user in &pin.used_by {
128 let mut parts = user.split('/');
129 let id = parts.next().unwrap_or_default();
130 let driver = parts.next();
131 let provider = parts.next();
132 assert!(
133 parts.next().is_none(),
134 "{}: used_by names {user}, which carries more than a capability, a driver, and a provider",
135 pin.name
136 );
137 assert!(
138 crate::profile::catalog::ALL.contains(&id),
139 "{}: used_by names {user}, which is no capability",
140 pin.name
141 );
142 if let Some(driver) = driver {
143 assert!(
144 drivers.iter().any(|known| known == driver),
145 "{}: used_by names the driver {driver}, which no binding ships",
146 pin.name
147 );
148 }
149 if let Some(provider) = provider {
150 assert!(
151 crate::landing::manifest::Provider::parse(provider)
152 .is_ok_and(|parsed| parsed.is_some()),
153 "{}: used_by names the provider {provider}, which no parameter admits",
154 pin.name
155 );
156 }
157 }
158 }
159 }
160
161 #[test]
164 fn a_provider_keyed_pin_reaches_that_provider_alone() {
165 let names = |provider| {
166 let mut params =
167 crate::landing::Params::for_test("acme/widget", Some(crate::landing::Style::Trunk));
168 params.set_code_scanning_for_test(Some(provider));
169 let selected = crate::profile::catalog::select(
170 ¶ms,
171 &crate::profile::catalog::Availability::embedded(),
172 );
173 pins_for(&selected)
174 .into_iter()
175 .map(|pin| pin.name)
176 .collect::<Vec<String>>()
177 };
178 let codeql = names(crate::landing::manifest::Provider::CodeQl);
179 let semgrep = names(crate::landing::manifest::Provider::Semgrep);
180 assert!(
181 codeql.iter().any(|name| name == "codeql-analyze"),
182 "{codeql:?}"
183 );
184 assert!(
185 !codeql.iter().any(|name| name == "semgrep-image"),
186 "a codeql landing records no semgrep tool: {codeql:?}"
187 );
188 assert!(
189 semgrep.iter().any(|name| name == "semgrep-image"),
190 "{semgrep:?}"
191 );
192 assert!(
193 !semgrep.iter().any(|name| name == "codeql-init"),
194 "a semgrep landing records no codeql tool: {semgrep:?}"
195 );
196 for pins in [&codeql, &semgrep] {
197 assert!(
198 pins.iter().any(|name| name == "checkout"),
199 "either provider checks out: {pins:?}"
200 );
201 }
202 }
203
204 #[test]
205 fn pins_filter_by_selected_capability() {
206 let params =
207 crate::landing::Params::for_test("acme/widget", Some(crate::landing::Style::Trunk));
208 let selected = crate::profile::catalog::select(
209 ¶ms,
210 &crate::profile::catalog::Availability::embedded(),
211 );
212 let rust: Vec<String> = pins_for(&selected)
213 .into_iter()
214 .map(|pin| pin.name)
215 .collect();
216 assert!(rust.contains(&"release-plz".to_owned()));
217 assert!(rust.contains(&"cargo-dist".to_owned()));
218 assert!(rust.contains(&"conventional-pre-commit".to_owned()));
219 assert!(!rust.contains(&"git-cliff".to_owned()));
220 assert!(!rust.contains(&"scorecard-action".to_owned()));
221 let guards = crate::landing::Params::for_test_release_less(
223 &[],
224 None,
225 crate::profile::ReleaseMode::None,
226 );
227 let selected = crate::profile::catalog::select(
228 &guards,
229 &crate::profile::catalog::Availability::embedded(),
230 );
231 let names: Vec<String> = pins_for(&selected)
232 .into_iter()
233 .map(|pin| pin.name)
234 .collect();
235 assert_eq!(names, ["conventional-pre-commit", "pre-commit-hooks"]);
236 }
237}