feature-check 2.3.2

Query a program for supported features
Documentation
// SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
// SPDX-License-Identifier: BSD-2-Clause
//! Parse version strings and compare them.
//!
//! The [`Version`] struct may be used to break a version string down
//! into its separate components and then compare it to another one,
//! e.g. to decide whether a certain feature is really supported.
//!
//! ```rust
//! use std::cmp;
//! # use std::error::Error;
//!
//! use feature_check::version::Version;
//!
//! # fn main() -> Result<(), Box<dyn Error>> {
//! let v1: Version = "2.1".parse()?;
//! let v2: Version = "2.2.b2".parse()?;
//! println!("{v1} {res:?} {v2}", res = v1.cmp(&v2));
//! println!("equal? {res}", res = v1 == v2);
//! println!("smaller: {res}", res = cmp::min(&v1, &v2));
//! println!("larger: {res}", res = cmp::max(&v1, &v2));
//! println!("v1: {v1}");
//! for comp in &v1 {
//!     println!(
//!         "- {num}/{rest}",
//!         num = match comp.num {
//!             Some(value) => value.to_string(),
//!             None => "(none)".to_string(),
//!         },
//!         rest = comp.rest,
//!     );
//! }
//! println!("v2: {v2}");
//! for comp in v2.into_iter() {
//!     println!(
//!         "- {num}/{rest}",
//!         num = match comp.num {
//!             Some(value) => value.to_string(),
//!             None => "(none)".to_string(),
//!         },
//!         rest = comp.rest,
//!     );
//! }
//! # Ok(())
//! # }
//! ```

use std::cmp::Ordering;
use std::error::Error;
use std::fmt::{Display, Formatter, Result as FmtResult};
use std::slice::Iter;
use std::str::FromStr;
use std::vec::IntoIter as VecIntoIter;

use anyhow::Error as AnyError;
use serde_derive::{Deserialize, Serialize};

use crate::expr::parser;

/// An error that occurred while parsing a version string.
#[derive(Debug)]
#[non_exhaustive]
pub enum ParseError {
    /// A parser failed.
    ParseFailure(String, AnyError),

    /// A parser left some bytes out.
    ParseLeftovers(String, usize),
}

impl Display for ParseError {
    #[inline]
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        match *self {
            Self::ParseFailure(ref value, _) => {
                write!(f, "Could not parse '{value}' as a version string")
            }
            Self::ParseLeftovers(ref value, ref count) => write!(
                f,
                "Could not parse '{value}' as a version string: {count} bytes left over"
            ),
        }
    }
}

impl Error for ParseError {
    #[inline]
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match *self {
            Self::ParseFailure(_, ref err) => Some(err.as_ref()),
            Self::ParseLeftovers(_, _) => None,
        }
    }
}

/// A single version component, e.g. "3" or "b2".
#[derive(Debug, Clone, Eq, PartialEq)]
#[non_exhaustive]
#[expect(
    clippy::module_name_repetitions,
    reason = "sensible name for the struct"
)]
pub struct VersionComponent {
    /// The numeric portion of the version component.
    pub num: Option<u32>,
    /// The freeform portion of the version component.
    pub rest: String,
}

/// Compare two already-extracted version components.
fn compare_single(left: &VersionComponent, right: &VersionComponent) -> Ordering {
    left.num.map_or_else(
        || {
            if right.num.is_some() {
                Ordering::Less
            } else {
                left.rest.cmp(&right.rest)
            }
        },
        |ver_left| {
            right.num.map_or(Ordering::Greater, |ver_right| {
                let res = ver_left.cmp(&ver_right);
                if res == Ordering::Equal {
                    left.rest.cmp(&right.rest)
                } else {
                    res
                }
            })
        },
    )
}

/// Compare two lists of already-extracted version components.
fn compare_components(left: &[VersionComponent], right: &[VersionComponent]) -> Ordering {
    left.split_first().map_or_else(
        || {
            right.first().map_or(Ordering::Equal, |ver_right| {
                if ver_right.num.is_some() {
                    Ordering::Less
                } else {
                    Ordering::Greater
                }
            })
        },
        |(comp_left, rest_left)| {
            right.split_first().map_or_else(
                || {
                    if comp_left.num.is_some() {
                        Ordering::Greater
                    } else {
                        Ordering::Less
                    }
                },
                |(comp_right, rest_right)| {
                    let res = compare_single(comp_left, comp_right);
                    if res == Ordering::Equal {
                        compare_components(rest_left, rest_right)
                    } else {
                        res
                    }
                },
            )
        },
    )
}

/// A version string, both in full and broken down into components.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(transparent)]
pub struct Version {
    /// The full version string.
    value: String,
    /// The components of the version string.
    #[serde(skip)]
    components: Vec<VersionComponent>,
}

impl Version {
    /// Create a version object with the specified attributes.
    #[inline]
    #[must_use]
    pub const fn new(value: String, components: Vec<VersionComponent>) -> Self {
        Self { value, components }
    }

    /// Return an iterator over the version components.
    #[inline]
    pub fn iter(&self) -> Iter<'_, VersionComponent> {
        self.components.iter()
    }
}

impl FromStr for Version {
    type Err = ParseError;

    #[inline]
    fn from_str(value: &str) -> Result<Self, Self::Err> {
        parser::parse_version(value)
    }
}

impl AsRef<str> for Version {
    #[inline]
    fn as_ref(&self) -> &str {
        &self.value
    }
}

impl Display for Version {
    #[inline]
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        write!(f, "{}", self.as_ref())
    }
}

impl PartialEq for Version {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        self.cmp(other) == Ordering::Equal
    }
}

impl Eq for Version {}

impl PartialOrd for Version {
    #[inline]
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Version {
    #[inline]
    fn cmp(&self, other: &Self) -> Ordering {
        compare_components(&self.components, &other.components)
    }
}

impl IntoIterator for Version {
    type Item = VersionComponent;
    type IntoIter = VecIntoIter<Self::Item>;

    #[inline]
    fn into_iter(self) -> Self::IntoIter {
        self.components.into_iter()
    }
}

impl<'data> IntoIterator for &'data Version {
    type Item = &'data VersionComponent;
    type IntoIter = Iter<'data, VersionComponent>;

    #[inline]
    fn into_iter(self) -> Self::IntoIter {
        self.components.iter()
    }
}

#[cfg(test)]
mod tests {
    #![expect(clippy::panic_in_result_fn, reason = "this is a test suite")]

    use std::error::Error;

    #[test]
    fn num_only() -> Result<(), Box<dyn Error>> {
        let expected: [super::VersionComponent; 1] = [super::VersionComponent {
            num: Some(616),
            rest: String::new(),
        }];
        let ver: super::Version = "616".parse()?;

        let components = ver.into_iter().collect::<Vec<_>>();
        assert_eq!(&expected[..], &*components);
        Ok(())
    }

    #[test]
    fn rest_only() -> Result<(), Box<dyn Error>> {
        let expected: [super::VersionComponent; 1] = [super::VersionComponent {
            num: None,
            rest: "whee".to_owned(),
        }];
        let ver: super::Version = "whee".parse()?;

        let components = ver.into_iter().collect::<Vec<_>>();
        assert_eq!(&expected[..], &*components);
        Ok(())
    }

    #[test]
    fn both() -> Result<(), Box<dyn Error>> {
        let expected: [super::VersionComponent; 1] = [super::VersionComponent {
            num: Some(29),
            rest: "palms".to_owned(),
        }];
        let ver: super::Version = "29palms".parse()?;

        let components = ver.into_iter().collect::<Vec<_>>();
        assert_eq!(&expected[..], &*components);
        Ok(())
    }

    #[test]
    fn three() -> Result<(), Box<dyn Error>> {
        let expected: [super::VersionComponent; 3] = [
            super::VersionComponent {
                num: Some(1),
                rest: String::new(),
            },
            super::VersionComponent {
                num: Some(5),
                rest: "a".to_owned(),
            },
            super::VersionComponent {
                num: None,
                rest: "beta3".to_owned(),
            },
        ];
        // let ver = super::Version::from_str("1.5a2.beta3")?;
        let ver: super::Version = "1.5a.beta3".parse()?;

        let components = ver.into_iter().collect::<Vec<_>>();
        assert_eq!(&expected[..], &*components);
        Ok(())
    }
}