deps_core/selection.rs
1//! Manifest-scoped state that can refine a registry's "latest matching" version selection.
2//!
3//! Beyond the plain requirement text — e.g. Composer's `minimum-stability` field (#424,
4//! #1433, #1444).
5//!
6//! [`SelectionContext`] used to carry Composer's `minimum-stability` as a raw
7//! `Option<String>`, with `Registry::get_latest_matching`/`select_latest_matching` each
8//! having a context-less trait method and a separate `_with_context` twin a caller had to
9//! remember to call instead. #1444 collapses that: [`crate::Registry::get_latest_matching`]
10//! and [`crate::Registry::select_latest_matching`] now take a required
11//! [`SelectionContext`] parameter directly (an explicit [`SelectionContext::none()`] is the
12//! only way to opt out, and that choice is now visible at every call site instead of an
13//! easy-to-forget separate method name), and the stability value itself is the typed
14//! [`StabilityFloor`] rather than a free-form string.
15use std::str::FromStr;
16
17/// A Composer `minimum-stability` floor (or per-dependency `@stability` flag), ranked
18/// `Dev < Alpha < Beta < Rc < Stable` (#424, #1444).
19///
20/// Declaration order is the precedence order: [`Ord`] compares two floors exactly as
21/// Composer's own stability scale does, so `StabilityFloor::Beta < StabilityFloor::Rc`
22/// holds via the derived comparison, with no separate rank table to keep in sync.
23///
24/// This is the *manifest-declared* stability floor's own type — strict, via
25/// [`FromStr`] — distinct from the lenient qualifier classification a *version string*'s
26/// own suffix (e.g. `1.0.0-beta.1`) goes through inside `deps-composer`'s
27/// `compare_versions`, which still accepts the `a`/`b` short aliases and maps an
28/// unrecognized word to [`Self::Stable`] — that leniency is unchanged by this type.
29///
30/// # Examples
31///
32/// ```
33/// use deps_core::selection::StabilityFloor;
34///
35/// assert!(StabilityFloor::Dev < StabilityFloor::Alpha);
36/// assert!(StabilityFloor::Alpha < StabilityFloor::Beta);
37/// assert!(StabilityFloor::Beta < StabilityFloor::Rc);
38/// assert!(StabilityFloor::Rc < StabilityFloor::Stable);
39/// ```
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
41pub enum StabilityFloor {
42 /// `dev` — the loosest floor; admits every stability level.
43 Dev,
44 /// `alpha`.
45 Alpha,
46 /// `beta`.
47 Beta,
48 /// `RC` (release candidate).
49 Rc,
50 /// `stable` — the strictest floor; Composer's own default.
51 Stable,
52}
53
54/// [`StabilityFloor::from_str`]'s error.
55///
56/// `raw` was not one of the five recognized stability keywords (`dev`, `alpha`, `beta`,
57/// `rc`, `stable`), matched ASCII-case-insensitively and with no surrounding-whitespace
58/// tolerance.
59///
60/// # Examples
61///
62/// ```
63/// use deps_core::selection::{StabilityFloor, UnknownStability};
64///
65/// assert_eq!("betta".parse::<StabilityFloor>(), Err(UnknownStability));
66/// assert_eq!(" beta".parse::<StabilityFloor>(), Err(UnknownStability));
67/// ```
68#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
69#[error("unrecognized stability keyword")]
70pub struct UnknownStability;
71
72impl FromStr for StabilityFloor {
73 type Err = UnknownStability;
74
75 /// Strict: accepts exactly `dev`, `alpha`, `beta`, `rc`, `stable`,
76 /// ASCII-case-insensitively (so `RC`, `Beta`, `STABLE` still parse) — no surrounding
77 /// whitespace tolerance, and no `a`/`b` short-alias acceptance (those are valid only
78 /// inside a *version qualifier* suffix, not as a `minimum-stability` config value or
79 /// `@flag`).
80 ///
81 /// This is the strict, config-value classifier; `deps-composer`'s own
82 /// `qualifier_stability` (private to that crate) is the deliberately separate, lenient
83 /// classifier for a version string's own qualifier suffix (accepts `a`/`b`, defaults an
84 /// unrecognized word to [`StabilityFloor::Stable`]) — if a keyword is ever added here,
85 /// check whether it belongs there too.
86 ///
87 /// # Examples
88 ///
89 /// ```
90 /// use deps_core::selection::StabilityFloor;
91 ///
92 /// assert_eq!("beta".parse(), Ok(StabilityFloor::Beta));
93 /// assert_eq!("RC".parse(), Ok(StabilityFloor::Rc));
94 /// assert_eq!("STABLE".parse(), Ok(StabilityFloor::Stable));
95 ///
96 /// assert!("a".parse::<StabilityFloor>().is_err());
97 /// assert!("b".parse::<StabilityFloor>().is_err());
98 /// assert!("".parse::<StabilityFloor>().is_err());
99 /// assert!(" beta".parse::<StabilityFloor>().is_err());
100 /// assert!("stable ".parse::<StabilityFloor>().is_err());
101 /// ```
102 fn from_str(s: &str) -> Result<Self, Self::Err> {
103 match s.to_ascii_lowercase().as_str() {
104 "dev" => Ok(Self::Dev),
105 "alpha" => Ok(Self::Alpha),
106 "beta" => Ok(Self::Beta),
107 "rc" => Ok(Self::Rc),
108 "stable" => Ok(Self::Stable),
109 _ => Err(UnknownStability),
110 }
111 }
112}
113
114/// One manifest-declared `minimum-stability` value that failed to parse.
115///
116/// Failed via [`StabilityFloor::from_str`] — returned by
117/// [`crate::ParseResult::invalid_minimum_stability`] and surfaced as a diagnostic by
118/// `deps_core::lsp_helpers::diagnostics`' shared notice.
119///
120/// # Examples
121///
122/// ```
123/// use deps_core::selection::InvalidStabilityOccurrence;
124/// use deps_core::position::{Position, Range};
125///
126/// let occurrence = InvalidStabilityOccurrence {
127/// range: Range::new(Position::new(0, 0), Position::new(0, 5)),
128/// raw: "betta".to_string(),
129/// };
130/// assert_eq!(occurrence.raw, "betta");
131/// ```
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct InvalidStabilityOccurrence {
134 /// Range of the offending value — the value's own span for a string that failed to
135 /// parse, or the declaring key's span for a non-string value.
136 pub range: crate::position::Range,
137 /// The string contents (for a string value that failed to parse) or the raw JSON
138 /// token text (for a non-string value).
139 pub raw: String,
140}
141
142/// Manifest-scoped state that can refine a registry's "latest matching" version selection
143/// beyond the plain requirement text — e.g. Composer's `minimum-stability` field (#424,
144/// #1433, #1444).
145///
146/// Opaque and ecosystem-owned: the type carries whatever an ecosystem's own [`crate::ParseResult`]
147/// puts into it (today, only Composer's `minimum-stability`), and only that ecosystem's own
148/// [`crate::Registry`] implementation reads it back — every other registry ignores it via
149/// [`crate::Registry::get_latest_matching`]/[`crate::Registry::select_latest_matching`]'s own
150/// default. Every LSP call site that needs "the latest version for this dependency" (hover,
151/// completion, code actions, and the background fetch path) obtains one from
152/// [`crate::ParseResult::selection_context`] and threads it into those two required-parameter
153/// trait methods, so a call site that already has a `ParseResult` in scope no longer has to
154/// independently remember to extract and thread the same manifest-level state by hand
155/// (#1433).
156///
157/// [`PartialEq`] is derived so a caller can detect *that* the context changed between two
158/// parses (e.g. `deps-lsp`'s edit-triggered refetch escalation) without needing to know what
159/// changed.
160///
161/// # Examples
162///
163/// ```
164/// use deps_core::selection::{SelectionContext, StabilityFloor};
165///
166/// let none = SelectionContext::none();
167/// assert_eq!(none.minimum_stability(), None);
168///
169/// let composer = SelectionContext::with_minimum_stability(StabilityFloor::Alpha);
170/// assert_eq!(composer.minimum_stability(), Some(StabilityFloor::Alpha));
171/// assert_ne!(none, composer);
172/// ```
173#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
174pub struct SelectionContext {
175 minimum_stability: Option<StabilityFloor>,
176}
177
178impl SelectionContext {
179 /// The empty context: every [`crate::Registry::get_latest_matching`]/
180 /// [`crate::Registry::select_latest_matching`] call degrades to plain requirement-text
181 /// matching. [`crate::ParseResult::selection_context`]'s default for every ecosystem with
182 /// no selection-context concept of its own.
183 ///
184 /// # Examples
185 ///
186 /// ```
187 /// use deps_core::selection::SelectionContext;
188 ///
189 /// assert_eq!(SelectionContext::none().minimum_stability(), None);
190 /// ```
191 #[must_use]
192 pub const fn none() -> Self {
193 Self {
194 minimum_stability: None,
195 }
196 }
197
198 /// Builds a context carrying a manifest's own `minimum-stability` floor
199 /// (`ComposerParseResult`'s `MinimumStability::Declared`, #424/#1433/#1444).
200 ///
201 /// # Examples
202 ///
203 /// ```
204 /// use deps_core::selection::{SelectionContext, StabilityFloor};
205 ///
206 /// let ctx = SelectionContext::with_minimum_stability(StabilityFloor::Beta);
207 /// assert_eq!(ctx.minimum_stability(), Some(StabilityFloor::Beta));
208 /// ```
209 #[must_use]
210 pub const fn with_minimum_stability(floor: StabilityFloor) -> Self {
211 Self {
212 minimum_stability: Some(floor),
213 }
214 }
215
216 /// The manifest's own stability floor, read by
217 /// [`crate::Registry::get_latest_matching`]/[`crate::Registry::select_latest_matching`]'s
218 /// one real implementor (`PackagistRegistry`) — every other registry ignores it via those
219 /// methods' own defaults.
220 ///
221 /// # Examples
222 ///
223 /// ```
224 /// use deps_core::selection::SelectionContext;
225 ///
226 /// assert_eq!(SelectionContext::none().minimum_stability(), None);
227 /// ```
228 #[must_use]
229 pub const fn minimum_stability(&self) -> Option<StabilityFloor> {
230 self.minimum_stability
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn stability_floor_from_str_accepts_all_five_words_case_insensitively() {
240 for (word, expected) in [
241 ("dev", StabilityFloor::Dev),
242 ("DEV", StabilityFloor::Dev),
243 ("alpha", StabilityFloor::Alpha),
244 ("Alpha", StabilityFloor::Alpha),
245 ("beta", StabilityFloor::Beta),
246 ("BETA", StabilityFloor::Beta),
247 ("rc", StabilityFloor::Rc),
248 ("RC", StabilityFloor::Rc),
249 ("stable", StabilityFloor::Stable),
250 ("Stable", StabilityFloor::Stable),
251 ] {
252 assert_eq!(
253 word.parse::<StabilityFloor>(),
254 Ok(expected),
255 "word: {word:?}"
256 );
257 }
258 }
259
260 #[test]
261 fn stability_floor_from_str_rejects_unrecognized_words() {
262 for word in ["a", "b", "", " beta", "stable ", "betta", "RC1"] {
263 assert!(
264 word.parse::<StabilityFloor>().is_err(),
265 "expected {word:?} to be rejected"
266 );
267 }
268 }
269
270 #[test]
271 fn stability_floor_ord_matches_composer_precedence() {
272 let mut floors = [
273 StabilityFloor::Stable,
274 StabilityFloor::Dev,
275 StabilityFloor::Rc,
276 StabilityFloor::Alpha,
277 StabilityFloor::Beta,
278 ];
279 floors.sort();
280 assert_eq!(
281 floors,
282 [
283 StabilityFloor::Dev,
284 StabilityFloor::Alpha,
285 StabilityFloor::Beta,
286 StabilityFloor::Rc,
287 StabilityFloor::Stable,
288 ]
289 );
290 }
291
292 #[test]
293 fn selection_context_none_has_no_minimum_stability() {
294 assert_eq!(SelectionContext::none().minimum_stability(), None);
295 assert_eq!(SelectionContext::default(), SelectionContext::none());
296 }
297
298 #[test]
299 fn selection_context_with_minimum_stability_roundtrips() {
300 let ctx = SelectionContext::with_minimum_stability(StabilityFloor::Rc);
301 assert_eq!(ctx.minimum_stability(), Some(StabilityFloor::Rc));
302 assert_ne!(ctx, SelectionContext::none());
303 }
304}