feat: Modrinth provider
This commit is contained in:
parent
06215367c2
commit
17f8b382a7
25 changed files with 1424 additions and 70 deletions
24
.vscode/settings.json
vendored
24
.vscode/settings.json
vendored
|
|
@ -20,29 +20,11 @@
|
|||
"**/.cargo": true,
|
||||
"**/.crates": true
|
||||
},
|
||||
"rust-analyzer.files.excludeDirs": [
|
||||
"_build",
|
||||
".flatpak-builder",
|
||||
".git",
|
||||
".gitlab",
|
||||
".gitlab-ci",
|
||||
".gradle",
|
||||
".idea",
|
||||
".next",
|
||||
".project",
|
||||
".settings",
|
||||
".venv",
|
||||
"bin",
|
||||
"hooks",
|
||||
"node_modules",
|
||||
"screenshots",
|
||||
"target",
|
||||
".cargo",
|
||||
".rustup",
|
||||
".crates"
|
||||
],
|
||||
"rust-analyzer.procMacro.enable": true,
|
||||
"rust-analyzer.procMacro.attributes.enable": true,
|
||||
"rust-analyzer.procMacro.ignored": {
|
||||
"async-trait": ["async_trait"]
|
||||
},
|
||||
"rust-analyzer.check.command": "clippy",
|
||||
"rust-analyzer.diagnostics.experimental.enable": false,
|
||||
"rust-analyzer.linkedProjects": [],
|
||||
|
|
|
|||
46
Cargo.lock
generated
46
Cargo.lock
generated
|
|
@ -3056,9 +3056,7 @@ dependencies = [
|
|||
"sanitize-filename",
|
||||
"sea-orm",
|
||||
"serde",
|
||||
"serde_ini",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"serde_json_path_to_error",
|
||||
"serde_with",
|
||||
"sha1",
|
||||
"sha2",
|
||||
|
|
@ -3950,12 +3948,6 @@ dependencies = [
|
|||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "result"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "194d8e591e405d1eecf28819740abed6d719d1a2db87fc0bcdedee9a26d55560"
|
||||
|
||||
[[package]]
|
||||
name = "rfc6979"
|
||||
version = "0.4.0"
|
||||
|
|
@ -4428,17 +4420,6 @@ dependencies = [
|
|||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_ini"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb236687e2bb073a7521c021949be944641e671b8505a94069ca37b656c81139"
|
||||
dependencies = [
|
||||
"result",
|
||||
"serde",
|
||||
"void",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.128"
|
||||
|
|
@ -4451,6 +4432,25 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json_path_to_error"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a"
|
||||
dependencies = [
|
||||
"itoa 1.0.11",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.19"
|
||||
|
|
@ -6007,12 +6007,6 @@ version = "0.9.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "void"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
||||
|
||||
[[package]]
|
||||
name = "vswhom"
|
||||
version = "0.1.0"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ members = [
|
|||
"packages/gamemode",
|
||||
"packages/macro",
|
||||
"packages/migration",
|
||||
"packages/serde_json_path_to_error"
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
|
@ -25,6 +26,7 @@ onelauncher_entity = { path = "./packages/entity" }
|
|||
onelauncher_gamemode = { path = "./packages/gamemode" }
|
||||
onelauncher_macro = { path = "./packages/macro" }
|
||||
onelauncher_migration = { path = "./packages/migration" }
|
||||
serde_json_path_to_error = { path = "./packages/serde_json_path_to_error" }
|
||||
|
||||
# tauri
|
||||
tauri = { version = "2.0", features = [
|
||||
|
|
@ -72,8 +74,6 @@ tracing-appender = { version = "0.2" }
|
|||
# serialization & text
|
||||
serde = { version = "1.0", features = [ "derive" ] }
|
||||
serde_json = { version = "1.0" }
|
||||
serde_ini = { version = "0.2" }
|
||||
serde_repr = { version = "0.1" }
|
||||
serde_with = { version = "=3.12.0", features = [ "chrono_0_4", "json", "macros" ] }
|
||||
regex = { version = "1.10" }
|
||||
url = { version = "2.5.2" }
|
||||
|
|
|
|||
|
|
@ -43,9 +43,7 @@ tracing-appender = { workspace = true }
|
|||
|
||||
# serialization
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_ini = { workspace = true }
|
||||
serde_repr = { workspace = true }
|
||||
serde_json = { package = "serde_json_path_to_error", path = "../serde_json_path_to_error" }
|
||||
serde_with = { workspace = true }
|
||||
|
||||
# utils
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"scripts": {
|
||||
"cargo": "cargo",
|
||||
"binary": "cargo run --bin",
|
||||
"example": "cargo run --example"
|
||||
"example": "cargo run --example",
|
||||
"test": "cargo test"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
pub mod cluster;
|
||||
pub mod credentials;
|
||||
pub mod ingress;
|
||||
pub mod game;
|
||||
pub mod proxy;
|
||||
pub mod ingress;
|
||||
pub mod java;
|
||||
pub mod packages;
|
||||
pub mod processes;
|
||||
pub mod setting_profiles;
|
||||
pub mod proxy;
|
||||
pub mod setting_profiles;
|
||||
|
|
|
|||
217
packages/core/src/api/packages/data.rs
Normal file
217
packages/core/src/api/packages/data.rs
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use onelauncher_entity::{loader::GameLoader, package::{PackageType, Provider}};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::serde_as;
|
||||
|
||||
// Divisible by 4 and 3
|
||||
pub const DEFAULT_LIMIT: usize = 24;
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Filters {
|
||||
pub game_versions: Option<Vec<String>>,
|
||||
pub loaders: Option<Vec<GameLoader>>,
|
||||
pub categories: Option<Vec<String>>,
|
||||
pub package_types: Option<Vec<PackageType>>,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Sort {
|
||||
#[default]
|
||||
Relevance,
|
||||
Downloads,
|
||||
Newest,
|
||||
Updated
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SearchQuery {
|
||||
pub query: Option<String>,
|
||||
pub offset: Option<usize>,
|
||||
pub limit: Option<usize>,
|
||||
pub sort: Option<String>,
|
||||
pub filters: Option<Filters>,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[serde_as]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ManagedPackage {
|
||||
pub provider: Provider,
|
||||
pub provider_id: String,
|
||||
pub package_type: PackageType,
|
||||
pub name: String,
|
||||
pub short_desc: String,
|
||||
pub body: String,
|
||||
pub version_ids: Vec<String>,
|
||||
pub mc_versions: Vec<String>,
|
||||
#[serde_as(as = "serde_with::VecSkipError<_>")]
|
||||
pub loaders: Vec<GameLoader>,
|
||||
pub icon_url: Option<String>,
|
||||
pub created: DateTime<Utc>,
|
||||
pub updated: DateTime<Utc>,
|
||||
pub client: PackageSide,
|
||||
pub server: PackageSide,
|
||||
pub categories: Vec<String>,
|
||||
pub license: Option<PackageLicense>,
|
||||
pub author: PackageAuthor,
|
||||
pub links: PackageLinks,
|
||||
pub status: PackageStatus,
|
||||
pub downloads: usize,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ManagedUser {
|
||||
pub id: String,
|
||||
pub username: String,
|
||||
#[serde(default)]
|
||||
pub url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub avatar_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub bio: Option<String>,
|
||||
#[serde(default = "default_is_org_user")]
|
||||
pub is_organization_user: bool,
|
||||
#[serde(default)]
|
||||
pub role: Option<String>,
|
||||
}
|
||||
|
||||
const fn default_is_org_user() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ManagedVersion {
|
||||
pub version_id: String,
|
||||
pub project_id: String,
|
||||
pub display_name: String,
|
||||
pub display_version: String,
|
||||
pub changelog: Option<String>,
|
||||
pub dependencies: Vec<ManagedVersionDependency>,
|
||||
pub mc_versions: Vec<String>,
|
||||
pub release_type: PackageReleaseType,
|
||||
pub loaders: Vec<GameLoader>,
|
||||
pub published: DateTime<Utc>,
|
||||
pub downloads: usize,
|
||||
pub files: Vec<ManagedVersionFile>,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ManagedVersionDependency {
|
||||
pub version_id: Option<String>,
|
||||
pub project_id: Option<String>,
|
||||
pub file_name: Option<String>,
|
||||
pub dependency_type: PackageDependencyType,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ManagedVersionFile {
|
||||
pub sha1: String,
|
||||
pub url: String,
|
||||
pub file_name: String,
|
||||
pub primary: bool,
|
||||
pub size: usize,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PackageAuthor {
|
||||
Team {
|
||||
team_id: String,
|
||||
org_id: Option<String>,
|
||||
},
|
||||
Users(Vec<ManagedUser>),
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PackageSide {
|
||||
#[default]
|
||||
Unknown,
|
||||
Required,
|
||||
Optional,
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
/// https://spdx.org/licenses/
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PackageLicense {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
/// https://docs.curseforge.com/rest-api/#tocS_ModLinks
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PackageLinks {
|
||||
pub website: Option<String>,
|
||||
pub issues: Option<String>,
|
||||
pub source: Option<String>,
|
||||
pub wiki: Option<String>,
|
||||
pub donation: Option<Vec<PackageDonationUrl>>,
|
||||
pub discord: Option<String>,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PackageDonationUrl {
|
||||
#[serde(alias = "id")]
|
||||
pub platform: PackageDonationPlatform,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
/// https://api.modrinth.com/v2/tag/donation_platform
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PackageDonationPlatform {
|
||||
Patreon,
|
||||
#[serde(alias = "bmac")]
|
||||
BuyMeACoffee,
|
||||
PayPal,
|
||||
GitHub,
|
||||
#[serde(alias = "ko-fi")]
|
||||
KoFi,
|
||||
#[default]
|
||||
Other,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PackageStatus {
|
||||
#[default]
|
||||
#[serde(alias = "approved")]
|
||||
Active,
|
||||
#[serde(alias = "archived", alias = "inactive")]
|
||||
Abandoned,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PackageDependencyType {
|
||||
Required,
|
||||
Optional,
|
||||
Embedded,
|
||||
Incompatible,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PackageReleaseType {
|
||||
#[default]
|
||||
Release,
|
||||
Beta,
|
||||
Alpha,
|
||||
}
|
||||
5
packages/core/src/api/packages/mod.rs
Normal file
5
packages/core/src/api/packages/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod data;
|
||||
pub mod provider;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
141
packages/core/src/api/packages/provider/mod.rs
Normal file
141
packages/core/src/api/packages/provider/mod.rs
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
use onelauncher_entity::loader::GameLoader;
|
||||
use onelauncher_entity::package::Provider;
|
||||
|
||||
use crate::error::LauncherResult;
|
||||
use crate::utils::pagination::Paginated;
|
||||
|
||||
use super::data::{ManagedPackage, ManagedUser, ManagedVersion, PackageAuthor, SearchQuery};
|
||||
|
||||
mod modrinth;
|
||||
|
||||
pub use modrinth::ModrinthProviderImpl;
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
pub trait ProviderExt {
|
||||
async fn search(&self, query: &SearchQuery) -> LauncherResult<Paginated<ManagedPackage>>;
|
||||
async fn get(&self, slug: &str) -> LauncherResult<ManagedPackage>;
|
||||
async fn get_multiple(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedPackage>>;
|
||||
async fn get_versions_by_hashes(
|
||||
&self,
|
||||
hashes: &[String],
|
||||
) -> LauncherResult<Vec<ManagedVersion>>;
|
||||
async fn get_version_by_hash(&self, hash: &str) -> LauncherResult<ManagedVersion>;
|
||||
|
||||
// async fn get_org_projects(&self, slug: &str) -> LauncherResult<Vec<ManagedPackage>>;
|
||||
|
||||
async fn get_users_from_author(
|
||||
&self,
|
||||
author: PackageAuthor,
|
||||
) -> LauncherResult<Vec<ManagedUser>>;
|
||||
async fn get_users(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedUser>>;
|
||||
async fn get_user(&self, slug: &str) -> LauncherResult<ManagedUser>;
|
||||
|
||||
async fn get_versions_paginated(
|
||||
&self,
|
||||
slug: &str,
|
||||
mc_versions: Option<Vec<String>>,
|
||||
loaders: Option<Vec<GameLoader>>,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
) -> LauncherResult<Paginated<ManagedVersion>>;
|
||||
|
||||
async fn get_versions(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedVersion>>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ProviderExt for Provider {
|
||||
async fn search(&self, query: &SearchQuery) -> LauncherResult<Paginated<ManagedPackage>> {
|
||||
match self {
|
||||
Self::Modrinth => ModrinthProviderImpl.search(query).await,
|
||||
_ => todo!("unimplemented provider"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get(&self, slug: &str) -> LauncherResult<ManagedPackage> {
|
||||
match self {
|
||||
Self::Modrinth => ModrinthProviderImpl.get(slug).await,
|
||||
_ => todo!("unimplemented provider"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_multiple(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedPackage>> {
|
||||
match self {
|
||||
Self::Modrinth => ModrinthProviderImpl.get_multiple(slugs).await,
|
||||
_ => todo!("unimplemented provider"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_versions_by_hashes(
|
||||
&self,
|
||||
hashes: &[String],
|
||||
) -> LauncherResult<Vec<ManagedVersion>> {
|
||||
match self {
|
||||
Self::Modrinth => {
|
||||
ModrinthProviderImpl
|
||||
.get_versions_by_hashes(hashes)
|
||||
.await
|
||||
}
|
||||
_ => todo!("unimplemented provider"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_version_by_hash(&self, hash: &str) -> LauncherResult<ManagedVersion> {
|
||||
match self {
|
||||
Self::Modrinth => ModrinthProviderImpl.get_version_by_hash(hash).await,
|
||||
_ => todo!("unimplemented provider"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_users_from_author(
|
||||
&self,
|
||||
author: PackageAuthor,
|
||||
) -> LauncherResult<Vec<ManagedUser>> {
|
||||
match self {
|
||||
Self::Modrinth => {
|
||||
ModrinthProviderImpl
|
||||
.get_users_from_author(author)
|
||||
.await
|
||||
}
|
||||
_ => todo!("unimplemented provider"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_users(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedUser>> {
|
||||
match self {
|
||||
Self::Modrinth => ModrinthProviderImpl.get_users(slugs).await,
|
||||
_ => todo!("unimplemented provider"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_user(&self, slug: &str) -> LauncherResult<ManagedUser> {
|
||||
match self {
|
||||
Self::Modrinth => ModrinthProviderImpl.get_user(slug).await,
|
||||
_ => todo!("unimplemented provider"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_versions_paginated(
|
||||
&self,
|
||||
slug: &str,
|
||||
mc_versions: Option<Vec<String>>,
|
||||
loaders: Option<Vec<GameLoader>>,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
) -> LauncherResult<Paginated<ManagedVersion>> {
|
||||
match self {
|
||||
Self::Modrinth => {
|
||||
ModrinthProviderImpl
|
||||
.get_versions_paginated(slug, mc_versions, loaders, offset, limit)
|
||||
.await
|
||||
}
|
||||
_ => todo!("unimplemented provider"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_versions(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedVersion>> {
|
||||
match self {
|
||||
Self::Modrinth => ModrinthProviderImpl.get_versions(slugs).await,
|
||||
_ => todo!("unimplemented provider"),
|
||||
}
|
||||
}
|
||||
}
|
||||
473
packages/core/src/api/packages/provider/modrinth.rs
Normal file
473
packages/core/src/api/packages/provider/modrinth.rs
Normal file
|
|
@ -0,0 +1,473 @@
|
|||
use super::ProviderExt;
|
||||
use crate::api::packages::data::{
|
||||
ManagedPackage, ManagedUser, ManagedVersion, ManagedVersionDependency, ManagedVersionFile,
|
||||
PackageAuthor, PackageDependencyType, PackageDonationUrl, PackageLicense, PackageLinks,
|
||||
PackageReleaseType, PackageSide, PackageStatus, SearchQuery, DEFAULT_LIMIT,
|
||||
};
|
||||
use crate::error::LauncherResult;
|
||||
use crate::utils::http;
|
||||
use crate::utils::pagination::Paginated;
|
||||
use chrono::{DateTime, Utc};
|
||||
use onelauncher_entity::loader::GameLoader;
|
||||
use onelauncher_entity::package::{PackageType, Provider};
|
||||
use reqwest::Method;
|
||||
use serde::Deserialize;
|
||||
use serde_with::serde_as;
|
||||
use url::Url;
|
||||
|
||||
macro_rules! url {
|
||||
($($arg:tt)*) => {
|
||||
format!("{}/v2{}", crate::constants::MODRINTH_API_URL, format!($($arg)*)).as_str()
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! url_v3 {
|
||||
($($arg:tt)*) => {
|
||||
format!("{}/v3{}", crate::constants::MODRINTH_API_URL, format!($($arg)*)).as_str()
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ModrinthProviderImpl;
|
||||
|
||||
impl ModrinthProviderImpl {
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ProviderExt for ModrinthProviderImpl {
|
||||
async fn search(&self, query: &SearchQuery) -> LauncherResult<Paginated<ManagedPackage>> {
|
||||
let mut url = Url::parse(url!("/search"))?;
|
||||
let mut params = url.query_pairs_mut();
|
||||
|
||||
params.append_pair("limit", &query.limit.unwrap_or(DEFAULT_LIMIT).to_string());
|
||||
params.append_pair("offset", &query.offset.unwrap_or(0).to_string());
|
||||
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get(&self, slug: &str) -> LauncherResult<ManagedPackage> {
|
||||
Ok(
|
||||
http::fetch_json::<ModrinthPackage>(Method::GET, url!("/project/{}", slug), None, None)
|
||||
.await?
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_multiple(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedPackage>> {
|
||||
Ok(http::fetch_json::<Vec<ModrinthPackage>>(
|
||||
Method::GET,
|
||||
url!(
|
||||
"/projects?ids=[{}]",
|
||||
slugs
|
||||
.iter()
|
||||
.map(|id| format!("\"{id}\""))
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_versions_by_hashes(
|
||||
&self,
|
||||
hashes: &[String],
|
||||
) -> LauncherResult<Vec<ManagedVersion>> {
|
||||
let body = serde_json::json!({
|
||||
"hashes": hashes,
|
||||
"algorithm": "sha1"
|
||||
});
|
||||
|
||||
Ok(http::fetch_json::<Vec<ModrinthVersion>>(
|
||||
Method::POST,
|
||||
url!("/version_files"),
|
||||
Some(body),
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_version_by_hash(&self, hash: &str) -> LauncherResult<ManagedVersion> {
|
||||
Ok(http::fetch_json::<ModrinthVersion>(
|
||||
Method::GET,
|
||||
url!("/version_file/{hash}"),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
.into())
|
||||
}
|
||||
|
||||
// async fn get_org_projects(&self, slug: &str) -> LauncherResult<Vec<ManagedPackage>> {
|
||||
// Ok(http::fetch_json(Method::GET, url_v3!("/organizations/{slug}/projects"), None, None).await?)
|
||||
// }
|
||||
|
||||
async fn get_users_from_author(&self, author: PackageAuthor) -> LauncherResult<Vec<ManagedUser>> {
|
||||
let (team_id, org_id) = match author {
|
||||
PackageAuthor::Team { team_id, org_id } => (team_id, org_id),
|
||||
PackageAuthor::Users(users) => {
|
||||
return Ok(users);
|
||||
}
|
||||
};
|
||||
|
||||
let mut users = Vec::new();
|
||||
|
||||
if let Some(org_id) = org_id {
|
||||
#[derive(Deserialize)]
|
||||
struct Organization {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub icon_url: Option<String>,
|
||||
pub description: String,
|
||||
// pub team_id: String,
|
||||
// pub members: Vec<TeamMember>,
|
||||
}
|
||||
|
||||
let organization = http::fetch_json::<Organization>(Method::GET, url_v3!("/organization/{org_id}"), None, None).await?;
|
||||
|
||||
let org_user = ManagedUser {
|
||||
id: organization.id.clone(),
|
||||
username: organization.name,
|
||||
avatar_url: organization.icon_url,
|
||||
bio: Some(organization.description),
|
||||
url: Some(format!("{}/organization/{}", Provider::Modrinth.website(), organization.id)),
|
||||
is_organization_user: true,
|
||||
role: None,
|
||||
};
|
||||
|
||||
users.push(org_user);
|
||||
}
|
||||
|
||||
let project_members = http::fetch_json::<Vec<TeamMember>>(
|
||||
Method::GET,
|
||||
url!("/team/{}/members", team_id),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let project_members = project_members
|
||||
.into_iter()
|
||||
.map(Into::<ManagedUser>::into)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
users.extend(project_members);
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
async fn get_users(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedUser>> {
|
||||
Ok(http::fetch_json::<Vec<ModrinthUser>>(
|
||||
Method::GET,
|
||||
url!("/users?ids=[{}]", &serde_json::to_string(&slugs)?),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_user(&self, slug: &str) -> LauncherResult<ManagedUser> {
|
||||
Ok(
|
||||
http::fetch_json::<ModrinthUser>(Method::GET, url!("/user/{slug}"), None, None)
|
||||
.await?
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_versions_paginated(
|
||||
&self,
|
||||
slug: &str,
|
||||
mc_versions: Option<Vec<String>>,
|
||||
loaders: Option<Vec<GameLoader>>,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
) -> LauncherResult<Paginated<ManagedVersion>> {
|
||||
let mut url = Url::parse(url!("/project/{slug}/version"))?;
|
||||
|
||||
if let Some(mc_versions) = mc_versions {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("game_versions", &serde_json::to_string(&mc_versions)?);
|
||||
}
|
||||
|
||||
if let Some(loaders) = loaders {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("loaders", &serde_json::to_string(&loaders)?);
|
||||
}
|
||||
|
||||
// url.query_pairs_mut()
|
||||
// .append_pair("offset", &offset.to_string())
|
||||
// .append_pair("limit", &limit.to_string());
|
||||
|
||||
let response =
|
||||
http::fetch_json::<Vec<ModrinthVersion>>(Method::GET, url.as_str(), None, None).await?;
|
||||
|
||||
let paginated = response
|
||||
.into_iter()
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.collect::<Vec<ModrinthVersion>>();
|
||||
|
||||
Ok(Paginated {
|
||||
offset,
|
||||
limit,
|
||||
total: paginated.len(),
|
||||
items: paginated.into_iter().map(Into::into).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_versions(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedVersion>> {
|
||||
Ok(http::fetch_json::<Vec<ModrinthVersion>>(
|
||||
Method::GET,
|
||||
url!(
|
||||
"/versions?ids=[{}]",
|
||||
slugs
|
||||
.iter()
|
||||
.map(|v| format!("\"{v}\""))
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Deserialize)]
|
||||
struct ModrinthPackage {
|
||||
// pub slug: String,
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub categories: Vec<String>,
|
||||
pub client_side: PackageSide,
|
||||
pub server_side: PackageSide,
|
||||
#[serde(default)]
|
||||
pub body: String,
|
||||
// #[serde(default)]
|
||||
// #[serde(alias = "display_categories")]
|
||||
// pub additional_categories: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub issues_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub source_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub wiki_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub discord_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub donation_urls: Vec<PackageDonationUrl>,
|
||||
pub project_type: PackageType,
|
||||
pub downloads: usize,
|
||||
#[serde(default)]
|
||||
pub icon_url: Option<String>,
|
||||
#[serde(alias = "project_id")]
|
||||
pub id: String,
|
||||
pub team: String,
|
||||
#[serde(default)]
|
||||
pub organization: Option<String>,
|
||||
#[serde(alias = "date_created")]
|
||||
pub published: DateTime<Utc>,
|
||||
#[serde(alias = "date_modified")]
|
||||
pub updated: DateTime<Utc>,
|
||||
// pub followers: u32,
|
||||
#[serde(default)]
|
||||
pub versions: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub game_versions: Vec<String>,
|
||||
#[serde_as(as = "serde_with::VecSkipError<_>")]
|
||||
pub loaders: Vec<GameLoader>,
|
||||
#[serde(default)]
|
||||
pub license: Option<PackageLicense>,
|
||||
// #[serde(default)]
|
||||
// pub gallery: Vec<Gallery>,
|
||||
#[serde(default)]
|
||||
pub status: PackageStatus,
|
||||
}
|
||||
|
||||
impl From<ModrinthPackage> for ManagedPackage {
|
||||
fn from(value: ModrinthPackage) -> Self {
|
||||
Self {
|
||||
provider: Provider::Modrinth,
|
||||
provider_id: value.id,
|
||||
package_type: value.project_type,
|
||||
name: value.title,
|
||||
short_desc: value.description,
|
||||
body: value.body,
|
||||
version_ids: value.versions,
|
||||
mc_versions: value.game_versions,
|
||||
loaders: value.loaders,
|
||||
icon_url: value.icon_url,
|
||||
created: value.published,
|
||||
updated: value.updated,
|
||||
client: value.client_side,
|
||||
server: value.server_side,
|
||||
categories: value.categories,
|
||||
license: value.license,
|
||||
author: PackageAuthor::Team {
|
||||
team_id: value.team,
|
||||
org_id: value.organization,
|
||||
},
|
||||
links: PackageLinks {
|
||||
source: value.source_url,
|
||||
discord: value.discord_url,
|
||||
issues: value.issues_url,
|
||||
wiki: value.wiki_url,
|
||||
donation: if value.donation_urls.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value.donation_urls)
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
status: value.status,
|
||||
downloads: value.downloads,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ModrinthVersion {
|
||||
pub name: String,
|
||||
pub version_number: String,
|
||||
pub changelog: Option<String>,
|
||||
pub dependencies: Vec<ModrinthDependency>,
|
||||
pub game_versions: Vec<String>,
|
||||
pub version_type: PackageReleaseType,
|
||||
pub loaders: Vec<GameLoader>,
|
||||
// pub featured: bool,
|
||||
pub id: String,
|
||||
pub project_id: String,
|
||||
pub date_published: DateTime<Utc>,
|
||||
pub downloads: usize,
|
||||
pub files: Vec<ModrinthFile>,
|
||||
}
|
||||
|
||||
impl From<ModrinthVersion> for ManagedVersion {
|
||||
fn from(value: ModrinthVersion) -> Self {
|
||||
Self {
|
||||
version_id: value.id,
|
||||
project_id: value.project_id,
|
||||
display_name: value.name,
|
||||
display_version: value.version_number,
|
||||
changelog: value.changelog,
|
||||
dependencies: value.dependencies.into_iter().map(Into::into).collect(),
|
||||
mc_versions: value.game_versions,
|
||||
release_type: value.version_type,
|
||||
loaders: value.loaders,
|
||||
published: value.date_published,
|
||||
downloads: value.downloads,
|
||||
files: value.files.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ModrinthDependency {
|
||||
pub version_id: Option<String>,
|
||||
pub project_id: Option<String>,
|
||||
pub file_name: Option<String>,
|
||||
pub dependency_type: PackageDependencyType,
|
||||
}
|
||||
|
||||
impl From<ModrinthDependency> for ManagedVersionDependency {
|
||||
fn from(value: ModrinthDependency) -> Self {
|
||||
Self {
|
||||
version_id: value.version_id,
|
||||
project_id: value.project_id,
|
||||
file_name: value.file_name,
|
||||
dependency_type: value.dependency_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ModrinthFile {
|
||||
pub hashes: ModrinthFileHashes,
|
||||
pub url: String,
|
||||
pub filename: String,
|
||||
pub primary: bool,
|
||||
pub size: usize,
|
||||
}
|
||||
|
||||
impl From<ModrinthFile> for ManagedVersionFile {
|
||||
fn from(value: ModrinthFile) -> Self {
|
||||
Self {
|
||||
sha1: value.hashes.sha1,
|
||||
url: value.url,
|
||||
file_name: value.filename,
|
||||
primary: value.primary,
|
||||
size: value.size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ModrinthFileHashes {
|
||||
pub sha1: String,
|
||||
// pub sha512: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TeamMember {
|
||||
#[serde(default)]
|
||||
pub role: Option<String>,
|
||||
pub user: ModrinthUser,
|
||||
}
|
||||
|
||||
impl From<TeamMember> for ManagedUser {
|
||||
fn from(value: TeamMember) -> Self {
|
||||
let mut user: Self = value.user.into();
|
||||
user.role = value.role;
|
||||
user
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ModrinthUser {
|
||||
pub id: String,
|
||||
pub username: String,
|
||||
pub avatar_url: String,
|
||||
pub bio: String,
|
||||
}
|
||||
|
||||
impl From<ModrinthUser> for ManagedUser {
|
||||
fn from(value: ModrinthUser) -> Self {
|
||||
Self {
|
||||
id: value.id.clone(),
|
||||
username: value.username,
|
||||
url: Some(format!(
|
||||
"{}/user/{}",
|
||||
Provider::Modrinth.website(),
|
||||
value.id
|
||||
)),
|
||||
avatar_url: Some(value.avatar_url),
|
||||
bio: if value.bio.is_empty() || value.bio.eq_ignore_ascii_case("a modrinth creator.") {
|
||||
None
|
||||
} else {
|
||||
Some(value.bio)
|
||||
},
|
||||
is_organization_user: false,
|
||||
role: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
44
packages/core/src/api/packages/tests.rs
Normal file
44
packages/core/src/api/packages/tests.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use onelauncher_entity::package::Provider;
|
||||
|
||||
use crate::{api::packages::provider::ProviderExt, error::LauncherResult, store::{Core, CoreOptions}};
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_get_provider() -> LauncherResult<()> {
|
||||
Core::initialize(CoreOptions::default()).await?;
|
||||
|
||||
let provider = Provider::Modrinth;
|
||||
|
||||
let res = provider.get("oneconfig").await?;
|
||||
|
||||
assert_eq!(res.provider_id, "AibBIVmj");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_get_multiple() -> LauncherResult<()> {
|
||||
Core::initialize(CoreOptions::default()).await?;
|
||||
|
||||
let provider = Provider::Modrinth;
|
||||
|
||||
let res = provider.get_multiple(&["oneconfig".to_string(), "chatting".to_string()]).await?;
|
||||
|
||||
assert_eq!(res.len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_get_versions() -> LauncherResult<()> {
|
||||
Core::initialize(CoreOptions::default()).await?;
|
||||
|
||||
let provider = Provider::Modrinth;
|
||||
|
||||
let res = provider.get_versions(&["ZvlCAdEF".to_string(), "GQANlg7p".to_string()]).await?;
|
||||
|
||||
assert_eq!(res.len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: Add more tests
|
||||
|
|
@ -37,6 +37,8 @@ pub enum LauncherError {
|
|||
RegexError(#[from] regex::Error),
|
||||
#[error("couldn't acquire semaphore: {0}")]
|
||||
SemaphoreError(#[from] tokio::sync::AcquireError),
|
||||
#[error(transparent)]
|
||||
UrlError(#[from] url::ParseError),
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
#[error(transparent)]
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ impl HashAlgorithm {
|
|||
|
||||
pub async fn hasher<F>(&self, closure: F) -> Result<String, IOError>
|
||||
where
|
||||
F: AsyncFn(&mut (dyn DynDigest)) -> Result<(), IOError>,
|
||||
F: AsyncFn(&mut (dyn DynDigest + Send + Sync)) -> Result<(), IOError> + Send + Sync,
|
||||
{
|
||||
let mut hasher = get_inner(self);
|
||||
closure(&mut *hasher).await?;
|
||||
|
|
@ -62,8 +62,8 @@ impl HashAlgorithm {
|
|||
}
|
||||
|
||||
pub async fn hash_file(&self, path: impl AsRef<std::path::Path>) -> Result<String, IOError> {
|
||||
self.hasher(async |hasher: &mut dyn DynDigest| {
|
||||
let path = path.as_ref();
|
||||
let path = path.as_ref();
|
||||
self.hasher(async |hasher: &mut (dyn DynDigest + Send + Sync)| {
|
||||
let file = tokio::fs::File::open(path).await?;
|
||||
let mut reader = tokio::io::BufReader::new(file);
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ impl FromStr for HashAlgorithm {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_inner(algorithm: &HashAlgorithm) -> Box<dyn DynDigest> {
|
||||
fn get_inner(algorithm: &HashAlgorithm) -> Box<dyn DynDigest + Send + Sync> {
|
||||
match algorithm {
|
||||
HashAlgorithm::Sha1 => Box::new(sha1::Sha1::default()),
|
||||
HashAlgorithm::Sha256 => Box::new(sha2::Sha256::default()),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use std::sync::{Arc, LazyLock};
|
|||
|
||||
use bytes::Bytes;
|
||||
use reqwest::Method;
|
||||
use serde::Deserialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ pub(crate) static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
|||
.expect("failed to build reqwest client!")
|
||||
});
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(body, headers, ingress))]
|
||||
#[tracing::instrument(level = "debug", skip(ingress))]
|
||||
#[onelauncher_macro::pin]
|
||||
pub async fn fetch_advanced(
|
||||
method: Method,
|
||||
|
|
@ -147,7 +147,7 @@ pub async fn fetch(method: Method, url: &str) -> LauncherResult<Bytes> {
|
|||
fetch_advanced(method, url, None, None, None, None).await
|
||||
}
|
||||
|
||||
pub async fn fetch_json<T: for<'de> Deserialize<'de>>(
|
||||
pub async fn fetch_json<T: DeserializeOwned>(
|
||||
method: Method,
|
||||
url: &str,
|
||||
body: Option<serde_json::Value>,
|
||||
|
|
@ -156,7 +156,7 @@ pub async fn fetch_json<T: for<'de> Deserialize<'de>>(
|
|||
fetch_json_advanced(method, url, body, None, None, ingress).await
|
||||
}
|
||||
|
||||
pub async fn fetch_json_advanced<T: for<'de> Deserialize<'de>>(
|
||||
pub async fn fetch_json_advanced<T: DeserializeOwned>(
|
||||
method: Method,
|
||||
url: &str,
|
||||
body: Option<serde_json::Value>,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod crypto;
|
||||
pub mod os_ext;
|
||||
pub mod http;
|
||||
pub mod io;
|
||||
pub mod io;
|
||||
pub mod pagination;
|
||||
31
packages/core/src/utils/pagination.rs
Normal file
31
packages/core/src/utils/pagination.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use std::fmt::Debug;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Paginated<T>
|
||||
where T: Clone + Serialize {
|
||||
pub total: usize,
|
||||
pub offset: usize,
|
||||
pub limit: usize,
|
||||
pub items: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T: Clone + Serialize> Debug for Paginated<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Paginated")
|
||||
.field("total", &self.total)
|
||||
.field("offset", &self.offset)
|
||||
.field("limit", &self.limit)
|
||||
.field("items", &self.items.len())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl <T: Clone + Serialize> Paginated<T> {
|
||||
#[must_use]
|
||||
pub const fn new(total: usize, offset: usize, limit: usize, items: Vec<T>) -> Self {
|
||||
Self { total, offset, limit, items }
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize};
|
|||
#[sea_orm(rs_type = "u8", db_type = "Integer")]
|
||||
pub enum GameLoader {
|
||||
#[default]
|
||||
#[serde(alias = "minecraft")]
|
||||
Vanilla = 0,
|
||||
Forge = 1,
|
||||
NeoForge = 2,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
pub mod cluster_stage;
|
||||
pub mod icon;
|
||||
pub mod loader;
|
||||
pub mod package;
|
||||
pub mod resolution;
|
||||
pub mod settings;
|
||||
pub mod settings;
|
||||
|
|
|
|||
70
packages/entity/src/domain/package.rs
Normal file
70
packages/entity/src/domain/package.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
use std::fmt::Display;
|
||||
|
||||
use sea_orm::{DeriveActiveEnum, EnumIter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, EnumIter, DeriveActiveEnum)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[sea_orm(rs_type = "u8", db_type = "Integer")]
|
||||
pub enum PackageType {
|
||||
Mod = 0,
|
||||
ResourcePack = 1,
|
||||
Shader = 2,
|
||||
DataPack = 3,
|
||||
ModPack = 4,
|
||||
}
|
||||
|
||||
impl PackageType {
|
||||
#[must_use]
|
||||
pub fn folder_name(&self) -> String {
|
||||
match self {
|
||||
Self::Mod => "mods",
|
||||
Self::ResourcePack => "resourcepacks",
|
||||
Self::Shader => "shaders",
|
||||
Self::DataPack => "datapacks",
|
||||
Self::ModPack => "modpacks",
|
||||
}.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, EnumIter, DeriveActiveEnum)]
|
||||
#[sea_orm(rs_type = "u8", db_type = "Integer")]
|
||||
pub enum Provider {
|
||||
Modrinth = 0,
|
||||
Curseforge = 1,
|
||||
SkyClient = 2,
|
||||
}
|
||||
|
||||
impl Display for Provider {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.name())
|
||||
}
|
||||
}
|
||||
|
||||
impl Provider {
|
||||
#[must_use]
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
Self::Modrinth => "Modrinth",
|
||||
Self::Curseforge => "Curseforge",
|
||||
Self::SkyClient => "SkyClient",
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the URL of the provider with a trailing slash
|
||||
#[must_use]
|
||||
pub fn website(&self) -> &str {
|
||||
match self {
|
||||
Self::Modrinth => "https://modrinth.com/",
|
||||
Self::Curseforge => "https://curseforge.com/",
|
||||
Self::SkyClient => "https://skyclient.co/",
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn get_providers() -> &'static [Self] {
|
||||
&[Self::Modrinth, Self::Curseforge, Self::SkyClient]
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::icon::Icon;
|
||||
use crate::{icon::Icon, package::{PackageType, Provider}};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "packages")]
|
||||
|
|
@ -17,8 +17,8 @@ pub struct Model {
|
|||
pub display_name: String,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub display_version: String,
|
||||
pub type_id: u32,
|
||||
pub provider_id: u32,
|
||||
pub project_type_id: PackageType,
|
||||
pub provider_id: Provider,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub provider_version: String,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ create table packages (
|
|||
file_name text not null,
|
||||
display_name text not null,
|
||||
display_version text not null,
|
||||
type_id integer not null,
|
||||
project_type_id integer not null,
|
||||
provider_id integer not null,
|
||||
provider_version text not null,
|
||||
mc_versions text not null,
|
||||
|
|
@ -30,6 +30,10 @@ create table packages (
|
|||
constraint packages_pk primary key (hash)
|
||||
);
|
||||
|
||||
create index packages_project_type_id_idx on packages (project_type_id);
|
||||
create index packages_provider_id_idx on packages (provider_id);
|
||||
create index packages_mc_loader_mc_versions_idx on packages (mc_loader, mc_versions);
|
||||
|
||||
create table setting_profiles (
|
||||
name text not null,
|
||||
java_id integer,
|
||||
|
|
|
|||
19
packages/serde_json_path_to_error/Cargo.toml
Normal file
19
packages/serde_json_path_to_error/Cargo.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "serde_json_path_to_error"
|
||||
version = "0.2.0"
|
||||
authors = [ "Polyfrost", "Ethan Brierley <ethanboxx@gmail.com>" ]
|
||||
license = "MIT/Apache-2.0"
|
||||
readme = "README.md"
|
||||
description = """
|
||||
A drop in replacement for serde_json where detailed errors are the default
|
||||
"""
|
||||
keywords = [ "serde", "json", "serialization" ]
|
||||
documentation = "https://docs.rs/serde_json_path_to_error/"
|
||||
repository = "https://github.com/eopb/serde_json_path_to_error"
|
||||
edition = "2024"
|
||||
exclude = [ ".github" ]
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_path_to_error = "0.1.17"
|
||||
23
packages/serde_json_path_to_error/LICENSE
Normal file
23
packages/serde_json_path_to_error/LICENSE
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
107
packages/serde_json_path_to_error/README.md
Normal file
107
packages/serde_json_path_to_error/README.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# serde_json_path_to_error
|
||||
|
||||
[](https://crates.io/crates/serde_json_path_to_error)
|
||||
[](https://crates.io/crates/serde_json_path_to_error)
|
||||
[](https://docs.rs/serde_json_path_to_error/)
|
||||
[](https://crates.io/crates/serde_json_path_to_error)
|
||||
|
||||
[API docs](https://docs.rs/serde_path_to_error/)
|
||||
|
||||
A drop in replacement for [serde_json] with errors enriched by [serde_path_to_error].
|
||||
|
||||
This is usually a better default since it makes it easier to debug when serialization or deserialization fails.
|
||||
Paths are particularly helpful when your schema is large or when it's difficult to see the raw data that causes an error.
|
||||
|
||||
This crate exposes the same items as [serde_json], just with different error types.
|
||||
For more detailed documentation see [serde_json].
|
||||
|
||||
## Migrating from [serde_json]
|
||||
|
||||
To enrich your errors simply replace your dependency on [serde_json] with one on serde_json_path_to_error.
|
||||
|
||||
```diff
|
||||
- serde_json = "1.0"
|
||||
+ serde_json = { package = "serde_json_path_to_error", version = "0.1" }
|
||||
```
|
||||
|
||||
Alternatively, you can add serde_json_path_to_error as a regular dependancy...
|
||||
|
||||
```text
|
||||
# cargo add serde_json_path_to_error
|
||||
```
|
||||
|
||||
..and rename the crate in your crate root to get the same API as [serde_json].
|
||||
|
||||
```rust
|
||||
extern crate serde_json_path_to_error as serde_json;
|
||||
```
|
||||
|
||||
In most cases, your project should continue to compile after migrating.
|
||||
Your errors will now be enriched with additional context showing the path to serialization and deserialization failures.
|
||||
|
||||
```rust
|
||||
// the rename trick shown above
|
||||
extern crate serde_json_path_to_error as serde_json;
|
||||
|
||||
# use std::collections::BTreeMap as Map;
|
||||
# use serde::Deserialize;
|
||||
#[derive(Deserialize)]
|
||||
struct Package {
|
||||
name: String,
|
||||
dependencies: Map<String, Dependency>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Dependency {
|
||||
version: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let j = r#"{
|
||||
"name": "demo",
|
||||
"dependencies": {
|
||||
"serde": {
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
|
||||
// Uses the enriched version from [serde_json_path_to_error] but with the exact same API
|
||||
// you've come to expect from [serde_json]
|
||||
let result: Result<Package, _> = serde_json::from_str(j);
|
||||
|
||||
match result {
|
||||
Ok(_) => panic!("expected a type error"),
|
||||
Err(err) => {
|
||||
// You get the error including the path as a default
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"dependencies.serde.version: invalid type: integer `1`, expected a string at line 5 column 28",
|
||||
);
|
||||
// You can get just the path
|
||||
assert_eq!(
|
||||
err.path().to_string(),
|
||||
"dependencies.serde.version",
|
||||
);
|
||||
// Or just the original serde_json error
|
||||
assert_eq!(
|
||||
err.into_inner().to_string(),
|
||||
"invalid type: integer `1`, expected a string at line 5 column 28",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Caveats
|
||||
|
||||
There are still a small number of items that don't return enriched errors.
|
||||
I'd be interested in accepting PRs that wrap these items.
|
||||
|
||||
- [serde_json::de::Deserializer] [#6](https://github.com/eopb/serde_json_path_to_error/issues/6)
|
||||
- [serde_json::de::StreamDeserializer] [#5](https://github.com/eopb/serde_json_path_to_error/issues/5)
|
||||
- [serde_json::ser::Serializer] [#4](https://github.com/eopb/serde_json_path_to_error/issues/4)
|
||||
- [serde_json::value::Serializer] [#3](https://github.com/eopb/serde_json_path_to_error/issues/3)
|
||||
|
||||
[serde_json]: https://docs.rs/serde_json/latest/serde_json/
|
||||
[serde_path_to_error]: https://docs.rs/serde_json/latest/serde_path_to_error/
|
||||
239
packages/serde_json_path_to_error/src/lib.rs
Normal file
239
packages/serde_json_path_to_error/src/lib.rs
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
|
||||
pub use serde_json::json;
|
||||
|
||||
pub use de::{Deserializer, StreamDeserializer};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use de::{from_reader, from_slice, from_str};
|
||||
/// Deserialize JSON data to a Rust data structure.
|
||||
pub mod de {
|
||||
// See caveats in README.md
|
||||
pub use serde_json::de::{Deserializer, StreamDeserializer};
|
||||
|
||||
use crate::Result;
|
||||
|
||||
use std::io;
|
||||
|
||||
pub use serde_json::de::{IoRead, Read, SliceRead, StrRead};
|
||||
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
|
||||
/// Deserialize an instance of type `T` from a string of JSON text.
|
||||
///
|
||||
/// Equivalent to [serde_json::from_str] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::from_str] for more documentation.
|
||||
pub fn from_str<'a, T>(s: &'a str) -> Result<T>
|
||||
where
|
||||
T: Deserialize<'a>,
|
||||
{
|
||||
let jd = &mut serde_json::Deserializer::from_str(s);
|
||||
|
||||
serde_path_to_error::deserialize(jd)
|
||||
}
|
||||
|
||||
/// Deserialize an instance of type `T` from an I/O stream of JSON.
|
||||
///
|
||||
/// Equivalent to [serde_json::from_reader] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::from_reader] for more documentation.
|
||||
pub fn from_reader<R, T>(rdr: R) -> Result<T>
|
||||
where
|
||||
R: io::Read,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let jd = &mut serde_json::Deserializer::from_reader(rdr);
|
||||
|
||||
serde_path_to_error::deserialize(jd)
|
||||
}
|
||||
|
||||
/// Deserialize an instance of type `T` from bytes of JSON text.
|
||||
///
|
||||
/// Equivalent to [serde_json::from_slice] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::from_slice] for more documentation.
|
||||
pub fn from_slice<'a, T>(v: &'a [u8]) -> Result<T>
|
||||
where
|
||||
T: Deserialize<'a>,
|
||||
{
|
||||
let jd = &mut serde_json::Deserializer::from_slice(v);
|
||||
|
||||
serde_path_to_error::deserialize(jd)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(inline)]
|
||||
pub use error::{Error, Result};
|
||||
/// When serializing or deserializing JSON goes wrong.
|
||||
pub mod error {
|
||||
pub use serde_json::error::Category;
|
||||
/// This type represents all possible errors that can occur when serializing or
|
||||
/// deserializing JSON data.
|
||||
pub type Error = serde_path_to_error::Error<serde_json::Error>;
|
||||
/// Alias for a `Result` with the error type `serde_json_path_to_error::Error`.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
}
|
||||
|
||||
#[doc(inline)]
|
||||
pub use ser::{
|
||||
to_string, to_string_pretty, to_vec, to_vec_pretty, to_writer, to_writer_pretty, Formatter,
|
||||
Serializer,
|
||||
};
|
||||
/// Serialize a Rust data structure into JSON data.
|
||||
pub mod ser {
|
||||
use crate::Result;
|
||||
use serde::Serialize;
|
||||
|
||||
static UTF8_ERROR: &str =
|
||||
"`serde_json` internally guarantees UTF8 and uses `String::from_utf8_unchecked`. \
|
||||
If this error throws, `serde_json` must have broken this guarantee";
|
||||
|
||||
pub use serde_json::ser::{CharEscape, CompactFormatter, Formatter, PrettyFormatter};
|
||||
|
||||
// See caveats in README.md
|
||||
pub use serde_json::ser::Serializer;
|
||||
|
||||
/// Serialize the given data structure as JSON into the I/O stream.
|
||||
///
|
||||
/// Equivalent to [serde_json::to_writer] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_writer] for more documentation.
|
||||
pub fn to_writer<W, T>(writer: W, value: &T) -> Result<()>
|
||||
where
|
||||
W: std::io::Write,
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let mut ser = Serializer::new(writer);
|
||||
serde_path_to_error::serialize(&value, &mut ser)
|
||||
}
|
||||
|
||||
/// Serialize the given data structure as a JSON byte vector.
|
||||
///
|
||||
/// Equivalent to [serde_json::to_vec] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_vec] for more documentation.
|
||||
pub fn to_vec<T>(value: &T) -> Result<Vec<u8>>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let mut bytes = Vec::new();
|
||||
|
||||
to_writer(&mut bytes, value)?;
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Serialize the given data structure as a String of JSON.
|
||||
///
|
||||
/// Equivalent to [serde_json::to_string] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_string] for more documentation.
|
||||
pub fn to_string<T>(value: &T) -> Result<String>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let vec = to_vec(value)?;
|
||||
|
||||
Ok(String::from_utf8(vec).expect(UTF8_ERROR))
|
||||
}
|
||||
|
||||
/// Serialize the given data structure as pretty-printed JSON into the I/O
|
||||
/// stream.
|
||||
///
|
||||
/// Equivalent to [serde_json::to_writer_pretty] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_writer_pretty] for more documentation.
|
||||
pub fn to_writer_pretty<W, T>(writer: W, value: &T) -> Result<()>
|
||||
where
|
||||
W: std::io::Write,
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let mut ser = Serializer::pretty(writer);
|
||||
serde_path_to_error::serialize(&value, &mut ser)
|
||||
}
|
||||
|
||||
/// Serialize the given data structure as a pretty-printed JSON byte vector.
|
||||
///
|
||||
/// Equivalent to [serde_json::to_vec_pretty] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_vec_pretty] for more documentation.
|
||||
pub fn to_vec_pretty<T>(value: &T) -> Result<Vec<u8>>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let mut bytes = Vec::new();
|
||||
|
||||
to_writer_pretty(&mut bytes, value)?;
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Serialize the given data structure as a pretty-printed String of JSON.
|
||||
///
|
||||
/// Equivalent to [serde_json::to_string_pretty] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_string_pretty] for more documentation.
|
||||
pub fn to_string_pretty<T>(value: &T) -> Result<String>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let vec = to_vec_pretty(value)?;
|
||||
|
||||
Ok(String::from_utf8(vec).expect(UTF8_ERROR))
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(inline)]
|
||||
pub use map::Map;
|
||||
/// A map of String to serde_json::Value.
|
||||
///
|
||||
/// See [serde_json::map] for more documentation.
|
||||
pub mod map {
|
||||
pub use serde_json::map::{
|
||||
Entry, IntoIter, Iter, IterMut, Keys, Map, OccupiedEntry, VacantEntry, Values, ValuesMut,
|
||||
};
|
||||
}
|
||||
|
||||
#[doc(inline)]
|
||||
pub use value::{from_value, to_value, Number, Value};
|
||||
/// The Value enum, a loosely typed way of representing any valid JSON value.
|
||||
///
|
||||
/// See [serde_json::value] for more documentation.
|
||||
pub mod value {
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
pub use serde_json::value::{Index, Map, Number, Value};
|
||||
|
||||
// See caveats in README.md
|
||||
pub use serde_json::value::Serializer;
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Convert a `T` into `serde_json::Value` which is an enum that can represent
|
||||
///
|
||||
/// Equivalent to [serde_json::to_value] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_value] for more documentation.
|
||||
pub fn to_value<T>(value: T) -> Result<Value>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
let mut track = serde_path_to_error::Track::new();
|
||||
let ps = serde_path_to_error::Serializer::new(Serializer, &mut track);
|
||||
|
||||
value.serialize(ps).map_err(|e| Error::new(track.path(), e))
|
||||
}
|
||||
|
||||
/// Interpret a `serde_json::Value` as an instance of type `T`.
|
||||
///
|
||||
/// Equivalent to [serde_json::from_value] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::from_value] for more documentation.
|
||||
pub fn from_value<T>(value: Value) -> Result<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
serde_path_to_error::deserialize(value)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue