feature-check 2.0.0

Query a program for supported features
Documentation
/*
 * Copyright (c) 2021, 2022  Peter Pentchev <roam@ringlet.net>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */
//! 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, v1.cmp(&v2), v2);
//! println!("equal? {}", v1 == v2);
//! println!("smaller: {}", cmp::min(&v1, &v2));
//! println!("larger: {}", cmp::max(&v1, &v2));
//! println!("v1: {}", v1);
//! for comp in v1.iter() {
//!     println!(
//!         "- {}/{}",
//!         match comp.num {
//!             Some(value) => value.to_string(),
//!             None => "(none)".to_string(),
//!         },
//!         comp.rest,
//!     );
//! }
//! println!("v2: {}", v2);
//! for comp in v2.into_iter() {
//!     println!(
//!         "- {}/{}",
//!         match comp.num {
//!             Some(value) => value.to_string(),
//!             None => "(none)".to_string(),
//!         },
//!         comp.rest,
//!     );
//! }
//! # Ok(())
//! # }
//! ```

use std::cmp::Ordering;
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::Serialize;
use thiserror::Error;

use crate::expr::parser;

/// An error that occurred while parsing a version string.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ParseError {
    /// A parser failed.
    #[error("Could not parse '{0}' as a version string")]
    ParseFailure(String, #[source] AnyError),

    /// A parser left some bytes out.
    #[error("Could not parse '{0}' as a version string: {1} bytes left over")]
    ParseLeftovers(String, usize),
}

/// A single version component, e.g. "3" or "b2".
#[derive(Debug, Clone, Eq, PartialEq)]
#[non_exhaustive]
#[allow(clippy::module_name_repetitions)]
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.get(0).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, 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 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(s: &str) -> Result<Self, Self::Err> {
        parser::parse_version(s)
    }
}

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()
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::default_numeric_fallback)]
    #![allow(clippy::panic_in_result_fn)]

    use std::error::Error;

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

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

    #[test]
    fn test_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 test_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 test_three() -> Result<(), Box<dyn Error>> {
        let expected: [super::VersionComponent; 3] = [
            super::VersionComponent {
                num: Some(1),
                rest: "".to_owned(),
            },
            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(())
    }
}