cargo/ops/cargo_add/
crate_spec.rs

1//! Crate name parsing.
2
3use anyhow::Context as _;
4
5use super::Dependency;
6use crate::util::toml_mut::dependency::RegistrySource;
7use crate::CargoResult;
8use cargo_util_schemas::manifest::PackageName;
9
10/// User-specified crate
11///
12/// This can be a
13/// - Name (e.g. `docopt`)
14/// - Name and a version req (e.g. `docopt@^0.8`)
15#[derive(Debug)]
16pub struct CrateSpec {
17    /// Crate name
18    name: String,
19    /// Optional version requirement
20    version_req: Option<String>,
21}
22
23impl CrateSpec {
24    /// Convert a string to a `Crate`
25    pub fn resolve(pkg_id: &str) -> CargoResult<Self> {
26        let (name, version) = pkg_id
27            .split_once('@')
28            .map(|(n, v)| (n, Some(v)))
29            .unwrap_or((pkg_id, None));
30
31        let package_name = PackageName::new(name);
32        if !pkg_id.contains("@") && package_name.is_err() {
33            for (idx, ch) in pkg_id.char_indices() {
34                if !(unicode_xid::UnicodeXID::is_xid_continue(ch) || ch == '-') {
35                    let mut suggested_pkg_id = pkg_id.to_string();
36                    suggested_pkg_id.insert_str(idx, "@");
37                    if let Ok(_) = CrateSpec::resolve(&suggested_pkg_id.as_str()) {
38                        let err = package_name.unwrap_err();
39                        return Err(
40                            anyhow::format_err!("{err}\n\n\
41                                help: if this is meant to be a package name followed by a version, insert an `@` like `{suggested_pkg_id}`").into());
42                    }
43                }
44            }
45        }
46
47        package_name?;
48
49        if let Some(version) = version {
50            semver::VersionReq::parse(version).with_context(|| {
51                if let Some(stripped) = version.strip_prefix("v") {
52                    return format!(
53                        "the version provided, `{version}` is not a \
54                         valid SemVer requirement\n\n\
55                         help: changing the package to `{name}@{stripped}`",
56                    );
57                }
58                format!("invalid version requirement `{version}`")
59            })?;
60        }
61
62        let id = Self {
63            name: name.to_owned(),
64            version_req: version.map(|s| s.to_owned()),
65        };
66
67        Ok(id)
68    }
69
70    /// Generate a dependency entry for this crate specifier
71    pub fn to_dependency(&self) -> CargoResult<Dependency> {
72        let mut dep = Dependency::new(self.name());
73        if let Some(version_req) = self.version_req() {
74            dep = dep.set_source(RegistrySource::new(version_req));
75        }
76        Ok(dep)
77    }
78
79    pub fn name(&self) -> &str {
80        &self.name
81    }
82
83    pub fn version_req(&self) -> Option<&str> {
84        self.version_req.as_deref()
85    }
86}