1use std::collections::HashSet;
13use std::fmt;
14
15use semver::{Version, VersionReq};
16
17#[derive(Debug, Clone)]
19pub enum Constraint {
20 Semver(VersionReq),
22 Latest,
24 RefPin(String),
26}
27
28impl fmt::Display for Constraint {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 Constraint::Semver(req) => write!(f, "{req}"),
32 Constraint::Latest => write!(f, "latest"),
33 Constraint::RefPin(reference) => write!(f, "ref:{reference}"),
34 }
35 }
36}
37
38pub fn parse_constraint(version: Option<&str>) -> Constraint {
46 let version = match version {
47 None => return Constraint::Latest,
48 Some(v) => v.trim(),
49 };
50
51 if version.is_empty() || version.eq_ignore_ascii_case("latest") {
52 return Constraint::Latest;
53 }
54
55 if let Some(stripped) = version.strip_prefix('v') {
56 if let Ok(ver) = Version::parse(stripped) {
57 let req = VersionReq::parse(&format!("={ver}")).expect("valid exact req");
58 return Constraint::Semver(req);
59 }
60
61 if let Ok(major) = stripped.parse::<u64>()
63 && let Some(next) = major.checked_add(1)
64 {
65 let req = VersionReq::parse(&format!(">={major}.0.0, <{next}.0.0"))
66 .expect("valid major range req");
67 return Constraint::Semver(req);
68 }
69
70 let parts: Vec<&str> = stripped.split('.').collect();
71 if parts.len() == 2
72 && let (Ok(major), Ok(minor)) = (parts[0].parse::<u64>(), parts[1].parse::<u64>())
73 && let Some(next) = minor.checked_add(1)
74 {
75 let req = VersionReq::parse(&format!(">={major}.{minor}.0, <{major}.{next}.0"))
76 .expect("valid minor range req");
77 return Constraint::Semver(req);
78 }
79 }
80
81 if let Ok(req) = VersionReq::parse(version) {
82 return Constraint::Semver(req);
83 }
84
85 Constraint::RefPin(version.to_string())
86}
87
88pub fn parse_tag(tag: &str) -> Option<Version> {
91 Version::parse(tag.strip_prefix('v').unwrap_or(tag)).ok()
92}
93
94pub fn satisfies(constraint: &Constraint, version: &Version) -> bool {
97 match constraint {
98 Constraint::Semver(req) => req.matches(version),
99 Constraint::Latest => true,
100 Constraint::RefPin(_) => false,
101 }
102}
103
104pub fn satisfies_requirement(requirement: &str, version: &Version) -> Result<bool, ParseError> {
107 let requirement = crate::package::parse_requirement(requirement)
108 .map_err(|error| ParseError::Requirement(requirement.to_owned(), error.to_string()))?;
109 Ok(requirement.matches(version))
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
114pub enum ParseError {
115 #[error("`{0}` is not a semver version: {1}")]
116 Version(String, String),
117 #[error("`{0}` is not a version requirement: {1}")]
118 Requirement(String, String),
119 #[error("`{0}` is not a version tag")]
120 Tag(String),
121}
122
123impl ParseError {
124 pub fn code(&self) -> &'static str {
126 match self {
127 Self::Version(..) => "version.invalid-version",
128 Self::Requirement(..) => "version.invalid-requirement",
129 Self::Tag(_) => "version.invalid-tag",
130 }
131 }
132}
133
134pub fn parse_version(text: &str) -> Result<Version, ParseError> {
136 Version::parse(text).map_err(|error| ParseError::Version(text.to_owned(), error.to_string()))
137}
138
139pub fn require_tag(tag: &str) -> Result<Version, ParseError> {
141 parse_tag(tag).ok_or_else(|| ParseError::Tag(tag.to_owned()))
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum SelectionPolicy {
147 PreferLockThenLatest,
149 LatestOnly,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
155pub enum SelectError {
156 #[error("{}", no_satisfying_message(.constraints, .available))]
158 NoSatisfying {
159 constraints: Vec<(String, VersionReq)>,
160 available: Vec<Version>,
161 },
162}
163
164impl SelectError {
165 pub fn code(&self) -> &'static str {
167 match self {
168 Self::NoSatisfying { .. } => "version.no-satisfying",
169 }
170 }
171}
172
173fn no_satisfying_message(constraints: &[(String, VersionReq)], available: &[Version]) -> String {
174 let wanted: Vec<String> = constraints
175 .iter()
176 .map(|(requester, req)| format!(" `{requester}` requires {req}"))
177 .collect();
178 let have: Vec<String> = available.iter().map(Version::to_string).collect();
179 format!(
180 "no version satisfies all constraints:\n{}\navailable versions: [{}]",
181 wanted.join("\n"),
182 have.join(", ")
183 )
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
188pub enum SettleError {
189 #[error("`{first}` requires ref:{first_ref}, but `{second}` requires ref:{second_ref}")]
190 PinsDiffer {
191 first: String,
192 first_ref: String,
193 second: String,
194 second_ref: String,
195 },
196 #[error("`{pinned_by}` requires ref:{reference}, but `{ranged_by}` requires {range}")]
198 PinBesideRange {
199 pinned_by: String,
200 reference: String,
201 ranged_by: String,
202 range: VersionReq,
203 },
204}
205
206impl SettleError {
207 pub fn code(&self) -> &'static str {
209 match self {
210 Self::PinsDiffer { .. } => "version.pins-differ",
211 Self::PinBesideRange { .. } => "version.pin-beside-range",
212 }
213 }
214}
215
216pub fn settle<'a>(constraints: &[(&str, &'a Constraint)]) -> Result<Option<&'a str>, SettleError> {
223 let mut pin: Option<(&str, &'a str)> = None;
224 for &(requester, constraint) in constraints {
225 if let Constraint::RefPin(reference) = constraint {
226 match pin {
227 Some((first, first_ref)) if first_ref != reference => {
228 return Err(SettleError::PinsDiffer {
229 first: first.to_string(),
230 first_ref: first_ref.to_string(),
231 second: requester.to_string(),
232 second_ref: reference.clone(),
233 });
234 }
235 Some(_) => {}
236 None => pin = Some((requester, reference)),
237 }
238 }
239 }
240 let Some((pinned_by, reference)) = pin else {
241 return Ok(None);
242 };
243 for &(requester, constraint) in constraints {
244 if let Constraint::Semver(range) = constraint {
245 return Err(SettleError::PinBesideRange {
246 pinned_by: pinned_by.to_string(),
247 reference: reference.to_string(),
248 ranged_by: requester.to_string(),
249 range: range.clone(),
250 });
251 }
252 }
253 Ok(Some(reference))
254}
255
256pub fn allows(
260 constraints: &[(&str, &Constraint)],
261 version: &Version,
262 excluded: &HashSet<Version>,
263) -> bool {
264 !excluded.contains(version)
265 && constraints.iter().all(|(_, constraint)| match constraint {
266 Constraint::Semver(req) => req.matches(version),
267 _ => true,
268 })
269}
270
271pub fn keeps_lock(
276 constraints: &[(&str, &Constraint)],
277 locked: &Version,
278 policy: SelectionPolicy,
279 excluded: &HashSet<Version>,
280) -> bool {
281 policy == SelectionPolicy::PreferLockThenLatest
282 && !constraints
283 .iter()
284 .any(|(_, constraint)| matches!(constraint, Constraint::Latest))
285 && allows(constraints, locked, excluded)
286}
287
288pub fn select(
293 available: &[Version],
294 constraints: &[(&str, &Constraint)],
295 locked: Option<&Version>,
296 policy: SelectionPolicy,
297 excluded: &HashSet<Version>,
298) -> Result<Option<usize>, SelectError> {
299 if available.is_empty() {
300 return Ok(None);
301 }
302 let allowed: Vec<usize> = (0..available.len())
303 .filter(|&i| allows(constraints, &available[i], excluded))
304 .collect();
305
306 let Some(newest) = allowed.iter().copied().max_by_key(|&i| (&available[i], i)) else {
307 let mut ascending = available.to_vec();
308 ascending.sort();
309 return Err(SelectError::NoSatisfying {
310 constraints: constraints
311 .iter()
312 .filter_map(|&(requester, constraint)| match constraint {
313 Constraint::Semver(req) => Some((requester.to_string(), req.clone())),
314 _ => None,
315 })
316 .collect(),
317 available: ascending,
318 });
319 };
320
321 let kept = locked
322 .filter(|locked| keeps_lock(constraints, locked, policy, excluded))
323 .and_then(|locked| allowed.iter().copied().find(|&i| available[i] == *locked));
324 Ok(Some(kept.unwrap_or(newest)))
325}
326
327#[derive(Debug, Clone, PartialEq, Eq)]
329pub enum Chosen<'a> {
330 Pin(&'a str),
332 Tag(&'a str),
334 Locked,
337 Untagged,
339}
340
341#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
343pub enum ChooseError {
344 #[error(transparent)]
345 Settle(#[from] SettleError),
346 #[error(transparent)]
347 Select(#[from] SelectError),
348}
349
350impl ChooseError {
351 pub fn code(&self) -> &'static str {
353 match self {
354 Self::Settle(error) => error.code(),
355 Self::Select(error) => error.code(),
356 }
357 }
358}
359
360pub fn choose<'a>(
365 tags: &'a [String],
366 constraints: &[(&str, &'a Constraint)],
367 locked: Option<&Version>,
368 policy: SelectionPolicy,
369 excluded: &HashSet<Version>,
370) -> Result<Chosen<'a>, ChooseError> {
371 if let Some(reference) = settle(constraints)? {
372 return Ok(Chosen::Pin(reference));
373 }
374 let (names, versions): (Vec<&str>, Vec<Version>) = tags
375 .iter()
376 .filter_map(|tag| Some((tag.as_str(), parse_tag(tag)?)))
377 .unzip();
378 if locked.is_some_and(|locked| keeps_lock(constraints, locked, policy, excluded)) {
379 return Ok(Chosen::Locked);
380 }
381 Ok(select(&versions, constraints, None, policy, excluded)?
382 .map_or(Chosen::Untagged, |index| Chosen::Tag(names[index])))
383}