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 "specificationm `{self}` is ambiguous
116help: re-run this command with one of the following specifications",
117 );
118 let mut vec = vec![ret, other];
119 vec.extend(ids);
120 minimize(&mut msg, &vec, self);
121 Err(anyhow::format_err!("{}", msg))
122 }
123 None => Ok(ret),
124 };
125
126 fn minimize(msg: &mut String, ids: &[PackageId], spec: &PackageIdSpec) {
127 let mut version_cnt = HashMap::new();
128 for id in ids {
129 *version_cnt.entry(id.version()).or_insert(0) += 1;
130 }
131 for id in ids {
132 if version_cnt[id.version()] == 1 {
133 msg.push_str(&format!("\n {}@{}", spec.name(), id.version()));
134 } else {
135 msg.push_str(&format!("\n {}", id.to_spec()));
136 }
137 }
138 }
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::PackageIdSpec;
145 use super::PackageIdSpecQuery;
146 use crate::core::{PackageId, SourceId};
147 use url::Url;
148
149 #[test]
150 fn matching() {
151 let url = Url::parse("https://example.com").unwrap();
152 let sid = SourceId::for_registry(&url).unwrap();
153
154 let foo = PackageId::try_new("foo", "1.2.3", sid).unwrap();
155 assert!(PackageIdSpec::parse("foo").unwrap().matches(foo));
156 assert!(!PackageIdSpec::parse("bar").unwrap().matches(foo));
157 assert!(PackageIdSpec::parse("foo:1.2.3").unwrap().matches(foo));
158 assert!(!PackageIdSpec::parse("foo:1.2.2").unwrap().matches(foo));
159 assert!(PackageIdSpec::parse("foo@1.2.3").unwrap().matches(foo));
160 assert!(!PackageIdSpec::parse("foo@1.2.2").unwrap().matches(foo));
161 assert!(PackageIdSpec::parse("foo@1.2").unwrap().matches(foo));
162 assert!(
163 PackageIdSpec::parse("https://example.com#foo@1.2")
164 .unwrap()
165 .matches(foo)
166 );
167 assert!(
168 !PackageIdSpec::parse("https://bob.com#foo@1.2")
169 .unwrap()
170 .matches(foo)
171 );
172 assert!(
173 PackageIdSpec::parse("registry+https://example.com#foo@1.2")
174 .unwrap()
175 .matches(foo)
176 );
177 assert!(
178 !PackageIdSpec::parse("git+https://example.com#foo@1.2")
179 .unwrap()
180 .matches(foo)
181 );
182
183 let meta = PackageId::try_new("meta", "1.2.3+hello", sid).unwrap();
184 assert!(PackageIdSpec::parse("meta").unwrap().matches(meta));
185 assert!(PackageIdSpec::parse("meta@1").unwrap().matches(meta));
186 assert!(PackageIdSpec::parse("meta@1.2").unwrap().matches(meta));
187 assert!(PackageIdSpec::parse("meta@1.2.3").unwrap().matches(meta));
188 assert!(
189 !PackageIdSpec::parse("meta@1.2.3-alpha.0")
190 .unwrap()
191 .matches(meta)
192 );
193 assert!(
194 PackageIdSpec::parse("meta@1.2.3+hello")
195 .unwrap()
196 .matches(meta)
197 );
198 assert!(
199 !PackageIdSpec::parse("meta@1.2.3+bye")
200 .unwrap()
201 .matches(meta)
202 );
203
204 let pre = PackageId::try_new("pre", "1.2.3-alpha.0", sid).unwrap();
205 assert!(PackageIdSpec::parse("pre").unwrap().matches(pre));
206 assert!(!PackageIdSpec::parse("pre@1").unwrap().matches(pre));
207 assert!(!PackageIdSpec::parse("pre@1.2").unwrap().matches(pre));
208 assert!(!PackageIdSpec::parse("pre@1.2.3").unwrap().matches(pre));
209 assert!(
210 PackageIdSpec::parse("pre@1.2.3-alpha.0")
211 .unwrap()
212 .matches(pre)
213 );
214 assert!(
215 !PackageIdSpec::parse("pre@1.2.3-alpha.1")
216 .unwrap()
217 .matches(pre)
218 );
219 assert!(
220 !PackageIdSpec::parse("pre@1.2.3-beta.0")
221 .unwrap()
222 .matches(pre)
223 );
224 assert!(
225 !PackageIdSpec::parse("pre@1.2.3+hello")
226 .unwrap()
227 .matches(pre)
228 );
229 assert!(
230 !PackageIdSpec::parse("pre@1.2.3-alpha.0+hello")
231 .unwrap()
232 .matches(pre)
233 );
234 }
235}