Skip to main content

feature_check/
version.rs

1// SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
2// SPDX-License-Identifier: BSD-2-Clause
3//! Parse version strings and compare them.
4//!
5//! The [`Version`] struct may be used to break a version string down
6//! into its separate components and then compare it to another one,
7//! e.g. to decide whether a certain feature is really supported.
8//!
9//! ```rust
10//! use std::cmp;
11//! # use std::error::Error;
12//!
13//! use feature_check::version::Version;
14//!
15//! # fn main() -> Result<(), Box<dyn Error>> {
16//! let v1: Version = "2.1".parse()?;
17//! let v2: Version = "2.2.b2".parse()?;
18//! println!("{v1} {res:?} {v2}", res = v1.cmp(&v2));
19//! println!("equal? {res}", res = v1 == v2);
20//! println!("smaller: {res}", res = cmp::min(&v1, &v2));
21//! println!("larger: {res}", res = cmp::max(&v1, &v2));
22//! println!("v1: {v1}");
23//! for comp in &v1 {
24//!     println!(
25//!         "- {num}/{rest}",
26//!         num = match comp.num {
27//!             Some(value) => value.to_string(),
28//!             None => "(none)".to_string(),
29//!         },
30//!         rest = comp.rest,
31//!     );
32//! }
33//! println!("v2: {v2}");
34//! for comp in v2.into_iter() {
35//!     println!(
36//!         "- {num}/{rest}",
37//!         num = match comp.num {
38//!             Some(value) => value.to_string(),
39//!             None => "(none)".to_string(),
40//!         },
41//!         rest = comp.rest,
42//!     );
43//! }
44//! # Ok(())
45//! # }
46//! ```
47
48use std::cmp::Ordering;
49use std::error::Error;
50use std::fmt::{Display, Formatter, Result as FmtResult};
51use std::slice::Iter;
52use std::str::FromStr;
53use std::vec::IntoIter as VecIntoIter;
54
55use anyhow::Error as AnyError;
56use serde_derive::{Deserialize, Serialize};
57
58use crate::expr::parser;
59
60/// An error that occurred while parsing a version string.
61#[derive(Debug)]
62#[non_exhaustive]
63pub enum ParseError {
64    /// A parser failed.
65    ParseFailure(String, AnyError),
66
67    /// A parser left some bytes out.
68    ParseLeftovers(String, usize),
69}
70
71impl Display for ParseError {
72    #[inline]
73    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
74        match *self {
75            Self::ParseFailure(ref value, _) => {
76                write!(f, "Could not parse '{value}' as a version string")
77            }
78            Self::ParseLeftovers(ref value, ref count) => write!(
79                f,
80                "Could not parse '{value}' as a version string: {count} bytes left over"
81            ),
82        }
83    }
84}
85
86impl Error for ParseError {
87    #[inline]
88    fn source(&self) -> Option<&(dyn Error + 'static)> {
89        match *self {
90            Self::ParseFailure(_, ref err) => Some(err.as_ref()),
91            Self::ParseLeftovers(_, _) => None,
92        }
93    }
94}
95
96/// A single version component, e.g. "3" or "b2".
97#[derive(Debug, Clone, Eq, PartialEq)]
98#[non_exhaustive]
99#[expect(
100    clippy::module_name_repetitions,
101    reason = "sensible name for the struct"
102)]
103pub struct VersionComponent {
104    /// The numeric portion of the version component.
105    pub num: Option<u32>,
106    /// The freeform portion of the version component.
107    pub rest: String,
108}
109
110/// Compare two already-extracted version components.
111fn compare_single(left: &VersionComponent, right: &VersionComponent) -> Ordering {
112    left.num.map_or_else(
113        || {
114            if right.num.is_some() {
115                Ordering::Less
116            } else {
117                left.rest.cmp(&right.rest)
118            }
119        },
120        |ver_left| {
121            right.num.map_or(Ordering::Greater, |ver_right| {
122                let res = ver_left.cmp(&ver_right);
123                if res == Ordering::Equal {
124                    left.rest.cmp(&right.rest)
125                } else {
126                    res
127                }
128            })
129        },
130    )
131}
132
133/// Compare two lists of already-extracted version components.
134fn compare_components(left: &[VersionComponent], right: &[VersionComponent]) -> Ordering {
135    left.split_first().map_or_else(
136        || {
137            right.first().map_or(Ordering::Equal, |ver_right| {
138                if ver_right.num.is_some() {
139                    Ordering::Less
140                } else {
141                    Ordering::Greater
142                }
143            })
144        },
145        |(comp_left, rest_left)| {
146            right.split_first().map_or_else(
147                || {
148                    if comp_left.num.is_some() {
149                        Ordering::Greater
150                    } else {
151                        Ordering::Less
152                    }
153                },
154                |(comp_right, rest_right)| {
155                    let res = compare_single(comp_left, comp_right);
156                    if res == Ordering::Equal {
157                        compare_components(rest_left, rest_right)
158                    } else {
159                        res
160                    }
161                },
162            )
163        },
164    )
165}
166
167/// A version string, both in full and broken down into components.
168#[derive(Debug, Clone, Deserialize, Serialize)]
169#[serde(transparent)]
170pub struct Version {
171    /// The full version string.
172    value: String,
173    /// The components of the version string.
174    #[serde(skip)]
175    components: Vec<VersionComponent>,
176}
177
178impl Version {
179    /// Create a version object with the specified attributes.
180    #[inline]
181    #[must_use]
182    pub const fn new(value: String, components: Vec<VersionComponent>) -> Self {
183        Self { value, components }
184    }
185
186    /// Return an iterator over the version components.
187    #[inline]
188    pub fn iter(&self) -> Iter<'_, VersionComponent> {
189        self.components.iter()
190    }
191}
192
193impl FromStr for Version {
194    type Err = ParseError;
195
196    #[inline]
197    fn from_str(value: &str) -> Result<Self, Self::Err> {
198        parser::parse_version(value)
199    }
200}
201
202impl AsRef<str> for Version {
203    #[inline]
204    fn as_ref(&self) -> &str {
205        &self.value
206    }
207}
208
209impl Display for Version {
210    #[inline]
211    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
212        write!(f, "{}", self.as_ref())
213    }
214}
215
216impl PartialEq for Version {
217    #[inline]
218    fn eq(&self, other: &Self) -> bool {
219        self.cmp(other) == Ordering::Equal
220    }
221}
222
223impl Eq for Version {}
224
225impl PartialOrd for Version {
226    #[inline]
227    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
228        Some(self.cmp(other))
229    }
230}
231
232impl Ord for Version {
233    #[inline]
234    fn cmp(&self, other: &Self) -> Ordering {
235        compare_components(&self.components, &other.components)
236    }
237}
238
239impl IntoIterator for Version {
240    type Item = VersionComponent;
241    type IntoIter = VecIntoIter<Self::Item>;
242
243    #[inline]
244    fn into_iter(self) -> Self::IntoIter {
245        self.components.into_iter()
246    }
247}
248
249impl<'data> IntoIterator for &'data Version {
250    type Item = &'data VersionComponent;
251    type IntoIter = Iter<'data, VersionComponent>;
252
253    #[inline]
254    fn into_iter(self) -> Self::IntoIter {
255        self.components.iter()
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    #![expect(clippy::panic_in_result_fn, reason = "this is a test suite")]
262
263    use std::error::Error;
264
265    #[test]
266    fn num_only() -> Result<(), Box<dyn Error>> {
267        let expected: [super::VersionComponent; 1] = [super::VersionComponent {
268            num: Some(616),
269            rest: String::new(),
270        }];
271        let ver: super::Version = "616".parse()?;
272
273        let components = ver.into_iter().collect::<Vec<_>>();
274        assert_eq!(&expected[..], &*components);
275        Ok(())
276    }
277
278    #[test]
279    fn rest_only() -> Result<(), Box<dyn Error>> {
280        let expected: [super::VersionComponent; 1] = [super::VersionComponent {
281            num: None,
282            rest: "whee".to_owned(),
283        }];
284        let ver: super::Version = "whee".parse()?;
285
286        let components = ver.into_iter().collect::<Vec<_>>();
287        assert_eq!(&expected[..], &*components);
288        Ok(())
289    }
290
291    #[test]
292    fn both() -> Result<(), Box<dyn Error>> {
293        let expected: [super::VersionComponent; 1] = [super::VersionComponent {
294            num: Some(29),
295            rest: "palms".to_owned(),
296        }];
297        let ver: super::Version = "29palms".parse()?;
298
299        let components = ver.into_iter().collect::<Vec<_>>();
300        assert_eq!(&expected[..], &*components);
301        Ok(())
302    }
303
304    #[test]
305    fn three() -> Result<(), Box<dyn Error>> {
306        let expected: [super::VersionComponent; 3] = [
307            super::VersionComponent {
308                num: Some(1),
309                rest: String::new(),
310            },
311            super::VersionComponent {
312                num: Some(5),
313                rest: "a".to_owned(),
314            },
315            super::VersionComponent {
316                num: None,
317                rest: "beta3".to_owned(),
318            },
319        ];
320        // let ver = super::Version::from_str("1.5a2.beta3")?;
321        let ver: super::Version = "1.5a.beta3".parse()?;
322
323        let components = ver.into_iter().collect::<Vec<_>>();
324        assert_eq!(&expected[..], &*components);
325        Ok(())
326    }
327}