various: resolve multi-platform lookup; improve error messages
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Iec6ee73639d0b42c96127db657575ab86a6a6964
This commit is contained in:
parent
0f8719eb52
commit
1079635cb9
5 changed files with 232 additions and 25 deletions
|
|
@ -35,8 +35,10 @@ async fn resolve_input(
|
|||
platforms: &HashMap<String, Box<dyn crate::platform::PlatformClient>>,
|
||||
lockfile: &LockFile,
|
||||
) -> Result<Project> {
|
||||
for platform in platforms.values() {
|
||||
if let Ok(project) = platform
|
||||
let mut projects = Vec::new();
|
||||
|
||||
for (platform_name, client) in platforms {
|
||||
match client
|
||||
.request_project_with_files(
|
||||
input,
|
||||
&lockfile.mc_versions,
|
||||
|
|
@ -44,11 +46,29 @@ async fn resolve_input(
|
|||
)
|
||||
.await
|
||||
{
|
||||
return Ok(project);
|
||||
Ok(project) => {
|
||||
log::debug!("Resolved '{input}' on {platform_name}");
|
||||
projects.push(project);
|
||||
},
|
||||
Err(e) => {
|
||||
log::debug!("Could not resolve '{input}' on {platform_name}: {e}");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Err(PakkerError::ProjectNotFound(input.to_string()))
|
||||
if projects.is_empty() {
|
||||
return Err(PakkerError::ProjectNotFound(input.to_string()));
|
||||
}
|
||||
|
||||
if projects.len() == 1 {
|
||||
return Ok(projects.remove(0));
|
||||
}
|
||||
|
||||
let mut merged = projects.remove(0);
|
||||
for project in projects {
|
||||
merged.merge(project);
|
||||
}
|
||||
Ok(merged)
|
||||
}
|
||||
|
||||
use std::path::Path;
|
||||
|
|
@ -111,16 +131,24 @@ pub async fn execute(
|
|||
}
|
||||
|
||||
// Load parent lockfile to get metadata
|
||||
let parent_lockfile = parent_paths
|
||||
let parent_lock_path = parent_paths
|
||||
.iter()
|
||||
.find(|path| path.exists())
|
||||
.and_then(|path| LockFile::load(path.parent()?).ok())
|
||||
.ok_or_else(|| {
|
||||
PakkerError::IoError(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
"Failed to load parent lockfile metadata",
|
||||
"Parent lockfile not found at expected paths",
|
||||
))
|
||||
})?;
|
||||
let parent_lockfile = LockFile::load_with_validation(
|
||||
parent_lock_path.parent().ok_or_else(|| {
|
||||
PakkerError::IoError(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
"Parent lockfile path has no parent directory",
|
||||
))
|
||||
})?,
|
||||
false,
|
||||
)?;
|
||||
|
||||
let minimal_lockfile = LockFile {
|
||||
target: parent_lockfile.target,
|
||||
|
|
|
|||
|
|
@ -702,7 +702,11 @@ fn execute_promote(projects: &[String]) -> Result<(), PakkerError> {
|
|||
})?;
|
||||
|
||||
// Load or create local lockfile
|
||||
let lockfile_path = config_dir.join("pakku-lock.json");
|
||||
let lockfile_path = if config_dir.join("pakker-lock.json").exists() {
|
||||
config_dir.join("pakker-lock.json")
|
||||
} else {
|
||||
config_dir.join("pakku-lock.json")
|
||||
};
|
||||
let mut local_lockfile = if lockfile_path.exists() {
|
||||
LockFile::load_with_validation(config_dir, false).map_err(|e| {
|
||||
PakkerError::Fork(format!("Failed to load local lockfile: {e}"))
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ impl IpcCoordinator {
|
|||
pakku_path
|
||||
} else {
|
||||
return Err(IpcError::PakkuJsonReadFailed(
|
||||
"pakku.json not found in working directory".to_string(),
|
||||
"pakker.json or pakku.json not found in working directory".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -533,10 +533,7 @@ mod tests {
|
|||
|
||||
for (tag, asset, expected) in cases {
|
||||
let result = extract_mc_versions(tag, asset);
|
||||
assert_eq!(
|
||||
result, expected,
|
||||
"Failed for tag: {tag}, asset: {asset}"
|
||||
);
|
||||
assert_eq!(result, expected, "Failed for tag: {tag}, asset: {asset}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -567,10 +564,7 @@ mod tests {
|
|||
|
||||
for (tag, asset, expected) in cases {
|
||||
let result = extract_loaders(tag, asset);
|
||||
assert_eq!(
|
||||
result, expected,
|
||||
"Failed for tag: {tag}, asset: {asset}"
|
||||
);
|
||||
assert_eq!(result, expected, "Failed for tag: {tag}, asset: {asset}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,21 +78,22 @@ impl PlatformClient for MultiplatformPlatform {
|
|||
let mut cf_project = cf_project;
|
||||
let mut mr_project = mr_project;
|
||||
|
||||
let mr_found_and_cf_missing = mr_project.is_some() && cf_project.is_none();
|
||||
if mr_found_and_cf_missing
|
||||
// Cross-reference using each platform's own slug on the other platform.
|
||||
// Modrinth projects store their slug under "modrinth"; CurseForge under
|
||||
// "curseforge". Many mods share the same slug across platforms.
|
||||
if cf_project.is_none()
|
||||
&& let Some(ref mr) = mr_project
|
||||
&& let Some(cf_slug) = mr.slug.get("curseforge")
|
||||
&& let Some(mr_slug) = mr.slug.get("modrinth")
|
||||
&& let Ok(Some(cf)) =
|
||||
self.curseforge.request_project_from_slug(cf_slug).await
|
||||
self.curseforge.request_project_from_slug(mr_slug).await
|
||||
{
|
||||
cf_project = Some(cf);
|
||||
}
|
||||
let cf_found_and_mr_missing = cf_project.is_some() && mr_project.is_none();
|
||||
if cf_found_and_mr_missing
|
||||
if mr_project.is_none()
|
||||
&& let Some(ref cf) = cf_project
|
||||
&& let Some(mr_slug) = cf.slug.get("modrinth")
|
||||
&& let Some(cf_slug) = cf.slug.get("curseforge")
|
||||
&& let Ok(Some(mr)) =
|
||||
self.modrinth.request_project_from_slug(mr_slug).await
|
||||
self.modrinth.request_project_from_slug(cf_slug).await
|
||||
{
|
||||
mr_project = Some(mr);
|
||||
}
|
||||
|
|
@ -232,3 +233,183 @@ impl PlatformClient for MultiplatformPlatform {
|
|||
Ok(all_projects)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
error::{PakkerError, Result},
|
||||
model::{Project, ProjectFile, ProjectSide, ProjectType},
|
||||
};
|
||||
|
||||
struct MockPlatform {
|
||||
projects: HashMap<String, Project>,
|
||||
slug_map: HashMap<String, Project>,
|
||||
}
|
||||
|
||||
impl MockPlatform {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
projects: HashMap::new(),
|
||||
slug_map: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_project(mut self, id: &str, project: Project) -> Self {
|
||||
self.projects.insert(id.to_string(), project);
|
||||
self
|
||||
}
|
||||
|
||||
fn with_slug(mut self, slug: &str, project: Project) -> Self {
|
||||
self.slug_map.insert(slug.to_string(), project);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl PlatformClient for MockPlatform {
|
||||
async fn request_project(
|
||||
&self,
|
||||
project_id: &str,
|
||||
_mc_versions: &[String],
|
||||
_loaders: &[String],
|
||||
) -> Result<Project> {
|
||||
self
|
||||
.projects
|
||||
.get(project_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| PakkerError::ProjectNotFound(project_id.to_string()))
|
||||
}
|
||||
|
||||
async fn request_project_files(
|
||||
&self,
|
||||
_project_id: &str,
|
||||
_mc_versions: &[String],
|
||||
_loaders: &[String],
|
||||
) -> Result<Vec<ProjectFile>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
async fn request_project_with_files(
|
||||
&self,
|
||||
project_id: &str,
|
||||
mc_versions: &[String],
|
||||
loaders: &[String],
|
||||
) -> Result<Project> {
|
||||
self.request_project(project_id, mc_versions, loaders).await
|
||||
}
|
||||
|
||||
async fn lookup_by_hash(&self, _hash: &str) -> Result<Option<Project>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn request_project_from_slug(
|
||||
&self,
|
||||
slug: &str,
|
||||
) -> Result<Option<Project>> {
|
||||
Ok(self.slug_map.get(slug).cloned())
|
||||
}
|
||||
|
||||
async fn request_projects_from_hashes(
|
||||
&self,
|
||||
_hashes: &[String],
|
||||
_algorithm: &str,
|
||||
) -> Result<Vec<Project>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
fn make_project(platform: &str, id: &str, slug: &str) -> Project {
|
||||
let mut project =
|
||||
Project::new(slug.to_string(), ProjectType::Mod, ProjectSide::Both);
|
||||
project.id.insert(platform.to_string(), id.to_string());
|
||||
project.slug.insert(platform.to_string(), slug.to_string());
|
||||
project.name.insert(platform.to_string(), slug.to_string());
|
||||
project
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cross_reference_modrinth_to_curseforge() {
|
||||
let mr_project = make_project("modrinth", "mr-abc", "sodium");
|
||||
let cf_project = make_project("curseforge", "12345", "sodium");
|
||||
|
||||
let modrinth =
|
||||
Arc::new(MockPlatform::new().with_project("sodium", mr_project.clone()));
|
||||
let curseforge =
|
||||
Arc::new(MockPlatform::new().with_slug("sodium", cf_project.clone()));
|
||||
|
||||
let platform = MultiplatformPlatform::new(curseforge, modrinth);
|
||||
let result = platform.request_project("sodium", &[], &[]).await.unwrap();
|
||||
|
||||
assert_eq!(result.id.get("modrinth"), Some(&"mr-abc".to_string()));
|
||||
assert_eq!(result.id.get("curseforge"), Some(&"12345".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cross_reference_curseforge_to_modrinth() {
|
||||
let cf_project = make_project("curseforge", "12345", "sodium");
|
||||
let mr_project = make_project("modrinth", "mr-abc", "sodium");
|
||||
|
||||
let modrinth =
|
||||
Arc::new(MockPlatform::new().with_slug("sodium", mr_project.clone()));
|
||||
let curseforge =
|
||||
Arc::new(MockPlatform::new().with_project("sodium", cf_project.clone()));
|
||||
|
||||
let platform = MultiplatformPlatform::new(curseforge, modrinth);
|
||||
let result = platform.request_project("sodium", &[], &[]).await.unwrap();
|
||||
|
||||
assert_eq!(result.id.get("curseforge"), Some(&"12345".to_string()));
|
||||
assert_eq!(result.id.get("modrinth"), Some(&"mr-abc".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_found_on_both_platforms_merged() {
|
||||
let cf_project = make_project("curseforge", "12345", "sodium");
|
||||
let mr_project = make_project("modrinth", "mr-abc", "sodium");
|
||||
|
||||
let modrinth =
|
||||
Arc::new(MockPlatform::new().with_project("sodium", mr_project));
|
||||
let curseforge =
|
||||
Arc::new(MockPlatform::new().with_project("sodium", cf_project));
|
||||
|
||||
let platform = MultiplatformPlatform::new(curseforge, modrinth);
|
||||
let result = platform.request_project("sodium", &[], &[]).await.unwrap();
|
||||
|
||||
assert_eq!(result.id.get("curseforge"), Some(&"12345".to_string()));
|
||||
assert_eq!(result.id.get("modrinth"), Some(&"mr-abc".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_not_found_on_either_platform() {
|
||||
let modrinth = Arc::new(MockPlatform::new());
|
||||
let curseforge = Arc::new(MockPlatform::new());
|
||||
|
||||
let platform = MultiplatformPlatform::new(curseforge, modrinth);
|
||||
let result = platform.request_project("nonexistent", &[], &[]).await;
|
||||
|
||||
assert!(matches!(result, Err(PakkerError::ProjectNotFound(_))));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_no_cross_reference_when_slug_absent() {
|
||||
// CurseForge returns a project, but slug lookup on Modrinth finds nothing
|
||||
let cf_project = make_project("curseforge", "12345", "rare-mod");
|
||||
|
||||
let modrinth = Arc::new(MockPlatform::new());
|
||||
let curseforge =
|
||||
Arc::new(MockPlatform::new().with_project("rare-mod", cf_project));
|
||||
|
||||
let platform = MultiplatformPlatform::new(curseforge, modrinth);
|
||||
let result = platform
|
||||
.request_project("rare-mod", &[], &[])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.id.get("curseforge"), Some(&"12345".to_string()));
|
||||
assert!(result.id.get("modrinth").is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue