Skip to main content

callisto_model/
version.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3
4use crate::Ecosystem;
5
6/// §7.7. `SemVer` is the only grammar with an implementation in the committed v0.1–v0.4
7/// scope; the rest are declared so `Ecosystem::version_grammar` is total.
8#[derive(
9    Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
10)]
11#[serde(rename_all = "camelCase")]
12#[non_exhaustive]
13pub enum VersionGrammar {
14    SemVer,
15    /// PEP 440 (§7.7) — declared, not implemented.
16    Pep440,
17    /// Maven's qualifier-ordering comparator (§7.7) — declared, not implemented.
18    Maven,
19}
20
21/// The parsed form, kept alongside `raw` so comparison and component access are cheap.
22/// `SemVer` is the only inhabited variant until a demand-gated grammar ships (§7.7).
23#[derive(Clone, Debug, PartialEq, Eq, Hash)]
24pub(crate) enum ParsedVersion {
25    SemVer(semver::Version),
26}
27
28/// A parsed version, tagged with the grammar it was parsed under. §7.7, P4.
29#[derive(Clone, Debug, PartialEq, Eq, Hash, JsonSchema)]
30#[schemars(with = "String")]
31pub struct Version {
32    pub(crate) grammar: VersionGrammar,
33    pub(crate) raw: String,
34    #[schemars(skip)]
35    pub(crate) parsed: ParsedVersion,
36}
37
38impl Version {
39    pub fn parse(raw: &str, grammar: VersionGrammar) -> Result<Self, VersionParseError> {
40        match grammar {
41            VersionGrammar::SemVer => {
42                let parsed = semver::Version::parse(raw).map_err(|e| VersionParseError {
43                    raw: raw.to_string(),
44                    grammar,
45                    message: e.to_string(),
46                })?;
47                Ok(Version {
48                    grammar,
49                    raw: raw.to_string(),
50                    parsed: ParsedVersion::SemVer(parsed),
51                })
52            }
53            VersionGrammar::Pep440 | VersionGrammar::Maven => Err(VersionParseError {
54                raw: raw.to_string(),
55                grammar,
56                message: format!("{grammar:?} has no versioning implementation yet (§7.7)"),
57            }),
58        }
59    }
60
61    pub fn semver(major: u64, minor: u64, patch: u64) -> Self {
62        let parsed = semver::Version::new(major, minor, patch);
63        Version {
64            grammar: VersionGrammar::SemVer,
65            raw: parsed.to_string(),
66            parsed: ParsedVersion::SemVer(parsed),
67        }
68    }
69
70    pub fn grammar(&self) -> VersionGrammar {
71        self.grammar
72    }
73
74    pub fn render(&self) -> &str {
75        &self.raw
76    }
77
78    pub fn raw(&self) -> &str {
79        &self.raw
80    }
81
82    pub fn major(&self) -> Option<u64> {
83        match &self.parsed {
84            ParsedVersion::SemVer(v) => Some(v.major),
85        }
86    }
87
88    pub fn minor(&self) -> Option<u64> {
89        match &self.parsed {
90            ParsedVersion::SemVer(v) => Some(v.minor),
91        }
92    }
93
94    pub fn patch(&self) -> Option<u64> {
95        match &self.parsed {
96            ParsedVersion::SemVer(v) => Some(v.patch),
97        }
98    }
99
100    pub fn is_prerelease(&self) -> bool {
101        match &self.parsed {
102            ParsedVersion::SemVer(v) => !v.pre.is_empty(),
103        }
104    }
105
106    pub fn compare(&self, other: &Version) -> Result<std::cmp::Ordering, GrammarMismatch> {
107        if self.grammar != other.grammar {
108            return Err(GrammarMismatch {
109                left: self.grammar,
110                right: other.grammar,
111            });
112        }
113        match (&self.parsed, &other.parsed) {
114            (ParsedVersion::SemVer(a), ParsedVersion::SemVer(b)) => Ok(a.cmp(b)),
115        }
116    }
117
118    pub fn partial_compare(&self, other: &Version) -> Option<std::cmp::Ordering> {
119        self.compare(other).ok()
120    }
121}
122
123impl std::fmt::Display for Version {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        write!(f, "{}", self.raw)
126    }
127}
128
129impl Serialize for Version {
130    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
131    where
132        S: Serializer,
133    {
134        serializer.serialize_str(self.render())
135    }
136}
137
138impl<'de> Deserialize<'de> for Version {
139    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
140    where
141        D: Deserializer<'de>,
142    {
143        let s = String::deserialize(deserializer)?;
144        Version::parse(&s, VersionGrammar::SemVer).map_err(serde::de::Error::custom)
145    }
146}
147
148/// Parsed version requirement.
149#[derive(Clone, Debug, PartialEq, Eq, JsonSchema)]
150#[schemars(with = "String")]
151pub struct VersionReq {
152    grammar: VersionGrammar,
153    ecosystem: Ecosystem,
154    #[schemars(skip)]
155    req: semver::VersionReq,
156    raw: String,
157}
158
159impl VersionReq {
160    pub fn parse(raw: &str, ecosystem: Ecosystem) -> Result<Self, VersionParseError> {
161        let grammar = ecosystem.version_grammar();
162        match grammar {
163            VersionGrammar::SemVer => {
164                let req = semver::VersionReq::parse(raw).map_err(|e| VersionParseError {
165                    raw: raw.to_string(),
166                    grammar,
167                    message: e.to_string(),
168                })?;
169                Ok(VersionReq {
170                    grammar,
171                    ecosystem,
172                    req,
173                    raw: raw.to_string(),
174                })
175            }
176            VersionGrammar::Pep440 | VersionGrammar::Maven => Err(VersionParseError {
177                raw: raw.to_string(),
178                grammar,
179                message: format!("{grammar:?} version requirements not implemented"),
180            }),
181        }
182    }
183
184    pub fn render(&self) -> &str {
185        &self.raw
186    }
187
188    pub fn ecosystem(&self) -> Ecosystem {
189        self.ecosystem
190    }
191
192    pub fn matches(&self, v: &Version) -> Result<bool, GrammarMismatch> {
193        if self.grammar != v.grammar() {
194            return Err(GrammarMismatch {
195                left: self.grammar,
196                right: v.grammar(),
197            });
198        }
199        match &v.parsed {
200            ParsedVersion::SemVer(sv) => Ok(self.req.matches(sv)),
201        }
202    }
203}
204
205impl Serialize for VersionReq {
206    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
207    where
208        S: Serializer,
209    {
210        serializer.serialize_str(self.render())
211    }
212}
213
214impl<'de> Deserialize<'de> for VersionReq {
215    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
216    where
217        D: Deserializer<'de>,
218    {
219        let s = String::deserialize(deserializer)?;
220        VersionReq::parse(&s, Ecosystem::Cargo)
221            .or_else(|_| VersionReq::parse(&s, Ecosystem::Npm))
222            .or_else(|_| VersionReq::parse(&s, Ecosystem::Pypi))
223            .map_err(serde::de::Error::custom)
224    }
225}
226
227#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
228#[error("`{raw}` is not a valid {grammar:?} version: {message}")]
229pub struct VersionParseError {
230    pub raw: String,
231    pub grammar: VersionGrammar,
232    pub message: String,
233}
234
235#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
236#[error("cannot compare a {left:?} version with a {right:?} version")]
237pub struct GrammarMismatch {
238    pub left: VersionGrammar,
239    pub right: VersionGrammar,
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn parses_valid_semver_and_exposes_components() {
248        let v = Version::parse("1.2.3", VersionGrammar::SemVer).unwrap();
249        assert_eq!(v.grammar(), VersionGrammar::SemVer);
250        assert_eq!(v.major(), Some(1));
251        assert_eq!(v.minor(), Some(2));
252        assert_eq!(v.patch(), Some(3));
253        assert!(!v.is_prerelease());
254    }
255
256    #[test]
257    fn serde_version_roundtrips() {
258        let v = Version::parse("1.2.3", VersionGrammar::SemVer).unwrap();
259        let json = serde_json::to_string(&v).unwrap();
260        assert_eq!(json, "\"1.2.3\"");
261        let deserialized: Version = serde_json::from_str(&json).unwrap();
262        assert_eq!(v, deserialized);
263    }
264}