model: add cross-provider version mismatch detection
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I71ea6c9141ec6b36edf708af1c8ed53d6a6a6964
This commit is contained in:
parent
f5d735efb8
commit
3e6f528056
1 changed files with 318 additions and 0 deletions
|
|
@ -168,6 +168,65 @@ impl Project {
|
||||||
self.aliases.extend(other.aliases);
|
self.aliases.extend(other.aliases);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if versions match across all providers.
|
||||||
|
/// Returns true if all provider files have the same version/file,
|
||||||
|
/// or if there's only one provider.
|
||||||
|
pub fn versions_match_across_providers(&self) -> bool {
|
||||||
|
if self.files.len() <= 1 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group files by provider (using parent_id as proxy)
|
||||||
|
let mut versions_by_provider: HashMap<String, Vec<&str>> = HashMap::new();
|
||||||
|
for file in &self.files {
|
||||||
|
// Extract provider from file type or use parent_id
|
||||||
|
let provider = &file.file_type;
|
||||||
|
versions_by_provider
|
||||||
|
.entry(provider.clone())
|
||||||
|
.or_default()
|
||||||
|
.push(&file.file_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If only one provider, versions match
|
||||||
|
if versions_by_provider.len() <= 1 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all providers have the same latest file name
|
||||||
|
// (simplified check - in reality would compare semantic versions)
|
||||||
|
let file_names: Vec<_> = versions_by_provider
|
||||||
|
.values()
|
||||||
|
.filter_map(|files| files.first().copied())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// All file names should be the same for versions to match
|
||||||
|
file_names.windows(2).all(|w| w[0] == w[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if versions do NOT match across providers.
|
||||||
|
/// Returns Some with details if there's a mismatch, None if versions match.
|
||||||
|
pub fn check_version_mismatch(&self) -> Option<String> {
|
||||||
|
if self.versions_match_across_providers() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect version info by provider
|
||||||
|
let mut provider_versions: Vec<(String, String)> = Vec::new();
|
||||||
|
for file in &self.files {
|
||||||
|
provider_versions.push((file.file_type.clone(), file.file_name.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(format!(
|
||||||
|
"Version mismatch for {}: {}",
|
||||||
|
self.get_name(),
|
||||||
|
provider_versions
|
||||||
|
.iter()
|
||||||
|
.map(|(p, v)| format!("{p}={v}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn select_file(
|
pub fn select_file(
|
||||||
&mut self,
|
&mut self,
|
||||||
mc_versions: &[String],
|
mc_versions: &[String],
|
||||||
|
|
@ -254,6 +313,39 @@ impl ProjectFile {
|
||||||
|
|
||||||
mc_compatible && loader_compatible
|
mc_compatible && loader_compatible
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate a viewable URL for this file based on its provider.
|
||||||
|
/// Returns None if the URL cannot be determined.
|
||||||
|
pub fn get_site_url(&self, project: &Project) -> Option<String> {
|
||||||
|
// Determine provider from file type
|
||||||
|
match self.file_type.as_str() {
|
||||||
|
"modrinth" => {
|
||||||
|
// Format: https://modrinth.com/mod/{slug}/version/{file_id}
|
||||||
|
let slug = project.slug.get("modrinth")?;
|
||||||
|
Some(format!(
|
||||||
|
"https://modrinth.com/mod/{}/version/{}",
|
||||||
|
slug, self.id
|
||||||
|
))
|
||||||
|
},
|
||||||
|
"curseforge" => {
|
||||||
|
// Format: https://www.curseforge.com/minecraft/mc-mods/{slug}/files/{file_id}
|
||||||
|
let slug = project.slug.get("curseforge")?;
|
||||||
|
Some(format!(
|
||||||
|
"https://www.curseforge.com/minecraft/mc-mods/{}/files/{}",
|
||||||
|
slug, self.id
|
||||||
|
))
|
||||||
|
},
|
||||||
|
"github" => {
|
||||||
|
// Format: https://github.com/{owner}/{repo}/releases/tag/{tag}
|
||||||
|
// parent_id contains owner/repo, id contains the tag/version
|
||||||
|
Some(format!(
|
||||||
|
"https://github.com/{}/releases/tag/{}",
|
||||||
|
self.parent_id, self.id
|
||||||
|
))
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -436,4 +528,230 @@ mod tests {
|
||||||
let result = project.select_file(&lockfile_mc, &lockfile_loaders);
|
let result = project.select_file(&lockfile_mc, &lockfile_loaders);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_versions_match_across_providers_single_file() {
|
||||||
|
let mut project =
|
||||||
|
Project::new("test-id".to_string(), ProjectType::Mod, ProjectSide::Both);
|
||||||
|
|
||||||
|
project.files.push(ProjectFile {
|
||||||
|
file_type: "modrinth".to_string(),
|
||||||
|
file_name: "test-1.0.0.jar".to_string(),
|
||||||
|
mc_versions: vec!["1.20.1".to_string()],
|
||||||
|
loaders: vec!["fabric".to_string()],
|
||||||
|
release_type: ReleaseType::Release,
|
||||||
|
url: "https://example.com/test.jar".to_string(),
|
||||||
|
id: "file1".to_string(),
|
||||||
|
parent_id: "mod123".to_string(),
|
||||||
|
hashes: HashMap::new(),
|
||||||
|
required_dependencies: vec![],
|
||||||
|
size: 1024,
|
||||||
|
date_published: "2024-01-01T00:00:00Z".to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(project.versions_match_across_providers());
|
||||||
|
assert!(project.check_version_mismatch().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_versions_match_across_providers_same_file() {
|
||||||
|
let mut project =
|
||||||
|
Project::new("test-id".to_string(), ProjectType::Mod, ProjectSide::Both);
|
||||||
|
|
||||||
|
// Same file name from different providers
|
||||||
|
project.files.push(ProjectFile {
|
||||||
|
file_type: "modrinth".to_string(),
|
||||||
|
file_name: "test-1.0.0.jar".to_string(),
|
||||||
|
mc_versions: vec!["1.20.1".to_string()],
|
||||||
|
loaders: vec!["fabric".to_string()],
|
||||||
|
release_type: ReleaseType::Release,
|
||||||
|
url: "https://modrinth.com/test.jar".to_string(),
|
||||||
|
id: "mr-file1".to_string(),
|
||||||
|
parent_id: "mod123".to_string(),
|
||||||
|
hashes: HashMap::new(),
|
||||||
|
required_dependencies: vec![],
|
||||||
|
size: 1024,
|
||||||
|
date_published: "2024-01-01T00:00:00Z".to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
project.files.push(ProjectFile {
|
||||||
|
file_type: "curseforge".to_string(),
|
||||||
|
file_name: "test-1.0.0.jar".to_string(),
|
||||||
|
mc_versions: vec!["1.20.1".to_string()],
|
||||||
|
loaders: vec!["fabric".to_string()],
|
||||||
|
release_type: ReleaseType::Release,
|
||||||
|
url: "https://curseforge.com/test.jar".to_string(),
|
||||||
|
id: "cf-file1".to_string(),
|
||||||
|
parent_id: "mod456".to_string(),
|
||||||
|
hashes: HashMap::new(),
|
||||||
|
required_dependencies: vec![],
|
||||||
|
size: 1024,
|
||||||
|
date_published: "2024-01-01T00:00:00Z".to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(project.versions_match_across_providers());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_versions_mismatch_across_providers() {
|
||||||
|
let mut project =
|
||||||
|
Project::new("test-id".to_string(), ProjectType::Mod, ProjectSide::Both);
|
||||||
|
|
||||||
|
project
|
||||||
|
.name
|
||||||
|
.insert("test".to_string(), "Test Mod".to_string());
|
||||||
|
|
||||||
|
// Different file names from different providers
|
||||||
|
project.files.push(ProjectFile {
|
||||||
|
file_type: "modrinth".to_string(),
|
||||||
|
file_name: "test-1.0.0.jar".to_string(),
|
||||||
|
mc_versions: vec!["1.20.1".to_string()],
|
||||||
|
loaders: vec!["fabric".to_string()],
|
||||||
|
release_type: ReleaseType::Release,
|
||||||
|
url: "https://modrinth.com/test.jar".to_string(),
|
||||||
|
id: "mr-file1".to_string(),
|
||||||
|
parent_id: "mod123".to_string(),
|
||||||
|
hashes: HashMap::new(),
|
||||||
|
required_dependencies: vec![],
|
||||||
|
size: 1024,
|
||||||
|
date_published: "2024-01-01T00:00:00Z".to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
project.files.push(ProjectFile {
|
||||||
|
file_type: "curseforge".to_string(),
|
||||||
|
file_name: "test-0.9.0.jar".to_string(), // Different version
|
||||||
|
mc_versions: vec!["1.20.1".to_string()],
|
||||||
|
loaders: vec!["fabric".to_string()],
|
||||||
|
release_type: ReleaseType::Release,
|
||||||
|
url: "https://curseforge.com/test.jar".to_string(),
|
||||||
|
id: "cf-file1".to_string(),
|
||||||
|
parent_id: "mod456".to_string(),
|
||||||
|
hashes: HashMap::new(),
|
||||||
|
required_dependencies: vec![],
|
||||||
|
size: 1024,
|
||||||
|
date_published: "2024-01-01T00:00:00Z".to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(!project.versions_match_across_providers());
|
||||||
|
let mismatch = project.check_version_mismatch();
|
||||||
|
assert!(mismatch.is_some());
|
||||||
|
let msg = mismatch.unwrap();
|
||||||
|
assert!(msg.contains("Version mismatch"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_site_url_modrinth() {
|
||||||
|
let mut project =
|
||||||
|
Project::new("test-id".to_string(), ProjectType::Mod, ProjectSide::Both);
|
||||||
|
project
|
||||||
|
.slug
|
||||||
|
.insert("modrinth".to_string(), "sodium".to_string());
|
||||||
|
|
||||||
|
let file = ProjectFile {
|
||||||
|
file_type: "modrinth".to_string(),
|
||||||
|
file_name: "sodium-1.0.0.jar".to_string(),
|
||||||
|
mc_versions: vec!["1.20.1".to_string()],
|
||||||
|
loaders: vec!["fabric".to_string()],
|
||||||
|
release_type: ReleaseType::Release,
|
||||||
|
url: "https://modrinth.com/sodium.jar".to_string(),
|
||||||
|
id: "abc123".to_string(),
|
||||||
|
parent_id: "sodium".to_string(),
|
||||||
|
hashes: HashMap::new(),
|
||||||
|
required_dependencies: vec![],
|
||||||
|
size: 1024,
|
||||||
|
date_published: "2024-01-01T00:00:00Z".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = file.get_site_url(&project);
|
||||||
|
assert!(url.is_some());
|
||||||
|
let url = url.unwrap();
|
||||||
|
assert!(url.contains("modrinth.com"));
|
||||||
|
assert!(url.contains("sodium"));
|
||||||
|
assert!(url.contains("abc123"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_site_url_curseforge() {
|
||||||
|
let mut project =
|
||||||
|
Project::new("test-id".to_string(), ProjectType::Mod, ProjectSide::Both);
|
||||||
|
project
|
||||||
|
.slug
|
||||||
|
.insert("curseforge".to_string(), "jei".to_string());
|
||||||
|
|
||||||
|
let file = ProjectFile {
|
||||||
|
file_type: "curseforge".to_string(),
|
||||||
|
file_name: "jei-1.0.0.jar".to_string(),
|
||||||
|
mc_versions: vec!["1.20.1".to_string()],
|
||||||
|
loaders: vec!["forge".to_string()],
|
||||||
|
release_type: ReleaseType::Release,
|
||||||
|
url: "https://curseforge.com/jei.jar".to_string(),
|
||||||
|
id: "12345".to_string(),
|
||||||
|
parent_id: "jei".to_string(),
|
||||||
|
hashes: HashMap::new(),
|
||||||
|
required_dependencies: vec![],
|
||||||
|
size: 1024,
|
||||||
|
date_published: "2024-01-01T00:00:00Z".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = file.get_site_url(&project);
|
||||||
|
assert!(url.is_some());
|
||||||
|
let url = url.unwrap();
|
||||||
|
assert!(url.contains("curseforge.com"));
|
||||||
|
assert!(url.contains("jei"));
|
||||||
|
assert!(url.contains("12345"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_site_url_github() {
|
||||||
|
let project =
|
||||||
|
Project::new("test-id".to_string(), ProjectType::Mod, ProjectSide::Both);
|
||||||
|
|
||||||
|
let file = ProjectFile {
|
||||||
|
file_type: "github".to_string(),
|
||||||
|
file_name: "mod-1.0.0.jar".to_string(),
|
||||||
|
mc_versions: vec!["1.20.1".to_string()],
|
||||||
|
loaders: vec!["fabric".to_string()],
|
||||||
|
release_type: ReleaseType::Release,
|
||||||
|
url:
|
||||||
|
"https://github.com/owner/repo/releases/download/v1.0.0/mod.jar"
|
||||||
|
.to_string(),
|
||||||
|
id: "v1.0.0".to_string(),
|
||||||
|
parent_id: "owner/repo".to_string(),
|
||||||
|
hashes: HashMap::new(),
|
||||||
|
required_dependencies: vec![],
|
||||||
|
size: 1024,
|
||||||
|
date_published: "2024-01-01T00:00:00Z".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = file.get_site_url(&project);
|
||||||
|
assert!(url.is_some());
|
||||||
|
let url = url.unwrap();
|
||||||
|
assert!(url.contains("github.com"));
|
||||||
|
assert!(url.contains("owner/repo"));
|
||||||
|
assert!(url.contains("v1.0.0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_site_url_unknown_type() {
|
||||||
|
let project =
|
||||||
|
Project::new("test-id".to_string(), ProjectType::Mod, ProjectSide::Both);
|
||||||
|
|
||||||
|
let file = ProjectFile {
|
||||||
|
file_type: "unknown".to_string(),
|
||||||
|
file_name: "mod.jar".to_string(),
|
||||||
|
mc_versions: vec!["1.20.1".to_string()],
|
||||||
|
loaders: vec!["fabric".to_string()],
|
||||||
|
release_type: ReleaseType::Release,
|
||||||
|
url: "https://example.com/mod.jar".to_string(),
|
||||||
|
id: "123".to_string(),
|
||||||
|
parent_id: "mod".to_string(),
|
||||||
|
hashes: HashMap::new(),
|
||||||
|
required_dependencies: vec![],
|
||||||
|
size: 1024,
|
||||||
|
date_published: "2024-01-01T00:00:00Z".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = file.get_site_url(&project);
|
||||||
|
assert!(url.is_none());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue