From f2f2630b2dabdfb58b70534201986fdaa6324eb9 Mon Sep 17 00:00:00 2001 From: nixxoq Date: Fri, 23 May 2025 19:59:05 +0300 Subject: [PATCH 01/11] feat(structs): remove unused `deserialise_optional_url` function [skip ci] --- src/structs/mod.rs | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/structs/mod.rs b/src/structs/mod.rs index 129e1c2..9aeef77 100644 --- a/src/structs/mod.rs +++ b/src/structs/mod.rs @@ -5,24 +5,3 @@ use serde::{Deserialize, Serialize}; use url::Url; pub type Date = chrono::DateTime; - -fn deserialise_optional_url<'de, D: serde::Deserializer<'de>>( - de: D, -) -> Result, D::Error> { - use serde::de::{Error, Unexpected}; - use std::borrow::Cow; - - let intermediate = >>::deserialize(de)?; - match intermediate.as_deref() { - None | Some("") => Ok(None), - Some(s) => Url::parse(s).map_or_else( - |err| { - Err(Error::invalid_value( - Unexpected::Str(s), - &err.to_string().as_str(), - )) - }, - |ok| Ok(Some(ok)), - ), - } -} From 33453e57543a1ca89e7e790fb81d2cc8d1646229 Mon Sep 17 00:00:00 2001 From: nixxoq Date: Fri, 23 May 2025 20:00:29 +0300 Subject: [PATCH 02/11] chore(search): remove unused function `deserialise_optional_url` callback [skip ci] --- src/structs/search.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/structs/search.rs b/src/structs/search.rs index 021d005..7b9de97 100644 --- a/src/structs/search.rs +++ b/src/structs/search.rs @@ -134,8 +134,7 @@ pub struct SearchHit { // TODO: read #7 in TODO.md file (structs\projects section) // pub project_type: projects::ProjectType, pub downloads: usize, - #[serde(deserialize_with = "deserialise_optional_url")] - pub icon_url: Option, + pub icon_url: Option, /// The RGB color of the project, automatically generated from the project icon pub color: Option, /// The ID of the moderation thread associated with this project From c9b667f70895ef29fd1968afa874300695f2fe55 Mon Sep 17 00:00:00 2001 From: nixxoq Date: Fri, 23 May 2025 20:01:49 +0300 Subject: [PATCH 03/11] feat(projects): implement License struct for the `Project` (related for `get_project_by_id`) --- TODO.md | 2 +- src/structs/projects.rs | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/TODO.md b/TODO.md index ac26cf6..cd58a0e 100644 --- a/TODO.md +++ b/TODO.md @@ -10,5 +10,5 @@ - [ ] - Implement custom struct for `requested_status` value (w/ Option) - [ ] - implement custom deserializer for URL links - [ ] - implement custom struct for donation links -- [ ] - implement custom struct for license +- [x] - implement custom struct for license - [x] - implement custom `ProjectStatus` struct diff --git a/src/structs/projects.rs b/src/structs/projects.rs index 0abc009..94040be 100644 --- a/src/structs/projects.rs +++ b/src/structs/projects.rs @@ -2,7 +2,7 @@ //! //! [documentation](https://docs.modrinth.com/api/operations/tags/projects/) -use super::Date; +use super::*; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] @@ -70,9 +70,7 @@ pub struct Project { pub approved: Option, pub queued: Option, pub followers: usize, - /// TODO: read #6 in TODO.md file (structs\projects section) - #[serde(skip)] - pub license: Option>, // placeholder + pub license: License, /// A list of the version IDs of the project (will never be empty unless draft status) pub versions: Vec, /// A list of all the game versions supported by the project @@ -101,3 +99,10 @@ pub enum MonetizationStatus { Demonetized, ForceDemonetized, } + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct License { + pub id: String, + pub name: String, + pub url: Option, +} From 59017068c3b09c1891facbc026bf6a77ab3bdf15 Mon Sep 17 00:00:00 2001 From: nixxoq Date: Fri, 23 May 2025 20:48:55 +0300 Subject: [PATCH 04/11] chore(tests/search): qol changes --- src/api/search.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/search.rs b/src/api/search.rs index fb7fc48..897c2b0 100644 --- a/src/api/search.rs +++ b/src/api/search.rs @@ -126,7 +126,8 @@ mod tests { ) .await?; - let title = response.hits.first().unwrap().slug.as_ref(); + let response = response.hits.first().unwrap(); + let title = response.slug.as_ref(); assert!(title.is_some()); assert_eq!(title.ok_or(0), Ok(&String::from("xaeros-minimap"))); From 3bb9f9b16e065b54604b087d74e0e0a654d5d8f0 Mon Sep 17 00:00:00 2001 From: nixxoq Date: Fri, 23 May 2025 20:49:34 +0300 Subject: [PATCH 05/11] feat(projects): implement ProjectSupportRange struct --- TODO.md | 2 +- src/structs/projects.rs | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index cd58a0e..48d8bad 100644 --- a/TODO.md +++ b/TODO.md @@ -5,7 +5,7 @@ ## structs\project.rs: -- [ ] - Implement custom struct for `client_side` and `server_side` values +- [x] - Implement custom struct for `client_side` and `server_side` values - [ ] - Implement custom struct for `status` value - [ ] - Implement custom struct for `requested_status` value (w/ Option) - [ ] - implement custom deserializer for URL links diff --git a/src/structs/projects.rs b/src/structs/projects.rs index 94040be..ebe7d21 100644 --- a/src/structs/projects.rs +++ b/src/structs/projects.rs @@ -106,3 +106,12 @@ pub struct License { pub name: String, pub url: Option, } + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum ProjectSupportRange { + Required, + Optional, + Unsupported, + Unknown, +} From a1ec1022652a3e60ca301f7e658a59d73b29789c Mon Sep 17 00:00:00 2001 From: nixxoq Date: Fri, 23 May 2025 20:50:18 +0300 Subject: [PATCH 06/11] feat(search): finalize SearchHit struct (still missing some functions, but anyway) --- src/structs/search.rs | 135 +++++++++++++++++++++--------------------- 1 file changed, 66 insertions(+), 69 deletions(-) diff --git a/src/structs/search.rs b/src/structs/search.rs index 7b9de97..dc03b02 100644 --- a/src/structs/search.rs +++ b/src/structs/search.rs @@ -7,6 +7,72 @@ pub struct ExtendedSearch { pub facets: Vec>, } +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Response { + pub hits: Vec, + /// The number of results that were skipped by the query + pub offset: usize, + /// The number of results that were returned by the query + pub limit: usize, + /// The total number of results that match the query + pub total_hits: usize, +} + +impl Response { + pub fn show_hits(&self) { + self.hits.iter().for_each(|h| println!("{h}")); + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct SearchHit { + /// The project's slug, used for vanity URLs. + pub slug: Option, + pub title: String, + pub description: String, + pub categories: Vec, + pub client_side: projects::ProjectSupportRange, + pub server_side: projects::ProjectSupportRange, + pub project_type: projects::ProjectType, + pub downloads: usize, + pub icon_url: Option, + /// The RGB color of the project, automatically generated from the project icon + pub color: Option, + /// The ID of the moderation thread associated with this project + pub thread_id: Option, + pub monetization_status: Option, + pub project_id: String, + /// Author + pub author: String, + /// A list of the project's primary/featured categories + pub display_categories: Vec, + #[serde(rename = "versions")] + /// A list of all the game versions supported by the project + pub game_versions: Vec, + pub follows: usize, + pub date_created: Date, + pub date_modified: Date, + /// The latest game version that this project supports + pub latest_version: String, + /// The SPDX license ID of a project + pub license: String, + pub gallery: Vec, + pub featured_gallery: Option, +} + +impl Display for SearchHit { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Title: {} ({} downloads)\nAuthor: {}\nCategories: {}", + self.title, + self.downloads, + self.author, + self.display_categories.join(",") + ) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Sort { Relevance, @@ -102,72 +168,3 @@ impl Display for Facet { write!(f, "{}", str) } } - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct Response { - pub hits: Vec, - /// The number of results that were skipped by the query - pub offset: usize, - /// The number of results that were returned by the query - pub limit: usize, - /// The total number of results that match the query - pub total_hits: usize, -} - -impl Response { - pub fn show_hits(&self) { - self.hits.iter().for_each(|h| println!("{h}")); - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct SearchHit { - /// The project's slug, used for vanity URLs. - pub slug: Option, - pub title: String, - pub description: String, - pub categories: Vec, - // TODO: read #1 in TODO.md file (structs\projects section) - // pub client_side: projects::ProjectSupportRange, - // pub server_side: projects::ProjectSupportRange, - - // TODO: read #7 in TODO.md file (structs\projects section) - // pub project_type: projects::ProjectType, - pub downloads: usize, - pub icon_url: Option, - /// The RGB color of the project, automatically generated from the project icon - pub color: Option, - /// The ID of the moderation thread associated with this project - pub thread_id: Option, - pub monetization_status: Option, - pub project_id: String, - /// Author - pub author: String, - /// A list of the project's primary/featured categories - pub display_categories: Vec, - #[serde(rename = "versions")] - /// A list of all the game versions supported by the project - pub game_versions: Vec, - pub follows: usize, - pub date_created: Date, - pub date_modified: Date, - /// The latest game version that this project supports - pub latest_version: String, - /// The SPDX license ID of a project - pub license: String, - pub gallery: Vec, - pub featured_gallery: Option, -} - -impl Display for SearchHit { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "Title: {} ({} downloads)\nAuthor: {}\nCategories: {}", - self.title, - self.downloads, - self.author, - self.display_categories.join(",") - ) - } -} From 84ac9a90e6eae3c214db86b669a12b1ef5026c47 Mon Sep 17 00:00:00 2001 From: nixxoq Date: Sun, 25 May 2025 12:38:35 +0300 Subject: [PATCH 07/11] feat(structs/projects): implement DonationLink, RequestedStatus, ProjectStatus fields feat(structs/projects): add reference-function `Project::donation_links` (refers `Project::donation_urls`) --- TODO.md | 7 ++-- src/structs/projects.rs | 73 ++++++++++++++++++++++++++++------------- 2 files changed, 53 insertions(+), 27 deletions(-) diff --git a/TODO.md b/TODO.md index 48d8bad..96197d3 100644 --- a/TODO.md +++ b/TODO.md @@ -6,9 +6,8 @@ ## structs\project.rs: - [x] - Implement custom struct for `client_side` and `server_side` values -- [ ] - Implement custom struct for `status` value -- [ ] - Implement custom struct for `requested_status` value (w/ Option) -- [ ] - implement custom deserializer for URL links -- [ ] - implement custom struct for donation links +- [x] - Implement custom struct for `status` value +- [x] - Implement custom struct for `requested_status` value (w/ Option) +- [x] - implement custom struct for donation links - [x] - implement custom struct for license - [x] - implement custom `ProjectStatus` struct diff --git a/src/structs/projects.rs b/src/structs/projects.rs index ebe7d21..a1ed59c 100644 --- a/src/structs/projects.rs +++ b/src/structs/projects.rs @@ -5,7 +5,7 @@ use super::*; use serde::{Deserialize, Serialize}; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct Project { /// The slug of a project, used for vanity URLs pub slug: String, @@ -16,45 +16,30 @@ pub struct Project { /// A list of the categories that the project has pub categories: Vec, /// The client side support of the project - pub client_side: String, // TODO: read #1 in TOOD.md file (structs\projects section) + pub client_side: ProjectSupportRange, /// The server side support of the project - pub server_side: String, // TODO: read #1 in TODO.md file (structs\projects section) + pub server_side: ProjectSupportRange, /// A long form description of the project pub body: String, /// The status of the project - /// - /// TODO: read #2 in TODO.md file (structs\projects section) - pub status: String, + pub status: ProjectStatus, /// The requested status when submitting for review or scheduling the project for release - /// - /// TODO: read #3 in TODO.md file (structs\projects section) - pub requested_status: Option, + pub requested_status: Option, /// A list of categories which are searchable but non-primary pub additional_categories: Vec, /// An optional link to where to submit bugs or issues with the project - /// - /// TODO: read #4 in TODO.md file (structs\projects section) pub issues_url: Option, /// An optional link to the source code of the project - /// - /// TODO: read #4 in TODO.md file (structs\projects section) pub source_url: Option, /// An optional link to the project’s wiki page or other relevant information - /// - /// TODO: read #4 in TODO.md file (structs\projects section) pub wiki_url: Option, /// An optional invite link to the project’s discord - /// - /// TODO: read #4 in TODO.md file (structs\projects section) pub discord_url: Option, - /// Donation links - /// - /// TODO: read #5 in TODO.md file (structs\projects section) - #[serde(skip)] - pub donation_links: Option>, + /// Donation links / urls + pub donation_urls: Vec, pub project_type: String, pub downloads: usize, - /// TODO: read #4 in TODO.md file (structs\projects section) + /// Project icon URL pub icon_url: Option, /// The RGB color of the project, automatically generated from the project icon pub color: Option, @@ -79,6 +64,16 @@ pub struct Project { pub loaders: Vec, } +impl Project { + /// Returns a read-only reference to the project's donation URLs. + /// + /// This method serves as an alias/getter for the [`Project::donation_urls`] field, + /// providing direct access to the same underlying data. + pub fn donation_links(&self) -> &Vec { + self.donation_urls.as_ref() + } +} + #[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum ProjectType { @@ -115,3 +110,35 @@ pub enum ProjectSupportRange { Unsupported, Unknown, } + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum ProjectStatus { + Approved, + Archived, + Rejected, + Draft, + Unlisted, + Processing, + Withheld, + Scheduled, + Private, + Unknown, +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum RequestedStatus { + Approved, + Archived, + Unlisted, + Private, + Draft, +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +pub struct DonationLink { + pub id: String, + pub platform: String, + pub url: String, +} From cee21e5f0f267a277ee233b6d9777f135cf13e50 Mon Sep 17 00:00:00 2001 From: nixxoq Date: Sun, 25 May 2025 12:41:52 +0300 Subject: [PATCH 08/11] feat(structs/search): finalize SearchHit struct (read description) feat(structs/search): implement `fetch_project` (fetches project and returns modified `SearchHit` structure with full project info; requires ownership) and `get_full_project` (fetches and returns project info; doesn't require ownership) functions --- src/structs/search.rs | 54 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/src/structs/search.rs b/src/structs/search.rs index dc03b02..d9d021a 100644 --- a/src/structs/search.rs +++ b/src/structs/search.rs @@ -58,6 +58,58 @@ pub struct SearchHit { pub license: String, pub gallery: Vec, pub featured_gallery: Option, + + #[serde(skip)] + pub project_info: Option, +} + +impl SearchHit { + /// Fetches the full project details for this search hit from the Modrinth API. + /// + /// A `SearchHit` provides a summarized view of a project. This asynchronous method + /// retrieves the complete [`Project`] data from the Modrinth API using the + /// [`self.project_id`] and populates the [`self.project_info`] field. + /// + /// This method consumes `self` and returns a modified `SearchHit` instance + /// with the `project_info` field updated. + /// + /// # Arguments + /// + /// * `api` - A reference to the [`ModrinthAPI`] client instance, used to perform the API request. + /// + /// # Returns + /// + /// `Result`: + /// - `Ok(self)`: The updated `SearchHit` instance with `self.project_info` populated + /// with the full [`Project`] data. + /// - `Err(crate::error::Error)`: An error occurred during the API request or data processing, + /// e.g., network issues, invalid project ID, rate limiting, or API response errors. + /// + pub async fn fetch_project(mut self, api: &ModrinthAPI) -> Result { + let result = api.get_project_by_id(self.project_id.as_str()).await?; + self.project_info = Some(result); + Ok(self) + } + + /// Retrieves the complete [`Project`] data for this search hit from the Modrinth API. + /// + /// This asynchronous method fetches the full [`Project`] object corresponding to + /// the [`self.project_id`] without modifying the `SearchHit` instance itself. + /// This is useful when you only need the full project data temporarily or in a + /// context where `SearchHit` ownership is not desired. + /// + /// # Arguments + /// + /// * `api` - A reference to the [`ModrinthAPI`] client instance, used to perform the API request. + /// + /// # Returns + /// + /// `Result`: + /// - `Ok(Project)`: The full [`Project`] data. + /// - `Err(crate::error::Error)`: An error occurred during the API request or data processing. + pub async fn get_full_project(&self, api: &ModrinthAPI) -> Result { + api.get_project_by_id(self.project_id.as_str()).await + } } impl Display for SearchHit { @@ -121,7 +173,7 @@ pub enum Facet { } impl Serialize for Facet { - fn serialize(&self, serializer: S) -> Result + fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { From 108c2bb301ed7d7629a06b4b3b8fd02a6840dfd9 Mon Sep 17 00:00:00 2001 From: nixxoq Date: Sun, 25 May 2025 12:42:18 +0300 Subject: [PATCH 09/11] feat(search): add tests related with fetching project info from SearchHit --- src/api/search.rs | 54 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/api/search.rs b/src/api/search.rs index 897c2b0..cea635e 100644 --- a/src/api/search.rs +++ b/src/api/search.rs @@ -133,4 +133,58 @@ mod tests { assert_eq!(title.ok_or(0), Ok(&String::from("xaeros-minimap"))); Ok(()) } + + #[tokio::test] + async fn test_fetching_project_with_mut() -> Result<()> { + let api = ModrinthAPI::default(); + let response = api + .extended_search( + "xaeros", + &Sort::Downloads, + None, + Some(ExtendedSearch { + offset: None, + facets: vec![vec![Facet::ProjectType(ProjectType::Mod)]], + }), + ) + .await?; + + let response = response.hits.first().unwrap(); + let title = response.slug.as_ref(); + + assert!(title.is_some()); + assert_eq!(title.ok_or(0), Ok(&String::from("xaeros-minimap"))); + + let hit = response.to_owned().fetch_project(&api).await?; + + assert_eq!(hit.slug.as_ref().unwrap(), &String::from("xaeros-minimap")); + Ok(()) + } + + #[tokio::test] + async fn test_fetching_project_without_mut() -> Result<()> { + let api = ModrinthAPI::default(); + let response = api + .extended_search( + "xaeros", + &Sort::Downloads, + None, + Some(ExtendedSearch { + offset: None, + facets: vec![vec![Facet::ProjectType(ProjectType::Mod)]], + }), + ) + .await?; + + let response = response.hits.first().unwrap(); + let title = response.slug.as_ref(); + + assert!(title.is_some()); + assert_eq!(title.ok_or(0), Ok(&String::from("xaeros-minimap"))); + + let hit = response.get_full_project(&api).await?; + + assert_eq!(hit.slug, String::from("xaeros-minimap")); + Ok(()) + } } From f94917ff48ff3def1d9a47417f8e9eb713034893 Mon Sep 17 00:00:00 2001 From: nixxoq Date: Sun, 25 May 2025 12:42:49 +0300 Subject: [PATCH 10/11] chore(structs): import some required functions --- src/structs/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/structs/mod.rs b/src/structs/mod.rs index 9aeef77..e5f9545 100644 --- a/src/structs/mod.rs +++ b/src/structs/mod.rs @@ -1,6 +1,7 @@ pub mod projects; pub mod search; +use crate::{ModrinthAPI, Result, structs::projects::Project}; use serde::{Deserialize, Serialize}; use url::Url; From fbd1c83675eaebe64f29a8c9d919b4eb9d129666 Mon Sep 17 00:00:00 2001 From: nixxoq Date: Sun, 25 May 2025 12:49:07 +0300 Subject: [PATCH 11/11] crate: bump version to 0.1.1 Note: Still requires structures and API functions for: - versions (https://docs.modrinth.com/api/operations/tags/versions) - version-files (https://docs.modrinth.com/api/operations/tags/version-files) These features will be implemented on 0.2.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b455293..7fbcdc9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "modrinth-api" -version = "0.1.0" +version = "0.1.1" edition = "2024" [dependencies]