use anyhow::bail;
use itertools::Itertools;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use std::hash::Hash;
use std::str::FromStr;
use std::sync::Arc;
use crate::addon::AddonKind;
use crate::loaders::Loader;
use crate::util::is_valid_identifier;
use crate::versions::{parse_versioned_string, VersionPattern};
pub type PackageID = Arc<str>;
#[derive(Debug, Clone, PartialOrd, Ord, Deserialize, Serialize)]
pub struct PkgRequest {
#[serde(default)]
pub source: PkgRequestSource,
pub id: PackageID,
#[serde(default)]
pub repository: Option<String>,
#[serde(default)]
pub content_version: VersionPattern,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum PkgRequestSource {
#[default]
UserRequire,
Bundled(ArcPkgReq),
Dependency(ArcPkgReq),
Refused(ArcPkgReq),
Repository,
}
impl Ord for PkgRequestSource {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.to_num().cmp(&other.to_num())
}
}
impl PartialOrd for PkgRequestSource {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl PkgRequestSource {
pub fn get_source(&self) -> Option<ArcPkgReq> {
match self {
Self::Dependency(source) | Self::Bundled(source) => Some(source.clone()),
_ => None,
}
}
pub fn get_original_source(&self) -> Option<&ArcPkgReq> {
match self {
Self::Dependency(source) | Self::Bundled(source) => match &source.source {
Self::Dependency(..) | Self::Bundled(..) => source.source.get_original_source(),
_ => Some(source),
},
_ => None,
}
}
pub fn is_user_bundled(&self) -> bool {
matches!(self, Self::Bundled(source) if source.source.is_user_bundled())
|| matches!(self, Self::UserRequire)
}
fn to_num(&self) -> u8 {
match self {
Self::UserRequire => 0,
Self::Bundled(..) => 1,
Self::Dependency(..) => 2,
Self::Refused(..) => 3,
Self::Repository => 4,
}
}
}
impl PkgRequest {
#[inline(always)]
pub fn new(
id: impl Into<PackageID>,
source: PkgRequestSource,
content_version: VersionPattern,
repository: Option<String>,
) -> Self {
Self {
id: id.into(),
source,
content_version,
repository,
}
}
#[inline(always)]
pub fn any(id: impl Into<PackageID>, source: PkgRequestSource) -> Self {
Self::new(id, source, VersionPattern::Any, None)
}
pub fn parse(string: impl AsRef<str>, source: PkgRequestSource) -> Self {
let string = string.as_ref();
let (id_and_repo, version) = parse_versioned_string(string);
let (id, repository) = if let Some(pos) = id_and_repo.find(":") {
let id = &id_and_repo[pos + 1..];
let repository = &id_and_repo[0..pos];
(id, Some(repository).filter(|x| !x.is_empty()))
} else {
(id_and_repo, None)
};
Self {
source,
id: id.into(),
content_version: version,
repository: repository.map(|x| x.to_string()),
}
}
pub fn with_content_version(&self, content_version: VersionPattern) -> Self {
Self {
source: self.source.clone(),
id: self.id.clone(),
repository: self.repository.clone(),
content_version,
}
}
pub fn debug_sources(&self) -> String {
self.debug_sources_inner(String::new())
}
pub fn to_string_no_version(&self) -> String {
if let Some(repo) = &self.repository {
format!("{repo}:{}", self.id)
} else {
self.id.to_string()
}
}
fn debug_sources_inner(&self, list: String) -> String {
match &self.source {
PkgRequestSource::UserRequire => format!("{}{list}", self.id),
PkgRequestSource::Dependency(source) => {
format!("{} -> {}", source.debug_sources_inner(list), self.id)
}
PkgRequestSource::Refused(source) => {
format!("{} =X=> {}", source.debug_sources_inner(list), self.id)
}
PkgRequestSource::Bundled(bundler) => {
format!("{} => {}", bundler.debug_sources_inner(list), self.id)
}
PkgRequestSource::Repository => format!("Repository -> {}{list}", self.id),
}
}
}
impl PartialEq for PkgRequest {
fn eq(&self, other: &Self) -> bool {
self.id == other.id && self.repository == other.repository
}
}
impl Eq for PkgRequest {}
impl Hash for PkgRequest {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
self.repository.hash(state);
}
}
impl Display for PkgRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(repo) = &self.repository {
write!(f, "{repo}:")?;
}
write!(f, "{}", self.id)
}
}
pub type ArcPkgReq = Arc<PkgRequest>;
#[derive(Deserialize, Serialize, Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum PackageStability {
Stable,
#[default]
Latest,
}
impl PackageStability {
pub fn parse_from_str(string: &str) -> Option<Self> {
match string {
"stable" => Some(Self::Stable),
"latest" => Some(Self::Latest),
_ => None,
}
}
}
pub const MAX_PACKAGE_ID_LENGTH: usize = 32;
pub fn is_valid_package_id(id: &str) -> bool {
if !is_valid_identifier(id) {
return false;
}
for c in id.chars() {
if c.is_ascii_uppercase() {
return false;
}
if c == '_' || c == '.' {
return false;
}
}
if id.len() > MAX_PACKAGE_ID_LENGTH {
return false;
}
true
}
#[derive(Deserialize, Serialize, PartialEq, Debug, Clone, Default)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(default)]
pub struct PackageAddonHashes<T: Default> {
pub sha256: T,
pub sha512: T,
}
impl PackageAddonOptionalHashes {
pub fn is_empty(&self) -> bool {
self.sha256.is_none() && self.sha512.is_none()
}
}
pub type PackageAddonOptionalHashes = PackageAddonHashes<Option<String>>;
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum PackageKind {
Mod,
ResourcePack,
Datapack,
Plugin,
Shader,
Bundle,
}
impl PackageKind {
pub fn to_addon_kind(&self) -> Option<AddonKind> {
match self {
Self::Mod => Some(AddonKind::Mod),
Self::ResourcePack => Some(AddonKind::ResourcePack),
Self::Datapack => Some(AddonKind::Datapack),
Self::Plugin => Some(AddonKind::Plugin),
Self::Shader => Some(AddonKind::Shader),
Self::Bundle => None,
}
}
}
impl FromStr for PackageKind {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"mod" => Ok(Self::Mod),
"resource_pack" => Ok(Self::ResourcePack),
"datapack" => Ok(Self::Datapack),
"plugin" => Ok(Self::Plugin),
"shader" => Ok(Self::Shader),
"bundle" => Ok(Self::Bundle),
other => bail!("Unknown package type '{other}'"),
}
}
}
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct PackageSearchParameters {
pub count: u8,
pub skip: usize,
pub search: Option<String>,
pub types: Vec<PackageKind>,
pub minecraft_versions: Vec<String>,
pub loaders: Vec<Loader>,
pub categories: Vec<PackageCategory>,
}
#[allow(missing_docs)]
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum PackageCategory {
Adventure,
Atmosphere,
Audio,
Blocks,
Building,
Cartoon,
Challenge,
Combat,
Compatability,
Decoration,
Economy,
Entities,
Equipment,
Exploration,
Extensive,
Fantasy,
Fonts,
Food,
GameMechanics,
Gui,
Items,
Language,
Library,
Lightweight,
Magic,
Minigame,
Mobs,
Multiplayer,
Optimization,
Realistic,
Simplistic,
Space,
Social,
Storage,
Structures,
Technology,
Transportation,
Tweaks,
Utility,
VanillaPlus,
Worldgen,
}
#[allow(missing_docs)]
#[derive(thiserror::Error, Debug)]
pub enum ResolutionError {
#[error("When resolving the package {0}: {1:?}")]
PackageContext(ArcPkgReq, Box<ResolutionError>),
#[error("Failed to preload packages")]
FailedToPreload(anyhow::Error),
#[error("Failed to get properties of package {0}: {1:?}")]
FailedToGetProperties(ArcPkgReq, anyhow::Error),
#[error("No valid versions found for package {0}")]
NoValidVersionsFound(ArcPkgReq),
#[error("{pkg} extends the functionality of the package {1}, which is not installed", pkg = .0.as_ref().map(|x| format!("The package {}", x.debug_sources())).unwrap_or("A package".into()))]
ExtensionNotFulfilled(Option<ArcPkgReq>, ArcPkgReq),
#[error("Package {0} has been explicitly required by package {1}. This means it must be required by the user in their config.")]
ExplicitRequireNotFulfilled(ArcPkgReq, ArcPkgReq),
#[error("Package {0} is incompatible with the packages {refusers}", refusers = .1.iter().join(", "))]
IncompatiblePackage(ArcPkgReq, Vec<Arc<str>>),
#[error("Failed to evaluate package {0}: {1:?}")]
FailedToEvaluate(ArcPkgReq, anyhow::Error),
#[error("Miscellaneous error: {0:?}")]
Misc(anyhow::Error),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_package_id_validation() {
assert!(is_valid_package_id("hello"));
assert!(is_valid_package_id("32"));
assert!(is_valid_package_id("hello-world"));
assert!(!is_valid_package_id("hello_world"));
assert!(!is_valid_package_id("hello.world"));
assert!(!is_valid_package_id("\\"));
assert!(!is_valid_package_id(
"very-very-long-long-long-package-name-thats-too-long"
));
}
#[test]
fn test_request_source_debug() {
let req = PkgRequest::parse(
"foo",
PkgRequestSource::Dependency(Arc::new(PkgRequest::parse(
"bar",
PkgRequestSource::Dependency(Arc::new(PkgRequest::parse(
"baz",
PkgRequestSource::Repository,
))),
))),
);
let debug = req.debug_sources();
assert_eq!(debug, "Repository -> baz -> bar -> foo");
}
#[test]
fn test_pkg_req_parsing() {
let req = PkgRequest::parse("foo", PkgRequestSource::UserRequire);
assert_eq!(req.id, "foo".into());
assert_eq!(req.repository, None);
let req = PkgRequest::parse("foo@1.19.2", PkgRequestSource::UserRequire);
assert_eq!(req.id, "foo".into());
assert_eq!(req.content_version, VersionPattern::Single("1.19.2".into()));
let req = PkgRequest::parse("modrinth:foo@1.19.2", PkgRequestSource::UserRequire);
assert_eq!(req.id, "foo".into());
assert_eq!(req.repository, Some("modrinth".into()));
assert_eq!(req.content_version, VersionPattern::Single("1.19.2".into()));
let req = PkgRequest::parse(":foo", PkgRequestSource::UserRequire);
assert_eq!(req.id, "foo".into());
assert_eq!(req.repository, None);
let _ = PkgRequest::parse(":@", PkgRequestSource::UserRequire);
}
}