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};
#[derive(
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, SerializeDisplay, DeserializeFromStr,
)]
pub struct Id(String);
impl Id {
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()
}
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();
let first = chars.next().ok_or(anyhow!("must not be empty"))?;
if !first.is_ascii_lowercase() {
bail!("must start with lowercase letter");
}
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");
}
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)
}
}
#[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)
}
}
#[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)
}
}