pakker/src/platform/multiplatform.rs
NotAShelf 20ea3c680b
platform: add rustdoc to various methods
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ic4d2bd6f3baf97ce30dbf8709331f6f66a6a6964
2026-04-21 19:27:31 +03:00

234 lines
6.8 KiB
Rust

use std::sync::Arc;
use async_trait::async_trait;
use super::traits::PlatformClient;
use crate::{
error::{PakkerError, Result},
model::{Project, ProjectFile},
};
/// Multiplatform platform client that aggregates CurseForge and Modrinth.
/// It attempts to resolve projects on both platforms and cross-references
/// them via slugs when a project exists on only one platform.
pub struct MultiplatformPlatform {
curseforge: Arc<dyn PlatformClient>,
modrinth: Arc<dyn PlatformClient>,
}
impl MultiplatformPlatform {
pub fn new(
curseforge: Arc<dyn PlatformClient>,
modrinth: Arc<dyn PlatformClient>,
) -> Self {
Self {
curseforge,
modrinth,
}
}
/// Try to fetch a project, returning Ok(None) for "not found" errors.
async fn try_request_project(
&self,
client: &Arc<dyn PlatformClient>,
identifier: &str,
) -> Result<Option<Project>> {
match client.request_project(identifier, &[], &[]).await {
Ok(project) => Ok(Some(project)),
Err(e) => {
let is_not_found = matches!(
e,
PakkerError::ProjectNotFound(_) | PakkerError::InvalidResponse(_)
);
if is_not_found { Ok(None) } else { Err(e) }
},
}
}
}
#[async_trait]
impl PlatformClient for MultiplatformPlatform {
async fn request_project(
&self,
identifier: &str,
_mc_versions: &[String],
_loaders: &[String],
) -> Result<Project> {
// Try both platforms in parallel
let cf_future = self.try_request_project(&self.curseforge, identifier);
let mr_future = self.try_request_project(&self.modrinth, identifier);
let (cf_result, mr_result) = tokio::join!(cf_future, mr_future);
// Handle results - extract Options, propagate first error if both fail
let (cf_project, mr_project) = match (cf_result, mr_result) {
(Ok(Some(cf)), Ok(Some(mr))) => (Some(cf), Some(mr)),
(Ok(None), Ok(None)) => (None, None),
(Ok(None), Ok(Some(mr))) => (None, Some(mr)),
(Ok(Some(cf)), Ok(None)) => (Some(cf), None),
(Err(_e), Ok(None)) | (Ok(None), Err(_e)) => (None, None),
(Err(e), Ok(Some(_))) | (Ok(Some(_)), Err(e)) => {
return Err(e);
},
(Err(e), Err(_)) => return Err(e),
};
// Cross-reference: if project exists on only one platform, fetch from the
// other using the slug
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
&& let Some(ref mr) = mr_project
&& let Some(cf_slug) = mr.slug.get("curseforge")
&& let Ok(Some(cf)) =
self.curseforge.request_project_from_slug(cf_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
&& let Some(ref cf) = cf_project
&& let Some(mr_slug) = cf.slug.get("modrinth")
&& let Ok(Some(mr)) =
self.modrinth.request_project_from_slug(mr_slug).await
{
mr_project = Some(mr);
}
// Merge projects or return whichever was found
let combined = match (cf_project, mr_project) {
(Some(cf), Some(mr)) => cf.merged(mr)?,
(Some(cf), None) => cf,
(None, Some(mr)) => mr,
(None, None) => {
return Err(PakkerError::ProjectNotFound(identifier.to_string()));
},
};
Ok(combined)
}
async fn request_project_files(
&self,
project_id: &str,
mc_versions: &[String],
loaders: &[String],
) -> Result<Vec<ProjectFile>> {
// Multiplatform doesn't directly support files - use
// request_project_with_files
let project = self
.request_project_with_files(project_id, mc_versions, loaders)
.await?;
Ok(project.files)
}
async fn request_project_with_files(
&self,
identifier: &str,
mc_versions: &[String],
loaders: &[String],
) -> Result<Project> {
// First get the combined project from both platforms
let mut project = self
.request_project(identifier, mc_versions, loaders)
.await?;
// Now fetch files from both platforms in parallel
let cf_project_id = project.id.get("curseforge").cloned();
let mr_project_id = project.id.get("modrinth").cloned();
let cf_files_future = async {
if let Some(ref id) = cf_project_id {
self
.curseforge
.request_project_files(id, mc_versions, loaders)
.await
} else {
Ok(Vec::new())
}
};
let mr_files_future = async {
if let Some(ref id) = mr_project_id {
self
.modrinth
.request_project_files(id, mc_versions, loaders)
.await
} else {
Ok(Vec::new())
}
};
let (cf_files, mr_files) = tokio::join!(cf_files_future, mr_files_future);
let mut all_files = cf_files?;
all_files.extend(mr_files?);
project.files = all_files;
Ok(project)
}
async fn lookup_by_hash(&self, hash: &str) -> Result<Option<Project>> {
// Try both platforms in parallel
let cf_future = self.curseforge.lookup_by_hash(hash);
let mr_future = self.modrinth.lookup_by_hash(hash);
let (cf_result, mr_result) = tokio::join!(cf_future, mr_future);
match (cf_result?, mr_result?) {
(Some(cf), Some(mr)) => cf.merged(mr).map(Some),
(Some(project), None) | (None, Some(project)) => Ok(Some(project)),
(None, None) => Ok(None),
}
}
async fn request_project_from_slug(
&self,
slug: &str,
) -> Result<Option<Project>> {
let cf_future = self.curseforge.request_project_from_slug(slug);
let mr_future = self.modrinth.request_project_from_slug(slug);
let (cf_result, mr_result) = tokio::join!(cf_future, mr_future);
match (cf_result, mr_result) {
(Ok(Some(cf)), Ok(Some(mr))) => cf.merged(mr).map(Some),
(Ok(Some(project)), Ok(None)) | (Ok(None), Ok(Some(project))) => {
Ok(Some(project))
},
(Ok(None), Ok(None)) => Ok(None),
(Err(e), _) | (_, Err(e)) => Err(e),
}
}
/// Delegates to both CurseForge and Modrinth in parallel, then deduplicates
/// results.
async fn request_projects_from_hashes(
&self,
hashes: &[String],
algorithm: &str,
) -> Result<Vec<Project>> {
let cf_future = self
.curseforge
.request_projects_from_hashes(hashes, algorithm);
let mr_future = self
.modrinth
.request_projects_from_hashes(hashes, algorithm);
let (cf_projects, mr_projects) = tokio::join!(cf_future, mr_future);
let mut all_projects = cf_projects?;
for mr_project in mr_projects? {
if !all_projects.iter().any(|p| {
p.id.get("modrinth") == mr_project.id.get("modrinth")
|| p.id.get("curseforge") == mr_project.id.get("curseforge")
}) {
all_projects.push(mr_project);
}
}
Ok(all_projects)
}
}