phpup 0.1.8

Cross-Platform PHP version manager
Documentation
use derive_more::Display;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{de, Deserialize, Deserializer, Serialize};
use std::fmt;
use std::str::FromStr;
use thiserror::Error;

static VERSION_REGEX: Lazy<Regex> = Lazy::new(|| {
    Regex::new(
        r"(?x)
                ^ # start
                ([1-9]+\d*|0) # major
                (?:
                    \.
                    ([1-9]+\d*|0) # minor
                    (?:
                        \.
                        ([1-9]+\d*|0) # patch
                        (?:
                            (alpha|beta|RC) # pre_type
                            ([1-9]+) # pre_version
                        )?
                    )?
                )?
                $ # end
            ",
    )
    .unwrap()
});

pub type Version = Major;

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct Major {
    version: usize,
    minor: Option<Minor>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
struct Minor {
    version: usize,
    patch: Option<Patch>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
struct Patch {
    version: usize,
    pre: Option<Pre>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
struct Pre {
    version: usize,
    pre_type: PreType,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Display, Serialize)]
pub enum PreType {
    #[display(fmt = "alpha")]
    Alpha,
    #[display(fmt = "beta")]
    Beta,
    #[display(fmt = "RC")]
    Rc,
}

impl Version {
    pub fn from_numbers(major: usize, minor: Option<usize>, patch: Option<usize>) -> Self {
        Self {
            version: major,
            minor: minor.map(|version| Minor {
                version,
                patch: patch.map(|version| Patch { version, pre: None }),
            }),
        }
    }
    pub fn from_major(major: usize) -> Self {
        Self {
            version: major,
            minor: None,
        }
    }
    /// Returns `true` if `self` includes `other`.
    /// - `3` includes `3`, `3.x`, `3.x.x`
    /// - `3.1` includes `3.1`, `3.1.x`
    pub fn includes(&self, other: &Self) -> bool {
        self.major_version() == other.major_version()
            && self.minor.map_or(true, |Minor { version, patch }| {
                Some(version) == other.minor_version()
                    && (patch.map_or(true, |Patch { version, pre }| {
                        Some(version) == other.patch_version()
                            && pre.map_or(true, |pre| Some(pre) == other.pre())
                    }))
            })
    }
    fn minor(self) -> Option<Minor> {
        self.minor
    }
    fn patch(self) -> Option<Patch> {
        self.minor().and_then(|minor| minor.patch)
    }
    fn pre(self) -> Option<Pre> {
        self.patch().and_then(|patch| patch.pre)
    }
    pub fn major_version(self) -> usize {
        self.version
    }
    pub fn minor_version(self) -> Option<usize> {
        self.minor().map(|minor| minor.version)
    }
    pub fn patch_version(self) -> Option<usize> {
        self.patch().map(|patch| patch.version)
    }
    pub fn pre_type(self) -> Option<PreType> {
        self.pre().map(|pre| pre.pre_type)
    }
    pub fn pre_version(self) -> Option<usize> {
        self.pre().map(|pre| pre.version)
    }

    pub fn is_same_major(self, other: Self) -> bool {
        self.major_version() == other.major_version()
    }
    pub fn is_same_minor(self, other: Self) -> bool {
        self.minor_version().is_some()
            && other.minor_version().is_some()
            && self.is_same_major(other)
            && self.minor_version() == other.minor_version()
    }
}

#[derive(Error, Debug)]
pub enum ParseError {
    #[error("Invalid version format: \"{0}\"")]
    InvalidVersionFormat(String),
}

impl FromStr for Version {
    type Err = ParseError;

    fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
        if s == "3.0.x (latest)" {
            return Ok(Version::from_numbers(
                3,
                Some(0),
                Some(if cfg!(target_os = "windows") { 17 } else { 18 }),
            ));
        }
        let cap = VERSION_REGEX
            .captures(s)
            .ok_or_else(|| ParseError::InvalidVersionFormat(s.to_owned()))?;
        let to_num = |m: regex::Match| m.as_str().parse().unwrap();
        let major = Major {
            version: to_num(cap.get(1).unwrap()),
            minor: cap.get(2).map(to_num).map(|version| Minor {
                version,
                patch: cap.get(3).map(to_num).map(|version| Patch {
                    version,
                    pre: cap.get(5).map(to_num).map(|version| Pre {
                        version,
                        pre_type: PreType::from_str(&cap[4]).unwrap(),
                    }),
                }),
            }),
        };
        Ok(major)
    }
}

impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        format!(
            "{}{}{}{}",
            self.major_version(),
            self.minor_version()
                .map(|v| format!(".{}", v))
                .unwrap_or_default(),
            self.patch_version()
                .map(|v| format!(".{}", v))
                .unwrap_or_default(),
            self.pre_type()
                .map(|t| format!("{}{}", t, self.pre_version().unwrap()))
                .unwrap_or_default(),
        )
        .fmt(f)
    }
}

struct VersionVisitor;
impl<'de> de::Visitor<'de> for VersionVisitor {
    type Value = Version;

    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
        formatter.write_str("struct Version")
    }

    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
    where
        E: de::Error,
    {
        v.parse().map_err(de::Error::custom)
    }
}

impl<'de> Deserialize<'de> for Version {
    fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_str(VersionVisitor)
    }
}

impl FromStr for PreType {
    type Err = ();
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "alpha" => Ok(PreType::Alpha),
            "beta" => Ok(PreType::Beta),
            "RC" => Ok(PreType::Rc),
            _ => Err(()),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;

    #[test]
    fn parsed_from_str() {
        let version3_1_4: Result<Version, _> = "3.1.4".parse();
        assert!(matches!(version3_1_4, Ok(_)));
        assert_eq!(
            version3_1_4.unwrap(),
            Version::from_numbers(3, Some(1), Some(4))
        );
    }

    #[test]
    fn deserialize_from_json() {
        let json = r#"
            { "3.1.4": ["abc", "cdf"] }
        "#;
        let parsed: Result<HashMap<Version, Vec<&str>>, _> = serde_json::from_str(json);
        println!("{:?}", parsed);
        assert!(parsed.is_ok());

        let version3_1_4 = Version::from_numbers(3, Some(1), Some(4));
        assert_eq!(
            parsed.unwrap().get(&version3_1_4),
            Some(&vec!["abc", "cdf"])
        );
    }

    #[test]
    fn includes_test() {
        let version3_1_4 = Version::from_numbers(3, Some(1), Some(4));
        let version3_1 = Version::from_numbers(3, Some(1), None);
        let version3 = Version::from_numbers(3, None, None);

        assert!(version3.includes(&version3));
        assert!(version3.includes(&version3_1));
        assert!(version3.includes(&version3_1_4));
        assert!(!version3_1.includes(&version3));
        assert!(version3_1.includes(&version3_1));
        assert!(version3_1.includes(&version3_1_4));
        assert!(!version3_1_4.includes(&version3));
        assert!(!version3_1_4.includes(&version3_1));
        assert!(version3_1_4.includes(&version3_1_4));
    }
}