Skip to main content

wdl_modules/
version_requirement.rs

1//! Version-requirement type wrapping a constrained subset of
2//! [`semver::VersionReq`].
3
4use std::fmt;
5use std::ops::Deref;
6use std::str::FromStr;
7
8use semver::VersionReq;
9use serde::Deserialize;
10use serde::Serialize;
11use thiserror::Error;
12
13/// An error parsing a [`VersionRequirement`].
14#[derive(Debug, Error)]
15#[error("`{0}` is not a valid semantic version requirement")]
16pub struct VersionRequirementError(String);
17
18/// A version requirement, parsed by [`semver::VersionReq`].
19#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(into = "String", try_from = "String")]
21pub struct VersionRequirement(VersionReq);
22
23impl VersionRequirement {
24    /// Consumes the [`VersionRequirement`] and returns the inner
25    /// [`VersionReq`].
26    pub fn into_inner(self) -> VersionReq {
27        self.0
28    }
29}
30
31impl Deref for VersionRequirement {
32    type Target = VersionReq;
33
34    fn deref(&self) -> &VersionReq {
35        &self.0
36    }
37}
38
39impl fmt::Display for VersionRequirement {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        self.0.fmt(f)
42    }
43}
44
45impl TryFrom<String> for VersionRequirement {
46    type Error = VersionRequirementError;
47
48    fn try_from(s: String) -> Result<Self, Self::Error> {
49        match VersionReq::parse(s.trim()) {
50            Ok(v) => Ok(Self(v)),
51            Err(_) => Err(VersionRequirementError(s)),
52        }
53    }
54}
55
56impl FromStr for VersionRequirement {
57    type Err = VersionRequirementError;
58
59    fn from_str(s: &str) -> Result<Self, Self::Err> {
60        Self::try_from(s.to_string())
61    }
62}
63
64impl From<VersionRequirement> for String {
65    fn from(req: VersionRequirement) -> Self {
66        req.0.to_string()
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use semver::Version;
73
74    use super::*;
75
76    #[test]
77    fn accepts_spec_operators() {
78        for s in [
79            "^1.2.0",
80            "~1.2.0",
81            "=1.2.0",
82            ">=1.0.0, <2.0.0",
83            ">1.0.0",
84            "<2.0.0",
85            ">=1.0.0",
86            "<=2.0.0",
87            "*",
88            "1.2.0",
89        ] {
90            assert!(s.parse::<VersionRequirement>().is_ok(), "rejected `{s}`");
91        }
92    }
93
94    #[test]
95    fn rejects_invalid_format() {
96        for bad in ["", "   ", "not-a-req"] {
97            assert!(
98                bad.parse::<VersionRequirement>().is_err(),
99                "accepted `{bad}`"
100            );
101        }
102    }
103
104    #[test]
105    fn error_message_includes_input() {
106        let err = "^1.foo".parse::<VersionRequirement>().unwrap_err();
107        assert_eq!(
108            err.to_string(),
109            "`^1.foo` is not a valid semantic version requirement"
110        );
111    }
112
113    #[test]
114    fn matches_versions_correctly() {
115        let req: VersionRequirement = "^1.2.0".parse().unwrap();
116        assert!(req.matches(&Version::parse("1.2.0").unwrap()));
117        assert!(req.matches(&Version::parse("1.9.99").unwrap()));
118        assert!(!req.matches(&Version::parse("2.0.0").unwrap()));
119        assert!(!req.matches(&Version::parse("1.1.0").unwrap()));
120    }
121
122    #[test]
123    fn round_trips_via_serde() {
124        let req: VersionRequirement = "^1.2.0".parse().unwrap();
125        let json = serde_json::to_string(&req).unwrap();
126        assert_eq!(json, r#""^1.2.0""#);
127        let parsed: VersionRequirement = serde_json::from_str(&json).unwrap();
128        assert_eq!(parsed, req);
129    }
130}