1use schemars::JsonSchema;
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3
4use crate::Ecosystem;
5
6#[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 Pep440,
17 Maven,
19}
20
21#[derive(Clone, Debug, PartialEq, Eq, Hash)]
24pub(crate) enum ParsedVersion {
25 SemVer(semver::Version),
26}
27
28#[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#[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}