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::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/// An error that occurred while parsing a version string.
64#[derive(Debug, Error)]
65#[non_exhaustive]
66pub enum ParseError {
67    /// A parser failed.
68    #[error("Could not parse '{0}' as a version string")]
69    ParseFailure(String, #[source] AnyError),
70
71    /// A parser left some bytes out.
72    #[error("Could not parse '{0}' as a version string: {1} bytes left over")]
73    ParseLeftovers(String, usize),
74}
75
76/// A single version component, e.g. "3" or "b2".
77#[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    /// The numeric portion of the version component.
86    pub num: Option<u32>,
87    /// The freeform portion of the version component.
88    pub rest: String,
89}
90
91/// Compare two already-extracted version components.
92fn 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
114/// Compare two lists of already-extracted version components.
115fn 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/// A version string, both in full and broken down into components.
149#[derive(Debug, Clone, Deserialize, Serialize)]
150#[cfg_attr(feature = "facet-unstable", derive(Facet))]
151#[serde(transparent)]
152pub struct Version {
153    /// The full version string.
154    value: String,
155    /// The components of the version string.
156    #[serde(skip)]
157    components: Vec<VersionComponent>,
158}
159
160impl Version {
161    /// Create a version object with the specified attributes.
162    #[inline]
163    #[must_use]
164    pub const fn new(value: String, components: Vec<VersionComponent>) -> Self {
165        Self { value, components }
166    }
167
168    /// Return an iterator over the version components.
169    #[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::from_str("1.5a2.beta3")?;
304        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}