cargo/core/
package_id_spec.rs
1use std::collections::HashMap;
2
3use anyhow::{bail, Context as _};
4
5use crate::core::PackageId;
6use crate::core::PackageIdSpec;
7use crate::util::edit_distance;
8use crate::util::errors::CargoResult;
9
10pub trait PackageIdSpecQuery {
11 fn query_str<I>(spec: &str, i: I) -> CargoResult<PackageId>
13 where
14 I: IntoIterator<Item = PackageId>;
15
16 fn matches(&self, package_id: PackageId) -> bool;
18
19 fn query<I>(&self, i: I) -> CargoResult<PackageId>
22 where
23 I: IntoIterator<Item = PackageId>;
24}
25
26impl PackageIdSpecQuery for PackageIdSpec {
27 fn query_str<I>(spec: &str, i: I) -> CargoResult<PackageId>
28 where
29 I: IntoIterator<Item = PackageId>,
30 {
31 let i: Vec<_> = i.into_iter().collect();
32 let spec = PackageIdSpec::parse(spec).with_context(|| {
33 let suggestion =
34 edit_distance::closest_msg(spec, i.iter(), |id| id.name().as_str(), "package");
35 format!("invalid package ID specification: `{}`{}", spec, suggestion)
36 })?;
37 spec.query(i)
38 }
39
40 fn matches(&self, package_id: PackageId) -> bool {
41 if self.name() != package_id.name().as_str() {
42 return false;
43 }
44
45 if let Some(ref v) = self.partial_version() {
46 if !v.matches(package_id.version()) {
47 return false;
48 }
49 }
50
51 if let Some(u) = &self.url() {
52 if *u != package_id.source_id().url() {
53 return false;
54 }
55 }
56
57 if let Some(k) = &self.kind() {
58 if *k != package_id.source_id().kind() {
59 return false;
60 }
61 }
62
63 true
64 }
65
66 fn query<I>(&self, i: I) -> CargoResult<PackageId>
67 where
68 I: IntoIterator<Item = PackageId>,
69 {
70 let all_ids: Vec<_> = i.into_iter().collect();
71 let mut ids = all_ids.iter().copied().filter(|&id| self.matches(id));
72 let Some(ret) = ids.next() else {
73 let mut suggestion = String::new();
74 let try_spec = |spec: PackageIdSpec, suggestion: &mut String| {
75 let try_matches: Vec<_> = all_ids
76 .iter()
77 .copied()
78 .filter(|&id| spec.matches(id))
79 .collect();
80 if !try_matches.is_empty() {
81 suggestion.push_str("\nhelp: there are similar package ID specifications:\n");
82 minimize(suggestion, &try_matches, self);
83 }
84 };
85 if self.url().is_some() {
86 let spec = PackageIdSpec::new(self.name().to_owned());
87 let spec = if let Some(version) = self.partial_version().cloned() {
88 spec.with_version(version)
89 } else {
90 spec
91 };
92 try_spec(spec, &mut suggestion);
93 }
94 if suggestion.is_empty() && self.version().is_some() {
95 try_spec(PackageIdSpec::new(self.name().to_owned()), &mut suggestion);
96 }
97 if suggestion.is_empty() {
98 suggestion.push_str(&edit_distance::closest_msg(
99 self.name(),
100 all_ids.iter(),
101 |id| id.name().as_str(),
102 "package",
103 ));
104 }
105
106 bail!(
107 "package ID specification `{}` did not match any packages{}",
108 self,
109 suggestion
110 );
111 };
112 return match ids.next() {
113 Some(other) => {
114 let mut msg = format!(
115 "There are multiple `{}` packages in \
116 your project, and the specification \
117 `{}` is ambiguous.\n\
118 Please re-run this command \
119 with one of the following \
120 specifications:",
121 self.name(),
122 self
123 );
124 let mut vec = vec![ret, other];
125 vec.extend(ids);
126 minimize(&mut msg, &vec, self);
127 Err(anyhow::format_err!("{}", msg))
128 }
129 None => Ok(ret),
130 };
131
132 fn minimize(msg: &mut String, ids: &[PackageId], spec: &PackageIdSpec) {
133 let mut version_cnt = HashMap::new();
134 for id in ids {
135 *version_cnt.entry(id.version()).or_insert(0) += 1;
136 }
137 for id in ids {
138 if version_cnt[id.version()] == 1 {
139 msg.push_str(&format!("\n {}@{}", spec.name(), id.version()));
140 } else {
141 msg.push_str(&format!("\n {}", id.to_spec()));
142 }
143 }
144 }
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::PackageIdSpec;
151 use super::PackageIdSpecQuery;
152 use crate::core::{PackageId, SourceId};
153 use url::Url;
154
155 #[test]
156 fn matching() {
157 let url = Url::parse("https://example.com").unwrap();
158 let sid = SourceId::for_registry(&url).unwrap();
159
160 let foo = PackageId::try_new("foo", "1.2.3", sid).unwrap();
161 assert!(PackageIdSpec::parse("foo").unwrap().matches(foo));
162 assert!(!PackageIdSpec::parse("bar").unwrap().matches(foo));
163 assert!(PackageIdSpec::parse("foo:1.2.3").unwrap().matches(foo));
164 assert!(!PackageIdSpec::parse("foo:1.2.2").unwrap().matches(foo));
165 assert!(PackageIdSpec::parse("foo@1.2.3").unwrap().matches(foo));
166 assert!(!PackageIdSpec::parse("foo@1.2.2").unwrap().matches(foo));
167 assert!(PackageIdSpec::parse("foo@1.2").unwrap().matches(foo));
168 assert!(PackageIdSpec::parse("https://example.com#foo@1.2")
169 .unwrap()
170 .matches(foo));
171 assert!(!PackageIdSpec::parse("https://bob.com#foo@1.2")
172 .unwrap()
173 .matches(foo));
174 assert!(PackageIdSpec::parse("registry+https://example.com#foo@1.2")
175 .unwrap()
176 .matches(foo));
177 assert!(!PackageIdSpec::parse("git+https://example.com#foo@1.2")
178 .unwrap()
179 .matches(foo));
180
181 let meta = PackageId::try_new("meta", "1.2.3+hello", sid).unwrap();
182 assert!(PackageIdSpec::parse("meta").unwrap().matches(meta));
183 assert!(PackageIdSpec::parse("meta@1").unwrap().matches(meta));
184 assert!(PackageIdSpec::parse("meta@1.2").unwrap().matches(meta));
185 assert!(PackageIdSpec::parse("meta@1.2.3").unwrap().matches(meta));
186 assert!(!PackageIdSpec::parse("meta@1.2.3-alpha.0")
187 .unwrap()
188 .matches(meta));
189 assert!(PackageIdSpec::parse("meta@1.2.3+hello")
190 .unwrap()
191 .matches(meta));
192 assert!(!PackageIdSpec::parse("meta@1.2.3+bye")
193 .unwrap()
194 .matches(meta));
195
196 let pre = PackageId::try_new("pre", "1.2.3-alpha.0", sid).unwrap();
197 assert!(PackageIdSpec::parse("pre").unwrap().matches(pre));
198 assert!(!PackageIdSpec::parse("pre@1").unwrap().matches(pre));
199 assert!(!PackageIdSpec::parse("pre@1.2").unwrap().matches(pre));
200 assert!(!PackageIdSpec::parse("pre@1.2.3").unwrap().matches(pre));
201 assert!(PackageIdSpec::parse("pre@1.2.3-alpha.0")
202 .unwrap()
203 .matches(pre));
204 assert!(!PackageIdSpec::parse("pre@1.2.3-alpha.1")
205 .unwrap()
206 .matches(pre));
207 assert!(!PackageIdSpec::parse("pre@1.2.3-beta.0")
208 .unwrap()
209 .matches(pre));
210 assert!(!PackageIdSpec::parse("pre@1.2.3+hello")
211 .unwrap()
212 .matches(pre));
213 assert!(!PackageIdSpec::parse("pre@1.2.3-alpha.0+hello")
214 .unwrap()
215 .matches(pre));
216 }
217}