Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ name = "dotium"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["modrinth", "curseforge", "github"]
modrinth = ["dep:ferinth"]
curseforge = ["dep:furse"]
github = ["dep:octocrab"]

[dependencies]
serde = { version = "~1.0.137", features = ["derive"] }
chrono = { version = "~0.4.19", features = ["serde"] }
octocrab = { version = "~0.16.0", optional = true }
url = { version = "~2.2.2", features = ["serde"] }
ferinth = { version = "~2.4.0", optional = true }
furse = { version = "~1.4.0", optional = true }
async-trait = "~0.1.56"
thiserror = "~1.0.31"
Empty file added src/curseforge/mod.rs
Empty file.
Empty file added src/github/mod.rs
Empty file.
88 changes: 82 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,84 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
//! # Dotium
//!
//! The uniform object provider for Minecraft Mod hosting platforms.
//! Implements traits for api wrappers Ferinth, Furse, and Octocrab to then be used platform independedly in GDLauncher

#[cfg(feature = "curseforge")]
mod curseforge;
#[cfg(feature = "github")]
mod github;
#[cfg(feature = "modrinth")]
mod modrinth;
pub mod project;
pub mod request;
pub mod version;

use async_trait::async_trait;
use project::{Author, Project};
use serde::{Deserialize, Serialize};
use version::Version;

#[derive(Debug, Clone)]
pub struct Platform {
/// The name of the platform
pub name: String,
/// The two-letter nick name of the Platform
pub short_name: String,
/// Logo of the platform
pub logo: request::Asset,
}

#[derive(thiserror::Error, Debug)]
#[error("The trait is not implemented")]
pub struct UnimplementedError;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better if it was a trait that depends on std::error:Error?
So that we can fall back to more normal methods if the need arises, or if we need something like the step the error happened for example


pub(crate) type Datetime = chrono::DateTime<chrono::Utc>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UtcDateTime, it can be confused with the generic one it shadows otherwise


#[async_trait]
pub trait Container<E: From<UnimplementedError>, ID: Sync + Send> {
fn new() -> Self;
fn get_platform() -> Platform;

// GATs unstable
// type Result<T> = std::result::Result<T, E>;

async fn get_project(&self, id: &ID) -> Result<Project<ID>, E>;

async fn get_projects(&self, ids: Vec<&ID>) -> Result<Vec<Project<ID>>, E> {
let mut projects = Vec::new();
for id in ids {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not totally sure about that, but wouldn't this mean that we are working a project at a time?
Could something like https://docs.rs/futures/latest/futures/future/fn.join_all.html help?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes too many concurrent requests completely fails, I had to deal with this problem when implementing concurrent requests, and it was a rather huge pain. I ended up using Semaphore which I don't think you can use with join_all(), and semaphore would need to import tokio which we certainly don't want.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like we have the first design problem lol
But well, it's not like it is a huge problem, and the solution (Having pending requests we can start at any time) is even worse

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well the only platform that doesn't implement multiple requests is github (the others overrides this default implementation), and we won't be using the multiple requests on github afaik.

projects.push(self.get_project(id).await?);
}
Ok(projects)
}

async fn get_project_body(&self, project_id: &ID) -> Result<String, E>;

async fn get_project_authors(&self, project_id: &ID) -> Result<Vec<Author<ID>>, E>;

async fn get_project_dependencies(
&self,
project_id: &ID,
) -> Result<(Vec<Project<ID>>, Vec<Version<ID>>), E>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would converting this to a Vec<(Project, Version) cause issues somewhere?
I thing that changing this before the project grows too big could be beneficial, as making sure that both vecs are in sync when we have to drop values from within (removing duplicates for example) can be difficult

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I did think about doing this too

Copy link
Copy Markdown
Contributor

@Eskaan Eskaan Jun 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes because this is not a sorted map. I specifically asked Modrinth on this:

For example, if I have a dependency on any version of Fabric API and a dependency on version 4.0.0 of Mod Menu, it will have both Fabric API and Mod Menu in the projects array and version 4.0.0 of Mod Menu in the versions array
If another version starts depending on version 4.0.1 of Mod Menu then Mod Menu 4.0.1 will appear in the versions array in addition to 4.0.0

That means, the two Vec's can have different lengths. Also they said that I shouldn't rely on their sorting.


async fn get_version(&self, project_id: &ID, id: &ID) -> Result<Version<ID>, E>;

async fn get_project_versions(&self, project_id: &ID) -> Result<Vec<Version<ID>>, E>;

async fn get_versions(&self, ids: Vec<(&ID, &ID)>) -> Result<Vec<Version<ID>>, E> {
let mut versions = Vec::new();
for id in ids {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as for get_projects

versions.push(self.get_version(id.0, id.1).await?);
}
Ok(versions)
}

// async fn search(
// &self,
// query: &str,
// project_type: Option<&str>,
// mc_version: Vec<&str>,
// modloader: &str,
// category: &str,
// ) -> Result<Vec<project::Project>, E>;
}
178 changes: 178 additions & 0 deletions src/modrinth/converters.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use super::ID;
use crate::{project, request, version};
use ferinth::structures::*;

