feat: Modrinth provider

This commit is contained in:
LynithDev 2025-04-24 12:03:39 +02:00
parent 06215367c2
commit 17f8b382a7
25 changed files with 1424 additions and 70 deletions

24
.vscode/settings.json vendored
View file

@ -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
View file

@ -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"

View file

@ -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" }

View file

@ -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

View file

@ -13,6 +13,7 @@
"scripts": {
"cargo": "cargo",
"binary": "cargo run --bin",
"example": "cargo run --example"
"example": "cargo run --example",
"test": "cargo test"
}
}

View file

@ -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;

View 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,
}

View file

@ -0,0 +1,5 @@
pub mod data;
pub mod provider;
#[cfg(test)]
mod tests;

View 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"),
}
}
}

View 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,
}
}
}

View 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

View file

@ -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)]

View file

@ -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()),

View file

@ -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>,

View file

@ -1,4 +1,5 @@
pub mod crypto;
pub mod os_ext;
pub mod http;
pub mod io;
pub mod io;
pub mod pagination;

View 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 }
}
}

View file

@ -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,

View file

@ -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;

View 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]
}
}

View file

@ -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")]

View file

@ -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,

View 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"

View 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.

View file

@ -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<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/

View 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)
}
}