feature-check 1.0.1

Query a program for supported features
Documentation
/*
 * Copyright (c) 2021  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;
//!
//! use feature_check::version as fversion;
//!
//! # fn main() -> Result<(), Box<dyn error::Error>> {
//! let v1: fversion::Version = "2.1".parse()?;
//! let v2: fversion::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;
use std::fmt;
use std::str::FromStr;

quick_error! {
    /// An error that occurred while parsing a version string.
    #[derive(Debug)]
    pub enum ParseError {
        /// An empty component between two dots.
        EmptyComponent {
            display("Empty version component")
        }
        /// A component with an unrecognized format.
        InvalidComponent(comp: String) {
            display( "Could not parse '{}' as a version component", comp)
        }
        /// Something weird where an integer should be.
        NotAnInteger(num: String, err: String) {
            display("Could not parse '{}' as an unsigned integer: {}", num, err)
        }
    }
}

const RE_VERSION_COMP_NUMERIC: &str = r"(?x) (?P<num> (?: 0 | [1-9][0-9]* )? ) (?P<rest> .* ) $";

/// A single version component, e.g. "3" or "b2".
#[derive(Debug, Clone)]
pub struct VersionComponent {
    /// The numeric portion of the version component.
    pub num: Option<i32>,
    /// The freeform portion of the version component.
    pub rest: String,
}

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

impl Version {
    fn compare_single(&self, left: &VersionComponent, right: &VersionComponent) -> cmp::Ordering {
        match left.num {
            Some(vleft) => match right.num {
                Some(vright) => match vleft.cmp(&vright) {
                    cmp::Ordering::Equal => left.rest.cmp(&right.rest),
                    other => other,
                },
                None => cmp::Ordering::Greater,
            },
            None => match right.num.is_some() {
                true => cmp::Ordering::Less,
                false => left.rest.cmp(&right.rest),
            },
        }
    }

    fn compare_components(
        &self,
        left: &[VersionComponent],
        right: &[VersionComponent],
    ) -> cmp::Ordering {
        match left.get(0) {
            Some(cleft) => match right.get(0) {
                Some(cright) => match self.compare_single(cleft, cright) {
                    cmp::Ordering::Equal => self.compare_components(&left[1..], &right[1..]),
                    other => other,
                },
                None => match cleft.num.is_some() {
                    true => cmp::Ordering::Greater,
                    false => cmp::Ordering::Less,
                },
            },
            None => match right.get(0) {
                Some(vright) => match vright.num.is_some() {
                    true => cmp::Ordering::Less,
                    false => cmp::Ordering::Greater,
                },
                None => cmp::Ordering::Equal,
            },
        }
    }

    /// Return an iterator over the version components.
    pub fn iter(&self) -> std::slice::Iter<VersionComponent> {
        self.components.iter()
    }
}

impl FromStr for Version {
    type Err = ParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let re_comp = regex::Regex::new(RE_VERSION_COMP_NUMERIC).unwrap();
        let res: Vec<VersionComponent> = s
            .split('.')
            .map(|comp| match comp.is_empty() {
                true => Err(ParseError::EmptyComponent),
                false => match re_comp.captures(comp) {
                    Some(caps) => {
                        let num = &caps["num"];
                        let rest = &caps["rest"];
                        match num.is_empty() {
                            true => Ok(VersionComponent {
                                num: None,
                                rest: rest.to_string(),
                            }),
                            false => match num.parse::<i32>() {
                                Ok(value) => Ok(VersionComponent {
                                    num: Some(value),
                                    rest: rest.to_string(),
                                }),
                                Err(err) => {
                                    Err(ParseError::NotAnInteger(num.to_string(), err.to_string()))
                                }
                            },
                        }
                    }
                    None => Err(ParseError::InvalidComponent(comp.to_string())),
                },
            })
            .collect::<Result<Vec<_>, _>>()?;
        Ok(Self {
            value: s.to_string(),
            components: res,
        })
    }
}

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

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

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

impl Eq for Version {}

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

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

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

    fn into_iter(self) -> Self::IntoIter {
        self.components.into_iter()
    }
}