From 17f8b382a7d30aaa877d658cb014378bd0c17c62 Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Thu, 24 Apr 2025 12:03:39 +0200 Subject: [PATCH] feat: Modrinth provider --- .vscode/settings.json | 24 +- Cargo.lock | 46 +- Cargo.toml | 4 +- packages/core/Cargo.toml | 4 +- packages/core/package.json | 3 +- packages/core/src/api/mod.rs | 7 +- packages/core/src/api/packages/data.rs | 217 ++++++++ packages/core/src/api/packages/mod.rs | 5 + .../core/src/api/packages/provider/mod.rs | 141 ++++++ .../src/api/packages/provider/modrinth.rs | 473 ++++++++++++++++++ packages/core/src/api/packages/tests.rs | 44 ++ packages/core/src/error.rs | 2 + packages/core/src/utils/crypto.rs | 8 +- packages/core/src/utils/http.rs | 8 +- packages/core/src/utils/mod.rs | 3 +- packages/core/src/utils/pagination.rs | 31 ++ packages/entity/src/domain/loader.rs | 1 + packages/entity/src/domain/mod.rs | 3 +- packages/entity/src/domain/package.rs | 70 +++ packages/entity/src/entities/packages.rs | 6 +- packages/migration/src/init.sql | 6 +- packages/serde_json_path_to_error/Cargo.toml | 19 + packages/serde_json_path_to_error/LICENSE | 23 + packages/serde_json_path_to_error/README.md | 107 ++++ packages/serde_json_path_to_error/src/lib.rs | 239 +++++++++ 25 files changed, 1424 insertions(+), 70 deletions(-) create mode 100644 packages/core/src/api/packages/data.rs create mode 100644 packages/core/src/api/packages/mod.rs create mode 100644 packages/core/src/api/packages/provider/mod.rs create mode 100644 packages/core/src/api/packages/provider/modrinth.rs create mode 100644 packages/core/src/api/packages/tests.rs create mode 100644 packages/core/src/utils/pagination.rs create mode 100644 packages/entity/src/domain/package.rs create mode 100644 packages/serde_json_path_to_error/Cargo.toml create mode 100644 packages/serde_json_path_to_error/LICENSE create mode 100644 packages/serde_json_path_to_error/README.md create mode 100644 packages/serde_json_path_to_error/src/lib.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index f7af7f7e..1d162a47 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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": [], diff --git a/Cargo.lock b/Cargo.lock index fc728e55..879900d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 607a21e4..673465c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 8ca91d72..d2f43e1a 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -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 diff --git a/packages/core/package.json b/packages/core/package.json index 61a53f9e..4f66b956 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -13,6 +13,7 @@ "scripts": { "cargo": "cargo", "binary": "cargo run --bin", - "example": "cargo run --example" + "example": "cargo run --example", + "test": "cargo test" } } diff --git a/packages/core/src/api/mod.rs b/packages/core/src/api/mod.rs index 8a0362f7..88a43f71 100644 --- a/packages/core/src/api/mod.rs +++ b/packages/core/src/api/mod.rs @@ -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; \ No newline at end of file +pub mod proxy; +pub mod setting_profiles; diff --git a/packages/core/src/api/packages/data.rs b/packages/core/src/api/packages/data.rs new file mode 100644 index 00000000..e1689419 --- /dev/null +++ b/packages/core/src/api/packages/data.rs @@ -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>, + pub loaders: Option>, + pub categories: Option>, + pub package_types: Option>, +} + +#[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, + pub offset: Option, + pub limit: Option, + pub sort: Option, + pub filters: Option, +} + +#[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, + pub mc_versions: Vec, + #[serde_as(as = "serde_with::VecSkipError<_>")] + pub loaders: Vec, + pub icon_url: Option, + pub created: DateTime, + pub updated: DateTime, + pub client: PackageSide, + pub server: PackageSide, + pub categories: Vec, + pub license: Option, + 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, + #[serde(default)] + pub avatar_url: Option, + #[serde(default)] + pub bio: Option, + #[serde(default = "default_is_org_user")] + pub is_organization_user: bool, + #[serde(default)] + pub role: Option, +} + +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, + pub dependencies: Vec, + pub mc_versions: Vec, + pub release_type: PackageReleaseType, + pub loaders: Vec, + pub published: DateTime, + pub downloads: usize, + pub files: Vec, +} + +#[onelauncher_macro::specta] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ManagedVersionDependency { + pub version_id: Option, + pub project_id: Option, + pub file_name: Option, + 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, + }, + Users(Vec), +} + +#[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, +} + +/// 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, + pub issues: Option, + pub source: Option, + pub wiki: Option, + pub donation: Option>, + pub discord: Option, +} + +#[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, +} \ No newline at end of file diff --git a/packages/core/src/api/packages/mod.rs b/packages/core/src/api/packages/mod.rs new file mode 100644 index 00000000..2992a718 --- /dev/null +++ b/packages/core/src/api/packages/mod.rs @@ -0,0 +1,5 @@ +pub mod data; +pub mod provider; + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/packages/core/src/api/packages/provider/mod.rs b/packages/core/src/api/packages/provider/mod.rs new file mode 100644 index 00000000..fdd99058 --- /dev/null +++ b/packages/core/src/api/packages/provider/mod.rs @@ -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>; + async fn get(&self, slug: &str) -> LauncherResult; + async fn get_multiple(&self, slugs: &[String]) -> LauncherResult>; + async fn get_versions_by_hashes( + &self, + hashes: &[String], + ) -> LauncherResult>; + async fn get_version_by_hash(&self, hash: &str) -> LauncherResult; + + // async fn get_org_projects(&self, slug: &str) -> LauncherResult>; + + async fn get_users_from_author( + &self, + author: PackageAuthor, + ) -> LauncherResult>; + async fn get_users(&self, slugs: &[String]) -> LauncherResult>; + async fn get_user(&self, slug: &str) -> LauncherResult; + + async fn get_versions_paginated( + &self, + slug: &str, + mc_versions: Option>, + loaders: Option>, + offset: usize, + limit: usize, + ) -> LauncherResult>; + + async fn get_versions(&self, slugs: &[String]) -> LauncherResult>; +} + +#[async_trait::async_trait(?Send)] +impl ProviderExt for Provider { + async fn search(&self, query: &SearchQuery) -> LauncherResult> { + match self { + Self::Modrinth => ModrinthProviderImpl.search(query).await, + _ => todo!("unimplemented provider"), + } + } + + async fn get(&self, slug: &str) -> LauncherResult { + match self { + Self::Modrinth => ModrinthProviderImpl.get(slug).await, + _ => todo!("unimplemented provider"), + } + } + + async fn get_multiple(&self, slugs: &[String]) -> LauncherResult> { + match self { + Self::Modrinth => ModrinthProviderImpl.get_multiple(slugs).await, + _ => todo!("unimplemented provider"), + } + } + + async fn get_versions_by_hashes( + &self, + hashes: &[String], + ) -> LauncherResult> { + match self { + Self::Modrinth => { + ModrinthProviderImpl + .get_versions_by_hashes(hashes) + .await + } + _ => todo!("unimplemented provider"), + } + } + + async fn get_version_by_hash(&self, hash: &str) -> LauncherResult { + match self { + Self::Modrinth => ModrinthProviderImpl.get_version_by_hash(hash).await, + _ => todo!("unimplemented provider"), + } + } + + async fn get_users_from_author( + &self, + author: PackageAuthor, + ) -> LauncherResult> { + match self { + Self::Modrinth => { + ModrinthProviderImpl + .get_users_from_author(author) + .await + } + _ => todo!("unimplemented provider"), + } + } + + async fn get_users(&self, slugs: &[String]) -> LauncherResult> { + match self { + Self::Modrinth => ModrinthProviderImpl.get_users(slugs).await, + _ => todo!("unimplemented provider"), + } + } + + async fn get_user(&self, slug: &str) -> LauncherResult { + match self { + Self::Modrinth => ModrinthProviderImpl.get_user(slug).await, + _ => todo!("unimplemented provider"), + } + } + + async fn get_versions_paginated( + &self, + slug: &str, + mc_versions: Option>, + loaders: Option>, + offset: usize, + limit: usize, + ) -> LauncherResult> { + 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> { + match self { + Self::Modrinth => ModrinthProviderImpl.get_versions(slugs).await, + _ => todo!("unimplemented provider"), + } + } +} diff --git a/packages/core/src/api/packages/provider/modrinth.rs b/packages/core/src/api/packages/provider/modrinth.rs new file mode 100644 index 00000000..17a5a9de --- /dev/null +++ b/packages/core/src/api/packages/provider/modrinth.rs @@ -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> { + 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 { + Ok( + http::fetch_json::(Method::GET, url!("/project/{}", slug), None, None) + .await? + .into(), + ) + } + + async fn get_multiple(&self, slugs: &[String]) -> LauncherResult> { + Ok(http::fetch_json::>( + Method::GET, + url!( + "/projects?ids=[{}]", + slugs + .iter() + .map(|id| format!("\"{id}\"")) + .collect::>() + .join(",") + ), + None, + None, + ) + .await? + .into_iter() + .map(Into::into) + .collect()) + } + + async fn get_versions_by_hashes( + &self, + hashes: &[String], + ) -> LauncherResult> { + let body = serde_json::json!({ + "hashes": hashes, + "algorithm": "sha1" + }); + + Ok(http::fetch_json::>( + 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 { + Ok(http::fetch_json::( + Method::GET, + url!("/version_file/{hash}"), + None, + None, + ) + .await? + .into()) + } + + // async fn get_org_projects(&self, slug: &str) -> LauncherResult> { + // Ok(http::fetch_json(Method::GET, url_v3!("/organizations/{slug}/projects"), None, None).await?) + // } + + async fn get_users_from_author(&self, author: PackageAuthor) -> LauncherResult> { + 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, + pub description: String, + // pub team_id: String, + // pub members: Vec, + } + + let organization = http::fetch_json::(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::>( + Method::GET, + url!("/team/{}/members", team_id), + None, + None, + ) + .await?; + + let project_members = project_members + .into_iter() + .map(Into::::into) + .collect::>(); + + users.extend(project_members); + Ok(users) + } + + async fn get_users(&self, slugs: &[String]) -> LauncherResult> { + Ok(http::fetch_json::>( + 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 { + Ok( + http::fetch_json::(Method::GET, url!("/user/{slug}"), None, None) + .await? + .into(), + ) + } + + async fn get_versions_paginated( + &self, + slug: &str, + mc_versions: Option>, + loaders: Option>, + offset: usize, + limit: usize, + ) -> LauncherResult> { + 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::>(Method::GET, url.as_str(), None, None).await?; + + let paginated = response + .into_iter() + .skip(offset) + .take(limit) + .collect::>(); + + Ok(Paginated { + offset, + limit, + total: paginated.len(), + items: paginated.into_iter().map(Into::into).collect(), + }) + } + + async fn get_versions(&self, slugs: &[String]) -> LauncherResult> { + Ok(http::fetch_json::>( + Method::GET, + url!( + "/versions?ids=[{}]", + slugs + .iter() + .map(|v| format!("\"{v}\"")) + .collect::>() + .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, + pub client_side: PackageSide, + pub server_side: PackageSide, + #[serde(default)] + pub body: String, + // #[serde(default)] + // #[serde(alias = "display_categories")] + // pub additional_categories: Vec, + #[serde(default)] + pub issues_url: Option, + #[serde(default)] + pub source_url: Option, + #[serde(default)] + pub wiki_url: Option, + #[serde(default)] + pub discord_url: Option, + #[serde(default)] + pub donation_urls: Vec, + pub project_type: PackageType, + pub downloads: usize, + #[serde(default)] + pub icon_url: Option, + #[serde(alias = "project_id")] + pub id: String, + pub team: String, + #[serde(default)] + pub organization: Option, + #[serde(alias = "date_created")] + pub published: DateTime, + #[serde(alias = "date_modified")] + pub updated: DateTime, + // pub followers: u32, + #[serde(default)] + pub versions: Vec, + #[serde(default)] + pub game_versions: Vec, + #[serde_as(as = "serde_with::VecSkipError<_>")] + pub loaders: Vec, + #[serde(default)] + pub license: Option, + // #[serde(default)] + // pub gallery: Vec, + #[serde(default)] + pub status: PackageStatus, +} + +impl From 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, + pub dependencies: Vec, + pub game_versions: Vec, + pub version_type: PackageReleaseType, + pub loaders: Vec, + // pub featured: bool, + pub id: String, + pub project_id: String, + pub date_published: DateTime, + pub downloads: usize, + pub files: Vec, +} + +impl From 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, + pub project_id: Option, + pub file_name: Option, + pub dependency_type: PackageDependencyType, +} + +impl From 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 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, + pub user: ModrinthUser, +} + +impl From 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 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, + } + } +} diff --git a/packages/core/src/api/packages/tests.rs b/packages/core/src/api/packages/tests.rs new file mode 100644 index 00000000..427314cf --- /dev/null +++ b/packages/core/src/api/packages/tests.rs @@ -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 diff --git a/packages/core/src/error.rs b/packages/core/src/error.rs index b827a9a9..065d8ac8 100644 --- a/packages/core/src/error.rs +++ b/packages/core/src/error.rs @@ -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)] diff --git a/packages/core/src/utils/crypto.rs b/packages/core/src/utils/crypto.rs index 724a1154..0576f8cf 100644 --- a/packages/core/src/utils/crypto.rs +++ b/packages/core/src/utils/crypto.rs @@ -40,7 +40,7 @@ impl HashAlgorithm { pub async fn hasher(&self, closure: F) -> Result 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) -> Result { - 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 { +fn get_inner(algorithm: &HashAlgorithm) -> Box { match algorithm { HashAlgorithm::Sha1 => Box::new(sha1::Sha1::default()), HashAlgorithm::Sha256 => Box::new(sha2::Sha256::default()), diff --git a/packages/core/src/utils/http.rs b/packages/core/src/utils/http.rs index b9466e7f..31a1d4f4 100644 --- a/packages/core/src/utils/http.rs +++ b/packages/core/src/utils/http.rs @@ -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 = 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 { fetch_advanced(method, url, None, None, None, None).await } -pub async fn fetch_json Deserialize<'de>>( +pub async fn fetch_json( method: Method, url: &str, body: Option, @@ -156,7 +156,7 @@ pub async fn fetch_json Deserialize<'de>>( fetch_json_advanced(method, url, body, None, None, ingress).await } -pub async fn fetch_json_advanced Deserialize<'de>>( +pub async fn fetch_json_advanced( method: Method, url: &str, body: Option, diff --git a/packages/core/src/utils/mod.rs b/packages/core/src/utils/mod.rs index 6a784770..48a06a26 100644 --- a/packages/core/src/utils/mod.rs +++ b/packages/core/src/utils/mod.rs @@ -1,4 +1,5 @@ pub mod crypto; pub mod os_ext; pub mod http; -pub mod io; \ No newline at end of file +pub mod io; +pub mod pagination; \ No newline at end of file diff --git a/packages/core/src/utils/pagination.rs b/packages/core/src/utils/pagination.rs new file mode 100644 index 00000000..77c039e5 --- /dev/null +++ b/packages/core/src/utils/pagination.rs @@ -0,0 +1,31 @@ +use std::fmt::Debug; + +use serde::{Deserialize, Serialize}; + +#[onelauncher_macro::specta] +#[derive(Clone, Serialize, Deserialize)] +pub struct Paginated +where T: Clone + Serialize { + pub total: usize, + pub offset: usize, + pub limit: usize, + pub items: Vec, +} + +impl Debug for Paginated { + 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 Paginated { + #[must_use] + pub const fn new(total: usize, offset: usize, limit: usize, items: Vec) -> Self { + Self { total, offset, limit, items } + } +} diff --git a/packages/entity/src/domain/loader.rs b/packages/entity/src/domain/loader.rs index 11922186..ed62c5db 100644 --- a/packages/entity/src/domain/loader.rs +++ b/packages/entity/src/domain/loader.rs @@ -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, diff --git a/packages/entity/src/domain/mod.rs b/packages/entity/src/domain/mod.rs index 2f991ef8..8a70f6b1 100644 --- a/packages/entity/src/domain/mod.rs +++ b/packages/entity/src/domain/mod.rs @@ -1,5 +1,6 @@ pub mod cluster_stage; pub mod icon; pub mod loader; +pub mod package; pub mod resolution; -pub mod settings; \ No newline at end of file +pub mod settings; diff --git a/packages/entity/src/domain/package.rs b/packages/entity/src/domain/package.rs new file mode 100644 index 00000000..ba74951e --- /dev/null +++ b/packages/entity/src/domain/package.rs @@ -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] + } +} \ No newline at end of file diff --git a/packages/entity/src/entities/packages.rs b/packages/entity/src/entities/packages.rs index 84a89d19..d3d18c90 100644 --- a/packages/entity/src/entities/packages.rs +++ b/packages/entity/src/entities/packages.rs @@ -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")] diff --git a/packages/migration/src/init.sql b/packages/migration/src/init.sql index bda6bb1c..f358b051 100644 --- a/packages/migration/src/init.sql +++ b/packages/migration/src/init.sql @@ -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, diff --git a/packages/serde_json_path_to_error/Cargo.toml b/packages/serde_json_path_to_error/Cargo.toml new file mode 100644 index 00000000..d0970ba7 --- /dev/null +++ b/packages/serde_json_path_to_error/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "serde_json_path_to_error" +version = "0.2.0" +authors = [ "Polyfrost", "Ethan Brierley " ] +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" diff --git a/packages/serde_json_path_to_error/LICENSE b/packages/serde_json_path_to_error/LICENSE new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/packages/serde_json_path_to_error/LICENSE @@ -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. diff --git a/packages/serde_json_path_to_error/README.md b/packages/serde_json_path_to_error/README.md new file mode 100644 index 00000000..f9a81498 --- /dev/null +++ b/packages/serde_json_path_to_error/README.md @@ -0,0 +1,107 @@ +# serde_json_path_to_error + +[![License](https://img.shields.io/crates/l/serde_json_path_to_error.svg)](https://crates.io/crates/serde_json_path_to_error) +[![Latest version](https://img.shields.io/crates/v/serde_json_path_to_error.svg)](https://crates.io/crates/serde_json_path_to_error) +[![Latest Docs](https://docs.rs/serde_json_path_to_error/badge.svg)](https://docs.rs/serde_json_path_to_error/) +[![downloads-badge](https://img.shields.io/crates/d/serde_json_path_to_error.svg)](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, +} + +#[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 = 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/ diff --git a/packages/serde_json_path_to_error/src/lib.rs b/packages/serde_json_path_to_error/src/lib.rs new file mode 100644 index 00000000..450676d4 --- /dev/null +++ b/packages/serde_json_path_to_error/src/lib.rs @@ -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 + 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(rdr: R) -> Result + 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 + 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; + /// Alias for a `Result` with the error type `serde_json_path_to_error::Error`. + pub type Result = std::result::Result; +} + +#[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(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(value: &T) -> Result> + 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(value: &T) -> Result + 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(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(value: &T) -> Result> + 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(value: &T) -> Result + 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(value: T) -> Result + 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(value: Value) -> Result + where + T: DeserializeOwned, + { + serde_path_to_error::deserialize(value) + } +} \ No newline at end of file