creeper 1.0.0-alpha.6

Minecraft Package Manager
use std::{
    fmt::Display,
    iter::repeat,
    ops::Deref,
    path::{Path, PathBuf},
    str::FromStr,
};

use anyhow::{anyhow, bail, ensure};
use semver::{Version, VersionReq};
use serde_with::{DeserializeFromStr, SerializeDisplay};

/// A package identifier.
///
/// # Format
///
/// A valid package identifier is a non-empty ascii string that
///
/// - starts with a lowercase letter `a-z`; and
///
/// - consists only of lowercase letters `a-z`, digits `0-9`, hyphens `-`, and underscores `_`; and
///
/// - does not end with a hyphen `-` or underscore `_`.
#[derive(
    Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, SerializeDisplay, DeserializeFromStr,
)]
pub struct Id(String);

impl Id {
    /// **Relative** storage path of this package to the storage root,
    /// sparsely indexed by the initial characters.
    pub fn indexed_path(&self) -> impl AsRef<Path> {
        let head4 = self
            .chars()
            .filter(char::is_ascii_lowercase)
            .chain(repeat('x'))
            .take(4)
            .collect::<String>();
        PathBuf::from(".")
            .join(&head4[0..2])
            .join(&head4[2..4])
            .join(&self.as_str())
    }

    pub fn minecraft() -> Self {
        "minecraft".parse().unwrap()
    }

    pub fn vanilla() -> Self {
        "vanilla".parse().unwrap()
    }

    pub fn forge() -> Self {
        "forge".parse().unwrap()
    }

    pub fn neoforge() -> Self {
        "neoforge".parse().unwrap()
    }

    pub fn fabric() -> Self {
        "fabric".parse().unwrap()
    }

    pub fn intermediary() -> Self {
        "intermediary".parse().unwrap()
    }

    /// Whether this is a regular package or a package to be specially handled by package manager.
    pub fn is_regular(&self) -> bool {
        const SPECIAL: [&str; 7] = [
            "root",
            "minecraft",
            "vanilla",
            "forge",
            "neoforge",
            "fabric",
            "intermediary",
        ];
        !SPECIAL.contains(&self.as_str())
    }

    pub fn is_valid_index_lv1(name: &str) -> bool {
        if name.len() != 2 {
            return false;
        }
        let mut chars = name.chars();
        if !chars.next().unwrap().is_ascii_lowercase() {
            return false;
        }
        chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-')
    }

    pub fn is_valid_index_lv2(name: &str) -> bool {
        if name.len() != 2 {
            return false;
        }
        name.chars()
            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-')
    }

    pub fn is_of_index(&self, lv1: &str, lv2: &str) -> bool {
        let head4 = self
            .chars()
            .filter(char::is_ascii_lowercase)
            .chain(repeat('x'))
            .take(4)
            .collect::<String>();
        &head4[0..2] == lv1 && &head4[2..4] == lv2
    }
}

impl Deref for Id {
    type Target = String;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl FromStr for Id {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut chars = s.chars();

        // non-empty
        let first = chars.next().ok_or(anyhow!("must not be empty"))?;

        // start with lowercase letter
        if !first.is_ascii_lowercase() {
            bail!("must start with lowercase letter");
        }

        // consist of valid characters
        if !chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_') {
            bail!("must consist only of lowercase letters, digits, hyphens, and underscores");
        }

        // does not end with hyphen or underscore
        if s.ends_with('-') || s.ends_with('_') {
            bail!("must not end with hyphen or underscore");
        }

        Ok(Id(s.to_string()))
    }
}

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

/// `<PACKAGE>@<VERSION>`.
#[derive(Clone, Debug, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr)]
pub struct IdVersion {
    pub id: Id,
    pub version: Version,
}

impl Display for IdVersion {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}@{}", self.id, self.version)
    }
}

impl FromStr for IdVersion {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let pieces = s.split('@').collect::<Vec<_>>();
        ensure!(pieces.len() == 2, "expected <PACKAGE>@<VERSION>, found {s}");

        let (id, version) = (pieces[0].parse()?, pieces[1].parse()?);

        let value = Self { id, version };

        Ok(value)
    }
}

/// `<PACKAGE>[@<VERSION_REQ>]`.
#[derive(Clone, Debug, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr)]
pub struct IdVersionReq {
    pub id: Id,
    pub version_req: VersionReq,
}

impl Display for IdVersionReq {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}@{}", self.id, self.version_req)
    }
}

impl FromStr for IdVersionReq {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let pieces = s.split('@').collect::<Vec<_>>();

        let (id, version_req) = match pieces.len() {
            1 => (pieces[0].parse()?, VersionReq::STAR),
            2 => (pieces[0].parse()?, pieces[1].parse()?),
            _ => bail!("expected <PACKAGE>[@<VERSION_REQ>], found {s}"),
        };

        let value = Self { id, version_req };

        Ok(value)
    }
}