#
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    /// Roughly equivalent to `PackageIdSpec::parse(spec)?.query(i)`
    12    fn query_str<I>(spec: &str, i: I) -> CargoResult<PackageId>
    13    where
    14        I: IntoIterator<Item = PackageId>;
    15
    16    /// Checks whether the given `PackageId` matches the `PackageIdSpec`.
    17    fn matches(&self, package_id: PackageId) -> bool;
    18
    19    /// Checks a list of `PackageId`s to find 1 that matches this `PackageIdSpec`. If 0, 2, or
    20    /// more are found, then this returns an error.
    21    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}