cargo/core/
package_id_spec.rs1use std::collections::HashMap;
2
3use anyhow::{Context as _, bail};
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!(
169 PackageIdSpec::parse("https://example.com#foo@1.2")
170 .unwrap()
171 .matches(foo)
172 );
173 assert!(
174 !PackageIdSpec::parse("https://bob.com#foo@1.2")
175 .unwrap()
176 .matches(foo)
177 );
178 assert!(
179 PackageIdSpec::parse("registry+https://example.com#foo@1.2")
180 .unwrap()
181 .matches(foo)
182 );
183 assert!(
184 !PackageIdSpec::parse("git+https://example.com#foo@1.2")
185 .unwrap()
186 .matches(foo)
187 );
188
189 let meta = PackageId::try_new("meta", "1.2.3+hello", sid).unwrap();
190 assert!(PackageIdSpec::parse("meta").unwrap().matches(meta));
191 assert!(PackageIdSpec::parse("meta@1").unwrap().matches(meta));
192 assert!(PackageIdSpec::parse("meta@1.2").unwrap().matches(meta));
193 assert!(PackageIdSpec::parse("meta@1.2.3").unwrap().matches(meta));
194 assert!(
195 !PackageIdSpec::parse("meta@1.2.3-alpha.0")
196 .unwrap()
197 .matches(meta)
198 );
199 assert!(
200 PackageIdSpec::parse("meta@1.2.3+hello")
201 .unwrap()
202 .matches(meta)
203 );
204 assert!(
205 !PackageIdSpec::parse("meta@1.2.3+bye")
206 .unwrap()
207 .matches(meta)
208 );
209
210 let pre = PackageId::try_new("pre", "1.2.3-alpha.0", sid).unwrap();
211 assert!(PackageIdSpec::parse("pre").unwrap().matches(pre));
212 assert!(!PackageIdSpec::parse("pre@1").unwrap().matches(pre));
213 assert!(!PackageIdSpec::parse("pre@1.2").unwrap().matches(pre));
214 assert!(!PackageIdSpec::parse("pre@1.2.3").unwrap().matches(pre));
215 assert!(
216 PackageIdSpec::parse("pre@1.2.3-alpha.0")
217 .unwrap()
218 .matches(pre)
219 );
220 assert!(
221 !PackageIdSpec::parse("pre@1.2.3-alpha.1")
222 .unwrap()
223 .matches(pre)
224 );
225 assert!(
226 !PackageIdSpec::parse("pre@1.2.3-beta.0")
227 .unwrap()
228 .matches(pre)
229 );
230 assert!(
231 !PackageIdSpec::parse("pre@1.2.3+hello")
232 .unwrap()
233 .matches(pre)
234 );
235 assert!(
236 !PackageIdSpec::parse("pre@1.2.3-alpha.0+hello")
237 .unwrap()
238 .matches(pre)
239 );
240 }
241}