1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
use std::fmt;
use std::str::FromStr;

use anyhow::{anyhow, bail, Context};
use semver::{Version, VersionReq};
use serde::de::{Deserialize, Deserializer, Error, Visitor};
use serde::ser::{Serialize, Serializer};

use crate::package_id::PackageId;
use crate::package_name::PackageName;

/// Describes a requirement on a package, consisting of a scope, name, and valid
/// version range.
///
/// Examples of package requirements:
/// * `roblox/roact@1.4.2`
/// * `lpghatguy/asink@0.2.0-alpha.3`
/// * `foo/bar@1`
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PackageReq {
    name: PackageName,
    version_req: VersionReq,
}

impl PackageReq {
    pub fn new(name: PackageName, version_req: VersionReq) -> Self {
        PackageReq { name, version_req }
    }

    pub fn name(&self) -> &PackageName {
        &self.name
    }

    pub fn version_req(&self) -> &VersionReq {
        &self.version_req
    }

    pub fn matches_id(&self, package_id: &PackageId) -> bool {
        self.matches(package_id.name(), package_id.version())
    }

    pub fn matches(&self, name: &PackageName, version: &Version) -> bool {
        self.name() == name && self.version_req.matches(version)
    }
}

impl fmt::Display for PackageReq {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        write!(formatter, "{}@{}", self.name, self.version_req)
    }
}

impl FromStr for PackageReq {
    type Err = anyhow::Error;

    fn from_str(value: &str) -> anyhow::Result<Self> {
        const BAD_FORMAT_MSG: &str = "a package requirement is of the form SCOPE/NAME@VERSION_REQ";

        let mut first_half = value.splitn(2, '/');
        let scope = first_half.next().ok_or_else(|| anyhow!(BAD_FORMAT_MSG))?;
        let name_and_version = first_half.next().ok_or_else(|| anyhow!(BAD_FORMAT_MSG))?;

        let mut second_half = name_and_version.splitn(2, '@');
        let name = second_half.next().ok_or_else(|| anyhow!(BAD_FORMAT_MSG))?;

        let version_req_source = second_half.next().ok_or_else(|| anyhow!(BAD_FORMAT_MSG))?;

        // The VersionReq type will successfully parse from an empty or
        // all-spaces string, yielding a wildcard. This is not behavior we want,
        // so let's check for that here.
        //
        // https://github.com/steveklabnik/semver-parser/issues/51
        if version_req_source.len() == 0 || version_req_source.chars().all(char::is_whitespace) {
            bail!(BAD_FORMAT_MSG);
        }

        let version_req = version_req_source
            .parse()
            .context("could not parse version requirement")?;

        let package_name = PackageName::new(scope, name).context(BAD_FORMAT_MSG)?;
        Ok(PackageReq::new(package_name, version_req))
    }
}

impl Serialize for PackageReq {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let combined_name = format!(
            "{}/{}@{}",
            self.name().scope(),
            self.name().name(),
            self.version_req()
        );
        serializer.serialize_str(&combined_name)
    }
}

impl<'de> Deserialize<'de> for PackageReq {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        deserializer.deserialize_str(PackageReqVisitor)
    }
}

struct PackageReqVisitor;

impl<'de> Visitor<'de> for PackageReqVisitor {
    type Value = PackageReq;

    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        write!(
            formatter,
            "a package requirement of the form SCOPE/NAME@VERSION_REQ"
        )
    }

    fn visit_str<E: Error>(self, value: &str) -> Result<Self::Value, E> {
        value.parse().map_err(|err| E::custom(err))
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn new() {
        let req = PackageReq::new(
            PackageName::new("foo", "bar").unwrap(),
            VersionReq::parse("1.2.3").unwrap(),
        );
        assert_eq!(req.name().scope(), "foo");
        assert_eq!(req.name().name(), "bar");
        assert_eq!(req.version_req(), &VersionReq::parse("1.2.3").unwrap());
    }

    #[test]
    fn display() {
        let req = PackageReq::new(
            PackageName::new("hello", "world").unwrap(),
            VersionReq::parse("0.2.3").unwrap(),
        );

        // The semver crate's VersionReq type stores and prints using the most
        // explicit version of a constraint. This normalization helps make
        // comparison and evaluation simpler, but make printing a little ugly.
        assert_eq!(req.to_string(), "hello/world@>=0.2.3, <0.3.0");
    }

    #[test]
    fn parse() {
        // If given a semver version, we default to the ^ operator, which means
        // "compatible with". This is a good default that Cargo also chooses.
        let default_compat: PackageReq = "hello/world@1.2.3".parse().unwrap();
        assert_eq!(default_compat.name().scope(), "hello");
        assert_eq!(default_compat.name().name(), "world");
        assert_eq!(
            default_compat.version_req(),
            &VersionReq::parse("^1.2.3").unwrap()
        );

        // Arbitrarily complex semver predicates can be chained together. This
        // range might mean "0.2.7 is really broken and I don't want it".
        let with_ops: PackageReq = "hello/world@>=0.2.0, <0.2.7".parse().unwrap();
        assert_eq!(with_ops.name().scope(), "hello");
        assert_eq!(with_ops.name().name(), "world");
        assert_eq!(
            with_ops.version_req(),
            &VersionReq::parse(">=0.2.0, <0.2.7").unwrap()
        );
    }

    #[test]
    fn parse_invalid() {
        // Package requirements require a version requirement.
        let no_version: Result<PackageReq, _> = "hello/world".parse();
        no_version.unwrap_err();
        let no_version_at: Result<PackageReq, _> = "hello/world@".parse();
        no_version_at.unwrap_err();
    }

    #[test]
    fn serialization() {
        let name = PackageName::new("lpghatguy", "asink").unwrap();
        let package_req = PackageReq::new(name, VersionReq::parse("2.3.1").unwrap());

        let serialized = serde_json::to_string(&package_req).unwrap();
        assert_eq!(serialized, "\"lpghatguy/asink@>=2.3.1, <3.0.0\"");

        let deserialized: PackageReq = serde_json::from_str(&serialized).unwrap();
        assert_eq!(deserialized, package_req);
    }
}