impl From<project_structs::Project> for project::Project<ID> {
fn from(from: project_structs::Project) -> Self {
project::Project {
id: from.id,
slug: from.slug,
project_type: from.project_type.into(),
name: from.title,
description: from.description,
links: project::Links {
github: from.source_url,
issues: from.issues_url,
wiki: from.wiki_url,
discord: from.discord_url,
donations: from.donation_urls.into_iter().map(Into::into).collect(),
},
requirements: project::ProjectRequirements {
server: from.server_side.into(),
client: from.client_side.into(),
},
categories: from.categories,
downloads: from.downloads,
followers: from.followers,
icon: from.icon_url.map(request::Asset::by_url),
status: from.status.into(),
published: from.published,
updated: from.updated,
created: from.published,
gallery: from.gallery.into_iter().map(Into::into).collect(),
allows_distribution: &from.license.id != "custom" && &from.license.id != "arr",
}
}
}

impl From<version_structs::Version> for version::Version<ID> {
fn from(from: version_structs::Version) -> Self {
Self {
id: from.id,
name: from.name,
identifier: from.version_number,
project_id: from.project_id,
files: from.files.into_iter().map(Into::into).collect(),
downloads: from.downloads,
loaders: from
.loaders
.into_iter()
.map(|loader| version::ModLoader::from(loader.as_ref()))
.collect(),
game_versions: from.game_versions,
published: from.date_published,
version_type: from.version_type.into(),
dependencies: from.dependencies.into_iter().map(Into::into).collect(),
}
}
}

impl From<version_structs::VersionFile> for request::Asset {
fn from(from: version_structs::VersionFile) -> Self {
Self {
url: from.url,
name: Some(from.filename),
description: None,
headers: std::collections::HashMap::new(),
request_type: request::RequestType::GET,
hash: Some(request::AssetHash {
hash: from.hashes.sha512,
algorithm: request::HashAlgorithm::SHA512,
}),
size: Some(from.size),
}
}
}

impl From<version_structs::Dependency> for version::VersionDependency<ID> {
fn from(from: version_structs::Dependency) -> Self {
Self {
project_id: from.project_id,
version: from.version_id,
dependency_type: from.dependency_type.into(),
}
}
}

impl From<user_structs::User> for project::Author<ID> {
fn from(from: user_structs::User) -> Self {
Self {
username: from.username,
name: from.name,
id: from.id,
avatar_url: Some(request::Asset::by_url(from.avatar_url)),
}
}
}

impl From<version_structs::DependencyType> for version::DependencyType {
fn from(from: version_structs::DependencyType) -> Self {
match from {
version_structs::DependencyType::Required => Self::RequiredDependency,
version_structs::DependencyType::Optional => Self::OptionalDependency,
version_structs::DependencyType::Incompatible => Self::Incompatible,
}
}
}

impl From<project_structs::ProjectType> for project::ProjectType {
fn from(from: project_structs::ProjectType) -> Self {
match from {
project_structs::ProjectType::Mod => project::ProjectType::Mod,
project_structs::ProjectType::Modpack => project::ProjectType::Modpack,
}
}
}

impl From<version_structs::VersionType> for version::VersionType {
fn from(from: version_structs::VersionType) -> Self {
match from {
version_structs::VersionType::Alpha => Self::Alpha,
version_structs::VersionType::Beta => Self::Beta,
version_structs::VersionType::Release => Self::Release,
}
}
}

impl From<&str> for version::ModLoader {
fn from(from: &str) -> Self {
match from.to_lowercase().as_str() {
"modloader" => Self::Modloader,
"fabric" => Self::Fabric,
"quilt" => Self::Quilt,
"forge" => Self::Forge,
"rift" => Self::Rift,
_ => Self::Unknown,
}
}
}

impl From<project_structs::ProjectSupportRange> for project::Requirement {
fn from(from: project_structs::ProjectSupportRange) -> Self {
match from {
project_structs::ProjectSupportRange::Required => Self::Required,
project_structs::ProjectSupportRange::Optional => Self::Optional,
project_structs::ProjectSupportRange::Unsupported => Self::Unsupported,
}
}
}

impl From<project_structs::ProjectStatus> for project::Status {
fn from(from: project_structs::ProjectStatus) -> Self {
match from {
project_structs::ProjectStatus::Approved => Self::Approved,
project_structs::ProjectStatus::Rejected => Self::Rejected,
project_structs::ProjectStatus::Draft => Self::New,
project_structs::ProjectStatus::Unlisted => Self::Unlisted,
project_structs::ProjectStatus::Archived => Self::Abandoned,
project_structs::ProjectStatus::Processing => Self::UnderReview,
project_structs::ProjectStatus::Unknown => Self::Unknown,
}
}
}

impl From<project_structs::DonationLink> for project::DonationLink {
fn from(from: project_structs::DonationLink) -> Self {
Self {
platform: from.platform,
url: from.url,
id: from.id,
}
}
}

impl From<project_structs::GalleryItem> for request::Asset {
fn from(from: project_structs::GalleryItem) -> Self {
Self::by_description(from.url, from.title, from.description)
}
}
Loading