1use std::cmp::Ordering;
49use std::fmt::{Display, Formatter, Result as FmtResult};
50use std::slice::Iter;
51use std::str::FromStr;
52use std::vec::IntoIter as VecIntoIter;
53
54use anyhow::Error as AnyError;
55use serde_derive::{Deserialize, Serialize};
56use thiserror::Error;
57
58#[cfg(feature = "facet-unstable")]
59use facet::Facet;
60
61use crate::expr::parser;
62
63#[derive(Debug, Error)]
65#[non_exhaustive]
66pub enum ParseError {
67 #[error("Could not parse '{0}' as a version string")]
69 ParseFailure(String, #[source] AnyError),
70
71 #[error("Could not parse '{0}' as a version string: {1} bytes left over")]
73 ParseLeftovers(String, usize),
74}
75
76#[derive(Debug, Clone, Eq, PartialEq)]
78#[cfg_attr(feature = "facet-unstable", derive(Facet))]
79#[non_exhaustive]
80#[expect(
81 clippy::module_name_repetitions,
82 reason = "sensible name for the struct"
83)]
84pub struct VersionComponent {
85 pub num: Option<u32>,
87 pub rest: String,
89}
90
91fn compare_single(left: &VersionComponent, right: &VersionComponent) -> Ordering {
93 left.num.map_or_else(
94 || {
95 if right.num.is_some() {
96 Ordering::Less
97 } else {
98 left.rest.cmp(&right.rest)
99 }
100 },
101 |ver_left| {
102 right.num.map_or(Ordering::Greater, |ver_right| {
103 let res = ver_left.cmp(&ver_right);
104 if res == Ordering::Equal {
105 left.rest.cmp(&right.rest)
106 } else {
107 res
108 }
109 })
110 },
111 )
112}
113
114fn compare_components(left: &[VersionComponent], right: &[VersionComponent]) -> Ordering {
116 left.split_first().map_or_else(
117 || {
118 right.first().map_or(Ordering::Equal, |ver_right| {
119 if ver_right.num.is_some() {
120 Ordering::Less
121 } else {
122 Ordering::Greater
123 }
124 })
125 },
126 |(comp_left, rest_left)| {
127 right.split_first().map_or_else(
128 || {
129 if comp_left.num.is_some() {
130 Ordering::Greater
131 } else {
132 Ordering::Less
133 }
134 },
135 |(comp_right, rest_right)| {
136 let res = compare_single(comp_left, comp_right);
137 if res == Ordering::Equal {
138 compare_components(rest_left, rest_right)
139 } else {
140 res
141 }
142 },
143 )
144 },
145 )
146}
147
148#[derive(Debug, Clone, Deserialize, Serialize)]
150#[cfg_attr(feature = "facet-unstable", derive(Facet))]
151#[serde(transparent)]
152pub struct Version {
153 value: String,
155 #[serde(skip)]
157 components: Vec<VersionComponent>,
158}
159
160impl Version {
161 #[inline]
163 #[must_use]
164 pub const fn new(value: String, components: Vec<VersionComponent>) -> Self {
165 Self { value, components }
166 }
167
168 #[inline]
170 pub fn iter(&self) -> Iter<'_, VersionComponent> {
171 self.components.iter()
172 }
173}
174
175impl FromStr for Version {
176 type Err = ParseError;
177
178 #[inline]
179 fn from_str(value: &str) -> Result<Self, Self::Err> {
180 parser::parse_version(value)
181 }
182}
183
184impl AsRef<str> for Version {
185 #[inline]
186 fn as_ref(&self) -> &str {
187 &self.value
188 }
189}
190
191impl Display for Version {
192 #[inline]
193 #[expect(clippy::min_ident_chars, reason = "Display trait")]
194 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
195 write!(f, "{}", self.as_ref())
196 }
197}
198
199impl PartialEq for Version {
200 #[inline]
201 fn eq(&self, other: &Self) -> bool {
202 self.cmp(other) == Ordering::Equal
203 }
204}
205
206impl Eq for Version {}
207
208impl PartialOrd for Version {
209 #[inline]
210 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
211 Some(self.cmp(other))
212 }
213}
214
215impl Ord for Version {
216 #[inline]
217 fn cmp(&self, other: &Self) -> Ordering {
218 compare_components(&self.components, &other.components)
219 }
220}
221
222impl IntoIterator for Version {
223 type Item = VersionComponent;
224 type IntoIter = VecIntoIter<Self::Item>;
225
226 #[inline]
227 fn into_iter(self) -> Self::IntoIter {
228 self.components.into_iter()
229 }
230}
231
232impl<'data> IntoIterator for &'data Version {
233 type Item = &'data VersionComponent;
234 type IntoIter = Iter<'data, VersionComponent>;
235
236 #[inline]
237 fn into_iter(self) -> Self::IntoIter {
238 self.components.iter()
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 #![expect(clippy::panic_in_result_fn, reason = "this is a test suite")]
245
246 use std::error::Error;
247
248 #[test]
249 fn num_only() -> Result<(), Box<dyn Error>> {
250 let expected: [super::VersionComponent; 1] = [super::VersionComponent {
251 num: Some(616),
252 rest: String::new(),
253 }];
254 let ver: super::Version = "616".parse()?;
255
256 let components = ver.into_iter().collect::<Vec<_>>();
257 assert_eq!(&expected[..], &*components);
258 Ok(())
259 }
260
261 #[test]
262 fn rest_only() -> Result<(), Box<dyn Error>> {
263 let expected: [super::VersionComponent; 1] = [super::VersionComponent {
264 num: None,
265 rest: "whee".to_owned(),
266 }];
267 let ver: super::Version = "whee".parse()?;
268
269 let components = ver.into_iter().collect::<Vec<_>>();
270 assert_eq!(&expected[..], &*components);
271 Ok(())
272 }
273
274 #[test]
275 fn both() -> Result<(), Box<dyn Error>> {
276 let expected: [super::VersionComponent; 1] = [super::VersionComponent {
277 num: Some(29),
278 rest: "palms".to_owned(),
279 }];
280 let ver: super::Version = "29palms".parse()?;
281
282 let components = ver.into_iter().collect::<Vec<_>>();
283 assert_eq!(&expected[..], &*components);
284 Ok(())
285 }
286
287 #[test]
288 fn three() -> Result<(), Box<dyn Error>> {
289 let expected: [super::VersionComponent; 3] = [
290 super::VersionComponent {
291 num: Some(1),
292 rest: String::new(),
293 },
294 super::VersionComponent {
295 num: Some(5),
296 rest: "a".to_owned(),
297 },
298 super::VersionComponent {
299 num: None,
300 rest: "beta3".to_owned(),
301 },
302 ];
303 let ver: super::Version = "1.5a.beta3".parse()?;
305
306 let components = ver.into_iter().collect::<Vec<_>>();
307 assert_eq!(&expected[..], &*components);
308 Ok(())
309 }
310}