blob: 9ad0f38d8249994c1198b64da7c78a583da52f73 [file] [edit]
use crate::util::data_structures::HashMap;
use anyhow::{Context as _, bail};
use crate::util::edit_distance;
use crate::util::errors::CargoResult;
use crate::workspace::PackageId;
use crate::workspace::PackageIdSpec;
pub trait PackageIdSpecQuery {
/// Roughly equivalent to `PackageIdSpec::parse(spec)?.query(i)`
fn query_str<I>(spec: &str, i: I) -> CargoResult<PackageId>
where
I: IntoIterator<Item = PackageId>;
/// Checks whether the given `PackageId` matches the `PackageIdSpec`.
fn matches(&self, package_id: PackageId) -> bool;
/// Checks a list of `PackageId`s to find 1 that matches this `PackageIdSpec`. If 0, 2, or
/// more are found, then this returns an error.
fn query<I>(&self, i: I) -> CargoResult<PackageId>
where
I: IntoIterator<Item = PackageId>;
}
impl PackageIdSpecQuery for PackageIdSpec {
fn query_str<I>(spec: &str, i: I) -> CargoResult<PackageId>
where
I: IntoIterator<Item = PackageId>,
{
let i: Vec<_> = i.into_iter().collect();
let spec = PackageIdSpec::parse(spec).with_context(|| {
let suggestion =
edit_distance::closest_msg(spec, i.iter(), |id| id.name().as_str(), "package");
format!("invalid package ID specification: `{}`{}", spec, suggestion)
})?;
spec.query(i)
}
fn matches(&self, package_id: PackageId) -> bool {
if self.name() != package_id.name().as_str() {
return false;
}
if let Some(ref v) = self.partial_version() {
if !v.matches(package_id.version()) {
return false;
}
}
if let Some(u) = &self.url() {
if *u != package_id.source_id().url() {
return false;
}
}
if let Some(k) = &self.kind() {
if *k != package_id.source_id().kind() {
return false;
}
}
true
}
fn query<I>(&self, i: I) -> CargoResult<PackageId>
where
I: IntoIterator<Item = PackageId>,
{
let all_ids: Vec<_> = i.into_iter().collect();
let mut ids = all_ids.iter().copied().filter(|&id| self.matches(id));
let Some(ret) = ids.next() else {
let mut suggestion = String::new();
let try_spec = |spec: PackageIdSpec, suggestion: &mut String| {
let try_matches: Vec<_> = all_ids
.iter()
.copied()
.filter(|&id| spec.matches(id))
.collect();
if !try_matches.is_empty() {
suggestion.push_str("\nhelp: there are similar package ID specifications:\n");
minimize(suggestion, &try_matches, self);
}
};
if self.url().is_some() {
let spec = PackageIdSpec::new(self.name().to_owned());
let spec = if let Some(version) = self.partial_version().cloned() {
spec.with_version(version)
} else {
spec
};
try_spec(spec, &mut suggestion);
}
if suggestion.is_empty() && self.version().is_some() {
try_spec(PackageIdSpec::new(self.name().to_owned()), &mut suggestion);
}
if suggestion.is_empty() {
suggestion.push_str(&edit_distance::closest_msg(
self.name(),
all_ids.iter(),
|id| id.name().as_str(),
"package",
));
}
bail!(
"package ID specification `{}` did not match any packages{}",
self,
suggestion
);
};
return match ids.next() {
Some(other) => {
let mut msg = format!(
"specification `{self}` is ambiguous
help: re-run this command with one of the following specifications",
);
let mut vec = vec![ret, other];
vec.extend(ids);
minimize(&mut msg, &vec, self);
Err(anyhow::format_err!("{}", msg))
}
None => Ok(ret),
};
fn minimize(msg: &mut String, ids: &[PackageId], spec: &PackageIdSpec) {
let mut version_cnt = HashMap::default();
for id in ids {
*version_cnt.entry(id.version()).or_insert(0) += 1;
}
for id in ids {
if version_cnt[id.version()] == 1 {
msg.push_str(&format!("\n {}@{}", spec.name(), id.version()));
} else {
msg.push_str(&format!("\n {}", id.to_spec()));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::PackageIdSpec;
use super::PackageIdSpecQuery;
use crate::workspace::{PackageId, SourceId};
use url::Url;
#[test]
fn matching() {
let url = Url::parse("https://example.com").unwrap();
let sid = SourceId::for_registry(&url).unwrap();
let foo = PackageId::try_new("foo", "1.2.3", sid).unwrap();
assert!(PackageIdSpec::parse("foo").unwrap().matches(foo));
assert!(!PackageIdSpec::parse("bar").unwrap().matches(foo));
assert!(PackageIdSpec::parse("foo:1.2.3").unwrap().matches(foo));
assert!(!PackageIdSpec::parse("foo:1.2.2").unwrap().matches(foo));
assert!(PackageIdSpec::parse("foo@1.2.3").unwrap().matches(foo));
assert!(!PackageIdSpec::parse("foo@1.2.2").unwrap().matches(foo));
assert!(PackageIdSpec::parse("foo@1.2").unwrap().matches(foo));
assert!(
PackageIdSpec::parse("https://example.com#foo@1.2")
.unwrap()
.matches(foo)
);
assert!(
!PackageIdSpec::parse("https://bob.com#foo@1.2")
.unwrap()
.matches(foo)
);
assert!(
PackageIdSpec::parse("registry+https://example.com#foo@1.2")
.unwrap()
.matches(foo)
);
assert!(
!PackageIdSpec::parse("git+https://example.com#foo@1.2")
.unwrap()
.matches(foo)
);
let meta = PackageId::try_new("meta", "1.2.3+hello", sid).unwrap();
assert!(PackageIdSpec::parse("meta").unwrap().matches(meta));
assert!(PackageIdSpec::parse("meta@1").unwrap().matches(meta));
assert!(PackageIdSpec::parse("meta@1.2").unwrap().matches(meta));
assert!(PackageIdSpec::parse("meta@1.2.3").unwrap().matches(meta));
assert!(
!PackageIdSpec::parse("meta@1.2.3-alpha.0")
.unwrap()
.matches(meta)
);
assert!(
PackageIdSpec::parse("meta@1.2.3+hello")
.unwrap()
.matches(meta)
);
assert!(
!PackageIdSpec::parse("meta@1.2.3+bye")
.unwrap()
.matches(meta)
);
let pre = PackageId::try_new("pre", "1.2.3-alpha.0", sid).unwrap();
assert!(PackageIdSpec::parse("pre").unwrap().matches(pre));
assert!(!PackageIdSpec::parse("pre@1").unwrap().matches(pre));
assert!(!PackageIdSpec::parse("pre@1.2").unwrap().matches(pre));
assert!(!PackageIdSpec::parse("pre@1.2.3").unwrap().matches(pre));
assert!(
PackageIdSpec::parse("pre@1.2.3-alpha.0")
.unwrap()
.matches(pre)
);
assert!(
!PackageIdSpec::parse("pre@1.2.3-alpha.1")
.unwrap()
.matches(pre)
);
assert!(
!PackageIdSpec::parse("pre@1.2.3-beta.0")
.unwrap()
.matches(pre)
);
assert!(
!PackageIdSpec::parse("pre@1.2.3+hello")
.unwrap()
.matches(pre)
);
assert!(
!PackageIdSpec::parse("pre@1.2.3-alpha.0+hello")
.unwrap()
.matches(pre)
);
}
}