├── .gitignore ├── header.png ├── src ├── request │ ├── mods │ │ ├── subscribe │ │ │ ├── mod.rs │ │ │ ├── subscribe_to_mod.rs │ │ │ └── unsubscribe_from_mod.rs │ │ ├── tags │ │ │ ├── mod.rs │ │ │ ├── get_mod_tags.rs │ │ │ ├── add_mod_tags.rs │ │ │ └── delete_mod_tags.rs │ │ ├── media │ │ │ ├── mod.rs │ │ │ ├── delete_mod_media.rs │ │ │ └── reorder_mod_media.rs │ │ ├── metadata │ │ │ ├── mod.rs │ │ │ ├── get_mod_metadata.rs │ │ │ ├── add_mod_metadata.rs │ │ │ └── delete_mod_metadata.rs │ │ ├── dependencies │ │ │ ├── mod.rs │ │ │ ├── get_mod_dependencies.rs │ │ │ ├── delete_mod_dependencies.rs │ │ │ └── add_mod_dependencies.rs │ │ ├── comments │ │ │ ├── mod.rs │ │ │ ├── delete_mod_comment.rs │ │ │ ├── get_mod_comment.rs │ │ │ ├── edit_mod_comment.rs │ │ │ ├── add_mod_comment.rs │ │ │ ├── update_mod_comment_karma.rs │ │ │ └── get_mod_comments.rs │ │ ├── get_mod.rs │ │ ├── delete_mod.rs │ │ ├── stats │ │ │ ├── get_mod_stats.rs │ │ │ ├── mod.rs │ │ │ └── get_mods_stats.rs │ │ ├── events │ │ │ ├── mod.rs │ │ │ ├── get_mods_events.rs │ │ │ └── get_mod_events.rs │ │ ├── get_mod_team_members.rs │ │ ├── submit_mod_rating.rs │ │ ├── get_mods.rs │ │ └── mod.rs │ ├── games │ │ ├── tags │ │ │ ├── mod.rs │ │ │ ├── rename_game_tag.rs │ │ │ ├── get_game_tags.rs │ │ │ ├── add_game_tags.rs │ │ │ └── delete_game_tags.rs │ │ ├── get_game_stats.rs │ │ ├── get_game.rs │ │ ├── get_games.rs │ │ ├── add_game_media.rs │ │ └── mod.rs │ ├── auth │ │ ├── mod.rs │ │ ├── logout.rs │ │ ├── get_terms.rs │ │ ├── email_request.rs │ │ └── email_exchange.rs │ ├── files │ │ ├── multipart │ │ │ ├── mod.rs │ │ │ ├── delete_multipart_upload_session.rs │ │ │ ├── complete_multipart_upload_session.rs │ │ │ ├── get_multipart_upload_sessions.rs │ │ │ ├── get_multipart_upload_parts.rs │ │ │ ├── create_multipart_upload_session.rs │ │ │ ├── add_multipart_upload_part.rs │ │ │ └── add_multipart_upload_file.rs │ │ ├── get_file.rs │ │ ├── delete_file.rs │ │ ├── manage_platform_status.rs │ │ ├── get_files.rs │ │ ├── mod.rs │ │ ├── edit_file.rs │ │ └── add_file.rs │ ├── user │ │ ├── get_authenticated_user.rs │ │ ├── mute_user.rs │ │ ├── unmute_user.rs │ │ ├── get_user_events.rs │ │ ├── get_muted_users.rs │ │ ├── get_user_files.rs │ │ ├── get_user_mods.rs │ │ ├── get_user_games.rs │ │ ├── get_user_ratings.rs │ │ └── get_user_subscriptions.rs │ ├── util.rs │ ├── multipart │ │ ├── encoding.rs │ │ └── boundary.rs │ ├── submit_report.rs │ ├── body.rs │ └── mod.rs ├── util │ ├── mod.rs │ ├── upload │ │ ├── error.rs │ │ ├── mod.rs │ │ └── byte_ranges.rs │ └── download │ │ ├── info │ │ ├── get_file.rs │ │ ├── get_primary_file.rs │ │ ├── get_file_by_version.rs │ │ └── mod.rs │ │ └── action.rs ├── types │ └── auth.rs ├── client │ ├── conn.rs │ └── host.rs └── response │ ├── error.rs │ └── future.rs ├── examples ├── README.md ├── mymods.rs ├── auth.rs ├── events.rs ├── details.rs ├── download.rs └── upload.rs ├── LICENSE-MIT ├── .github └── workflows │ ├── docs.yml │ └── ci.yml └── Cargo.toml /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | .env 5 | -------------------------------------------------------------------------------- /header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickelc/modio-rs/HEAD/header.png -------------------------------------------------------------------------------- /src/request/mods/subscribe/mod.rs: -------------------------------------------------------------------------------- 1 | mod subscribe_to_mod; 2 | mod unsubscribe_from_mod; 3 | 4 | pub use subscribe_to_mod::SubscribeToMod; 5 | pub use unsubscribe_from_mod::UnsubscribeFromMod; 6 | -------------------------------------------------------------------------------- /src/request/mods/tags/mod.rs: -------------------------------------------------------------------------------- 1 | mod add_mod_tags; 2 | mod delete_mod_tags; 3 | mod get_mod_tags; 4 | 5 | pub use add_mod_tags::AddModTags; 6 | pub use delete_mod_tags::DeleteModTags; 7 | pub use get_mod_tags::GetModTags; 8 | -------------------------------------------------------------------------------- /src/request/mods/media/mod.rs: -------------------------------------------------------------------------------- 1 | mod add_mod_media; 2 | mod delete_mod_media; 3 | mod reorder_mod_media; 4 | 5 | pub use add_mod_media::AddModMedia; 6 | pub use delete_mod_media::DeleteModMedia; 7 | pub use reorder_mod_media::ReorderModMedia; 8 | -------------------------------------------------------------------------------- /src/request/mods/metadata/mod.rs: -------------------------------------------------------------------------------- 1 | mod add_mod_metadata; 2 | mod delete_mod_metadata; 3 | mod get_mod_metadata; 4 | 5 | pub use add_mod_metadata::AddModMetadata; 6 | pub use delete_mod_metadata::DeleteModMetadata; 7 | pub use get_mod_metadata::GetModMetadata; 8 | -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod download; 2 | pub mod upload; 3 | 4 | mod data_from_request; 5 | mod pagination; 6 | 7 | pub use data_from_request::{DataError, DataFromRequest, DataFuture}; 8 | pub use download::Download; 9 | pub use pagination::{Page, Paginate, PaginateError, Paginator}; 10 | -------------------------------------------------------------------------------- /src/request/mods/dependencies/mod.rs: -------------------------------------------------------------------------------- 1 | mod add_mod_dependencies; 2 | mod delete_mod_dependencies; 3 | mod get_mod_dependencies; 4 | 5 | pub use add_mod_dependencies::AddModDependencies; 6 | pub use delete_mod_dependencies::DeleteModDependencies; 7 | pub use get_mod_dependencies::GetModDependencies; 8 | -------------------------------------------------------------------------------- /src/request/games/tags/mod.rs: -------------------------------------------------------------------------------- 1 | mod add_game_tags; 2 | mod delete_game_tags; 3 | mod get_game_tags; 4 | mod rename_game_tag; 5 | 6 | pub use add_game_tags::AddGameTags; 7 | pub use delete_game_tags::DeleteGameTags; 8 | pub use get_game_tags::GetGameTags; 9 | pub use rename_game_tag::RenameGameTag; 10 | -------------------------------------------------------------------------------- /src/request/mods/comments/mod.rs: -------------------------------------------------------------------------------- 1 | mod add_mod_comment; 2 | mod delete_mod_comment; 3 | mod edit_mod_comment; 4 | mod get_mod_comment; 5 | mod get_mod_comments; 6 | mod update_mod_comment_karma; 7 | 8 | pub use add_mod_comment::AddModComment; 9 | pub use delete_mod_comment::DeleteModComment; 10 | pub use edit_mod_comment::EditModComment; 11 | pub use get_mod_comment::GetModComment; 12 | pub use get_mod_comments::GetModComments; 13 | pub use update_mod_comment_karma::UpdateModCommentKarma; 14 | -------------------------------------------------------------------------------- /src/request/auth/mod.rs: -------------------------------------------------------------------------------- 1 | mod email_exchange; 2 | mod email_request; 3 | mod external_auth; 4 | mod get_terms; 5 | mod logout; 6 | 7 | /// External authentication providers for [`ExternalAuth`]. 8 | pub mod external { 9 | pub use super::external_auth::{ 10 | Discord, EpicGames, Google, MetaQuest, OpenID, Steam, Switch, Xbox, PSN, 11 | }; 12 | 13 | pub(crate) use super::external_auth::Provider; 14 | } 15 | 16 | pub use email_exchange::EmailExchange; 17 | pub use email_request::EmailRequest; 18 | pub use external_auth::ExternalAuth; 19 | pub use get_terms::GetTerms; 20 | pub use logout::Logout; 21 | -------------------------------------------------------------------------------- /src/request/files/multipart/mod.rs: -------------------------------------------------------------------------------- 1 | mod add_multipart_upload_file; 2 | mod add_multipart_upload_part; 3 | mod complete_multipart_upload_session; 4 | mod create_multipart_upload_session; 5 | mod delete_multipart_upload_session; 6 | mod get_multipart_upload_parts; 7 | mod get_multipart_upload_sessions; 8 | 9 | pub use add_multipart_upload_file::AddMultipartUploadFile; 10 | pub use add_multipart_upload_part::{AddMultipartUploadPart, ContentRange}; 11 | pub use complete_multipart_upload_session::CompleteMultipartUploadSession; 12 | pub use create_multipart_upload_session::CreateMultipartUploadSession; 13 | pub use delete_multipart_upload_session::DeleteMultipartUploadSession; 14 | pub use get_multipart_upload_parts::GetMultipartUploadParts; 15 | pub use get_multipart_upload_sessions::GetMultipartUploadSessions; 16 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Examples of using modio 2 | 3 | Run examples with `cargo run --example example_name` 4 | 5 | ### Available examples 6 | 7 | * [`auth`](auth.rs) - Request an access token and print the authenticated user. See [Email Authentication Flow](https://docs.mod.io/restapiref/#authenticate-via-email). 8 | 9 | * [`details`](details.rs) - Print some mod details (profile, dependencies, stats, files). 10 | 11 | * [`download`](download.rs) - Download the latest modfile for a given mod of a game. 12 | 13 | * [`events`](events.rs) - Poll the user events from [`/me/events`](https://docs.mod.io/restapiref/#get-user-events) every 10 seconds. 14 | 15 | * [`mymods`](mymods.rs) - List all mods the *authenticated user* added or is team member of. See [`/me/mods`](https://docs.mod.io/restapiref/#get-user-mods). 16 | 17 | * [`upload`](upload.rs) - Upload a modfile via the multipart upload API. 18 | -------------------------------------------------------------------------------- /src/request/auth/logout.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::{NoContent, ResponseFuture}; 6 | 7 | /// Log out by revoking the current access token. 8 | pub struct Logout<'a> { 9 | http: &'a Client, 10 | } 11 | 12 | impl<'a> Logout<'a> { 13 | pub(crate) const fn new(http: &'a Client) -> Self { 14 | Self { http } 15 | } 16 | } 17 | 18 | impl IntoFuture for Logout<'_> { 19 | type Output = Output; 20 | type IntoFuture = ResponseFuture; 21 | 22 | fn into_future(self) -> Self::IntoFuture { 23 | let route = Route::OAuthLogout; 24 | match RequestBuilder::from_route(&route).empty() { 25 | Ok(req) => self.http.request(req), 26 | Err(err) => ResponseFuture::failed(err), 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/request/auth/get_terms.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::auth::Terms; 7 | 8 | /// Retrieve texts and links for a users agreement and consent. 9 | pub struct GetTerms<'a> { 10 | http: &'a Client, 11 | } 12 | 13 | impl<'a> GetTerms<'a> { 14 | pub(crate) const fn new(http: &'a Client) -> Self { 15 | Self { http } 16 | } 17 | } 18 | 19 | impl IntoFuture for GetTerms<'_> { 20 | type Output = Output; 21 | type IntoFuture = ResponseFuture; 22 | 23 | fn into_future(self) -> Self::IntoFuture { 24 | let route = Route::Terms; 25 | match RequestBuilder::from_route(&route).empty() { 26 | Ok(req) => self.http.request(req), 27 | Err(err) => ResponseFuture::failed(err), 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/request/user/get_authenticated_user.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::User; 7 | 8 | /// Get the authenticated user details. 9 | pub struct GetAuthenticatedUser<'a> { 10 | http: &'a Client, 11 | } 12 | 13 | impl<'a> GetAuthenticatedUser<'a> { 14 | pub(crate) const fn new(http: &'a Client) -> Self { 15 | Self { http } 16 | } 17 | } 18 | 19 | impl IntoFuture for GetAuthenticatedUser<'_> { 20 | type Output = Output; 21 | type IntoFuture = ResponseFuture; 22 | 23 | fn into_future(self) -> Self::IntoFuture { 24 | let route = Route::UserAuthenticated; 25 | 26 | match RequestBuilder::from_route(&route).empty() { 27 | Ok(req) => self.http.request(req), 28 | Err(err) => ResponseFuture::failed(err), 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/request/user/mute_user.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::{NoContent, ResponseFuture}; 6 | use crate::types::id::UserId; 7 | 8 | /// Mute a user. 9 | pub struct MuteUser<'a> { 10 | http: &'a Client, 11 | user_id: UserId, 12 | } 13 | 14 | impl<'a> MuteUser<'a> { 15 | pub(crate) const fn new(http: &'a Client, user_id: UserId) -> Self { 16 | Self { http, user_id } 17 | } 18 | } 19 | 20 | impl IntoFuture for MuteUser<'_> { 21 | type Output = Output; 22 | type IntoFuture = ResponseFuture; 23 | 24 | fn into_future(self) -> Self::IntoFuture { 25 | let route = Route::MuteUser { 26 | user_id: self.user_id, 27 | }; 28 | match RequestBuilder::from_route(&route).empty() { 29 | Ok(req) => self.http.request(req), 30 | Err(err) => ResponseFuture::failed(err), 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/request/user/unmute_user.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::{NoContent, ResponseFuture}; 6 | use crate::types::id::UserId; 7 | 8 | /// Unmute a previously muted user. 9 | pub struct UnmuteUser<'a> { 10 | http: &'a Client, 11 | user_id: UserId, 12 | } 13 | 14 | impl<'a> UnmuteUser<'a> { 15 | pub(crate) const fn new(http: &'a Client, user_id: UserId) -> Self { 16 | Self { http, user_id } 17 | } 18 | } 19 | 20 | impl IntoFuture for UnmuteUser<'_> { 21 | type Output = Output; 22 | type IntoFuture = ResponseFuture; 23 | 24 | fn into_future(self) -> Self::IntoFuture { 25 | let route = Route::UnmuteUser { 26 | user_id: self.user_id, 27 | }; 28 | match RequestBuilder::from_route(&route).empty() { 29 | Ok(req) => self.http.request(req), 30 | Err(err) => ResponseFuture::failed(err), 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/request/games/get_game_stats.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::games::Statistics; 7 | use crate::types::id::GameId; 8 | 9 | /// Get game stats for a game. 10 | pub struct GetGameStats<'a> { 11 | http: &'a Client, 12 | game_id: GameId, 13 | } 14 | 15 | impl<'a> GetGameStats<'a> { 16 | pub(crate) const fn new(http: &'a Client, game_id: GameId) -> Self { 17 | Self { http, game_id } 18 | } 19 | } 20 | 21 | impl IntoFuture for GetGameStats<'_> { 22 | type Output = Output; 23 | type IntoFuture = ResponseFuture; 24 | 25 | fn into_future(self) -> Self::IntoFuture { 26 | let route = Route::GetGameStats { 27 | game_id: self.game_id, 28 | }; 29 | match RequestBuilder::from_route(&route).empty() { 30 | Ok(req) => self.http.request(req), 31 | Err(err) => ResponseFuture::failed(err), 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 nickelc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/request/mods/get_mod.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::id::{GameId, ModId}; 7 | use crate::types::mods::Mod; 8 | 9 | /// Get a mod. 10 | pub struct GetMod<'a> { 11 | http: &'a Client, 12 | game_id: GameId, 13 | mod_id: ModId, 14 | } 15 | 16 | impl<'a> GetMod<'a> { 17 | pub(crate) const fn new(http: &'a Client, game_id: GameId, mod_id: ModId) -> Self { 18 | Self { 19 | http, 20 | game_id, 21 | mod_id, 22 | } 23 | } 24 | } 25 | 26 | impl IntoFuture for GetMod<'_> { 27 | type Output = Output; 28 | type IntoFuture = ResponseFuture; 29 | 30 | fn into_future(self) -> Self::IntoFuture { 31 | let route = Route::GetMod { 32 | game_id: self.game_id, 33 | mod_id: self.mod_id, 34 | }; 35 | match RequestBuilder::from_route(&route).empty() { 36 | Ok(req) => self.http.request(req), 37 | Err(err) => ResponseFuture::failed(err), 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/request/mods/delete_mod.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::{NoContent, ResponseFuture}; 6 | use crate::types::id::{GameId, ModId}; 7 | 8 | /// Delete a mod profile. 9 | pub struct DeleteMod<'a> { 10 | http: &'a Client, 11 | game_id: GameId, 12 | mod_id: ModId, 13 | } 14 | 15 | impl<'a> DeleteMod<'a> { 16 | pub(crate) const fn new(http: &'a Client, game_id: GameId, mod_id: ModId) -> Self { 17 | Self { 18 | http, 19 | game_id, 20 | mod_id, 21 | } 22 | } 23 | } 24 | 25 | impl IntoFuture for DeleteMod<'_> { 26 | type Output = Output; 27 | type IntoFuture = ResponseFuture; 28 | 29 | fn into_future(self) -> Self::IntoFuture { 30 | let route = Route::DeleteMod { 31 | game_id: self.game_id, 32 | mod_id: self.mod_id, 33 | }; 34 | 35 | match RequestBuilder::from_route(&route).empty() { 36 | Ok(req) => self.http.request(req), 37 | Err(err) => ResponseFuture::failed(err), 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/request/auth/email_request.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use serde_derive::Serialize; 4 | 5 | use crate::client::Client; 6 | use crate::request::{Output, RequestBuilder, Route}; 7 | use crate::response::ResponseFuture; 8 | use crate::types::Message; 9 | 10 | /// Request a security code for a user. 11 | pub struct EmailRequest<'a> { 12 | http: &'a Client, 13 | fields: EmailRequestFields<'a>, 14 | } 15 | 16 | #[derive(Serialize)] 17 | struct EmailRequestFields<'a> { 18 | email: &'a str, 19 | } 20 | 21 | impl<'a> EmailRequest<'a> { 22 | pub(crate) const fn new(http: &'a Client, email: &'a str) -> Self { 23 | Self { 24 | http, 25 | fields: EmailRequestFields { email }, 26 | } 27 | } 28 | } 29 | 30 | impl IntoFuture for EmailRequest<'_> { 31 | type Output = Output; 32 | type IntoFuture = ResponseFuture; 33 | 34 | fn into_future(self) -> Self::IntoFuture { 35 | let route = Route::OAuthEmailRequest; 36 | match RequestBuilder::from_route(&route).form(&self.fields) { 37 | Ok(req) => self.http.request(req), 38 | Err(err) => ResponseFuture::failed(err), 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/request/mods/stats/get_mod_stats.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::id::{GameId, ModId}; 7 | use crate::types::mods::Statistics; 8 | 9 | /// Get mod stats for a mod. 10 | pub struct GetModStats<'a> { 11 | http: &'a Client, 12 | game_id: GameId, 13 | mod_id: ModId, 14 | } 15 | 16 | impl<'a> GetModStats<'a> { 17 | pub(crate) const fn new(http: &'a Client, game_id: GameId, mod_id: ModId) -> Self { 18 | Self { 19 | http, 20 | game_id, 21 | mod_id, 22 | } 23 | } 24 | } 25 | 26 | impl IntoFuture for GetModStats<'_> { 27 | type Output = Output; 28 | type IntoFuture = ResponseFuture; 29 | 30 | fn into_future(self) -> Self::IntoFuture { 31 | let route = Route::GetModStats { 32 | game_id: self.game_id, 33 | mod_id: self.mod_id, 34 | }; 35 | match RequestBuilder::from_route(&route).empty() { 36 | Ok(req) => self.http.request(req), 37 | Err(err) => ResponseFuture::failed(err), 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/request/mods/subscribe/subscribe_to_mod.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::id::{GameId, ModId}; 7 | use crate::types::mods::Mod; 8 | 9 | /// Subscribe the authenticated user to a mod. 10 | pub struct SubscribeToMod<'a> { 11 | http: &'a Client, 12 | game_id: GameId, 13 | mod_id: ModId, 14 | } 15 | 16 | impl<'a> SubscribeToMod<'a> { 17 | pub(crate) const fn new(http: &'a Client, game_id: GameId, mod_id: ModId) -> Self { 18 | Self { 19 | http, 20 | game_id, 21 | mod_id, 22 | } 23 | } 24 | } 25 | 26 | impl IntoFuture for SubscribeToMod<'_> { 27 | type Output = Output; 28 | type IntoFuture = ResponseFuture; 29 | 30 | fn into_future(self) -> Self::IntoFuture { 31 | let route = Route::SubscribeToMod { 32 | game_id: self.game_id, 33 | mod_id: self.mod_id, 34 | }; 35 | match RequestBuilder::from_route(&route).empty() { 36 | Ok(req) => self.http.request(req), 37 | Err(err) => ResponseFuture::failed(err), 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/request/mods/subscribe/unsubscribe_from_mod.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::{NoContent, ResponseFuture}; 6 | use crate::types::id::{GameId, ModId}; 7 | 8 | /// Unsubscribe the authenticated user from a mod. 9 | pub struct UnsubscribeFromMod<'a> { 10 | http: &'a Client, 11 | game_id: GameId, 12 | mod_id: ModId, 13 | } 14 | 15 | impl<'a> UnsubscribeFromMod<'a> { 16 | pub(crate) const fn new(http: &'a Client, game_id: GameId, mod_id: ModId) -> Self { 17 | Self { 18 | http, 19 | game_id, 20 | mod_id, 21 | } 22 | } 23 | } 24 | 25 | impl IntoFuture for UnsubscribeFromMod<'_> { 26 | type Output = Output; 27 | type IntoFuture = ResponseFuture; 28 | 29 | fn into_future(self) -> Self::IntoFuture { 30 | let route = Route::UnsubscribeFromMod { 31 | game_id: self.game_id, 32 | mod_id: self.mod_id, 33 | }; 34 | match RequestBuilder::from_route(&route).empty() { 35 | Ok(req) => self.http.request(req), 36 | Err(err) => ResponseFuture::failed(err), 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | permissions: 8 | contents: read 9 | pages: write 10 | id-token: write 11 | 12 | jobs: 13 | build: 14 | name: Build docs 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v5 19 | with: 20 | persist-credentials: false 21 | - uses: dtolnay/rust-toolchain@nightly 22 | 23 | - name: Build docs 24 | run: cargo doc --no-deps --all-features 25 | 26 | - name: Prepare docs 27 | run: | 28 | mkdir -p _site/master 29 | echo '' > _site/index.html 30 | echo '' > _site/master/index.html 31 | mv target/doc/* _site/master 32 | 33 | - uses: actions/upload-pages-artifact@v3 34 | 35 | deploy: 36 | name: Deploy to GitHub Pages 37 | needs: build 38 | 39 | environment: 40 | name: github-pages 41 | url: ${{ steps.deployment.outputs.page_url }} 42 | 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Deploy to GitHub Pages 46 | id: deployment 47 | uses: actions/deploy-pages@v4 48 | -------------------------------------------------------------------------------- /src/request/mods/metadata/get_mod_metadata.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::id::{GameId, ModId}; 7 | use crate::types::mods::MetadataMap; 8 | 9 | /// Get all metadata for a mod as searchable key value pairs. 10 | pub struct GetModMetadata<'a> { 11 | http: &'a Client, 12 | game_id: GameId, 13 | mod_id: ModId, 14 | } 15 | 16 | impl<'a> GetModMetadata<'a> { 17 | pub(crate) const fn new(http: &'a Client, game_id: GameId, mod_id: ModId) -> Self { 18 | Self { 19 | http, 20 | game_id, 21 | mod_id, 22 | } 23 | } 24 | } 25 | 26 | impl IntoFuture for GetModMetadata<'_> { 27 | type Output = Output; 28 | type IntoFuture = ResponseFuture; 29 | 30 | fn into_future(self) -> Self::IntoFuture { 31 | let route = Route::GetModMetadata { 32 | game_id: self.game_id, 33 | mod_id: self.mod_id, 34 | }; 35 | match RequestBuilder::from_route(&route).empty() { 36 | Ok(req) => self.http.request(req), 37 | Err(err) => ResponseFuture::failed(err), 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/request/user/get_user_events.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Filter, Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::{Event, List}; 7 | 8 | /// Get events that have been fired specific to the authenticated user. 9 | pub struct GetUserEvents<'a> { 10 | http: &'a Client, 11 | filter: Option, 12 | } 13 | 14 | impl<'a> GetUserEvents<'a> { 15 | pub(crate) const fn new(http: &'a Client) -> Self { 16 | Self { http, filter: None } 17 | } 18 | 19 | pub fn filter(mut self, filter: Filter) -> Self { 20 | self.filter = Some(filter); 21 | self 22 | } 23 | } 24 | 25 | impl IntoFuture for GetUserEvents<'_> { 26 | type Output = Output>; 27 | type IntoFuture = ResponseFuture>; 28 | 29 | fn into_future(self) -> Self::IntoFuture { 30 | let route = Route::UserEvents; 31 | let mut builder = RequestBuilder::from_route(&route); 32 | if let Some(filter) = self.filter { 33 | builder = builder.filter(filter); 34 | } 35 | match builder.empty() { 36 | Ok(req) => self.http.request(req), 37 | Err(err) => ResponseFuture::failed(err), 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/request/mods/events/mod.rs: -------------------------------------------------------------------------------- 1 | mod get_mod_events; 2 | mod get_mods_events; 3 | 4 | pub use get_mod_events::GetModEvents; 5 | pub use get_mods_events::GetModsEvents; 6 | 7 | /// Mod event filters and sorting. 8 | /// 9 | /// # Filters 10 | /// - `Id` 11 | /// - `ModId` 12 | /// - `UserId` 13 | /// - `DateAdded` 14 | /// - `EventType` 15 | /// 16 | /// # Sorting 17 | /// - `Id` 18 | /// - `DateAdded` 19 | /// 20 | /// See the [modio docs](https://docs.mod.io/restapiref/#events) for more information. 21 | /// 22 | /// By default this returns up to `100` items. You can limit the result by using `limit` and 23 | /// `offset`. 24 | /// 25 | /// # Example 26 | /// ``` 27 | /// use modio::request::filter::prelude::*; 28 | /// use modio::request::mods::events::filters::EventType as Filter; 29 | /// use modio::types::mods::EventType; 30 | /// 31 | /// let filter = Id::gt(1024).and(Filter::eq(EventType::MODFILE_CHANGED)); 32 | /// ``` 33 | pub mod filters { 34 | #[doc(inline)] 35 | pub use crate::request::filter::prelude::DateAdded; 36 | #[doc(inline)] 37 | pub use crate::request::filter::prelude::Id; 38 | #[doc(inline)] 39 | pub use crate::request::filter::prelude::ModId; 40 | 41 | filter!(UserId, USER_ID, "user_id", Eq, NotEq, In, Cmp, OrderBy); 42 | filter!(EventType, EVENT_TYPE, "event_type", Eq, NotEq, In, OrderBy); 43 | } 44 | -------------------------------------------------------------------------------- /src/request/files/get_file.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::files::File; 7 | use crate::types::id::{FileId, GameId, ModId}; 8 | 9 | /// Get a file. 10 | pub struct GetFile<'a> { 11 | http: &'a Client, 12 | game_id: GameId, 13 | mod_id: ModId, 14 | file_id: FileId, 15 | } 16 | 17 | impl<'a> GetFile<'a> { 18 | pub(crate) const fn new( 19 | http: &'a Client, 20 | game_id: GameId, 21 | mod_id: ModId, 22 | file_id: FileId, 23 | ) -> Self { 24 | Self { 25 | http, 26 | game_id, 27 | mod_id, 28 | file_id, 29 | } 30 | } 31 | } 32 | 33 | impl IntoFuture for GetFile<'_> { 34 | type Output = Output; 35 | type IntoFuture = ResponseFuture; 36 | 37 | fn into_future(self) -> Self::IntoFuture { 38 | let route = Route::GetFile { 39 | game_id: self.game_id, 40 | mod_id: self.mod_id, 41 | file_id: self.file_id, 42 | }; 43 | match RequestBuilder::from_route(&route).empty() { 44 | Ok(req) => self.http.request(req), 45 | Err(err) => ResponseFuture::failed(err), 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/request/files/delete_file.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::{NoContent, ResponseFuture}; 6 | use crate::types::id::{FileId, GameId, ModId}; 7 | 8 | /// Delete a modfile. 9 | pub struct DeleteFile<'a> { 10 | http: &'a Client, 11 | game_id: GameId, 12 | mod_id: ModId, 13 | file_id: FileId, 14 | } 15 | 16 | impl<'a> DeleteFile<'a> { 17 | pub(crate) const fn new( 18 | http: &'a Client, 19 | game_id: GameId, 20 | mod_id: ModId, 21 | file_id: FileId, 22 | ) -> Self { 23 | Self { 24 | http, 25 | game_id, 26 | mod_id, 27 | file_id, 28 | } 29 | } 30 | } 31 | 32 | impl IntoFuture for DeleteFile<'_> { 33 | type Output = Output; 34 | type IntoFuture = ResponseFuture; 35 | 36 | fn into_future(self) -> Self::IntoFuture { 37 | let route = Route::DeleteFile { 38 | game_id: self.game_id, 39 | mod_id: self.mod_id, 40 | file_id: self.file_id, 41 | }; 42 | 43 | match RequestBuilder::from_route(&route).empty() { 44 | Ok(req) => self.http.request(req), 45 | Err(err) => ResponseFuture::failed(err), 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/mymods.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::process; 3 | 4 | use modio::request::filter::prelude::*; 5 | use modio::Client; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<(), Box> { 9 | dotenv::dotenv().ok(); 10 | tracing_subscriber::fmt::init(); 11 | 12 | // Fetch the access token / api key from the environment of the current process. 13 | let (api_key, token) = match (env::var("MODIO_API_KEY"), env::var("MODIO_TOKEN")) { 14 | (Ok(api_key), Ok(token)) => (api_key, token), 15 | _ => { 16 | eprintln!("missing MODIO_TOKEN and MODIO_API_KEY environment variable"); 17 | process::exit(1); 18 | } 19 | }; 20 | let host = env::var("MODIO_HOST").unwrap_or_else(|_| "api.test.mod.io".to_string()); 21 | 22 | let client = Client::builder(api_key).token(token).host(host).build()?; 23 | 24 | // Create a mod filter for `id` in (1043, 1041), limited to 30 results 25 | // and ordered by `id` desc. 26 | let filter = Id::_in(vec![1043, 1041]) 27 | .limit(30) 28 | .offset(0) 29 | .order_by(Id::desc()); 30 | 31 | // Create the call for `/me/mods` and wait for the result. 32 | let list = client.get_user_mods().filter(filter).await?.data().await?; 33 | for mod_ in list.data { 34 | println!("{:#?}", mod_); 35 | } 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /src/request/games/get_game.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::games::Game; 7 | use crate::types::id::GameId; 8 | 9 | /// Get a game. 10 | pub struct GetGame<'a> { 11 | http: &'a Client, 12 | id: GameId, 13 | show_hidden_tags: Option, 14 | } 15 | 16 | impl<'a> GetGame<'a> { 17 | pub(crate) const fn new(http: &'a Client, id: GameId) -> Self { 18 | Self { 19 | http, 20 | id, 21 | show_hidden_tags: None, 22 | } 23 | } 24 | 25 | /// Show the hidden tags associated with the given game. 26 | pub const fn show_hidden_tags(mut self, value: bool) -> Self { 27 | self.show_hidden_tags = Some(value); 28 | self 29 | } 30 | } 31 | 32 | impl IntoFuture for GetGame<'_> { 33 | type Output = Output; 34 | type IntoFuture = ResponseFuture; 35 | 36 | fn into_future(self) -> Self::IntoFuture { 37 | let route = Route::GetGame { 38 | id: self.id, 39 | show_hidden_tags: self.show_hidden_tags, 40 | }; 41 | match RequestBuilder::from_route(&route).empty() { 42 | Ok(req) => self.http.request(req), 43 | Err(err) => ResponseFuture::failed(err), 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/request/mods/stats/mod.rs: -------------------------------------------------------------------------------- 1 | mod get_mod_stats; 2 | mod get_mods_stats; 3 | 4 | pub use get_mod_stats::GetModStats; 5 | pub use get_mods_stats::GetModsStats; 6 | 7 | /// Mod statistics filters & sorting 8 | /// 9 | /// # Filters 10 | /// - `ModId` 11 | /// - `Popularity` 12 | /// - `Downloads` 13 | /// - `Subscribers` 14 | /// - `RatingsPositive` 15 | /// - `RatingsNegative` 16 | /// 17 | /// # Sorting 18 | /// - `ModId` 19 | /// - `Popularity` 20 | /// - `Downloads` 21 | /// - `Subscribers` 22 | /// - `RatingsPositive` 23 | /// - `RatingsNegative` 24 | /// 25 | /// # Example 26 | /// ``` 27 | /// use modio::request::filter::prelude::*; 28 | /// use modio::request::mods::stats::filters::{ModId, Popularity}; 29 | /// 30 | /// let filter = ModId::_in(vec![1, 2]).order_by(Popularity::desc()); 31 | /// ``` 32 | #[rustfmt::skip] 33 | pub mod filters { 34 | #[doc(inline)] 35 | pub use crate::request::filter::prelude::ModId; 36 | 37 | filter!(Popularity, POPULARITY, "popularity_rank_position", Eq, NotEq, In, Cmp, OrderBy); 38 | filter!(Downloads, DOWNLOADS, "downloads_total", Eq, NotEq, In, Cmp, OrderBy); 39 | filter!(Subscribers, SUBSCRIBERS, "subscribers_total", Eq, NotEq, In, Cmp, OrderBy); 40 | filter!(RatingsPositive, RATINGS_POSITIVE, "ratings_positive", Eq, NotEq, In, Cmp, OrderBy); 41 | filter!(RatingsNegative, RATINGS_NEGATIVE, "ratings_negative", Eq, NotEq, In, Cmp, OrderBy); 42 | } 43 | -------------------------------------------------------------------------------- /src/request/util.rs: -------------------------------------------------------------------------------- 1 | use serde::ser::{Serialize, SerializeMap, Serializer}; 2 | 3 | pub struct ArrayParams<'a, T> { 4 | name: &'static str, 5 | items: &'a [T], 6 | } 7 | 8 | impl<'a, T> ArrayParams<'a, T> { 9 | pub const fn new(name: &'static str, items: &'a [T]) -> Self { 10 | Self { name, items } 11 | } 12 | } 13 | 14 | impl Serialize for ArrayParams<'_, T> { 15 | fn serialize(&self, serializer: S) -> Result { 16 | let mut map = serializer.serialize_map(Some(self.items.len()))?; 17 | for item in self.items { 18 | map.serialize_entry(self.name, item)?; 19 | } 20 | map.end() 21 | } 22 | } 23 | 24 | #[cfg(test)] 25 | mod tests { 26 | use serde_test::{assert_ser_tokens, Token}; 27 | 28 | use super::ArrayParams; 29 | 30 | #[test] 31 | pub fn serialize_array_params() { 32 | let params = ArrayParams::new("foo[]", &["aaa", "bbb", "ccc"]); 33 | 34 | assert_ser_tokens( 35 | ¶ms, 36 | &[ 37 | Token::Map { len: Some(3) }, 38 | Token::Str("foo[]"), 39 | Token::Str("aaa"), 40 | Token::Str("foo[]"), 41 | Token::Str("bbb"), 42 | Token::Str("foo[]"), 43 | Token::Str("ccc"), 44 | Token::MapEnd, 45 | ], 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/types/auth.rs: -------------------------------------------------------------------------------- 1 | use serde_derive::Deserialize; 2 | use url::Url; 3 | 4 | use super::{utils, Timestamp}; 5 | 6 | /// See the [Access Token Object](https://docs.mod.io/restapiref/#access-token-object) docs for more 7 | /// information. 8 | #[derive(Deserialize)] 9 | #[non_exhaustive] 10 | pub struct AccessToken { 11 | #[serde(rename = "access_token")] 12 | pub value: String, 13 | #[serde(rename = "date_expires")] 14 | pub expired_at: Option, 15 | } 16 | 17 | /// See the [Terms Object](https://docs.mod.io/restapiref/#terms-object) docs for more information. 18 | #[derive(Debug, Deserialize)] 19 | #[non_exhaustive] 20 | pub struct Terms { 21 | pub plaintext: String, 22 | pub html: String, 23 | pub links: Links, 24 | } 25 | 26 | /// Part of [`Terms`] 27 | /// 28 | /// See the [Terms Object](https://docs.mod.io/restapiref/#terms-object) docs for more information. 29 | #[derive(Debug, Deserialize)] 30 | #[non_exhaustive] 31 | pub struct Links { 32 | pub website: Link, 33 | pub terms: Link, 34 | pub privacy: Link, 35 | pub manage: Link, 36 | } 37 | 38 | /// Part of [`Terms`] 39 | /// 40 | /// See the [Terms Object](https://docs.mod.io/restapiref/#terms-object) docs for more information. 41 | #[derive(Debug, Deserialize)] 42 | #[non_exhaustive] 43 | pub struct Link { 44 | pub text: String, 45 | #[serde(with = "utils::url")] 46 | pub url: Url, 47 | pub required: bool, 48 | } 49 | -------------------------------------------------------------------------------- /src/request/games/tags/rename_game_tag.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use serde_derive::Serialize; 4 | 5 | use crate::client::Client; 6 | use crate::request::{Output, RequestBuilder, Route}; 7 | use crate::response::ResponseFuture; 8 | use crate::types::id::GameId; 9 | use crate::types::Message; 10 | 11 | /// Rename an existing tag, updating all mods in the progress. 12 | pub struct RenameGameTag<'a> { 13 | http: &'a Client, 14 | game_id: GameId, 15 | fields: RenameGameTagFields<'a>, 16 | } 17 | 18 | #[derive(Serialize)] 19 | struct RenameGameTagFields<'a> { 20 | from: &'a str, 21 | to: &'a str, 22 | } 23 | 24 | impl<'a> RenameGameTag<'a> { 25 | pub(crate) const fn new(http: &'a Client, game_id: GameId, from: &'a str, to: &'a str) -> Self { 26 | Self { 27 | http, 28 | game_id, 29 | fields: RenameGameTagFields { from, to }, 30 | } 31 | } 32 | } 33 | 34 | impl IntoFuture for RenameGameTag<'_> { 35 | type Output = Output; 36 | type IntoFuture = ResponseFuture; 37 | 38 | fn into_future(self) -> Self::IntoFuture { 39 | let route = Route::RenameGameTags { 40 | game_id: self.game_id, 41 | }; 42 | match RequestBuilder::from_route(&route).form(&self.fields) { 43 | Ok(req) => self.http.request(req), 44 | Err(err) => ResponseFuture::failed(err), 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/request/mods/comments/delete_mod_comment.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::{NoContent, ResponseFuture}; 6 | use crate::types::id::{CommentId, GameId, ModId}; 7 | 8 | /// Delete a comment from a mod profile. 9 | pub struct DeleteModComment<'a> { 10 | http: &'a Client, 11 | game_id: GameId, 12 | mod_id: ModId, 13 | comment_id: CommentId, 14 | } 15 | 16 | impl<'a> DeleteModComment<'a> { 17 | pub(crate) const fn new( 18 | http: &'a Client, 19 | game_id: GameId, 20 | mod_id: ModId, 21 | comment_id: CommentId, 22 | ) -> Self { 23 | Self { 24 | http, 25 | game_id, 26 | mod_id, 27 | comment_id, 28 | } 29 | } 30 | } 31 | 32 | impl IntoFuture for DeleteModComment<'_> { 33 | type Output = Output; 34 | type IntoFuture = ResponseFuture; 35 | 36 | fn into_future(self) -> Self::IntoFuture { 37 | let route = Route::DeleteModComment { 38 | game_id: self.game_id, 39 | mod_id: self.mod_id, 40 | comment_id: self.comment_id, 41 | }; 42 | 43 | match RequestBuilder::from_route(&route).empty() { 44 | Ok(req) => self.http.request(req), 45 | Err(err) => ResponseFuture::failed(err), 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/request/mods/comments/get_mod_comment.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::id::{CommentId, GameId, ModId}; 7 | use crate::types::mods::Comment; 8 | 9 | /// Get a comment posted on a mod profile. 10 | pub struct GetModComment<'a> { 11 | http: &'a Client, 12 | game_id: GameId, 13 | mod_id: ModId, 14 | comment_id: CommentId, 15 | } 16 | 17 | impl<'a> GetModComment<'a> { 18 | pub(crate) const fn new( 19 | http: &'a Client, 20 | game_id: GameId, 21 | mod_id: ModId, 22 | comment_id: CommentId, 23 | ) -> Self { 24 | Self { 25 | http, 26 | game_id, 27 | mod_id, 28 | comment_id, 29 | } 30 | } 31 | } 32 | 33 | impl IntoFuture for GetModComment<'_> { 34 | type Output = Output; 35 | type IntoFuture = ResponseFuture; 36 | 37 | fn into_future(self) -> Self::IntoFuture { 38 | let route = Route::GetModComment { 39 | game_id: self.game_id, 40 | mod_id: self.mod_id, 41 | comment_id: self.comment_id, 42 | }; 43 | match RequestBuilder::from_route(&route).empty() { 44 | Ok(req) => self.http.request(req), 45 | Err(err) => ResponseFuture::failed(err), 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/request/files/multipart/delete_multipart_upload_session.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::{NoContent, ResponseFuture}; 6 | use crate::types::files::multipart::UploadId; 7 | use crate::types::id::{GameId, ModId}; 8 | 9 | pub struct DeleteMultipartUploadSession<'a> { 10 | http: &'a Client, 11 | game_id: GameId, 12 | mod_id: ModId, 13 | upload_id: UploadId, 14 | } 15 | 16 | impl<'a> DeleteMultipartUploadSession<'a> { 17 | pub(crate) const fn new( 18 | http: &'a Client, 19 | game_id: GameId, 20 | mod_id: ModId, 21 | upload_id: UploadId, 22 | ) -> Self { 23 | Self { 24 | http, 25 | game_id, 26 | mod_id, 27 | upload_id, 28 | } 29 | } 30 | } 31 | 32 | impl IntoFuture for DeleteMultipartUploadSession<'_> { 33 | type Output = Output; 34 | type IntoFuture = ResponseFuture; 35 | 36 | fn into_future(self) -> Self::IntoFuture { 37 | let route = Route::DeleteMultipartUploadSession { 38 | game_id: self.game_id, 39 | mod_id: self.mod_id, 40 | upload_id: self.upload_id, 41 | }; 42 | match RequestBuilder::from_route(&route).empty() { 43 | Ok(req) => self.http.request(req), 44 | Err(err) => ResponseFuture::failed(err), 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/request/multipart/encoding.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use percent_encoding::utf8_percent_encode as percent_encode; 4 | use percent_encoding::{AsciiSet, NON_ALPHANUMERIC}; 5 | 6 | // https://url.spec.whatwg.org/#fragment-percent-encode-set 7 | const FRAGMENT_ENCODE_SET: &AsciiSet = &percent_encoding::CONTROLS 8 | .add(b' ') 9 | .add(b'"') 10 | .add(b'<') 11 | .add(b'>') 12 | .add(b'`'); 13 | 14 | // https://url.spec.whatwg.org/#path-percent-encode-set 15 | const PATH_ENCODE_SET: &AsciiSet = &FRAGMENT_ENCODE_SET.add(b'#').add(b'?').add(b'{').add(b'}'); 16 | 17 | const PATH_SEGMENT_ENCODE_SET: &AsciiSet = &PATH_ENCODE_SET.add(b'/').add(b'%'); 18 | 19 | // https://tools.ietf.org/html/rfc8187#section-3.2.1 20 | const ATTR_CHAR_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC 21 | .remove(b'!') 22 | .remove(b'#') 23 | .remove(b'$') 24 | .remove(b'&') 25 | .remove(b'+') 26 | .remove(b'-') 27 | .remove(b'.') 28 | .remove(b'^') 29 | .remove(b'_') 30 | .remove(b'`') 31 | .remove(b'|') 32 | .remove(b'~'); 33 | 34 | #[inline] 35 | pub fn percent_encode_path_segment(input: &str) -> Cow<'_, str> { 36 | percent_encode(input, PATH_SEGMENT_ENCODE_SET).into() 37 | } 38 | 39 | #[inline] 40 | pub fn percent_encode_attr_char(input: &str) -> Cow<'_, str> { 41 | percent_encode(input, ATTR_CHAR_ENCODE_SET).into() 42 | } 43 | 44 | #[inline] 45 | pub fn percent_encode_noop(input: &str) -> Cow<'_, str> { 46 | Cow::Borrowed(input) 47 | } 48 | -------------------------------------------------------------------------------- /src/request/files/multipart/complete_multipart_upload_session.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::files::multipart::{UploadId, UploadSession}; 7 | use crate::types::id::{GameId, ModId}; 8 | 9 | pub struct CompleteMultipartUploadSession<'a> { 10 | http: &'a Client, 11 | game_id: GameId, 12 | mod_id: ModId, 13 | upload_id: UploadId, 14 | } 15 | 16 | impl<'a> CompleteMultipartUploadSession<'a> { 17 | pub(crate) const fn new( 18 | http: &'a Client, 19 | game_id: GameId, 20 | mod_id: ModId, 21 | upload_id: UploadId, 22 | ) -> Self { 23 | Self { 24 | http, 25 | game_id, 26 | mod_id, 27 | upload_id, 28 | } 29 | } 30 | } 31 | 32 | impl IntoFuture for CompleteMultipartUploadSession<'_> { 33 | type Output = Output; 34 | type IntoFuture = ResponseFuture; 35 | 36 | fn into_future(self) -> Self::IntoFuture { 37 | let route = Route::CompleteMultipartUploadSession { 38 | game_id: self.game_id, 39 | mod_id: self.mod_id, 40 | upload_id: self.upload_id, 41 | }; 42 | match RequestBuilder::from_route(&route).empty() { 43 | Ok(req) => self.http.request(req), 44 | Err(err) => ResponseFuture::failed(err), 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/auth.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::io::{self, Write}; 3 | 4 | use modio::Client; 5 | 6 | fn prompt(prompt: &str) -> io::Result { 7 | print!("{}", prompt); 8 | io::stdout().flush()?; 9 | let mut buffer = String::new(); 10 | io::stdin().read_line(&mut buffer)?; 11 | Ok(buffer.trim().to_string()) 12 | } 13 | 14 | #[tokio::main] 15 | async fn main() -> Result<(), Box> { 16 | dotenv::dotenv().ok(); 17 | tracing_subscriber::fmt::init(); 18 | 19 | let host = env::var("MODIO_HOST").unwrap_or_else(|_| "api.test.mod.io".to_string()); 20 | 21 | let api_key = prompt("Enter api key: ")?; 22 | let email = prompt("Enter email: ")?; 23 | 24 | let client = Client::builder(api_key).host(host).build()?; 25 | 26 | let terms = client.get_terms().await?.data().await?; 27 | println!("Terms:\n{}\n", terms.plaintext); 28 | 29 | match &*prompt("Accept? [Y/n]: ")? { 30 | "" | "y" | "Y" => {} 31 | _ => return Ok(()), 32 | } 33 | 34 | client.request_code(&email).await?; 35 | 36 | let code = prompt("Enter security code: ").expect("read code"); 37 | let token = client.request_token(&code).await?.data().await?; 38 | println!("Access token:\n{}", token.value); 39 | 40 | // Consume the endpoint and create an endpoint with new credentials. 41 | let client = client.with_token(token.value); 42 | let user = client.get_authenticated_user().await?.data().await?; 43 | println!("Authenticated user:\n{:#?}", user); 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /src/request/user/get_muted_users.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Filter, Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::{List, User}; 7 | use crate::util::{Paginate, Paginator}; 8 | 9 | /// Get all users muted by the authenticated user. 10 | pub struct GetMutedUsers<'a> { 11 | http: &'a Client, 12 | filter: Option, 13 | } 14 | 15 | impl<'a> GetMutedUsers<'a> { 16 | pub(crate) const fn new(http: &'a Client) -> Self { 17 | Self { http, filter: None } 18 | } 19 | 20 | pub fn filter(mut self, filter: Filter) -> Self { 21 | self.filter = Some(filter); 22 | self 23 | } 24 | } 25 | 26 | impl IntoFuture for GetMutedUsers<'_> { 27 | type Output = Output>; 28 | type IntoFuture = ResponseFuture>; 29 | 30 | fn into_future(self) -> Self::IntoFuture { 31 | let route = Route::UserMuted; 32 | let mut builder = RequestBuilder::from_route(&route); 33 | if let Some(filter) = self.filter { 34 | builder = builder.filter(filter); 35 | } 36 | match builder.empty() { 37 | Ok(req) => self.http.request(req), 38 | Err(err) => ResponseFuture::failed(err), 39 | } 40 | } 41 | } 42 | 43 | impl<'a> Paginate<'a> for GetMutedUsers<'a> { 44 | type Output = User; 45 | 46 | fn paged(&'a self) -> Paginator<'a, Self::Output> { 47 | let route = Route::UserMuted; 48 | 49 | Paginator::new(self.http, route, self.filter.clone()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/request/user/get_user_files.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Filter, Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::files::File; 7 | use crate::types::List; 8 | use crate::util::{Paginate, Paginator}; 9 | 10 | /// Get all modfiles the authenticated user uploaded. 11 | pub struct GetUserFiles<'a> { 12 | http: &'a Client, 13 | filter: Option, 14 | } 15 | 16 | impl<'a> GetUserFiles<'a> { 17 | pub(crate) const fn new(http: &'a Client) -> Self { 18 | Self { http, filter: None } 19 | } 20 | 21 | pub fn filter(mut self, filter: Filter) -> Self { 22 | self.filter = Some(filter); 23 | self 24 | } 25 | } 26 | 27 | impl IntoFuture for GetUserFiles<'_> { 28 | type Output = Output>; 29 | type IntoFuture = ResponseFuture>; 30 | 31 | fn into_future(self) -> Self::IntoFuture { 32 | let route = Route::UserFiles; 33 | let mut builder = RequestBuilder::from_route(&route); 34 | if let Some(filter) = self.filter { 35 | builder = builder.filter(filter); 36 | } 37 | match builder.empty() { 38 | Ok(req) => self.http.request(req), 39 | Err(err) => ResponseFuture::failed(err), 40 | } 41 | } 42 | } 43 | 44 | impl<'a> Paginate<'a> for GetUserFiles<'a> { 45 | type Output = File; 46 | 47 | fn paged(&'a self) -> Paginator<'a, Self::Output> { 48 | let route = Route::UserFiles; 49 | 50 | Paginator::new(self.http, route, self.filter.clone()) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/request/auth/email_exchange.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use serde_derive::Serialize; 4 | 5 | use crate::client::Client; 6 | use crate::request::{Output, RequestBuilder, Route}; 7 | use crate::response::ResponseFuture; 8 | use crate::types::auth::AccessToken; 9 | 10 | /// Request an access token in exchange for a security code. 11 | pub struct EmailExchange<'a> { 12 | http: &'a Client, 13 | fields: EmailExchangeFields<'a>, 14 | } 15 | 16 | #[derive(Serialize)] 17 | struct EmailExchangeFields<'a> { 18 | security_code: &'a str, 19 | #[serde(rename = "date_expires", skip_serializing_if = "Option::is_none")] 20 | expired_at: Option, 21 | } 22 | 23 | impl<'a> EmailExchange<'a> { 24 | pub(crate) const fn new(http: &'a Client, security_code: &'a str) -> Self { 25 | Self { 26 | http, 27 | fields: EmailExchangeFields { 28 | security_code, 29 | expired_at: None, 30 | }, 31 | } 32 | } 33 | 34 | pub const fn expired_at(mut self, expired_at: u64) -> Self { 35 | self.fields.expired_at = Some(expired_at); 36 | self 37 | } 38 | } 39 | 40 | impl IntoFuture for EmailExchange<'_> { 41 | type Output = Output; 42 | type IntoFuture = ResponseFuture; 43 | 44 | fn into_future(self) -> Self::IntoFuture { 45 | let route = Route::OAuthEmailExchange; 46 | match RequestBuilder::from_route(&route).form(&self.fields) { 47 | Ok(req) => self.http.request(req), 48 | Err(err) => ResponseFuture::failed(err), 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/request/user/get_user_mods.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Filter, Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::mods::Mod; 7 | use crate::types::List; 8 | use crate::util::{Paginate, Paginator}; 9 | 10 | /// Get all mods the authenticated user added or is a team member of. 11 | pub struct GetUserMods<'a> { 12 | http: &'a Client, 13 | filter: Option, 14 | } 15 | 16 | impl<'a> GetUserMods<'a> { 17 | pub(crate) const fn new(http: &'a Client) -> Self { 18 | Self { http, filter: None } 19 | } 20 | 21 | pub fn filter(mut self, filter: Filter) -> Self { 22 | self.filter = Some(filter); 23 | self 24 | } 25 | } 26 | 27 | impl IntoFuture for GetUserMods<'_> { 28 | type Output = Output>; 29 | type IntoFuture = ResponseFuture>; 30 | 31 | fn into_future(self) -> Self::IntoFuture { 32 | let route = Route::UserMods; 33 | let mut builder = RequestBuilder::from_route(&route); 34 | if let Some(filter) = self.filter { 35 | builder = builder.filter(filter); 36 | } 37 | match builder.empty() { 38 | Ok(req) => self.http.request(req), 39 | Err(err) => ResponseFuture::failed(err), 40 | } 41 | } 42 | } 43 | 44 | impl<'a> Paginate<'a> for GetUserMods<'a> { 45 | type Output = Mod; 46 | 47 | fn paged(&'a self) -> Paginator<'a, Self::Output> { 48 | let route = Route::UserMods; 49 | 50 | Paginator::new(self.http, route, self.filter.clone()) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/request/user/get_user_games.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Filter, Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::games::Game; 7 | use crate::types::List; 8 | use crate::util::{Paginate, Paginator}; 9 | 10 | /// Get all games the authenticated user added or is a team member of. 11 | pub struct GetUserGames<'a> { 12 | http: &'a Client, 13 | filter: Option, 14 | } 15 | 16 | impl<'a> GetUserGames<'a> { 17 | pub(crate) const fn new(http: &'a Client) -> Self { 18 | Self { http, filter: None } 19 | } 20 | 21 | pub fn filter(mut self, filter: Filter) -> Self { 22 | self.filter = Some(filter); 23 | self 24 | } 25 | } 26 | 27 | impl IntoFuture for GetUserGames<'_> { 28 | type Output = Output>; 29 | type IntoFuture = ResponseFuture>; 30 | 31 | fn into_future(self) -> Self::IntoFuture { 32 | let route = Route::UserGames; 33 | let mut builder = RequestBuilder::from_route(&route); 34 | if let Some(filter) = self.filter { 35 | builder = builder.filter(filter); 36 | } 37 | match builder.empty() { 38 | Ok(req) => self.http.request(req), 39 | Err(err) => ResponseFuture::failed(err), 40 | } 41 | } 42 | } 43 | 44 | impl<'a> Paginate<'a> for GetUserGames<'a> { 45 | type Output = Game; 46 | 47 | fn paged(&'a self) -> Paginator<'a, Self::Output> { 48 | let route = Route::UserGames; 49 | 50 | Paginator::new(self.http, route, self.filter.clone()) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/request/mods/get_mod_team_members.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Filter, Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::id::{GameId, ModId}; 7 | use crate::types::mods::TeamMember; 8 | use crate::types::List; 9 | 10 | /// Get all users that are part of a mod team. 11 | pub struct GetModTeamMembers<'a> { 12 | http: &'a Client, 13 | game_id: GameId, 14 | mod_id: ModId, 15 | filter: Option, 16 | } 17 | 18 | impl<'a> GetModTeamMembers<'a> { 19 | pub(crate) const fn new(http: &'a Client, game_id: GameId, mod_id: ModId) -> Self { 20 | Self { 21 | http, 22 | game_id, 23 | mod_id, 24 | filter: None, 25 | } 26 | } 27 | 28 | pub fn filter(mut self, filter: Filter) -> Self { 29 | self.filter = Some(filter); 30 | self 31 | } 32 | } 33 | 34 | impl IntoFuture for GetModTeamMembers<'_> { 35 | type Output = Output>; 36 | type IntoFuture = ResponseFuture>; 37 | 38 | fn into_future(self) -> Self::IntoFuture { 39 | let route = Route::GetModTeamMembers { 40 | game_id: self.game_id, 41 | mod_id: self.mod_id, 42 | }; 43 | let mut builder = RequestBuilder::from_route(&route); 44 | if let Some(filter) = self.filter { 45 | builder = builder.filter(filter); 46 | } 47 | match builder.empty() { 48 | Ok(req) => self.http.request(req), 49 | Err(err) => ResponseFuture::failed(err), 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/request/user/get_user_ratings.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Filter, Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::mods::Rating; 7 | use crate::types::List; 8 | use crate::util::{Paginate, Paginator}; 9 | 10 | /// Get all mod ratings submitted by the authenticated user. 11 | pub struct GetUserRatings<'a> { 12 | http: &'a Client, 13 | filter: Option, 14 | } 15 | 16 | impl<'a> GetUserRatings<'a> { 17 | pub(crate) const fn new(http: &'a Client) -> Self { 18 | Self { http, filter: None } 19 | } 20 | 21 | pub fn filter(mut self, filter: Filter) -> Self { 22 | self.filter = Some(filter); 23 | self 24 | } 25 | } 26 | 27 | impl IntoFuture for GetUserRatings<'_> { 28 | type Output = Output>; 29 | type IntoFuture = ResponseFuture>; 30 | 31 | fn into_future(self) -> Self::IntoFuture { 32 | let route = Route::UserRatings; 33 | let mut builder = RequestBuilder::from_route(&route); 34 | if let Some(filter) = self.filter { 35 | builder = builder.filter(filter); 36 | } 37 | match builder.empty() { 38 | Ok(req) => self.http.request(req), 39 | Err(err) => ResponseFuture::failed(err), 40 | } 41 | } 42 | } 43 | 44 | impl<'a> Paginate<'a> for GetUserRatings<'a> { 45 | type Output = Rating; 46 | 47 | fn paged(&'a self) -> Paginator<'a, Self::Output> { 48 | let route = Route::UserRatings; 49 | 50 | Paginator::new(self.http, route, self.filter.clone()) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/request/user/get_user_subscriptions.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Filter, Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::mods::Mod; 7 | use crate::types::List; 8 | use crate::util::{Paginate, Paginator}; 9 | 10 | /// Get all mod the authenticated user is subscribed to. 11 | pub struct GetUserSubscriptions<'a> { 12 | http: &'a Client, 13 | filter: Option, 14 | } 15 | 16 | impl<'a> GetUserSubscriptions<'a> { 17 | pub(crate) const fn new(http: &'a Client) -> Self { 18 | Self { http, filter: None } 19 | } 20 | 21 | pub fn filter(mut self, filter: Filter) -> Self { 22 | self.filter = Some(filter); 23 | self 24 | } 25 | } 26 | 27 | impl IntoFuture for GetUserSubscriptions<'_> { 28 | type Output = Output>; 29 | type IntoFuture = ResponseFuture>; 30 | 31 | fn into_future(self) -> Self::IntoFuture { 32 | let route = Route::UserSubscriptions; 33 | let mut builder = RequestBuilder::from_route(&route); 34 | if let Some(filter) = self.filter { 35 | builder = builder.filter(filter); 36 | } 37 | match builder.empty() { 38 | Ok(req) => self.http.request(req), 39 | Err(err) => ResponseFuture::failed(err), 40 | } 41 | } 42 | } 43 | 44 | impl<'a> Paginate<'a> for GetUserSubscriptions<'a> { 45 | type Output = Mod; 46 | 47 | fn paged(&'a self) -> Paginator<'a, Self::Output> { 48 | let route = Route::UserSubscriptions; 49 | 50 | Paginator::new(self.http, route, self.filter.clone()) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/request/mods/comments/edit_mod_comment.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use serde_derive::Serialize; 4 | 5 | use crate::client::Client; 6 | use crate::request::{Output, RequestBuilder, Route}; 7 | use crate::response::ResponseFuture; 8 | use crate::types::id::{CommentId, GameId, ModId}; 9 | use crate::types::mods::Comment; 10 | 11 | /// Edit a comment for a mod. 12 | pub struct EditModComment<'a> { 13 | http: &'a Client, 14 | game_id: GameId, 15 | mod_id: ModId, 16 | comment_id: CommentId, 17 | fields: EditModCommentFields<'a>, 18 | } 19 | 20 | #[derive(Serialize)] 21 | struct EditModCommentFields<'a> { 22 | content: &'a str, 23 | } 24 | 25 | impl<'a> EditModComment<'a> { 26 | pub(crate) const fn new( 27 | http: &'a Client, 28 | game_id: GameId, 29 | mod_id: ModId, 30 | comment_id: CommentId, 31 | content: &'a str, 32 | ) -> Self { 33 | Self { 34 | http, 35 | game_id, 36 | mod_id, 37 | comment_id, 38 | fields: EditModCommentFields { content }, 39 | } 40 | } 41 | } 42 | 43 | impl IntoFuture for EditModComment<'_> { 44 | type Output = Output; 45 | type IntoFuture = ResponseFuture; 46 | 47 | fn into_future(self) -> Self::IntoFuture { 48 | let route = Route::EditModComment { 49 | game_id: self.game_id, 50 | mod_id: self.mod_id, 51 | comment_id: self.comment_id, 52 | }; 53 | 54 | match RequestBuilder::from_route(&route).form(&self.fields) { 55 | Ok(req) => self.http.request(req), 56 | Err(err) => ResponseFuture::failed(err), 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/util/upload/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | type Source = Box; 4 | 5 | pub struct Error { 6 | kind: ErrorKind, 7 | source: Option, 8 | } 9 | 10 | impl Error { 11 | pub const fn kind(&self) -> &ErrorKind { 12 | &self.kind 13 | } 14 | } 15 | 16 | impl Error { 17 | #[inline] 18 | pub(crate) fn request>(source: E) -> Self { 19 | Self { 20 | kind: ErrorKind::Request, 21 | source: Some(source.into()), 22 | } 23 | } 24 | 25 | #[inline] 26 | pub(crate) fn body>(source: E) -> Self { 27 | Self { 28 | kind: ErrorKind::Body, 29 | source: Some(source.into()), 30 | } 31 | } 32 | } 33 | 34 | impl fmt::Debug for Error { 35 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 36 | let mut s = f.debug_struct("Error"); 37 | 38 | s.field("kind", &self.kind); 39 | if let Some(ref source) = self.source { 40 | s.field("source", source); 41 | } 42 | s.finish() 43 | } 44 | } 45 | 46 | impl fmt::Display for Error { 47 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 48 | match self.kind { 49 | ErrorKind::Request => f.write_str("request failed"), 50 | ErrorKind::Body => f.write_str("failed to load response body"), 51 | } 52 | } 53 | } 54 | 55 | impl std::error::Error for Error { 56 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 57 | self.source.as_ref().map(|e| &**e as _) 58 | } 59 | } 60 | 61 | #[derive(Debug)] 62 | #[non_exhaustive] 63 | pub enum ErrorKind { 64 | Request, 65 | Body, 66 | } 67 | -------------------------------------------------------------------------------- /src/request/files/multipart/get_multipart_upload_sessions.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::files::multipart::UploadSession; 7 | use crate::types::id::{GameId, ModId}; 8 | use crate::types::List; 9 | use crate::util::{Paginate, Paginator}; 10 | 11 | pub struct GetMultipartUploadSessions<'a> { 12 | http: &'a Client, 13 | game_id: GameId, 14 | mod_id: ModId, 15 | } 16 | 17 | impl<'a> GetMultipartUploadSessions<'a> { 18 | pub(crate) const fn new(http: &'a Client, game_id: GameId, mod_id: ModId) -> Self { 19 | Self { 20 | http, 21 | game_id, 22 | mod_id, 23 | } 24 | } 25 | } 26 | 27 | impl IntoFuture for GetMultipartUploadSessions<'_> { 28 | type Output = Output>; 29 | type IntoFuture = ResponseFuture>; 30 | 31 | fn into_future(self) -> Self::IntoFuture { 32 | let route = Route::GetMultipartUploadSessions { 33 | game_id: self.game_id, 34 | mod_id: self.mod_id, 35 | }; 36 | match RequestBuilder::from_route(&route).empty() { 37 | Ok(req) => self.http.request(req), 38 | Err(err) => ResponseFuture::failed(err), 39 | } 40 | } 41 | } 42 | 43 | impl<'a> Paginate<'a> for GetMultipartUploadSessions<'a> { 44 | type Output = UploadSession; 45 | 46 | fn paged(&'a self) -> Paginator<'a, Self::Output> { 47 | let route = Route::GetMultipartUploadSessions { 48 | game_id: self.game_id, 49 | mod_id: self.mod_id, 50 | }; 51 | Paginator::new(self.http, route, None) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/client/conn.rs: -------------------------------------------------------------------------------- 1 | type HttpConnector = hyper_util::client::legacy::connect::HttpConnector; 2 | 3 | #[cfg(all( 4 | feature = "native-tls", 5 | not(any(feature = "rustls-native-roots", feature = "rustls-webpki-roots")) 6 | ))] 7 | type HttpsConnector = hyper_tls::HttpsConnector; 8 | 9 | #[cfg(any(feature = "rustls-native-roots", feature = "rustls-webpki-roots"))] 10 | type HttpsConnector = hyper_rustls::HttpsConnector; 11 | 12 | #[cfg(any( 13 | feature = "native-tls", 14 | feature = "rustls-native-roots", 15 | feature = "rustls-webpki-roots" 16 | ))] 17 | pub type Connector = HttpsConnector; 18 | 19 | #[cfg(not(any( 20 | feature = "native-tls", 21 | feature = "rustls-native-roots", 22 | feature = "rustls-webpki-roots" 23 | )))] 24 | pub type Connector = HttpConnector; 25 | 26 | pub fn create_connector() -> Connector { 27 | let mut connector = HttpConnector::new(); 28 | connector.enforce_http(false); 29 | 30 | #[cfg(feature = "rustls-native-roots")] 31 | let connector = hyper_rustls::HttpsConnectorBuilder::new() 32 | .with_native_roots() 33 | .expect("failed to load native roots") 34 | .https_only() 35 | .enable_http1() 36 | .wrap_connector(connector); 37 | 38 | #[cfg(all(feature = "rustls-webpki-roots", not(feature = "rustls-native-roots")))] 39 | let connector = hyper_rustls::HttpsConnectorBuilder::new() 40 | .with_webpki_roots() 41 | .https_only() 42 | .enable_http1() 43 | .wrap_connector(connector); 44 | 45 | #[cfg(all( 46 | feature = "native-tls", 47 | not(any(feature = "rustls-native-roots", feature = "rustls-webpki-roots")) 48 | ))] 49 | let connector = hyper_tls::HttpsConnector::new_with_connector(connector); 50 | 51 | connector 52 | } 53 | -------------------------------------------------------------------------------- /src/request/mods/submit_mod_rating.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use serde_derive::Serialize; 4 | 5 | use crate::client::Client; 6 | use crate::request::{Output, RequestBuilder, Route}; 7 | use crate::response::ResponseFuture; 8 | use crate::types::id::{GameId, ModId}; 9 | use crate::types::Message; 10 | 11 | /// Submit a positive or negative rating for a mod. 12 | pub struct SubmitModRating<'a> { 13 | http: &'a Client, 14 | game_id: GameId, 15 | mod_id: ModId, 16 | fields: AddModRatingFields, 17 | } 18 | 19 | #[derive(Serialize)] 20 | struct AddModRatingFields { 21 | rating: i8, 22 | } 23 | 24 | impl<'a> SubmitModRating<'a> { 25 | pub(crate) const fn new(http: &'a Client, game_id: GameId, mod_id: ModId) -> Self { 26 | Self { 27 | http, 28 | game_id, 29 | mod_id, 30 | fields: AddModRatingFields { rating: 1 }, 31 | } 32 | } 33 | 34 | pub const fn positive(mut self) -> Self { 35 | self.fields.rating = 1; 36 | self 37 | } 38 | 39 | pub const fn negative(mut self) -> Self { 40 | self.fields.rating = -1; 41 | self 42 | } 43 | 44 | pub const fn reset(mut self) -> Self { 45 | self.fields.rating = 0; 46 | self 47 | } 48 | } 49 | 50 | impl IntoFuture for SubmitModRating<'_> { 51 | type Output = Output; 52 | type IntoFuture = ResponseFuture; 53 | 54 | fn into_future(self) -> Self::IntoFuture { 55 | let route = Route::RateMod { 56 | game_id: self.game_id, 57 | mod_id: self.mod_id, 58 | }; 59 | match RequestBuilder::from_route(&route).form(&self.fields) { 60 | Ok(req) => self.http.request(req), 61 | Err(err) => ResponseFuture::failed(err), 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/request/mods/dependencies/get_mod_dependencies.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::id::{GameId, ModId}; 7 | use crate::types::mods::Dependency; 8 | use crate::types::List; 9 | use crate::util::{Paginate, Paginator}; 10 | 11 | /// Get all dependencies a mod has selected. 12 | pub struct GetModDependencies<'a> { 13 | http: &'a Client, 14 | game_id: GameId, 15 | mod_id: ModId, 16 | recursive: Option, 17 | } 18 | 19 | impl<'a> GetModDependencies<'a> { 20 | pub(crate) const fn new(http: &'a Client, game_id: GameId, mod_id: ModId) -> Self { 21 | Self { 22 | http, 23 | game_id, 24 | mod_id, 25 | recursive: None, 26 | } 27 | } 28 | } 29 | 30 | impl IntoFuture for GetModDependencies<'_> { 31 | type Output = Output>; 32 | type IntoFuture = ResponseFuture>; 33 | 34 | fn into_future(self) -> Self::IntoFuture { 35 | let route = Route::GetModDependencies { 36 | game_id: self.game_id, 37 | mod_id: self.mod_id, 38 | recursive: self.recursive, 39 | }; 40 | match RequestBuilder::from_route(&route).empty() { 41 | Ok(req) => self.http.request(req), 42 | Err(err) => ResponseFuture::failed(err), 43 | } 44 | } 45 | } 46 | 47 | impl<'a> Paginate<'a> for GetModDependencies<'a> { 48 | type Output = Dependency; 49 | 50 | fn paged(&'a self) -> Paginator<'a, Self::Output> { 51 | let route = Route::GetModDependencies { 52 | game_id: self.game_id, 53 | mod_id: self.mod_id, 54 | recursive: self.recursive, 55 | }; 56 | Paginator::new(self.http, route, None) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/request/mods/comments/add_mod_comment.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use serde_derive::Serialize; 4 | 5 | use crate::client::Client; 6 | use crate::request::{Output, RequestBuilder, Route}; 7 | use crate::response::ResponseFuture; 8 | use crate::types::id::{CommentId, GameId, ModId}; 9 | use crate::types::mods::Comment; 10 | 11 | /// Add a comment for a mod. 12 | pub struct AddModComment<'a> { 13 | http: &'a Client, 14 | game_id: GameId, 15 | mod_id: ModId, 16 | fields: AddModCommentFields<'a>, 17 | } 18 | 19 | #[derive(Serialize)] 20 | struct AddModCommentFields<'a> { 21 | content: &'a str, 22 | #[serde(skip_serializing_if = "Option::is_none")] 23 | reply_id: Option, 24 | } 25 | 26 | impl<'a> AddModComment<'a> { 27 | pub(crate) const fn new( 28 | http: &'a Client, 29 | game_id: GameId, 30 | mod_id: ModId, 31 | content: &'a str, 32 | ) -> Self { 33 | Self { 34 | http, 35 | game_id, 36 | mod_id, 37 | fields: AddModCommentFields { 38 | content, 39 | reply_id: None, 40 | }, 41 | } 42 | } 43 | 44 | pub const fn reply_id(mut self, reply_id: CommentId) -> Self { 45 | self.fields.reply_id = Some(reply_id); 46 | self 47 | } 48 | } 49 | 50 | impl IntoFuture for AddModComment<'_> { 51 | type Output = Output; 52 | type IntoFuture = ResponseFuture; 53 | 54 | fn into_future(self) -> Self::IntoFuture { 55 | let route = Route::AddModComment { 56 | game_id: self.game_id, 57 | mod_id: self.mod_id, 58 | }; 59 | match RequestBuilder::from_route(&route).form(&self.fields) { 60 | Ok(req) => self.http.request(req), 61 | Err(err) => ResponseFuture::failed(err), 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/events.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::process; 3 | use std::time::{Duration, SystemTime, UNIX_EPOCH}; 4 | 5 | use tokio::time::{self, Instant}; 6 | 7 | use modio::request::filter::prelude::*; 8 | use modio::Client; 9 | 10 | const TEN_SECS: Duration = Duration::from_secs(10); 11 | 12 | fn current_timestamp() -> u64 { 13 | SystemTime::now() 14 | .duration_since(UNIX_EPOCH) 15 | .unwrap() 16 | .as_secs() 17 | } 18 | 19 | #[tokio::main] 20 | async fn main() -> Result<(), Box> { 21 | dotenv::dotenv().ok(); 22 | tracing_subscriber::fmt::init(); 23 | 24 | // Fetch the access token & api key from the environment of the current process. 25 | let (api_key, token) = match (env::var("MODIO_API_KEY"), env::var("MODIO_TOKEN")) { 26 | (Ok(api_key), Ok(token)) => (api_key, token), 27 | _ => { 28 | eprintln!("missing MODIO_TOKEN and MODIO_API_KEY environment variable"); 29 | process::exit(1); 30 | } 31 | }; 32 | let host = env::var("MODIO_HOST").unwrap_or_else(|_| "api.test.mod.io".to_string()); 33 | 34 | let client = Client::builder(api_key).token(token).host(host).build()?; 35 | 36 | // Creates an `Interval` that yields every 10 seconds starting in 10 seconds. 37 | let mut interval = time::interval_at(Instant::now() + TEN_SECS, TEN_SECS); 38 | 39 | loop { 40 | let tstamp = current_timestamp(); 41 | interval.tick().await; 42 | 43 | // Create an event filter for `date_added` > time. 44 | let filter = DateAdded::gt(tstamp); 45 | println!("event filter: {}", filter); 46 | 47 | let list = client 48 | .get_user_events() 49 | .filter(filter) 50 | .await? 51 | .data() 52 | .await?; 53 | 54 | println!("event count: {}", list.data.len()); 55 | println!("{:#?}", list); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/request/mods/get_mods.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Filter, Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::id::GameId; 7 | use crate::types::mods::Mod; 8 | use crate::types::List; 9 | use crate::util::{Paginate, Paginator}; 10 | 11 | /// Get all mods for a game. 12 | pub struct GetMods<'a> { 13 | http: &'a Client, 14 | game_id: GameId, 15 | filter: Option, 16 | } 17 | 18 | impl<'a> GetMods<'a> { 19 | pub(crate) const fn new(http: &'a Client, game_id: GameId) -> Self { 20 | Self { 21 | http, 22 | game_id, 23 | filter: None, 24 | } 25 | } 26 | 27 | /// Set the filter for the request. 28 | /// 29 | /// See [Filters and sorting](super::filters). 30 | pub fn filter(mut self, filter: Filter) -> Self { 31 | self.filter = Some(filter); 32 | self 33 | } 34 | } 35 | 36 | impl IntoFuture for GetMods<'_> { 37 | type Output = Output>; 38 | type IntoFuture = ResponseFuture>; 39 | 40 | fn into_future(self) -> Self::IntoFuture { 41 | let route = Route::GetMods { 42 | game_id: self.game_id, 43 | }; 44 | let mut builder = RequestBuilder::from_route(&route); 45 | if let Some(filter) = self.filter { 46 | builder = builder.filter(filter); 47 | } 48 | match builder.empty() { 49 | Ok(req) => self.http.request(req), 50 | Err(err) => ResponseFuture::failed(err), 51 | } 52 | } 53 | } 54 | 55 | impl<'a> Paginate<'a> for GetMods<'a> { 56 | type Output = Mod; 57 | 58 | fn paged(&'a self) -> Paginator<'a, Self::Output> { 59 | let route = Route::GetMods { 60 | game_id: self.game_id, 61 | }; 62 | Paginator::new(self.http, route, self.filter.clone()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/request/files/multipart/get_multipart_upload_parts.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::files::multipart::{UploadId, UploadPart}; 7 | use crate::types::id::{GameId, ModId}; 8 | use crate::types::List; 9 | use crate::util::{Paginate, Paginator}; 10 | 11 | pub struct GetMultipartUploadParts<'a> { 12 | http: &'a Client, 13 | game_id: GameId, 14 | mod_id: ModId, 15 | upload_id: UploadId, 16 | } 17 | 18 | impl<'a> GetMultipartUploadParts<'a> { 19 | pub(crate) const fn new( 20 | http: &'a Client, 21 | game_id: GameId, 22 | mod_id: ModId, 23 | upload_id: UploadId, 24 | ) -> Self { 25 | Self { 26 | http, 27 | game_id, 28 | mod_id, 29 | upload_id, 30 | } 31 | } 32 | } 33 | 34 | impl IntoFuture for GetMultipartUploadParts<'_> { 35 | type Output = Output>; 36 | type IntoFuture = ResponseFuture>; 37 | 38 | fn into_future(self) -> Self::IntoFuture { 39 | let route = Route::GetMultipartUploadParts { 40 | game_id: self.game_id, 41 | mod_id: self.mod_id, 42 | upload_id: self.upload_id, 43 | }; 44 | match RequestBuilder::from_route(&route).empty() { 45 | Ok(req) => self.http.request(req), 46 | Err(err) => ResponseFuture::failed(err), 47 | } 48 | } 49 | } 50 | 51 | impl<'a> Paginate<'a> for GetMultipartUploadParts<'a> { 52 | type Output = UploadPart; 53 | 54 | fn paged(&'a self) -> Paginator<'a, Self::Output> { 55 | let route = Route::GetMultipartUploadParts { 56 | game_id: self.game_id, 57 | mod_id: self.mod_id, 58 | upload_id: self.upload_id, 59 | }; 60 | Paginator::new(self.http, route, None) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/request/games/tags/get_game_tags.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::games::TagOption; 7 | use crate::types::id::GameId; 8 | use crate::types::List; 9 | use crate::util::{Paginate, Paginator}; 10 | 11 | /// Get all tags for the corresponding game, that can be applied to any of its mods. 12 | pub struct GetGameTags<'a> { 13 | http: &'a Client, 14 | game_id: GameId, 15 | show_hidden_tags: Option, 16 | } 17 | 18 | impl<'a> GetGameTags<'a> { 19 | pub(crate) const fn new(http: &'a Client, game_id: GameId) -> Self { 20 | Self { 21 | http, 22 | game_id, 23 | show_hidden_tags: None, 24 | } 25 | } 26 | 27 | /// Show the hidden tags associated with the given game. 28 | pub const fn show_hidden_tags(mut self, value: bool) -> Self { 29 | self.show_hidden_tags = Some(value); 30 | self 31 | } 32 | } 33 | 34 | impl IntoFuture for GetGameTags<'_> { 35 | type Output = Output>; 36 | type IntoFuture = ResponseFuture>; 37 | 38 | fn into_future(self) -> Self::IntoFuture { 39 | let route = Route::GetGameTags { 40 | game_id: self.game_id, 41 | show_hidden_tags: self.show_hidden_tags, 42 | }; 43 | match RequestBuilder::from_route(&route).empty() { 44 | Ok(req) => self.http.request(req), 45 | Err(err) => ResponseFuture::failed(err), 46 | } 47 | } 48 | } 49 | 50 | impl<'a> Paginate<'a> for GetGameTags<'a> { 51 | type Output = TagOption; 52 | 53 | fn paged(&'a self) -> Paginator<'a, Self::Output> { 54 | let route = Route::GetGameTags { 55 | game_id: self.game_id, 56 | show_hidden_tags: self.show_hidden_tags, 57 | }; 58 | 59 | Paginator::new(self.http, route, None) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/request/mods/tags/get_mod_tags.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Filter, Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::id::{GameId, ModId}; 7 | use crate::types::mods::Tag; 8 | use crate::types::List; 9 | use crate::util::{Paginate, Paginator}; 10 | 11 | /// Get all tags for a mod. 12 | pub struct GetModTags<'a> { 13 | http: &'a Client, 14 | game_id: GameId, 15 | mod_id: ModId, 16 | filter: Option, 17 | } 18 | 19 | impl<'a> GetModTags<'a> { 20 | pub(crate) const fn new(http: &'a Client, game_id: GameId, mod_id: ModId) -> Self { 21 | Self { 22 | http, 23 | game_id, 24 | mod_id, 25 | filter: None, 26 | } 27 | } 28 | 29 | pub fn filter(mut self, filter: Filter) -> Self { 30 | self.filter = Some(filter); 31 | self 32 | } 33 | } 34 | 35 | impl IntoFuture for GetModTags<'_> { 36 | type Output = Output>; 37 | type IntoFuture = ResponseFuture>; 38 | 39 | fn into_future(self) -> Self::IntoFuture { 40 | let route = Route::GetModTags { 41 | game_id: self.game_id, 42 | mod_id: self.mod_id, 43 | }; 44 | let mut builder = RequestBuilder::from_route(&route); 45 | if let Some(filter) = self.filter { 46 | builder = builder.filter(filter); 47 | } 48 | match builder.empty() { 49 | Ok(req) => self.http.request(req), 50 | Err(err) => ResponseFuture::failed(err), 51 | } 52 | } 53 | } 54 | 55 | impl<'a> Paginate<'a> for GetModTags<'a> { 56 | type Output = Tag; 57 | 58 | fn paged(&'a self) -> Paginator<'a, Self::Output> { 59 | let route = Route::GetModTags { 60 | game_id: self.game_id, 61 | mod_id: self.mod_id, 62 | }; 63 | Paginator::new(self.http, route, self.filter.clone()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/request/mods/events/get_mods_events.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Filter, Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::id::GameId; 7 | use crate::types::mods::Event; 8 | use crate::types::List; 9 | use crate::util::{Paginate, Paginator}; 10 | 11 | /// Get all mod events for a game. 12 | pub struct GetModsEvents<'a> { 13 | http: &'a Client, 14 | game_id: GameId, 15 | filter: Option, 16 | } 17 | 18 | impl<'a> GetModsEvents<'a> { 19 | pub(crate) const fn new(http: &'a Client, game_id: GameId) -> Self { 20 | Self { 21 | http, 22 | game_id, 23 | filter: None, 24 | } 25 | } 26 | 27 | /// Set the filter for the request. 28 | /// 29 | /// See [Filters and sorting](super::filters). 30 | pub fn filter(mut self, filter: Filter) -> Self { 31 | self.filter = Some(filter); 32 | self 33 | } 34 | } 35 | 36 | impl IntoFuture for GetModsEvents<'_> { 37 | type Output = Output>; 38 | type IntoFuture = ResponseFuture>; 39 | 40 | fn into_future(self) -> Self::IntoFuture { 41 | let route = Route::GetModsEvents { 42 | game_id: self.game_id, 43 | }; 44 | let mut builder = RequestBuilder::from_route(&route); 45 | if let Some(filter) = self.filter { 46 | builder = builder.filter(filter); 47 | } 48 | match builder.empty() { 49 | Ok(req) => self.http.request(req), 50 | Err(err) => ResponseFuture::failed(err), 51 | } 52 | } 53 | } 54 | 55 | impl<'a> Paginate<'a> for GetModsEvents<'a> { 56 | type Output = Event; 57 | 58 | fn paged(&'a self) -> Paginator<'a, Self::Output> { 59 | let route = Route::GetModsEvents { 60 | game_id: self.game_id, 61 | }; 62 | Paginator::new(self.http, route, self.filter.clone()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/request/mods/comments/update_mod_comment_karma.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use serde_derive::Serialize; 4 | 5 | use crate::client::Client; 6 | use crate::request::{Output, RequestBuilder, Route}; 7 | use crate::response::ResponseFuture; 8 | use crate::types::id::{CommentId, GameId, ModId}; 9 | use crate::types::mods::Comment; 10 | 11 | /// Update the Karma rating in single increments or decrements for a mod comment. 12 | pub struct UpdateModCommentKarma<'a> { 13 | http: &'a Client, 14 | game_id: GameId, 15 | mod_id: ModId, 16 | comment_id: CommentId, 17 | fields: UpdateModCommentKarmaFields, 18 | } 19 | 20 | #[derive(Serialize)] 21 | struct UpdateModCommentKarmaFields { 22 | karma: i8, 23 | } 24 | 25 | impl<'a> UpdateModCommentKarma<'a> { 26 | pub(crate) const fn new( 27 | http: &'a Client, 28 | game_id: GameId, 29 | mod_id: ModId, 30 | comment_id: CommentId, 31 | ) -> Self { 32 | Self { 33 | http, 34 | game_id, 35 | mod_id, 36 | comment_id, 37 | fields: UpdateModCommentKarmaFields { karma: 1 }, 38 | } 39 | } 40 | 41 | pub const fn positive(mut self) -> Self { 42 | self.fields.karma = 1; 43 | self 44 | } 45 | 46 | pub const fn negative(mut self) -> Self { 47 | self.fields.karma = -1; 48 | self 49 | } 50 | } 51 | 52 | impl IntoFuture for UpdateModCommentKarma<'_> { 53 | type Output = Output; 54 | type IntoFuture = ResponseFuture; 55 | 56 | fn into_future(self) -> Self::IntoFuture { 57 | let route = Route::UpdateModCommentKarma { 58 | game_id: self.game_id, 59 | mod_id: self.mod_id, 60 | comment_id: self.comment_id, 61 | }; 62 | match RequestBuilder::from_route(&route).form(&self.fields) { 63 | Ok(req) => self.http.request(req), 64 | Err(err) => ResponseFuture::failed(err), 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/request/files/manage_platform_status.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use serde_derive::Serialize; 4 | 5 | use crate::client::Client; 6 | use crate::request::{Output, RequestBuilder, Route}; 7 | use crate::response::ResponseFuture; 8 | use crate::types::files::File; 9 | use crate::types::id::{FileId, GameId, ModId}; 10 | use crate::types::TargetPlatform; 11 | 12 | /// Manage the platform status of a particular modfile. 13 | pub struct ManagePlatformStatus<'a> { 14 | http: &'a Client, 15 | game_id: GameId, 16 | mod_id: ModId, 17 | file_id: FileId, 18 | fields: ManagePlatformStatusFields<'a>, 19 | } 20 | 21 | #[derive(Serialize)] 22 | struct ManagePlatformStatusFields<'a> { 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | approved: Option<&'a [TargetPlatform]>, 25 | #[serde(skip_serializing_if = "Option::is_none")] 26 | denied: Option<&'a [TargetPlatform]>, 27 | } 28 | 29 | impl<'a> ManagePlatformStatus<'a> { 30 | pub(crate) const fn new( 31 | http: &'a Client, 32 | game_id: GameId, 33 | mod_id: ModId, 34 | file_id: FileId, 35 | ) -> Self { 36 | Self { 37 | http, 38 | game_id, 39 | mod_id, 40 | file_id, 41 | fields: ManagePlatformStatusFields { 42 | approved: None, 43 | denied: None, 44 | }, 45 | } 46 | } 47 | } 48 | 49 | impl IntoFuture for ManagePlatformStatus<'_> { 50 | type Output = Output; 51 | type IntoFuture = ResponseFuture; 52 | 53 | fn into_future(self) -> Self::IntoFuture { 54 | let route = Route::ManagePlatformStatus { 55 | game_id: self.game_id, 56 | mod_id: self.mod_id, 57 | file_id: self.file_id, 58 | }; 59 | match RequestBuilder::from_route(&route).form(&self.fields) { 60 | Ok(req) => self.http.request(req), 61 | Err(err) => ResponseFuture::failed(err), 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/request/mods/stats/get_mods_stats.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Filter, Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::id::GameId; 7 | use crate::types::mods::Statistics; 8 | use crate::types::List; 9 | use crate::util::{Paginate, Paginator}; 10 | 11 | /// Get all mod stats for mods of a game. 12 | pub struct GetModsStats<'a> { 13 | http: &'a Client, 14 | game_id: GameId, 15 | filter: Option, 16 | } 17 | 18 | impl<'a> GetModsStats<'a> { 19 | pub(crate) const fn new(http: &'a Client, game_id: GameId) -> Self { 20 | Self { 21 | http, 22 | game_id, 23 | filter: None, 24 | } 25 | } 26 | 27 | /// Set the filter for the request. 28 | /// 29 | /// See [Filters and sorting](super::filters). 30 | pub fn filter(mut self, filter: Filter) -> Self { 31 | self.filter = Some(filter); 32 | self 33 | } 34 | } 35 | 36 | impl IntoFuture for GetModsStats<'_> { 37 | type Output = Output>; 38 | type IntoFuture = ResponseFuture>; 39 | 40 | fn into_future(self) -> Self::IntoFuture { 41 | let route = Route::GetModsStats { 42 | game_id: self.game_id, 43 | }; 44 | let mut builder = RequestBuilder::from_route(&route); 45 | if let Some(filter) = self.filter { 46 | builder = builder.filter(filter); 47 | } 48 | match builder.empty() { 49 | Ok(req) => self.http.request(req), 50 | Err(err) => ResponseFuture::failed(err), 51 | } 52 | } 53 | } 54 | 55 | impl<'a> Paginate<'a> for GetModsStats<'a> { 56 | type Output = Statistics; 57 | 58 | fn paged(&'a self) -> Paginator<'a, Self::Output> { 59 | let route = Route::GetModsStats { 60 | game_id: self.game_id, 61 | }; 62 | Paginator::new(self.http, route, self.filter.clone()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/request/mods/comments/get_mod_comments.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Filter, Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::id::{GameId, ModId}; 7 | use crate::types::mods::Comment; 8 | use crate::types::List; 9 | use crate::util::{Paginate, Paginator}; 10 | 11 | /// Get all comments posted on a mod profile. 12 | pub struct GetModComments<'a> { 13 | http: &'a Client, 14 | game_id: GameId, 15 | mod_id: ModId, 16 | filter: Option, 17 | } 18 | 19 | impl<'a> GetModComments<'a> { 20 | pub(crate) const fn new(http: &'a Client, game_id: GameId, mod_id: ModId) -> Self { 21 | Self { 22 | http, 23 | game_id, 24 | mod_id, 25 | filter: None, 26 | } 27 | } 28 | 29 | pub fn filter(mut self, filter: Filter) -> Self { 30 | self.filter = Some(filter); 31 | self 32 | } 33 | } 34 | 35 | impl IntoFuture for GetModComments<'_> { 36 | type Output = Output>; 37 | type IntoFuture = ResponseFuture>; 38 | 39 | fn into_future(self) -> Self::IntoFuture { 40 | let route = Route::GetModComments { 41 | game_id: self.game_id, 42 | mod_id: self.mod_id, 43 | }; 44 | let mut builder = RequestBuilder::from_route(&route); 45 | if let Some(filter) = self.filter { 46 | builder = builder.filter(filter); 47 | } 48 | match builder.empty() { 49 | Ok(req) => self.http.request(req), 50 | Err(err) => ResponseFuture::failed(err), 51 | } 52 | } 53 | } 54 | 55 | impl<'a> Paginate<'a> for GetModComments<'a> { 56 | type Output = Comment; 57 | 58 | fn paged(&'a self) -> Paginator<'a, Self::Output> { 59 | let route = Route::GetModComments { 60 | game_id: self.game_id, 61 | mod_id: self.mod_id, 62 | }; 63 | Paginator::new(self.http, route, self.filter.clone()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/details.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::process; 3 | 4 | use modio::types::id::Id; 5 | use modio::Client; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<(), Box> { 9 | dotenv::dotenv().ok(); 10 | tracing_subscriber::fmt::init(); 11 | 12 | // Fetch the access token / api key from the environment of the current process. 13 | let client = match (env::var("MODIO_API_KEY"), env::var("MODIO_TOKEN")) { 14 | (Ok(api_key), Ok(token)) => Client::builder(api_key).token(token), 15 | (_, Ok(api_key)) => Client::builder(api_key), 16 | _ => { 17 | eprintln!("missing MODIO_TOKEN or MODIO_API_KEY environment variable"); 18 | process::exit(1); 19 | } 20 | }; 21 | let host = env::var("MODIO_HOST").unwrap_or_else(|_| "api.test.mod.io".to_string()); 22 | 23 | // Creates a `Modio` endpoint for the test environment. 24 | let client = client.host(host).build()?; 25 | 26 | // OpenXcom: The X-Com Files 27 | let (game_id, mod_id) = (Id::new(51), Id::new(158)); 28 | 29 | // Get mod with its dependencies and all files 30 | let resp = client.get_mod(game_id, mod_id).await?; 31 | let m = resp.data().await?; 32 | 33 | let resp = client.get_mod_dependencies(game_id, mod_id).await?; 34 | let deps = resp.data().await?; 35 | 36 | let resp = client.get_files(game_id, mod_id).await?; 37 | let files = resp.data().await?; 38 | 39 | println!("{}, {}\n", m.name, m.profile_url); 40 | println!( 41 | "deps: {:?}", 42 | deps.data.into_iter().map(|d| d.mod_id).collect::>() 43 | ); 44 | println!( 45 | "stats: downloads={} subscribers={}\n", 46 | m.stats.downloads_total, m.stats.subscribers_total, 47 | ); 48 | let primary = m.modfile.as_ref().map(|f| f.id); 49 | println!("files:"); 50 | for file in files.data { 51 | let primary = if primary == Some(file.id) { "*" } else { " " }; 52 | println!("{} id: {} version: {:?}", primary, file.id, file.version); 53 | } 54 | Ok(()) 55 | } 56 | -------------------------------------------------------------------------------- /src/request/files/get_files.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Filter, Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::files::File; 7 | use crate::types::id::{GameId, ModId}; 8 | use crate::types::List; 9 | use crate::util::{Paginate, Paginator}; 10 | 11 | /// Get all files that are published for a mod. 12 | pub struct GetFiles<'a> { 13 | http: &'a Client, 14 | game_id: GameId, 15 | mod_id: ModId, 16 | filter: Option, 17 | } 18 | 19 | impl<'a> GetFiles<'a> { 20 | pub(crate) const fn new(http: &'a Client, game_id: GameId, mod_id: ModId) -> Self { 21 | Self { 22 | http, 23 | game_id, 24 | mod_id, 25 | filter: None, 26 | } 27 | } 28 | 29 | /// Set the filter for the request. 30 | /// 31 | /// See [Filters and sorting](super::filters). 32 | pub fn filter(mut self, filter: Filter) -> Self { 33 | self.filter = Some(filter); 34 | self 35 | } 36 | } 37 | 38 | impl IntoFuture for GetFiles<'_> { 39 | type Output = Output>; 40 | type IntoFuture = ResponseFuture>; 41 | 42 | fn into_future(self) -> Self::IntoFuture { 43 | let route = Route::GetFiles { 44 | game_id: self.game_id, 45 | mod_id: self.mod_id, 46 | }; 47 | let mut builder = RequestBuilder::from_route(&route); 48 | if let Some(filter) = self.filter { 49 | builder = builder.filter(filter); 50 | } 51 | match builder.empty() { 52 | Ok(req) => self.http.request(req), 53 | Err(err) => ResponseFuture::failed(err), 54 | } 55 | } 56 | } 57 | 58 | impl<'a> Paginate<'a> for GetFiles<'a> { 59 | type Output = File; 60 | 61 | fn paged(&'a self) -> Paginator<'a, Self::Output> { 62 | let route = Route::GetFiles { 63 | game_id: self.game_id, 64 | mod_id: self.mod_id, 65 | }; 66 | Paginator::new(self.http, route, self.filter.clone()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/request/submit_report.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use serde_derive::Serialize; 4 | 5 | use crate::client::Client; 6 | use crate::request::{Output, RequestBuilder, Route}; 7 | use crate::response::ResponseFuture; 8 | use crate::types::id::ResourceId; 9 | use crate::types::Message; 10 | 11 | /// Report a game, guide, mod or user. 12 | pub struct SubmitReport<'a> { 13 | http: &'a Client, 14 | fields: SubmitReportFields<'a>, 15 | } 16 | 17 | #[derive(Serialize)] 18 | struct SubmitReportFields<'a> { 19 | resource: &'a str, 20 | id: ResourceId, 21 | #[serde(rename = "type")] 22 | kind: u8, 23 | summary: &'a str, 24 | name: Option<&'a str>, 25 | contact: Option<&'a str>, 26 | } 27 | 28 | impl<'a> SubmitReport<'a> { 29 | pub(crate) const fn new( 30 | http: &'a Client, 31 | resource: &'a str, 32 | id: ResourceId, 33 | kind: u8, 34 | summary: &'a str, 35 | ) -> Self { 36 | Self { 37 | http, 38 | fields: SubmitReportFields { 39 | resource, 40 | id, 41 | kind, 42 | summary, 43 | name: None, 44 | contact: None, 45 | }, 46 | } 47 | } 48 | 49 | /// Name of the user submitting the report. 50 | pub const fn name(mut self, name: &'a str) -> Self { 51 | self.fields.name = Some(name); 52 | self 53 | } 54 | 55 | /// Contact details of the user submitting the report. 56 | pub const fn contact(mut self, contact: &'a str) -> Self { 57 | self.fields.contact = Some(contact); 58 | self 59 | } 60 | } 61 | 62 | impl IntoFuture for SubmitReport<'_> { 63 | type Output = Output; 64 | type IntoFuture = ResponseFuture; 65 | 66 | fn into_future(self) -> Self::IntoFuture { 67 | let route = Route::SubmitReport; 68 | match RequestBuilder::from_route(&route).form(&self.fields) { 69 | Ok(req) => self.http.request(req), 70 | Err(err) => ResponseFuture::failed(err), 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/request/mods/events/get_mod_events.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Filter, Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::id::{GameId, ModId}; 7 | use crate::types::mods::Event; 8 | use crate::types::List; 9 | use crate::util::{Paginate, Paginator}; 10 | 11 | /// Get the event log for a mod. 12 | pub struct GetModEvents<'a> { 13 | http: &'a Client, 14 | game_id: GameId, 15 | mod_id: ModId, 16 | filter: Option, 17 | } 18 | 19 | impl<'a> GetModEvents<'a> { 20 | pub(crate) const fn new(http: &'a Client, game_id: GameId, mod_id: ModId) -> Self { 21 | Self { 22 | http, 23 | game_id, 24 | mod_id, 25 | filter: None, 26 | } 27 | } 28 | 29 | /// Set the filter for the request. 30 | /// 31 | /// See [Filters and sorting](super::filters). 32 | pub fn filter(mut self, filter: Filter) -> Self { 33 | self.filter = Some(filter); 34 | self 35 | } 36 | } 37 | 38 | impl IntoFuture for GetModEvents<'_> { 39 | type Output = Output>; 40 | type IntoFuture = ResponseFuture>; 41 | 42 | fn into_future(self) -> Self::IntoFuture { 43 | let route = Route::GetModEvents { 44 | game_id: self.game_id, 45 | mod_id: self.mod_id, 46 | }; 47 | let mut builder = RequestBuilder::from_route(&route); 48 | if let Some(filter) = self.filter { 49 | builder = builder.filter(filter); 50 | } 51 | match builder.empty() { 52 | Ok(req) => self.http.request(req), 53 | Err(err) => ResponseFuture::failed(err), 54 | } 55 | } 56 | } 57 | 58 | impl<'a> Paginate<'a> for GetModEvents<'a> { 59 | type Output = Event; 60 | 61 | fn paged(&'a self) -> Paginator<'a, Self::Output> { 62 | let route = Route::GetModEvents { 63 | game_id: self.game_id, 64 | mod_id: self.mod_id, 65 | }; 66 | Paginator::new(self.http, route, self.filter.clone()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/request/games/get_games.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use crate::client::Client; 4 | use crate::request::{Filter, Output, RequestBuilder, Route}; 5 | use crate::response::ResponseFuture; 6 | use crate::types::games::Game; 7 | use crate::types::List; 8 | use crate::util::{Paginate, Paginator}; 9 | 10 | /// Get all games. 11 | pub struct GetGames<'a> { 12 | http: &'a Client, 13 | show_hidden_tags: Option, 14 | filter: Option, 15 | } 16 | 17 | impl<'a> GetGames<'a> { 18 | pub(crate) const fn new(http: &'a Client) -> Self { 19 | Self { 20 | http, 21 | show_hidden_tags: None, 22 | filter: None, 23 | } 24 | } 25 | 26 | /// Show the hidden tags associated with the given game. 27 | pub const fn show_hidden_tags(mut self, value: bool) -> Self { 28 | self.show_hidden_tags = Some(value); 29 | self 30 | } 31 | 32 | /// Set the filter for the request. 33 | /// 34 | /// See [Filters and sorting](super::filters). 35 | pub fn filter(mut self, filter: Filter) -> Self { 36 | self.filter = Some(filter); 37 | self 38 | } 39 | } 40 | 41 | impl IntoFuture for GetGames<'_> { 42 | type Output = Output>; 43 | type IntoFuture = ResponseFuture>; 44 | 45 | fn into_future(self) -> Self::IntoFuture { 46 | let route = Route::GetGames { 47 | show_hidden_tags: self.show_hidden_tags, 48 | }; 49 | let mut builder = RequestBuilder::from_route(&route); 50 | if let Some(filter) = self.filter { 51 | builder = builder.filter(filter); 52 | } 53 | match builder.empty() { 54 | Ok(req) => self.http.request(req), 55 | Err(err) => ResponseFuture::failed(err), 56 | } 57 | } 58 | } 59 | 60 | impl<'a> Paginate<'a> for GetGames<'a> { 61 | type Output = Game; 62 | 63 | fn paged(&'a self) -> Paginator<'a, Self::Output> { 64 | let route = Route::GetGames { 65 | show_hidden_tags: self.show_hidden_tags, 66 | }; 67 | Paginator::new(self.http, route, self.filter.clone()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/util/download/info/get_file.rs: -------------------------------------------------------------------------------- 1 | use std::future::{Future, IntoFuture}; 2 | use std::pin::Pin; 3 | use std::task::{ready, Context, Poll}; 4 | 5 | use futures_util::future::Either; 6 | use http::StatusCode; 7 | 8 | use crate::client::Client; 9 | use crate::request::files::GetFile as GetModFile; 10 | use crate::response::{DataFuture, ResponseFuture}; 11 | use crate::types::files::File; 12 | use crate::types::id::{FileId, GameId, ModId}; 13 | use crate::util::download::{Error, ErrorKind}; 14 | 15 | pin_project_lite::pin_project! { 16 | pub struct GetFile { 17 | game_id: GameId, 18 | mod_id: ModId, 19 | file_id: FileId, 20 | #[pin] 21 | future: Either, DataFuture>, 22 | } 23 | } 24 | 25 | impl GetFile { 26 | pub(crate) fn new(http: &Client, game_id: GameId, mod_id: ModId, file_id: FileId) -> Self { 27 | Self { 28 | game_id, 29 | mod_id, 30 | file_id, 31 | future: Either::Left(GetModFile::new(http, game_id, mod_id, file_id).into_future()), 32 | } 33 | } 34 | } 35 | 36 | impl Future for GetFile { 37 | type Output = Result; 38 | 39 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 40 | let mut this = self.project(); 41 | loop { 42 | match this.future.as_mut().as_pin_mut() { 43 | Either::Left(fut) => { 44 | let resp = ready!(fut.poll(cx)).map_err(Error::request)?; 45 | 46 | if resp.status() == StatusCode::NOT_FOUND { 47 | let kind = ErrorKind::FileNotFound { 48 | game_id: *this.game_id, 49 | mod_id: *this.mod_id, 50 | file_id: *this.file_id, 51 | }; 52 | return Poll::Ready(Err(Error::new(kind))); 53 | } 54 | this.future.set(Either::Right(resp.data())); 55 | } 56 | Either::Right(fut) => { 57 | return fut.poll(cx).map_err(Error::body); 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/request/files/multipart/create_multipart_upload_session.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use serde_derive::Serialize; 4 | 5 | use crate::client::Client; 6 | use crate::request::{Output, RequestBuilder, Route}; 7 | use crate::response::ResponseFuture; 8 | use crate::types::files::multipart::UploadSession; 9 | use crate::types::id::{GameId, ModId}; 10 | 11 | /// Create a session for uploading files in multiple part. 12 | pub struct CreateMultipartUploadSession<'a> { 13 | http: &'a Client, 14 | game_id: GameId, 15 | mod_id: ModId, 16 | fields: CreateMultipartUploadSessionFields<'a>, 17 | } 18 | 19 | #[derive(Serialize)] 20 | struct CreateMultipartUploadSessionFields<'a> { 21 | filename: &'a str, 22 | #[serde(skip_serializing_if = "Option::is_none")] 23 | nonce: Option<&'a str>, 24 | } 25 | 26 | impl<'a> CreateMultipartUploadSession<'a> { 27 | pub(crate) const fn new( 28 | http: &'a Client, 29 | game_id: GameId, 30 | mod_id: ModId, 31 | filename: &'a str, 32 | ) -> Self { 33 | Self { 34 | http, 35 | game_id, 36 | mod_id, 37 | fields: CreateMultipartUploadSessionFields { 38 | filename, 39 | nonce: None, 40 | }, 41 | } 42 | } 43 | 44 | /// An optional nonce to provide to prevent duplicate upload sessions 45 | /// from being created concurrently. 46 | /// 47 | /// Maximun 64 characters (Recommended: SHA-256) 48 | pub const fn nonce(mut self, nonce: &'a str) -> Self { 49 | self.fields.nonce = Some(nonce); 50 | self 51 | } 52 | } 53 | 54 | impl IntoFuture for CreateMultipartUploadSession<'_> { 55 | type Output = Output; 56 | type IntoFuture = ResponseFuture; 57 | 58 | fn into_future(self) -> Self::IntoFuture { 59 | let route = Route::CreateMultipartUploadSession { 60 | game_id: self.game_id, 61 | mod_id: self.mod_id, 62 | }; 63 | match RequestBuilder::from_route(&route).form(&self.fields) { 64 | Ok(req) => self.http.request(req), 65 | Err(err) => ResponseFuture::failed(err), 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/request/files/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod multipart; 2 | 3 | mod add_file; 4 | mod delete_file; 5 | mod edit_file; 6 | mod get_file; 7 | mod get_files; 8 | mod manage_platform_status; 9 | 10 | pub use add_file::AddFile; 11 | pub use delete_file::DeleteFile; 12 | pub use edit_file::EditFile; 13 | pub use get_file::GetFile; 14 | pub use get_files::GetFiles; 15 | pub use manage_platform_status::ManagePlatformStatus; 16 | 17 | /// Modfile filters and sorting. 18 | /// 19 | /// # Filters 20 | /// - `Fulltext` 21 | /// - `Id` 22 | /// - `ModId` 23 | /// - `DateAdded` 24 | /// - `DateScanned` 25 | /// - `VirusStatus` 26 | /// - `VirusPositive` 27 | /// - `Filesize` 28 | /// - `Filehash` 29 | /// - `Filename` 30 | /// - `Version` 31 | /// - `Changelog` 32 | /// 33 | /// # Sorting 34 | /// - `Id` 35 | /// - `ModId` 36 | /// - `DateAdded` 37 | /// - `Version` 38 | /// 39 | /// See [modio docs](https://docs.mod.io/restapiref/#get-modfiles) for more information. 40 | /// 41 | /// By default this returns up to `100` items. You can limit the result by using `limit` and 42 | /// `offset`. 43 | /// 44 | /// # Example 45 | /// ``` 46 | /// use modio::request::filter::prelude::*; 47 | /// use modio::request::files::filters::Id; 48 | /// 49 | /// let filter = Id::_in(vec![1, 2]).order_by(Id::desc()); 50 | /// ``` 51 | #[rustfmt::skip] 52 | pub mod filters { 53 | #[doc(inline)] 54 | pub use crate::request::filter::prelude::Fulltext; 55 | #[doc(inline)] 56 | pub use crate::request::filter::prelude::Id; 57 | #[doc(inline)] 58 | pub use crate::request::filter::prelude::ModId; 59 | #[doc(inline)] 60 | pub use crate::request::filter::prelude::DateAdded; 61 | 62 | filter!(DateScanned, DATE_SCANNED, "date_scanned", Eq, NotEq, In, Cmp); 63 | filter!(VirusStatus, VIRUS_STATUS, "virus_status", Eq, NotEq, In, Cmp); 64 | filter!(VirusPositive, VIRUS_POSITIVE, "virus_positive", Eq, NotEq, In, Cmp); 65 | filter!(Filesize, FILESIZE, "filesize", Eq, NotEq, In, Cmp, OrderBy); 66 | filter!(Filehash, FILEHASH, "filehash", Eq, NotEq, In, Like); 67 | filter!(Filename, FILENAME, "filename", Eq, NotEq, In, Like); 68 | filter!(Version, VERSION, "version", Eq, NotEq, In, Like, OrderBy); 69 | filter!(Changelog, CHANGELOG, "changelog", Eq, NotEq, In, Like); 70 | } 71 | -------------------------------------------------------------------------------- /src/request/games/add_game_media.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | use std::path::Path; 3 | 4 | use crate::client::Client; 5 | use crate::request::multipart::{Form, Part}; 6 | use crate::request::{Output, RequestBuilder, Route}; 7 | use crate::response::ResponseFuture; 8 | use crate::types::id::GameId; 9 | use crate::types::Message; 10 | 11 | /// Update new media to a game. 12 | pub struct AddGameMedia<'a> { 13 | http: &'a Client, 14 | game_id: GameId, 15 | logo: Option, 16 | icon: Option, 17 | header: Option, 18 | } 19 | 20 | impl<'a> AddGameMedia<'a> { 21 | pub(crate) const fn new(http: &'a Client, game_id: GameId) -> Self { 22 | Self { 23 | http, 24 | game_id, 25 | logo: None, 26 | icon: None, 27 | header: None, 28 | } 29 | } 30 | 31 | pub fn logo>(mut self, file: P) -> Self { 32 | let part = Part::file(file, "logo.png").mime(mime::IMAGE_STAR); 33 | self.logo = Some(part); 34 | self 35 | } 36 | 37 | pub fn icon>(mut self, file: P) -> Self { 38 | let part = Part::file(file, "icon.png").mime(mime::IMAGE_STAR); 39 | self.icon = Some(part); 40 | self 41 | } 42 | 43 | pub fn header>(mut self, file: P) -> Self { 44 | let part = Part::file(file, "header.png").mime(mime::IMAGE_STAR); 45 | self.header = Some(part); 46 | self 47 | } 48 | } 49 | 50 | impl IntoFuture for AddGameMedia<'_> { 51 | type Output = Output; 52 | type IntoFuture = ResponseFuture; 53 | 54 | fn into_future(self) -> Self::IntoFuture { 55 | let route = Route::AddGameMedia { 56 | game_id: self.game_id, 57 | }; 58 | 59 | let mut form = Form::new(); 60 | if let Some(value) = self.logo { 61 | form = form.part("logo", value); 62 | } 63 | if let Some(value) = self.icon { 64 | form = form.part("icon", value); 65 | } 66 | if let Some(value) = self.header { 67 | form = form.part("header", value); 68 | } 69 | 70 | match RequestBuilder::from_route(&route).multipart(form) { 71 | Ok(req) => self.http.request(req), 72 | Err(err) => ResponseFuture::failed(err), 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "modio" 3 | version = "0.14.2" # don't forget to update html_root_url 4 | description = "Rust interface for integrating https://mod.io - a modding API for game developers" 5 | repository = "https://github.com/nickelc/modio-rs" 6 | license = "MIT OR Apache-2.0" 7 | authors = ["Constantin Nickel "] 8 | keywords = ["modio", "client"] 9 | categories = ["api-bindings", "web-programming::http-client"] 10 | edition = "2021" 11 | rust-version = "1.83" 12 | include = ["src/**/*", "LICENSE-*", "README.md", "CHANGELOG.md"] 13 | 14 | [features] 15 | default = ["rustls-native-roots"] 16 | native-tls = ["dep:hyper-tls"] 17 | rustls-native-roots = ["dep:hyper-rustls", "hyper-rustls?/native-tokio"] 18 | rustls-webpki-roots = ["dep:hyper-rustls", "hyper-rustls?/webpki-tokio"] 19 | trace = ["tower-http/trace"] 20 | 21 | [dependencies] 22 | bitflags = "2.8.0" 23 | bytes = "1.9.0" 24 | futures-util = { version = "0.3.31", default-features = false } 25 | serde = "1.0.217" 26 | serde_derive = "1.0.217" 27 | serde_json = "1.0.135" 28 | serde_urlencoded = "0.7.1" 29 | form_urlencoded = "1.2.1" 30 | pin-project-lite = "0.2.16" 31 | percent-encoding = "2.3.1" 32 | url = "2.5.4" 33 | uuid = { version = "1.18.1", features = ["serde"] } 34 | mime = "0.3.17" 35 | tokio = { version = "1.43.0", default-features = false, features = ["fs", "io-util"] } 36 | tokio-util = { version = "0.7.13", default-features = false } 37 | 38 | # http 39 | http = "1.2.0" 40 | http-body = "1.0.1" 41 | http-body-util = "0.1.2" 42 | hyper = { version = "1.6.0", default-features = false } 43 | hyper-util = { version = "0.1.10", default-features = false, features = ["client-legacy", "http1", "tokio"] } 44 | tower = { version = "0.5.1", default-features = false, features = ["buffer", "util"] } 45 | tower-http = { version = "0.6.2", default-features = false, features = ["compression-br", "decompression-br", "follow-redirect", "util"] } 46 | 47 | # tls 48 | hyper-rustls = { version = "0.27.3", optional = true, default-features = false, features = ["http1", "ring"] } 49 | hyper-tls = { version = "0.6.0", optional = true } 50 | 51 | [dev-dependencies] 52 | dotenv = "0.15.0" 53 | md5 = "0.8.0" 54 | serde_test = "1.0.177" 55 | tokio = { version = "1.43.0", features = ["full"] } 56 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 57 | 58 | [package.metadata.docs.rs] 59 | all-features = true 60 | rustdoc-args = ["--cfg", "docsrs"] 61 | -------------------------------------------------------------------------------- /examples/download.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::io::{self, Write}; 3 | use std::process; 4 | 5 | use modio::types::id::Id; 6 | use modio::util::Download; 7 | use modio::Client; 8 | 9 | fn prompt(prompt: &str) -> io::Result { 10 | print!("{}", prompt); 11 | io::stdout().flush()?; 12 | let mut buffer = String::new(); 13 | io::stdin().read_line(&mut buffer)?; 14 | Ok(buffer.trim().parse().expect("Invalid value")) 15 | } 16 | 17 | #[tokio::main] 18 | async fn main() -> Result<(), Box> { 19 | dotenv::dotenv().ok(); 20 | tracing_subscriber::fmt::init(); 21 | 22 | // Fetch the access token / api key from the environment of the current process. 23 | let client = match (env::var("MODIO_API_KEY"), env::var("MODIO_TOKEN")) { 24 | (Ok(api_key), Ok(token)) => Client::builder(api_key).token(token), 25 | (_, Ok(api_key)) => Client::builder(api_key), 26 | _ => { 27 | eprintln!("missing MODIO_TOKEN or MODIO_API_KEY environment variable"); 28 | process::exit(1); 29 | } 30 | }; 31 | let host = env::var("MODIO_HOST").unwrap_or_else(|_| "api.test.mod.io".to_string()); 32 | 33 | // Creates a `Modio` endpoint for the test environment. 34 | let client = client.host(host).build()?; 35 | 36 | let game_id = Id::new(prompt("Enter game id: ")?); 37 | let mod_id = Id::new(prompt("Enter mod id: ")?); 38 | 39 | // Create the call for `/games/{game_id}/mods/{mod_id}` and wait for the result. 40 | let resp = client.get_mod(game_id, mod_id).await?; 41 | let m = resp.data().await?; 42 | if let Some(file) = m.modfile { 43 | // Download the file and calculate its md5 digest. 44 | let mut ctx = md5::Context::new(); 45 | let mut size = 0; 46 | 47 | println!("mod: {}", m.name); 48 | println!("url: {}", file.download.binary_url); 49 | println!("filename: {}", file.filename); 50 | println!("filesize: {}", file.filesize); 51 | println!("reported md5: {}", file.filehash.md5); 52 | 53 | let mut chunked = client.download(file).chunked().await?; 54 | while let Some(bytes) = chunked.data().await { 55 | let bytes = bytes?; 56 | size += bytes.len(); 57 | ctx.consume(bytes); 58 | } 59 | 60 | println!("computed md5: {:x}", ctx.finalize()); 61 | println!("downloaded size: {}", size); 62 | } else { 63 | println!("The mod has no files."); 64 | } 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /src/request/files/multipart/add_multipart_upload_part.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::future::IntoFuture; 3 | 4 | use bytes::Bytes; 5 | use futures_util::TryStream; 6 | use http::header::CONTENT_RANGE; 7 | 8 | use crate::client::Client; 9 | use crate::request::{Output, RequestBuilder, Route}; 10 | use crate::response::ResponseFuture; 11 | use crate::types::files::multipart::{UploadId, UploadPart}; 12 | use crate::types::id::{GameId, ModId}; 13 | 14 | pub struct AddMultipartUploadPart<'a, S> { 15 | http: &'a Client, 16 | game_id: GameId, 17 | mod_id: ModId, 18 | upload_id: UploadId, 19 | range: ContentRange, 20 | stream: S, 21 | } 22 | 23 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 24 | pub struct ContentRange { 25 | pub start: u64, 26 | pub end: u64, 27 | pub total: u64, 28 | } 29 | 30 | impl<'a, S> AddMultipartUploadPart<'a, S> { 31 | pub(crate) const fn new( 32 | http: &'a Client, 33 | game_id: GameId, 34 | mod_id: ModId, 35 | upload_id: UploadId, 36 | range: ContentRange, 37 | stream: S, 38 | ) -> Self 39 | where 40 | S: TryStream + Send + 'static, 41 | S::Ok: Into, 42 | S::Error: Into>, 43 | { 44 | Self { 45 | http, 46 | game_id, 47 | mod_id, 48 | upload_id, 49 | range, 50 | stream, 51 | } 52 | } 53 | } 54 | 55 | impl fmt::Display for ContentRange { 56 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 57 | write!(f, "bytes {}-{}/{}", self.start, self.end, self.total) 58 | } 59 | } 60 | 61 | impl IntoFuture for AddMultipartUploadPart<'_, S> 62 | where 63 | S: TryStream + Send + 'static, 64 | S::Ok: Into, 65 | S::Error: Into>, 66 | { 67 | type Output = Output; 68 | type IntoFuture = ResponseFuture; 69 | 70 | fn into_future(self) -> Self::IntoFuture { 71 | let route = Route::AddMultipartUploadPart { 72 | game_id: self.game_id, 73 | mod_id: self.mod_id, 74 | upload_id: self.upload_id, 75 | }; 76 | 77 | let range = self.range.to_string(); 78 | let builder = RequestBuilder::from_route(&route).header(CONTENT_RANGE, range); 79 | match builder.stream(self.stream) { 80 | Ok(req) => self.http.request(req), 81 | Err(err) => ResponseFuture::failed(err), 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/request/mods/tags/add_mod_tags.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use serde_derive::Serialize; 4 | 5 | use crate::client::Client; 6 | use crate::request::{ArrayParams, Output, RequestBuilder, Route}; 7 | use crate::response::ResponseFuture; 8 | use crate::types::id::{GameId, ModId}; 9 | use crate::types::Message; 10 | 11 | /// Add tags to a mod profile. 12 | pub struct AddModTags<'a> { 13 | http: &'a Client, 14 | game_id: GameId, 15 | mod_id: ModId, 16 | fields: AddModTagsFields<'a>, 17 | } 18 | 19 | #[derive(Serialize)] 20 | struct AddModTagsFields<'a> { 21 | #[serde(flatten)] 22 | tags: ArrayParams<'a, &'a str>, 23 | } 24 | 25 | impl<'a> AddModTagsFields<'a> { 26 | const fn new(tags: &'a [&'a str]) -> Self { 27 | Self { 28 | tags: ArrayParams::new("tags[]", tags), 29 | } 30 | } 31 | } 32 | 33 | impl<'a> AddModTags<'a> { 34 | pub(crate) const fn new( 35 | http: &'a Client, 36 | game_id: GameId, 37 | mod_id: ModId, 38 | tags: &'a [&'a str], 39 | ) -> Self { 40 | Self { 41 | http, 42 | game_id, 43 | mod_id, 44 | fields: AddModTagsFields::new(tags), 45 | } 46 | } 47 | } 48 | 49 | impl IntoFuture for AddModTags<'_> { 50 | type Output = Output; 51 | type IntoFuture = ResponseFuture; 52 | 53 | fn into_future(self) -> Self::IntoFuture { 54 | let route = Route::AddModTags { 55 | game_id: self.game_id, 56 | mod_id: self.mod_id, 57 | }; 58 | match RequestBuilder::from_route(&route).form(&self.fields) { 59 | Ok(req) => self.http.request(req), 60 | Err(err) => ResponseFuture::failed(err), 61 | } 62 | } 63 | } 64 | 65 | #[cfg(test)] 66 | mod tests { 67 | use serde_test::{assert_ser_tokens, Token}; 68 | 69 | use super::AddModTagsFields; 70 | 71 | #[test] 72 | pub fn serialize_fields() { 73 | let fields = AddModTagsFields::new(&["aaa", "bbb"]); 74 | 75 | assert_ser_tokens( 76 | &fields, 77 | &[ 78 | Token::Map { len: None }, 79 | Token::Str("tags[]"), 80 | Token::Str("aaa"), 81 | Token::Str("tags[]"), 82 | Token::Str("bbb"), 83 | Token::MapEnd, 84 | ], 85 | ); 86 | 87 | let qs = serde_urlencoded::to_string(&fields).unwrap(); 88 | assert_eq!("tags%5B%5D=aaa&tags%5B%5D=bbb", qs); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/util/download/info/get_primary_file.rs: -------------------------------------------------------------------------------- 1 | use std::future::{Future, IntoFuture}; 2 | use std::pin::Pin; 3 | use std::task::{ready, Context, Poll}; 4 | 5 | use futures_util::future::Either; 6 | use http::StatusCode; 7 | 8 | use crate::client::Client; 9 | use crate::request::mods::GetMod; 10 | use crate::response::{DataFuture, ResponseFuture}; 11 | use crate::types::files::File; 12 | use crate::types::id::{GameId, ModId}; 13 | use crate::types::mods::Mod; 14 | use crate::util::download::{Error, ErrorKind}; 15 | 16 | pin_project_lite::pin_project! { 17 | pub struct GetPrimaryFile { 18 | game_id: GameId, 19 | mod_id: ModId, 20 | #[pin] 21 | future: Either, DataFuture>, 22 | } 23 | } 24 | 25 | impl GetPrimaryFile { 26 | pub(crate) fn new(http: &Client, game_id: GameId, mod_id: ModId) -> Self { 27 | Self { 28 | game_id, 29 | mod_id, 30 | future: Either::Left(GetMod::new(http, game_id, mod_id).into_future()), 31 | } 32 | } 33 | } 34 | 35 | impl Future for GetPrimaryFile { 36 | type Output = Result; 37 | 38 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 39 | let mut this = self.project(); 40 | loop { 41 | match this.future.as_mut().as_pin_mut() { 42 | Either::Left(fut) => { 43 | let resp = ready!(fut.poll(cx)).map_err(Error::request)?; 44 | 45 | if resp.status() == StatusCode::NOT_FOUND { 46 | let kind = ErrorKind::ModNotFound { 47 | game_id: *this.game_id, 48 | mod_id: *this.mod_id, 49 | }; 50 | return Poll::Ready(Err(Error::new(kind))); 51 | } 52 | this.future.set(Either::Right(resp.data())); 53 | } 54 | Either::Right(fut) => { 55 | let mod_ = ready!(fut.poll(cx)).map_err(Error::body)?; 56 | 57 | let Some(file) = mod_.modfile else { 58 | let kind = ErrorKind::NoPrimaryFile { 59 | game_id: *this.game_id, 60 | mod_id: *this.mod_id, 61 | }; 62 | return Poll::Ready(Err(Error::new(kind))); 63 | }; 64 | 65 | return Poll::Ready(Ok(file)); 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/request/mods/tags/delete_mod_tags.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use serde_derive::Serialize; 4 | 5 | use crate::client::Client; 6 | use crate::request::{ArrayParams, Output, RequestBuilder, Route}; 7 | use crate::response::{NoContent, ResponseFuture}; 8 | use crate::types::id::{GameId, ModId}; 9 | 10 | /// Delete tags from a mod profile. 11 | pub struct DeleteModTags<'a> { 12 | http: &'a Client, 13 | game_id: GameId, 14 | mod_id: ModId, 15 | fields: DeleteModTagsFields<'a>, 16 | } 17 | 18 | #[derive(Serialize)] 19 | struct DeleteModTagsFields<'a> { 20 | #[serde(flatten)] 21 | tags: ArrayParams<'a, &'a str>, 22 | } 23 | 24 | impl<'a> DeleteModTagsFields<'a> { 25 | const fn new(tags: &'a [&'a str]) -> Self { 26 | Self { 27 | tags: ArrayParams::new("tags[]", tags), 28 | } 29 | } 30 | } 31 | 32 | impl<'a> DeleteModTags<'a> { 33 | pub(crate) const fn new( 34 | http: &'a Client, 35 | game_id: GameId, 36 | mod_id: ModId, 37 | tags: &'a [&'a str], 38 | ) -> Self { 39 | Self { 40 | http, 41 | game_id, 42 | mod_id, 43 | fields: DeleteModTagsFields::new(tags), 44 | } 45 | } 46 | } 47 | 48 | impl IntoFuture for DeleteModTags<'_> { 49 | type Output = Output; 50 | type IntoFuture = ResponseFuture; 51 | 52 | fn into_future(self) -> Self::IntoFuture { 53 | let route = Route::DeleteModTags { 54 | game_id: self.game_id, 55 | mod_id: self.mod_id, 56 | }; 57 | match RequestBuilder::from_route(&route).form(&self.fields) { 58 | Ok(req) => self.http.request(req), 59 | Err(err) => ResponseFuture::failed(err), 60 | } 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use serde_test::{assert_ser_tokens, Token}; 67 | 68 | use super::DeleteModTagsFields; 69 | 70 | #[test] 71 | pub fn serialize_fields() { 72 | let fields = DeleteModTagsFields::new(&["aaa", "bbb"]); 73 | 74 | assert_ser_tokens( 75 | &fields, 76 | &[ 77 | Token::Map { len: None }, 78 | Token::Str("tags[]"), 79 | Token::Str("aaa"), 80 | Token::Str("tags[]"), 81 | Token::Str("bbb"), 82 | Token::MapEnd, 83 | ], 84 | ); 85 | 86 | let qs = serde_urlencoded::to_string(&fields).unwrap(); 87 | assert_eq!("tags%5B%5D=aaa&tags%5B%5D=bbb", qs); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/request/multipart/boundary.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Write; 2 | 3 | use bytes::{Bytes, BytesMut}; 4 | 5 | const BOUNDARY_LEN: usize = 16 * 4 + 3; // "{a:016x}-{b:016x}-{c:016x}-{d:016x}" 6 | const BOUNDARY_DASHES: &[u8] = b"--"; 7 | const BOUNDARY_TERMINATOR: &[u8] = b"--\r\n"; 8 | const CRLF: &[u8] = b"\r\n"; 9 | 10 | pub struct Boundary(Bytes); 11 | 12 | impl Boundary { 13 | pub const LEN: usize = BOUNDARY_LEN; // "{a:016x}-{b:016x}-{c:016x}-{d:016x}" 14 | 15 | pub fn generate() -> Self { 16 | let a = random(); 17 | let b = random(); 18 | let c = random(); 19 | let d = random(); 20 | 21 | let mut buf = BytesMut::with_capacity(Self::LEN); 22 | let _ = write!(&mut buf, "{a:016x}-{b:016x}-{c:016x}-{d:016x}"); 23 | Self(buf.freeze()) 24 | } 25 | 26 | pub fn value(&self) -> &[u8] { 27 | &self.0 28 | } 29 | 30 | pub fn delimiter(&self) -> Bytes { 31 | let capacity = BOUNDARY_DASHES.len() + Self::LEN + CRLF.len(); 32 | let mut buf = BytesMut::with_capacity(capacity); 33 | buf.extend_from_slice(BOUNDARY_DASHES); 34 | buf.extend_from_slice(&self.0); 35 | buf.extend_from_slice(CRLF); 36 | buf.freeze() 37 | } 38 | 39 | pub fn terminator(&self) -> Bytes { 40 | let capacity = BOUNDARY_DASHES.len() + Self::LEN + BOUNDARY_TERMINATOR.len(); 41 | let mut buf = BytesMut::with_capacity(capacity); 42 | buf.extend_from_slice(BOUNDARY_DASHES); 43 | buf.extend_from_slice(&self.0); 44 | buf.extend_from_slice(BOUNDARY_TERMINATOR); 45 | buf.freeze() 46 | } 47 | } 48 | 49 | fn random() -> u64 { 50 | use std::cell::Cell; 51 | use std::collections::hash_map::RandomState; 52 | use std::hash::{BuildHasher, Hasher}; 53 | use std::num::Wrapping; 54 | 55 | thread_local! { 56 | static RNG: Cell> = Cell::new(Wrapping(seed())); 57 | } 58 | 59 | fn seed() -> u64 { 60 | let seed = RandomState::new(); 61 | 62 | let mut out = 0; 63 | let mut cnt = 0; 64 | while out == 0 { 65 | cnt += 1; 66 | let mut hasher = seed.build_hasher(); 67 | hasher.write_usize(cnt); 68 | out = hasher.finish(); 69 | } 70 | out 71 | } 72 | 73 | RNG.with(|rng| { 74 | let mut n = rng.get(); 75 | debug_assert_ne!(n.0, 0); 76 | n ^= n >> 12; 77 | n ^= n << 25; 78 | n ^= n >> 27; 79 | rng.set(n); 80 | n.0.wrapping_mul(0x2545_f491_4f6c_dd1d) 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /src/request/files/edit_file.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use serde_derive::Serialize; 4 | 5 | use crate::client::Client; 6 | use crate::request::{Output, RequestBuilder, Route}; 7 | use crate::response::ResponseFuture; 8 | use crate::types::files::File; 9 | use crate::types::id::{FileId, GameId, ModId}; 10 | 11 | /// Edit the details of a modfile. 12 | pub struct EditFile<'a> { 13 | http: &'a Client, 14 | game_id: GameId, 15 | mod_id: ModId, 16 | file_id: FileId, 17 | fields: EditFileFields<'a>, 18 | } 19 | 20 | #[derive(Serialize)] 21 | struct EditFileFields<'a> { 22 | #[serde(skip_serializing_if = "Option::is_none")] 23 | active: Option, 24 | #[serde(skip_serializing_if = "Option::is_none")] 25 | changelog: Option<&'a str>, 26 | #[serde(skip_serializing_if = "Option::is_none")] 27 | metadata_blob: Option<&'a str>, 28 | #[serde(skip_serializing_if = "Option::is_none")] 29 | version: Option<&'a str>, 30 | } 31 | 32 | impl<'a> EditFile<'a> { 33 | pub(crate) const fn new( 34 | http: &'a Client, 35 | game_id: GameId, 36 | mod_id: ModId, 37 | file_id: FileId, 38 | ) -> Self { 39 | Self { 40 | http, 41 | game_id, 42 | mod_id, 43 | file_id, 44 | fields: EditFileFields { 45 | active: None, 46 | changelog: None, 47 | metadata_blob: None, 48 | version: None, 49 | }, 50 | } 51 | } 52 | 53 | pub const fn active(mut self, active: bool) -> Self { 54 | self.fields.active = Some(active); 55 | self 56 | } 57 | 58 | pub const fn changelog(mut self, changelog: &'a str) -> Self { 59 | self.fields.changelog = Some(changelog); 60 | self 61 | } 62 | 63 | pub const fn metadata_blob(mut self, metadata: &'a str) -> Self { 64 | self.fields.metadata_blob = Some(metadata); 65 | self 66 | } 67 | 68 | pub const fn version(mut self, version: &'a str) -> Self { 69 | self.fields.version = Some(version); 70 | self 71 | } 72 | } 73 | 74 | impl IntoFuture for EditFile<'_> { 75 | type Output = Output; 76 | type IntoFuture = ResponseFuture; 77 | 78 | fn into_future(self) -> Self::IntoFuture { 79 | let route = Route::EditFile { 80 | game_id: self.game_id, 81 | mod_id: self.mod_id, 82 | file_id: self.file_id, 83 | }; 84 | match RequestBuilder::from_route(&route).form(&self.fields) { 85 | Ok(req) => self.http.request(req), 86 | Err(err) => ResponseFuture::failed(err), 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/request/mods/dependencies/delete_mod_dependencies.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use serde_derive::Serialize; 4 | 5 | use crate::client::Client; 6 | use crate::request::{ArrayParams, Output, RequestBuilder, Route}; 7 | use crate::response::{NoContent, ResponseFuture}; 8 | use crate::types::id::{GameId, ModId}; 9 | 10 | /// Delete mod dependencies a mod has selected. 11 | pub struct DeleteModDependencies<'a> { 12 | http: &'a Client, 13 | game_id: GameId, 14 | mod_id: ModId, 15 | fields: DeleteModDependenciesFields<'a>, 16 | } 17 | 18 | #[derive(Serialize)] 19 | struct DeleteModDependenciesFields<'a> { 20 | #[serde(flatten)] 21 | dependencies: ArrayParams<'a, ModId>, 22 | } 23 | 24 | impl<'a> DeleteModDependenciesFields<'a> { 25 | const fn new(deps: &'a [ModId]) -> Self { 26 | Self { 27 | dependencies: ArrayParams::new("dependencies[]", deps), 28 | } 29 | } 30 | } 31 | 32 | impl<'a> DeleteModDependencies<'a> { 33 | pub(crate) const fn new( 34 | http: &'a Client, 35 | game_id: GameId, 36 | mod_id: ModId, 37 | deps: &'a [ModId], 38 | ) -> Self { 39 | Self { 40 | http, 41 | game_id, 42 | mod_id, 43 | fields: DeleteModDependenciesFields::new(deps), 44 | } 45 | } 46 | } 47 | 48 | impl IntoFuture for DeleteModDependencies<'_> { 49 | type Output = Output; 50 | type IntoFuture = ResponseFuture; 51 | 52 | fn into_future(self) -> Self::IntoFuture { 53 | let route = Route::DeleteModDependencies { 54 | game_id: self.game_id, 55 | mod_id: self.mod_id, 56 | }; 57 | match RequestBuilder::from_route(&route).form(&self.fields) { 58 | Ok(req) => self.http.request(req), 59 | Err(err) => ResponseFuture::failed(err), 60 | } 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use serde_test::{assert_ser_tokens, Token}; 67 | 68 | use super::{DeleteModDependenciesFields, ModId}; 69 | 70 | #[test] 71 | pub fn serialize_fields() { 72 | let deps = [ModId::new(1), ModId::new(2)]; 73 | let fields = DeleteModDependenciesFields::new(&deps); 74 | 75 | assert_ser_tokens( 76 | &fields, 77 | &[ 78 | Token::Map { len: None }, 79 | Token::Str("dependencies[]"), 80 | Token::U64(1), 81 | Token::Str("dependencies[]"), 82 | Token::U64(2), 83 | Token::MapEnd, 84 | ], 85 | ); 86 | 87 | let qs = serde_urlencoded::to_string(&fields).unwrap(); 88 | assert_eq!("dependencies%5B%5D=1&dependencies%5B%5D=2", qs); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /examples/upload.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::PathBuf; 3 | use std::process; 4 | use std::str::FromStr; 5 | 6 | use modio::types::files::multipart::UploadSession; 7 | use modio::util::upload::{self, ContentRange}; 8 | use modio::util::DataFromRequest; 9 | use modio::Client; 10 | use tokio::fs::File; 11 | use tokio::io::{AsyncReadExt, BufReader}; 12 | use tokio_util::io::ReaderStream; 13 | 14 | #[tokio::main] 15 | async fn main() -> Result<(), Box> { 16 | dotenv::dotenv().ok(); 17 | tracing_subscriber::fmt::init(); 18 | 19 | let mut it = env::args().skip(1); 20 | 21 | fn parse_next>(it: &mut It) -> Option { 22 | it.next().and_then(|s| s.parse().ok()) 23 | } 24 | 25 | let game_id = parse_next(&mut it).expect("GameId expected"); 26 | let mod_id = parse_next(&mut it).expect("ModId expected"); 27 | let modfile: PathBuf = parse_next(&mut it).expect("Mod file expected"); 28 | 29 | // Fetch the access token / api key from the environment of the current process. 30 | let client = match (env::var("MODIO_API_KEY"), env::var("MODIO_TOKEN")) { 31 | (Ok(api_key), Ok(token)) => Client::builder(api_key).token(token), 32 | (Ok(api_key), _) => Client::builder(api_key), 33 | _ => { 34 | eprintln!("missing MODIO_TOKEN or MODIO_API_KEY environment variable"); 35 | process::exit(1); 36 | } 37 | }; 38 | let host = env::var("MODIO_HOST").unwrap_or_else(|_| "api.test.mod.io".to_string()); 39 | 40 | // Creates a `Modio` endpoint for the test environment. 41 | let client = client.host(host).build()?; 42 | 43 | let UploadSession { id: upload_id, .. } = client 44 | .create_multipart_upload_session(game_id, mod_id, "modfile.zip") 45 | .data() 46 | .await?; 47 | 48 | let file = File::open(modfile).await?; 49 | let file_size = file.metadata().await?.len(); 50 | 51 | for (start, end) in upload::byte_ranges(file_size) { 52 | let input = BufReader::new(file.try_clone().await?); 53 | let part = input.take(upload::MULTIPART_FILE_PART_SIZE); 54 | let stream = ReaderStream::new(part); 55 | 56 | let range = ContentRange { 57 | start, 58 | end, 59 | total: file_size, 60 | }; 61 | 62 | // Add file part to the upload session. 63 | client 64 | .add_multipart_upload_part(game_id, mod_id, upload_id, range, stream) 65 | .await?; 66 | } 67 | 68 | // Complete the multipart upload session. 69 | client 70 | .complete_multipart_upload_session(game_id, mod_id, upload_id) 71 | .await?; 72 | 73 | // Finalize upload to the mod with file details. 74 | let modfile = client 75 | .add_multipart_upload_file(game_id, mod_id, upload_id) 76 | .active(true) 77 | .version("1.0") 78 | .data() 79 | .await?; 80 | 81 | dbg!(modfile); 82 | 83 | Ok(()) 84 | } 85 | -------------------------------------------------------------------------------- /src/request/games/mod.rs: -------------------------------------------------------------------------------- 1 | mod add_game_media; 2 | mod get_game; 3 | mod get_game_stats; 4 | mod get_games; 5 | 6 | pub mod tags; 7 | 8 | pub use add_game_media::AddGameMedia; 9 | pub use get_game::GetGame; 10 | pub use get_game_stats::GetGameStats; 11 | pub use get_games::GetGames; 12 | 13 | /// Game filters and sorting. 14 | /// 15 | /// # Filters 16 | /// - `Fulltext` 17 | /// - `Id` 18 | /// - `Status` 19 | /// - `SubmittedBy` 20 | /// - `DateAdded` 21 | /// - `DateUpdated` 22 | /// - `DateLive` 23 | /// - `Name` 24 | /// - `NameId` 25 | /// - `Summary` 26 | /// - `InstructionsUrl` 27 | /// - `UgcName` 28 | /// - `PresentationOption` 29 | /// - `SubmissionOption` 30 | /// - `CurationOption` 31 | /// - `CommunityOptions` 32 | /// - `RevenueOptions` 33 | /// - `ApiAccessOptions` 34 | /// - `MaturityOptions` 35 | /// 36 | /// # Sorting 37 | /// - `Id` 38 | /// - `Status` 39 | /// - `Name` 40 | /// - `NameId` 41 | /// - `DateUpdated` 42 | /// 43 | /// See [modio docs](https://docs.mod.io/restapiref/#get-games) for more information. 44 | /// 45 | /// By default this returns up to `100` items. You can limit the result by using `limit` and 46 | /// `offset`. 47 | /// 48 | /// # Example 49 | /// ``` 50 | /// use modio::request::filter::prelude::*; 51 | /// use modio::request::games::filters::Id; 52 | /// 53 | /// let filter = Id::_in(vec![1, 2]).order_by(Id::desc()); 54 | /// ``` 55 | #[rustfmt::skip] 56 | pub mod filters { 57 | #[doc(inline)] 58 | pub use crate::request::filter::prelude::Fulltext; 59 | #[doc(inline)] 60 | pub use crate::request::filter::prelude::Id; 61 | #[doc(inline)] 62 | pub use crate::request::filter::prelude::Name; 63 | #[doc(inline)] 64 | pub use crate::request::filter::prelude::NameId; 65 | #[doc(inline)] 66 | pub use crate::request::filter::prelude::Status; 67 | #[doc(inline)] 68 | pub use crate::request::filter::prelude::DateAdded; 69 | #[doc(inline)] 70 | pub use crate::request::filter::prelude::DateUpdated; 71 | #[doc(inline)] 72 | pub use crate::request::filter::prelude::DateLive; 73 | #[doc(inline)] 74 | pub use crate::request::filter::prelude::SubmittedBy; 75 | 76 | filter!(Summary, SUMMARY, "summary", Eq, NotEq, Like); 77 | filter!(InstructionsUrl, INSTRUCTIONS_URL, "instructions_url", Eq, NotEq, In, Like); 78 | filter!(UgcName, UGC_NAME, "ugc_name", Eq, NotEq, In, Like); 79 | filter!(PresentationOption, PRESENTATION_OPTION, "presentation_option", Eq, NotEq, In, Cmp, Bit); 80 | filter!(SubmissionOption, SUBMISSION_OPTION, "submission_option", Eq, NotEq, In, Cmp, Bit); 81 | filter!(CurationOption, CURATION_OPTION, "curation_option", Eq, NotEq, In, Cmp, Bit); 82 | filter!(CommunityOptions, COMMUNITY_OPTIONS, "community_options", Eq, NotEq, In, Cmp, Bit); 83 | filter!(RevenueOptions, REVENUE_OPTIONS, "revenue_options", Eq, NotEq, In, Cmp, Bit); 84 | filter!(ApiAccessOptions, API_ACCESS_OPTIONS, "api_access_options", Eq, NotEq, In, Cmp, Bit); 85 | filter!(MaturityOptions, MATURITY_OPTIONS, "maturity_options", Eq, NotEq, In, Cmp, Bit); 86 | } 87 | -------------------------------------------------------------------------------- /src/response/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::str::Utf8Error; 3 | 4 | use hyper::body::Bytes; 5 | 6 | type Source = Box; 7 | 8 | /// Failure when processing a response body. 9 | pub struct BodyError { 10 | inner: Box, 11 | } 12 | 13 | impl BodyError { 14 | #[inline] 15 | pub(super) fn new(kind: BodyErrorKind, source: Option) -> Self { 16 | Self { 17 | inner: Box::new(ErrorImpl { kind, source }), 18 | } 19 | } 20 | 21 | pub(crate) fn decode(bytes: Bytes, source: serde_json::Error) -> Self { 22 | Self::new(BodyErrorKind::Decode { bytes }, Some(Box::new(source))) 23 | } 24 | 25 | pub(crate) fn invalid_utf8(bytes: Vec, source: Utf8Error) -> Self { 26 | Self::new(BodyErrorKind::InvalidUtf8 { bytes }, Some(Box::new(source))) 27 | } 28 | 29 | /// Returns true if the body could not be loaded. 30 | pub fn is_loading(&self) -> bool { 31 | matches!(self.inner.kind, BodyErrorKind::Loading) 32 | } 33 | 34 | /// Returns true if the response body could not be deserialized. 35 | pub fn is_decode(&self) -> bool { 36 | matches!(self.inner.kind, BodyErrorKind::Decode { .. }) 37 | } 38 | 39 | /// Returns true if the response body contains invalid utf8 data. 40 | pub fn is_invalid_utf8(&self) -> bool { 41 | matches!(self.inner.kind, BodyErrorKind::InvalidUtf8 { .. }) 42 | } 43 | 44 | /// Returns a slice of `u8`s bytes that were attempted to deserialize or convert to a `String`. 45 | pub fn as_bytes(&self) -> Option<&[u8]> { 46 | match &self.inner.kind { 47 | BodyErrorKind::Decode { bytes } => Some(bytes), 48 | BodyErrorKind::InvalidUtf8 { bytes } => Some(bytes), 49 | _ => None, 50 | } 51 | } 52 | } 53 | 54 | impl fmt::Debug for BodyError { 55 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 56 | let mut f = f.debug_struct("BodyError"); 57 | f.field("kind", &self.inner.kind); 58 | if let Some(ref source) = self.inner.source { 59 | f.field("source", source); 60 | } 61 | f.finish() 62 | } 63 | } 64 | 65 | impl fmt::Display for BodyError { 66 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 67 | match self.inner.kind { 68 | BodyErrorKind::Loading => f.write_str("failed to retrieve response body"), 69 | BodyErrorKind::Decode { .. } => f.write_str("failed to decode response body"), 70 | BodyErrorKind::InvalidUtf8 { .. } => { 71 | f.write_str("response body is not a valid utf8 string") 72 | } 73 | } 74 | } 75 | } 76 | 77 | impl std::error::Error for BodyError { 78 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 79 | self.inner.source.as_ref().map(|e| &**e as _) 80 | } 81 | } 82 | 83 | struct ErrorImpl { 84 | kind: BodyErrorKind, 85 | source: Option, 86 | } 87 | 88 | #[derive(Debug)] 89 | #[non_exhaustive] 90 | pub(super) enum BodyErrorKind { 91 | Loading, 92 | Decode { bytes: Bytes }, 93 | InvalidUtf8 { bytes: Vec }, 94 | } 95 | -------------------------------------------------------------------------------- /src/request/mods/dependencies/add_mod_dependencies.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use serde_derive::Serialize; 4 | 5 | use crate::client::Client; 6 | use crate::request::{ArrayParams, Output, RequestBuilder, Route}; 7 | use crate::response::ResponseFuture; 8 | use crate::types::id::{GameId, ModId}; 9 | use crate::types::Message; 10 | 11 | /// Add mod dependencies required by a mod. 12 | pub struct AddModDependencies<'a> { 13 | http: &'a Client, 14 | game_id: GameId, 15 | mod_id: ModId, 16 | fields: AddModDependenciesFields<'a>, 17 | } 18 | 19 | #[derive(Serialize)] 20 | struct AddModDependenciesFields<'a> { 21 | #[serde(flatten)] 22 | dependencies: ArrayParams<'a, ModId>, 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | sync: Option, 25 | } 26 | 27 | impl<'a> AddModDependenciesFields<'a> { 28 | const fn new(deps: &'a [ModId]) -> Self { 29 | Self { 30 | dependencies: ArrayParams::new("dependencies[]", deps), 31 | sync: None, 32 | } 33 | } 34 | } 35 | 36 | impl<'a> AddModDependencies<'a> { 37 | pub(crate) const fn new( 38 | http: &'a Client, 39 | game_id: GameId, 40 | mod_id: ModId, 41 | deps: &'a [ModId], 42 | ) -> Self { 43 | Self { 44 | http, 45 | game_id, 46 | mod_id, 47 | fields: AddModDependenciesFields::new(deps), 48 | } 49 | } 50 | 51 | /// Replace all existing dependencies with the new ones. 52 | pub const fn replace(mut self, value: bool) -> Self { 53 | self.fields.sync = Some(value); 54 | self 55 | } 56 | } 57 | 58 | impl IntoFuture for AddModDependencies<'_> { 59 | type Output = Output; 60 | type IntoFuture = ResponseFuture; 61 | 62 | fn into_future(self) -> Self::IntoFuture { 63 | let route = Route::AddModDependencies { 64 | game_id: self.game_id, 65 | mod_id: self.mod_id, 66 | }; 67 | match RequestBuilder::from_route(&route).form(&self.fields) { 68 | Ok(req) => self.http.request(req), 69 | Err(err) => ResponseFuture::failed(err), 70 | } 71 | } 72 | } 73 | 74 | #[cfg(test)] 75 | mod tests { 76 | use serde_test::{assert_ser_tokens, Token}; 77 | 78 | use super::{AddModDependenciesFields, ModId}; 79 | 80 | #[test] 81 | pub fn serialize_fields() { 82 | let deps = [ModId::new(1), ModId::new(2)]; 83 | let mut fields = AddModDependenciesFields::new(&deps); 84 | fields.sync = Some(true); 85 | 86 | assert_ser_tokens( 87 | &fields, 88 | &[ 89 | Token::Map { len: None }, 90 | Token::Str("dependencies[]"), 91 | Token::U64(1), 92 | Token::Str("dependencies[]"), 93 | Token::U64(2), 94 | Token::Str("sync"), 95 | Token::Some, 96 | Token::Bool(true), 97 | Token::MapEnd, 98 | ], 99 | ); 100 | 101 | let qs = serde_urlencoded::to_string(&fields).unwrap(); 102 | assert_eq!("dependencies%5B%5D=1&dependencies%5B%5D=2&sync=true", qs); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/request/body.rs: -------------------------------------------------------------------------------- 1 | use std::pin::Pin; 2 | use std::task::{ready, Context, Poll}; 3 | 4 | use bytes::Bytes; 5 | use futures_util::TryStream; 6 | use http_body::Frame; 7 | use http_body_util::BodyExt; 8 | 9 | type Error = Box; 10 | 11 | type BoxBody = http_body_util::combinators::UnsyncBoxBody; 12 | 13 | fn boxed(body: B) -> BoxBody 14 | where 15 | B: http_body::Body + Send + 'static, 16 | B::Error: Into, 17 | { 18 | try_downcast(body).unwrap_or_else(|body| body.map_err(Into::into).boxed_unsync()) 19 | } 20 | 21 | fn try_downcast(k: K) -> Result 22 | where 23 | T: 'static, 24 | K: Send + 'static, 25 | { 26 | let mut k = Some(k); 27 | if let Some(k) = ::downcast_mut::>(&mut k) { 28 | Ok(k.take().unwrap()) 29 | } else { 30 | Err(k.unwrap()) 31 | } 32 | } 33 | 34 | #[derive(Debug)] 35 | pub struct Body(BoxBody); 36 | 37 | impl Body { 38 | pub fn new(body: B) -> Self 39 | where 40 | B: http_body::Body + Send + 'static, 41 | B::Error: Into, 42 | { 43 | try_downcast(body).unwrap_or_else(|body| Self(boxed(body))) 44 | } 45 | 46 | pub fn empty() -> Self { 47 | Self::new(http_body_util::Empty::new()) 48 | } 49 | 50 | pub fn from_stream(stream: S) -> Self 51 | where 52 | S: TryStream + Send + 'static, 53 | S::Ok: Into, 54 | S::Error: Into, 55 | { 56 | Self::new(StreamBody { stream }) 57 | } 58 | } 59 | 60 | impl Default for Body { 61 | fn default() -> Self { 62 | Self::empty() 63 | } 64 | } 65 | 66 | impl From for Body { 67 | fn from(value: String) -> Self { 68 | Self::new(http_body_util::Full::from(value)) 69 | } 70 | } 71 | 72 | impl http_body::Body for Body { 73 | type Data = Bytes; 74 | type Error = Error; 75 | 76 | #[inline] 77 | fn poll_frame( 78 | mut self: Pin<&mut Self>, 79 | cx: &mut Context<'_>, 80 | ) -> Poll, Self::Error>>> { 81 | Pin::new(&mut self.0).poll_frame(cx) 82 | } 83 | 84 | #[inline] 85 | fn size_hint(&self) -> http_body::SizeHint { 86 | self.0.size_hint() 87 | } 88 | 89 | #[inline] 90 | fn is_end_stream(&self) -> bool { 91 | self.0.is_end_stream() 92 | } 93 | } 94 | 95 | pin_project_lite::pin_project! { 96 | struct StreamBody { 97 | #[pin] 98 | stream: S, 99 | } 100 | } 101 | 102 | impl http_body::Body for StreamBody 103 | where 104 | S: TryStream, 105 | S::Ok: Into, 106 | S::Error: Into, 107 | { 108 | type Data = Bytes; 109 | type Error = Error; 110 | 111 | fn poll_frame( 112 | mut self: Pin<&mut Self>, 113 | cx: &mut Context<'_>, 114 | ) -> Poll, Self::Error>>> { 115 | let this = self.as_mut().project(); 116 | 117 | match ready!(this.stream.try_poll_next(cx)) { 118 | Some(Ok(chunk)) => Poll::Ready(Some(Ok(Frame::data(chunk.into())))), 119 | Some(Err(err)) => Poll::Ready(Some(Err(err.into()))), 120 | None => Poll::Ready(None), 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/response/future.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::marker::PhantomData; 3 | use std::pin::Pin; 4 | use std::task::{Context, Poll}; 5 | 6 | use http::{header, StatusCode}; 7 | 8 | use super::{ErrorResponseFuture, Output, Response}; 9 | use crate::client; 10 | use crate::error::{self, Error}; 11 | 12 | /// A `Future` that will resolve to a [`Response`]. 13 | pub struct ResponseFuture { 14 | state: State, 15 | phantom: PhantomData, 16 | } 17 | 18 | enum State { 19 | InFlight(client::service::ResponseFuture), 20 | ErrorResponse(StatusCode, ErrorResponseFuture), 21 | Failed(Error), 22 | Completed, 23 | } 24 | 25 | impl ResponseFuture { 26 | pub(crate) fn new(fut: client::service::ResponseFuture) -> Self { 27 | Self { 28 | state: State::InFlight(fut), 29 | phantom: PhantomData, 30 | } 31 | } 32 | 33 | pub(crate) fn failed(error: Error) -> Self { 34 | Self { 35 | state: State::Failed(error), 36 | phantom: PhantomData, 37 | } 38 | } 39 | } 40 | 41 | impl Future for ResponseFuture { 42 | type Output = Output; 43 | 44 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 45 | loop { 46 | let state = std::mem::replace(&mut self.state, State::Completed); 47 | 48 | match state { 49 | State::InFlight(mut fut) => { 50 | let resp = match Pin::new(&mut fut).poll(cx) { 51 | Poll::Ready(Ok(resp)) => Response::new(resp), 52 | Poll::Ready(Err(err)) => return Poll::Ready(Err(error::request(err))), 53 | Poll::Pending => { 54 | self.state = State::InFlight(fut); 55 | return Poll::Pending; 56 | } 57 | }; 58 | 59 | if resp.status().is_success() { 60 | return Poll::Ready(Ok(resp)); 61 | } 62 | 63 | let retry_after = resp 64 | .headers() 65 | .get(header::RETRY_AFTER) 66 | .and_then(|v| v.to_str().ok()) 67 | .and_then(|v| v.parse().ok()); 68 | 69 | if let Some(retry_after) = retry_after { 70 | return Poll::Ready(Err(error::ratelimit(retry_after))); 71 | } 72 | 73 | self.state = State::ErrorResponse(resp.status(), resp.error()); 74 | } 75 | State::ErrorResponse(status, mut fut) => { 76 | let error = match Pin::new(&mut fut).poll(cx) { 77 | Poll::Ready(Ok(resp)) => resp.error, 78 | Poll::Ready(Err(err)) => return Poll::Ready(Err(error::request(err))), 79 | Poll::Pending => { 80 | self.state = State::ErrorResponse(status, fut); 81 | return Poll::Pending; 82 | } 83 | }; 84 | 85 | return Poll::Ready(Err(error::error_for_status(status, error))); 86 | } 87 | State::Failed(err) => return Poll::Ready(Err(err)), 88 | State::Completed => panic!("future is already completed"), 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/client/host.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use http::uri::Authority; 4 | 5 | use crate::types::id::{GameId, UserId}; 6 | 7 | pub const DEFAULT_HOST: &str = "api.mod.io"; 8 | pub const TEST_HOST: &str = "api.test.mod.io"; 9 | 10 | #[derive(Clone, Default)] 11 | pub enum Host { 12 | #[default] 13 | Default, 14 | Test, 15 | Dynamic, 16 | DynamicWithCustom(Authority), 17 | Game(GameId), 18 | User(UserId), 19 | Custom(Authority), 20 | } 21 | 22 | pub struct Display<'a> { 23 | host: &'a Host, 24 | game_id: Option, 25 | } 26 | 27 | impl Host { 28 | pub fn display(&self, game_id: Option) -> Display<'_> { 29 | Display { 30 | host: self, 31 | game_id, 32 | } 33 | } 34 | } 35 | 36 | impl fmt::Display for Display<'_> { 37 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 38 | match self.host { 39 | Host::Default => f.write_str(DEFAULT_HOST), 40 | Host::Test => f.write_str(TEST_HOST), 41 | Host::Dynamic => { 42 | if let Some(game_id) = self.game_id { 43 | f.write_fmt(format_args!("g-{game_id}.modapi.io")) 44 | } else { 45 | f.write_str(DEFAULT_HOST) 46 | } 47 | } 48 | Host::DynamicWithCustom(host) => { 49 | if let Some(game_id) = self.game_id { 50 | f.write_fmt(format_args!("g-{game_id}.modapi.io")) 51 | } else { 52 | f.write_str(host.as_str()) 53 | } 54 | } 55 | Host::Game(game_id) => f.write_fmt(format_args!("g-{game_id}.modapi.io")), 56 | Host::User(user_id) => f.write_fmt(format_args!("g-{user_id}.modapi.io")), 57 | Host::Custom(host) => f.write_str(host.as_str()), 58 | } 59 | } 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use super::*; 65 | 66 | #[test] 67 | fn default() { 68 | let host = Host::Default; 69 | assert_eq!(DEFAULT_HOST, host.display(None).to_string()); 70 | assert_eq!(DEFAULT_HOST, host.display(Some(GameId::new(1))).to_string()); 71 | } 72 | 73 | #[test] 74 | fn test() { 75 | let host = Host::Test; 76 | assert_eq!(TEST_HOST, host.display(None).to_string()); 77 | assert_eq!(TEST_HOST, host.display(Some(GameId::new(1))).to_string()); 78 | } 79 | 80 | #[test] 81 | fn dynamic() { 82 | let host = Host::Dynamic; 83 | assert_eq!(DEFAULT_HOST, host.display(None).to_string()); 84 | assert_eq!( 85 | "g-1.modapi.io", 86 | host.display(Some(GameId::new(1))).to_string() 87 | ); 88 | } 89 | 90 | #[test] 91 | fn dynamic_with_custom() { 92 | let host = Host::DynamicWithCustom(Authority::from_static("custom")); 93 | assert_eq!("custom", host.display(None).to_string()); 94 | assert_eq!( 95 | "g-1.modapi.io", 96 | host.display(Some(GameId::new(1))).to_string() 97 | ); 98 | } 99 | 100 | #[test] 101 | fn custom() { 102 | let host = Host::Custom(Authority::from_static("custom")); 103 | assert_eq!("custom", host.display(None).to_string()); 104 | assert_eq!("custom", host.display(Some(GameId::new(1))).to_string()); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/util/download/action.rs: -------------------------------------------------------------------------------- 1 | use crate::types::files::File; 2 | use crate::types::id::{FileId, GameId, ModId}; 3 | use crate::types::mods::Mod; 4 | 5 | #[derive(Debug)] 6 | pub enum DownloadAction { 7 | /// Download the primary modfile of a mod. 8 | Primary { game_id: GameId, mod_id: ModId }, 9 | /// Download a specific modfile of a mod. 10 | File { 11 | game_id: GameId, 12 | mod_id: ModId, 13 | file_id: FileId, 14 | }, 15 | /// Download a specific modfile. 16 | FileObj(Box), 17 | /// Download a specific version of a mod. 18 | Version { 19 | game_id: GameId, 20 | mod_id: ModId, 21 | version: String, 22 | policy: ResolvePolicy, 23 | }, 24 | } 25 | 26 | /// Defines the policy for `DownloadAction::Version` when multiple files are found. 27 | #[derive(Debug)] 28 | pub enum ResolvePolicy { 29 | /// Download the latest file. 30 | Latest, 31 | /// Fail with [`ErrorKind::MultipleFilesFound`] as error kind. 32 | /// 33 | /// [`ErrorKind::MultipleFilesFound`]: super::ErrorKind::MultipleFilesFound 34 | Fail, 35 | } 36 | 37 | /// Convert `Mod` to [`DownloadAction::File`] or [`DownloadAction::Primary`] if `Mod::modfile` is `None` 38 | impl From for DownloadAction { 39 | fn from(m: Mod) -> DownloadAction { 40 | if let Some(file) = m.modfile { 41 | DownloadAction::from(file) 42 | } else { 43 | DownloadAction::Primary { 44 | game_id: m.game_id, 45 | mod_id: m.id, 46 | } 47 | } 48 | } 49 | } 50 | 51 | /// Convert `File` to [`DownloadAction::FileObj`] 52 | impl From for DownloadAction { 53 | fn from(file: File) -> DownloadAction { 54 | DownloadAction::FileObj(Box::new(file)) 55 | } 56 | } 57 | 58 | /// Convert `(GameId, ModId)` to [`DownloadAction::Primary`] 59 | impl From<(GameId, ModId)> for DownloadAction { 60 | fn from((game_id, mod_id): (GameId, ModId)) -> DownloadAction { 61 | DownloadAction::Primary { game_id, mod_id } 62 | } 63 | } 64 | 65 | /// Convert `(GameId, ModId, FileId)` to [`DownloadAction::File`] 66 | impl From<(GameId, ModId, FileId)> for DownloadAction { 67 | fn from((game_id, mod_id, file_id): (GameId, ModId, FileId)) -> DownloadAction { 68 | DownloadAction::File { 69 | game_id, 70 | mod_id, 71 | file_id, 72 | } 73 | } 74 | } 75 | 76 | /// Convert `(GameId, ModId, String)` to [`DownloadAction::Version`] with resolve policy 77 | /// set to `ResolvePolicy::Latest` 78 | impl From<(GameId, ModId, String)> for DownloadAction { 79 | fn from((game_id, mod_id, version): (GameId, ModId, String)) -> DownloadAction { 80 | DownloadAction::Version { 81 | game_id, 82 | mod_id, 83 | version, 84 | policy: ResolvePolicy::Latest, 85 | } 86 | } 87 | } 88 | 89 | /// Convert `(GameId, ModId, &'a str)` to [`DownloadAction::Version`] with resolve policy 90 | /// set to `ResolvePolicy::Latest` 91 | impl<'a> From<(GameId, ModId, &'a str)> for DownloadAction { 92 | fn from((game_id, mod_id, version): (GameId, ModId, &'a str)) -> DownloadAction { 93 | DownloadAction::Version { 94 | game_id, 95 | mod_id, 96 | version: version.to_string(), 97 | policy: ResolvePolicy::Latest, 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/request/mods/mod.rs: -------------------------------------------------------------------------------- 1 | mod add_mod; 2 | mod delete_mod; 3 | mod edit_mod; 4 | mod get_mod; 5 | mod get_mod_team_members; 6 | mod get_mods; 7 | mod submit_mod_rating; 8 | 9 | pub mod comments; 10 | pub mod dependencies; 11 | pub mod events; 12 | pub mod media; 13 | pub mod metadata; 14 | pub mod stats; 15 | pub mod subscribe; 16 | pub mod tags; 17 | 18 | pub use add_mod::AddMod; 19 | pub use delete_mod::DeleteMod; 20 | pub use edit_mod::EditMod; 21 | pub use get_mod::GetMod; 22 | pub use get_mod_team_members::GetModTeamMembers; 23 | pub use get_mods::GetMods; 24 | pub use submit_mod_rating::SubmitModRating; 25 | 26 | /// Mod filters & sorting 27 | /// 28 | /// # Filters 29 | /// - `Fulltext` 30 | /// - `Id` 31 | /// - `GameId` 32 | /// - `Status` 33 | /// - `Visible` 34 | /// - `SubmittedBy` 35 | /// - `DateAdded` 36 | /// - `DateUpdated` 37 | /// - `DateLive` 38 | /// - `MaturityOption` 39 | /// - `Name` 40 | /// - `NameId` 41 | /// - `Summary` 42 | /// - `Description` 43 | /// - `Homepage` 44 | /// - `Modfile` 45 | /// - `MetadataBlob` 46 | /// - `MetadataKVP` 47 | /// - `Tags` 48 | /// 49 | /// # Sorting 50 | /// - `Id` 51 | /// - `Name` 52 | /// - `Downloads` 53 | /// - `Popular` 54 | /// - `Ratings` 55 | /// - `Subscribers` 56 | /// 57 | /// See the [modio docs](https://docs.mod.io/restapiref/#get-mods) for more information. 58 | /// 59 | /// By default this returns up to `100` items. you can limit the result by using `limit` and 60 | /// `offset`. 61 | /// 62 | /// # Example 63 | /// ``` 64 | /// use modio::request::filter::prelude::*; 65 | /// use modio::request::mods::filters::Id; 66 | /// use modio::request::mods::filters::GameId; 67 | /// use modio::request::mods::filters::Tags; 68 | /// 69 | /// let filter = Id::_in(vec![1, 2]).order_by(Id::desc()); 70 | /// 71 | /// let filter = GameId::eq(6).and(Tags::eq("foobar")).limit(10); 72 | /// ``` 73 | #[rustfmt::skip] 74 | pub mod filters { 75 | #[doc(inline)] 76 | pub use crate::request::filter::prelude::Fulltext; 77 | #[doc(inline)] 78 | pub use crate::request::filter::prelude::Id; 79 | #[doc(inline)] 80 | pub use crate::request::filter::prelude::Name; 81 | #[doc(inline)] 82 | pub use crate::request::filter::prelude::NameId; 83 | #[doc(inline)] 84 | pub use crate::request::filter::prelude::Status; 85 | #[doc(inline)] 86 | pub use crate::request::filter::prelude::DateAdded; 87 | #[doc(inline)] 88 | pub use crate::request::filter::prelude::DateUpdated; 89 | #[doc(inline)] 90 | pub use crate::request::filter::prelude::DateLive; 91 | #[doc(inline)] 92 | pub use crate::request::filter::prelude::SubmittedBy; 93 | 94 | filter!(GameId, GAME_ID, "game_id", Eq, NotEq, In, Cmp, OrderBy); 95 | filter!(Visible, VISIBLE, "visible", Eq); 96 | filter!(MaturityOption, MATURITY_OPTION, "maturity_option", Eq, Cmp, Bit); 97 | filter!(Summary, SUMMARY, "summary", Like); 98 | filter!(Description, DESCRIPTION, "description", Like); 99 | filter!(Homepage, HOMEPAGE, "homepage_url", Eq, NotEq, Like, In); 100 | filter!(Modfile, MODFILE, "modfile", Eq, NotEq, In, Cmp); 101 | filter!(MetadataBlob, METADATA_BLOB, "metadata_blob", Eq, NotEq, Like); 102 | filter!(MetadataKVP, METADATA_KVP, "metadata_kvp", Eq, NotEq, Like); 103 | filter!(Tags, TAGS, "tags", Eq, NotEq, Like, In); 104 | 105 | filter!(Downloads, DOWNLOADS, "downloads", OrderBy); 106 | filter!(Popular, POPULAR, "popular", OrderBy); 107 | filter!(Ratings, RATINGS, "ratings", OrderBy); 108 | filter!(Subscribers, SUBSCRIBERS, "subscribers", OrderBy); 109 | } 110 | -------------------------------------------------------------------------------- /src/request/games/tags/add_game_tags.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use serde_derive::Serialize; 4 | 5 | use crate::client::Client; 6 | use crate::request::{ArrayParams, Output, RequestBuilder, Route}; 7 | use crate::response::ResponseFuture; 8 | use crate::types::games::TagType; 9 | use crate::types::id::GameId; 10 | use crate::types::Message; 11 | 12 | /// Add tags which can by applied to mod profiles. 13 | pub struct AddGameTags<'a> { 14 | http: &'a Client, 15 | game_id: GameId, 16 | fields: AddGameTagsFields<'a>, 17 | } 18 | 19 | #[derive(Serialize)] 20 | struct AddGameTagsFields<'a> { 21 | name: &'a str, 22 | #[serde(rename = "type")] 23 | kind: TagType, 24 | #[serde(skip_serializing_if = "Option::is_none")] 25 | hidden: Option, 26 | #[serde(skip_serializing_if = "Option::is_none")] 27 | locked: Option, 28 | #[serde(flatten)] 29 | tags: ArrayParams<'a, &'a str>, 30 | } 31 | 32 | impl<'a> AddGameTagsFields<'a> { 33 | const fn new(name: &'a str, kind: TagType, tags: &'a [&'a str]) -> Self { 34 | Self { 35 | name, 36 | kind, 37 | hidden: None, 38 | locked: None, 39 | tags: ArrayParams::new("tags[]", tags), 40 | } 41 | } 42 | } 43 | 44 | impl<'a> AddGameTags<'a> { 45 | pub(crate) const fn new( 46 | http: &'a Client, 47 | game_id: GameId, 48 | name: &'a str, 49 | kind: TagType, 50 | tags: &'a [&'a str], 51 | ) -> Self { 52 | Self { 53 | http, 54 | game_id, 55 | fields: AddGameTagsFields::new(name, kind, tags), 56 | } 57 | } 58 | 59 | pub const fn hidden(mut self, hidden: bool) -> Self { 60 | self.fields.hidden = Some(hidden); 61 | self 62 | } 63 | 64 | pub const fn locked(mut self, locked: bool) -> Self { 65 | self.fields.locked = Some(locked); 66 | self 67 | } 68 | } 69 | 70 | impl IntoFuture for AddGameTags<'_> { 71 | type Output = Output; 72 | type IntoFuture = ResponseFuture; 73 | 74 | fn into_future(self) -> Self::IntoFuture { 75 | let route = Route::AddGameTags { 76 | game_id: self.game_id, 77 | }; 78 | match RequestBuilder::from_route(&route).form(&self.fields) { 79 | Ok(req) => self.http.request(req), 80 | Err(err) => ResponseFuture::failed(err), 81 | } 82 | } 83 | } 84 | 85 | #[cfg(test)] 86 | mod tests { 87 | use serde_test::{assert_ser_tokens, Token}; 88 | 89 | use super::{AddGameTagsFields, TagType}; 90 | 91 | #[test] 92 | pub fn serialize_fields() { 93 | let fields = AddGameTagsFields::new("aaa", TagType::Checkboxes, &["bbb", "ccc"]); 94 | 95 | assert_ser_tokens( 96 | &fields, 97 | &[ 98 | Token::Map { len: None }, 99 | Token::Str("name"), 100 | Token::Str("aaa"), 101 | Token::Str("type"), 102 | Token::UnitVariant { 103 | name: "TagType", 104 | variant: "checkboxes", 105 | }, 106 | Token::Str("tags[]"), 107 | Token::Str("bbb"), 108 | Token::Str("tags[]"), 109 | Token::Str("ccc"), 110 | Token::MapEnd, 111 | ], 112 | ); 113 | 114 | let qs = serde_urlencoded::to_string(&fields).unwrap(); 115 | assert_eq!("name=aaa&type=checkboxes&tags%5B%5D=bbb&tags%5B%5D=ccc", qs); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/util/upload/mod.rs: -------------------------------------------------------------------------------- 1 | //! Upload interface for mod files. 2 | 3 | mod byte_ranges; 4 | mod error; 5 | mod multipart; 6 | 7 | /// Required size (50MB) of upload parts except the last part. 8 | pub const MULTIPART_FILE_PART_SIZE: u64 = 50 * 1024 * 1024; 9 | 10 | pub use crate::request::files::multipart::ContentRange; 11 | pub use byte_ranges::{byte_ranges, ByteRanges}; 12 | pub use error::{Error, ErrorKind}; 13 | pub use multipart::MultipartUploader; 14 | 15 | use crate::client::Client; 16 | use crate::types::files::multipart::UploadId; 17 | use crate::types::id::{GameId, ModId}; 18 | use multipart::{Init, Started}; 19 | 20 | /// Extension trait for uploading files in multiple parts. 21 | pub trait MultipartUpload: private::Sealed { 22 | /// Returns [`MultipartUploader`] for uploading files in multiple parts. 23 | /// 24 | /// # Example 25 | /// ```no_run 26 | /// # use modio::Client; 27 | /// # 28 | /// # #[tokio::main] 29 | /// # async fn main() -> Result<(), Box> { 30 | /// # let client = Client::builder("MODIO_API_KEY".to_owned()).build()?; 31 | /// use modio::types::id::Id; 32 | /// use modio::util::upload::{self, ContentRange, MultipartUpload}; 33 | /// use tokio::fs::File; 34 | /// use tokio::io::{AsyncReadExt, BufReader}; 35 | /// use tokio_util::io::ReaderStream; 36 | /// 37 | /// let uploader = client 38 | /// .upload(Id::new(51), Id::new(1234), "modfile.zip") 39 | /// .nonce("xxxxxx") // Max 64 characters (Recommended: SHA-256) 40 | /// .await?; 41 | /// 42 | /// let file = File::open("modfile.zip").await?; 43 | /// let file_size = file.metadata().await?.len(); 44 | /// 45 | /// for (start, end) in upload::byte_ranges(file_size) { 46 | /// let input = BufReader::new(file.try_clone().await?); 47 | /// let part = input.take(upload::MULTIPART_FILE_PART_SIZE); 48 | /// let stream = ReaderStream::new(part); 49 | /// 50 | /// let range = ContentRange { 51 | /// start, 52 | /// end, 53 | /// total: file_size, 54 | /// }; 55 | /// 56 | /// // Add file part to the upload session. 57 | /// uploader.add_part(range, stream).await?; 58 | /// } 59 | /// 60 | /// // Complete the multipart upload session. 61 | /// let uploader = uploader.complete().await?; 62 | /// 63 | /// // Finalize upload to the mod with file details. 64 | /// uploader.active(true).version("1.0").await?; 65 | /// # Ok(()) 66 | /// # } 67 | /// ``` 68 | fn upload<'a>( 69 | &'a self, 70 | game_id: GameId, 71 | mod_id: ModId, 72 | filename: &'a str, 73 | ) -> MultipartUploader<'a, Init<'a>>; 74 | 75 | fn upload_for( 76 | &self, 77 | game_id: GameId, 78 | mod_id: ModId, 79 | upload_id: UploadId, 80 | ) -> MultipartUploader<'_, Started<'_>>; 81 | } 82 | 83 | impl MultipartUpload for Client { 84 | fn upload<'a>( 85 | &'a self, 86 | game_id: GameId, 87 | mod_id: ModId, 88 | filename: &'a str, 89 | ) -> MultipartUploader<'a, Init<'a>> { 90 | MultipartUploader::init(self, game_id, mod_id, filename) 91 | } 92 | 93 | fn upload_for( 94 | &self, 95 | game_id: GameId, 96 | mod_id: ModId, 97 | upload_id: UploadId, 98 | ) -> MultipartUploader<'_, Started<'_>> { 99 | MultipartUploader::started(self, game_id, mod_id, upload_id) 100 | } 101 | } 102 | mod private { 103 | use crate::Client; 104 | 105 | pub trait Sealed {} 106 | 107 | impl Sealed for Client {} 108 | } 109 | -------------------------------------------------------------------------------- /src/request/games/tags/delete_game_tags.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use serde_derive::Serialize; 4 | 5 | use crate::client::Client; 6 | use crate::request::{ArrayParams, Output, RequestBuilder, Route}; 7 | use crate::response::{NoContent, ResponseFuture}; 8 | use crate::types::id::GameId; 9 | 10 | /// Delete an entire group of tags or individual tags. 11 | pub struct DeleteGameTags<'a> { 12 | http: &'a Client, 13 | game_id: GameId, 14 | fields: DeleteGameTagsFields<'a>, 15 | } 16 | 17 | #[derive(Serialize)] 18 | struct DeleteGameTagsFields<'a> { 19 | name: &'a str, 20 | #[serde(flatten)] 21 | tags: ArrayParams<'a, &'a str>, 22 | } 23 | 24 | impl<'a> DeleteGameTagsFields<'a> { 25 | const ALL_TAGS: ArrayParams<'static, &'static str> = ArrayParams::new("tags[]", &[""]); 26 | 27 | const fn new(name: &'a str) -> Self { 28 | Self { 29 | name, 30 | tags: Self::ALL_TAGS, 31 | } 32 | } 33 | 34 | const fn set_tags(&mut self, tags: &'a [&'a str]) { 35 | if tags.is_empty() { 36 | self.tags = Self::ALL_TAGS; 37 | } else { 38 | self.tags = ArrayParams::new("tags[]", tags); 39 | } 40 | } 41 | } 42 | 43 | impl<'a> DeleteGameTags<'a> { 44 | pub(crate) const fn new(http: &'a Client, game_id: GameId, name: &'a str) -> Self { 45 | Self { 46 | http, 47 | game_id, 48 | fields: DeleteGameTagsFields::new(name), 49 | } 50 | } 51 | 52 | pub const fn tags(mut self, tags: &'a [&'a str]) -> Self { 53 | self.fields.set_tags(tags); 54 | self 55 | } 56 | } 57 | 58 | impl IntoFuture for DeleteGameTags<'_> { 59 | type Output = Output; 60 | type IntoFuture = ResponseFuture; 61 | 62 | fn into_future(self) -> Self::IntoFuture { 63 | let route = Route::DeleteGameTags { 64 | game_id: self.game_id, 65 | }; 66 | match RequestBuilder::from_route(&route).form(&self.fields) { 67 | Ok(req) => self.http.request(req), 68 | Err(err) => ResponseFuture::failed(err), 69 | } 70 | } 71 | } 72 | 73 | #[cfg(test)] 74 | mod tests { 75 | use serde_test::{assert_ser_tokens, Token}; 76 | 77 | use super::DeleteGameTagsFields; 78 | 79 | #[test] 80 | pub fn serialize_fields() { 81 | let mut fields = DeleteGameTagsFields::new("aaa"); 82 | fields.set_tags(&["bbb", "ccc"]); 83 | 84 | assert_ser_tokens( 85 | &fields, 86 | &[ 87 | Token::Map { len: None }, 88 | Token::Str("name"), 89 | Token::Str("aaa"), 90 | Token::Str("tags[]"), 91 | Token::Str("bbb"), 92 | Token::Str("tags[]"), 93 | Token::Str("ccc"), 94 | Token::MapEnd, 95 | ], 96 | ); 97 | 98 | let qs = serde_urlencoded::to_string(&fields).unwrap(); 99 | assert_eq!("name=aaa&tags%5B%5D=bbb&tags%5B%5D=ccc", qs); 100 | } 101 | 102 | #[test] 103 | pub fn serialize_fields_all_tags() { 104 | let fields = DeleteGameTagsFields::new("aaa"); 105 | 106 | assert_ser_tokens( 107 | &fields, 108 | &[ 109 | Token::Map { len: None }, 110 | Token::Str("name"), 111 | Token::Str("aaa"), 112 | Token::Str("tags[]"), 113 | Token::Str(""), 114 | Token::MapEnd, 115 | ], 116 | ); 117 | 118 | let qs = serde_urlencoded::to_string(&fields).unwrap(); 119 | assert_eq!("name=aaa&tags%5B%5D=", qs); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lints: 7 | name: Rustfmt and clippy 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v5 13 | with: 14 | persist-credentials: false 15 | 16 | - name: Install rust (stable) 17 | uses: dtolnay/rust-toolchain@stable 18 | with: 19 | components: clippy, rustfmt 20 | 21 | - name: Run rustfmt 22 | run: cargo fmt --check 23 | 24 | - name: Run clippy 25 | run: cargo clippy --all-features -- -D warnings 26 | 27 | docs: 28 | name: Build docs 29 | runs-on: ubuntu-latest 30 | 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v5 34 | with: 35 | persist-credentials: false 36 | 37 | - name: Install rust (nightly) 38 | uses: dtolnay/rust-toolchain@nightly 39 | 40 | - name: Generate docs 41 | run: cargo doc --no-deps --all-features 42 | 43 | build_and_test: 44 | name: Build and test 45 | runs-on: ubuntu-latest 46 | 47 | strategy: 48 | matrix: 49 | rust: [stable, beta, nightly] 50 | 51 | steps: 52 | - name: Checkout 53 | uses: actions/checkout@v5 54 | with: 55 | persist-credentials: false 56 | 57 | - name: Install rust (${{ matrix.rust }}) 58 | uses: dtolnay/rust-toolchain@master 59 | with: 60 | toolchain: ${{ matrix.rust }} 61 | 62 | - name: Check default features 63 | run: cargo check --examples --tests 64 | 65 | - name: Check no default features 66 | run: cargo check --examples --tests --no-default-features 67 | 68 | - name: Check `rustls-webpki-roots` feature 69 | run: cargo check --examples --tests --no-default-features --features rustls-webpki-roots 70 | 71 | - name: Check `native-tls` feature 72 | run: cargo check --examples --tests --no-default-features --features native-tls 73 | 74 | - name: Check `default-tls` and `native-tls` feature 75 | run: cargo check --examples --tests --features native-tls 76 | 77 | - name: Tests 78 | run: cargo test --all-features 79 | 80 | minimal_versions: 81 | name: Minimal crate versions 82 | runs-on: ubuntu-latest 83 | 84 | steps: 85 | - name: Checkout 86 | uses: actions/checkout@v5 87 | with: 88 | persist-credentials: false 89 | 90 | - name: Install nightly toolchain 91 | uses: dtolnay/rust-toolchain@nightly 92 | 93 | - name: Install stable toolchain 94 | uses: dtolnay/rust-toolchain@stable 95 | 96 | - name: Install cargo-hack 97 | uses: taiki-e/install-action@cargo-hack 98 | 99 | - name: Install cargo-minimal-versions 100 | uses: taiki-e/install-action@cargo-minimal-versions 101 | 102 | - name: Check minimal versions 103 | run: cargo minimal-versions check --no-default-features --features rustls-webpki-roots 104 | 105 | MSRV: 106 | runs-on: ubuntu-latest 107 | 108 | steps: 109 | - name: Checkout 110 | uses: actions/checkout@v5 111 | with: 112 | persist-credentials: false 113 | 114 | - name: Get MSRV from package metadata 115 | id: msrv 116 | run: cargo metadata --no-deps --format-version 1 | jq -r '"version=" + (.packages[] | select(.name == "modio")).rust_version' >> $GITHUB_OUTPUT 117 | 118 | - name: Install rust (${{ steps.msrv.outputs.version }}) 119 | uses: dtolnay/rust-toolchain@master 120 | with: 121 | toolchain: ${{ steps.msrv.outputs.version }} 122 | 123 | - run: cargo check --all-features 124 | -------------------------------------------------------------------------------- /src/request/mod.rs: -------------------------------------------------------------------------------- 1 | //! Typed request builders for the different endpoints. 2 | 3 | use bytes::Bytes; 4 | use futures_util::TryStream; 5 | use http::header::{HeaderName, HeaderValue, CONTENT_LENGTH, CONTENT_TYPE}; 6 | use http::request::Builder; 7 | use serde::ser::Serialize; 8 | 9 | use crate::error::{self, Error}; 10 | use crate::response::Response; 11 | 12 | use self::body::Body; 13 | use self::multipart::Form; 14 | use self::routing::Parts; 15 | pub(crate) use self::routing::Route; 16 | use self::util::ArrayParams; 17 | 18 | pub use self::filter::Filter; 19 | pub use self::submit_report::SubmitReport; 20 | 21 | pub(crate) mod body; 22 | mod multipart; 23 | mod routing; 24 | mod submit_report; 25 | mod util; 26 | 27 | pub mod auth; 28 | #[macro_use] 29 | pub mod filter; 30 | pub mod files; 31 | pub mod games; 32 | pub mod mods; 33 | pub mod user; 34 | 35 | pub(crate) type Request = http::Request; 36 | pub(crate) type Output = Result, Error>; 37 | 38 | #[derive(Clone, Copy, Debug)] 39 | pub(crate) struct TokenRequired(pub bool); 40 | 41 | pub(crate) struct RequestBuilder { 42 | inner: Builder, 43 | } 44 | 45 | impl RequestBuilder { 46 | pub fn from_route(route: &Route) -> Self { 47 | let Parts { 48 | game_id, 49 | method, 50 | path, 51 | token_required, 52 | } = route.into_parts(); 53 | 54 | let mut builder = Builder::new() 55 | .uri(path) 56 | .method(method) 57 | .extension(TokenRequired(token_required)); 58 | 59 | if let Some(game_id) = game_id { 60 | builder = builder.extension(game_id); 61 | } 62 | 63 | Self { inner: builder } 64 | } 65 | 66 | pub fn header(self, key: K, value: V) -> Self 67 | where 68 | K: TryInto, 69 | >::Error: Into, 70 | V: TryInto, 71 | >::Error: Into, 72 | { 73 | Self { 74 | inner: self.inner.header(key, value), 75 | } 76 | } 77 | 78 | pub fn filter(self, filter: Filter) -> Self { 79 | Self { 80 | inner: self.inner.extension(filter), 81 | } 82 | } 83 | 84 | pub fn empty(self) -> Result { 85 | build(self.inner, Body::empty()) 86 | } 87 | 88 | pub fn stream(self, stream: S) -> Result 89 | where 90 | S: TryStream + Send + 'static, 91 | S::Ok: Into, 92 | S::Error: Into>, 93 | { 94 | build(self.inner, Body::from_stream(stream)) 95 | } 96 | 97 | pub fn form(self, form: &T) -> Result { 98 | let body = serde_urlencoded::to_string(form).map_err(error::builder)?; 99 | let builder = self.inner.header( 100 | CONTENT_TYPE, 101 | HeaderValue::from_static("application/x-www-form-urlencoded"), 102 | ); 103 | build(builder, Body::from(body)) 104 | } 105 | 106 | pub fn multipart(self, form: Form) -> Result { 107 | let mut builder = match HeaderValue::from_maybe_shared(form.content_type()) { 108 | Ok(value) => self.inner.header(CONTENT_TYPE, value), 109 | Err(_) => self.inner, 110 | }; 111 | 112 | builder = match form.compute_length() { 113 | Some(length) => builder.header(CONTENT_LENGTH, length), 114 | None => builder, 115 | }; 116 | build(builder, Body::from_stream(form.stream())) 117 | } 118 | } 119 | 120 | #[inline] 121 | fn build(builder: Builder, body: Body) -> Result { 122 | builder.body(body).map_err(error::builder) 123 | } 124 | -------------------------------------------------------------------------------- /src/request/mods/metadata/add_mod_metadata.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::future::IntoFuture; 3 | 4 | use crate::client::Client; 5 | use crate::request::{ArrayParams, Output, RequestBuilder, Route}; 6 | use crate::response::ResponseFuture; 7 | use crate::types::id::{GameId, ModId}; 8 | use crate::types::mods::MetadataMap; 9 | use crate::types::Message; 10 | 11 | /// Add metadata for a mod as searchable key value pairs. 12 | pub struct AddModMetadata<'a> { 13 | http: &'a Client, 14 | game_id: GameId, 15 | mod_id: ModId, 16 | fields: AddModMetadataFields, 17 | } 18 | 19 | struct AddModMetadataFields { 20 | metadata: MetadataMap, 21 | } 22 | 23 | impl AddModMetadataFields { 24 | fn flatten(&self) -> Vec { 25 | let sorted = self.metadata.iter().collect::>(); 26 | let mut metadata = Vec::new(); 27 | for (key, values) in sorted { 28 | for value in values { 29 | let mut v = key.clone(); 30 | v.push(':'); 31 | v.push_str(value); 32 | metadata.push(v); 33 | } 34 | } 35 | metadata 36 | } 37 | } 38 | 39 | impl<'a> AddModMetadata<'a> { 40 | pub(crate) const fn new( 41 | http: &'a Client, 42 | game_id: GameId, 43 | mod_id: ModId, 44 | metadata: MetadataMap, 45 | ) -> Self { 46 | Self { 47 | http, 48 | game_id, 49 | mod_id, 50 | fields: AddModMetadataFields { metadata }, 51 | } 52 | } 53 | } 54 | 55 | impl IntoFuture for AddModMetadata<'_> { 56 | type Output = Output; 57 | type IntoFuture = ResponseFuture; 58 | 59 | fn into_future(self) -> Self::IntoFuture { 60 | let route = Route::AddModMetadata { 61 | game_id: self.game_id, 62 | mod_id: self.mod_id, 63 | }; 64 | let metadata = self.fields.flatten(); 65 | let form = ArrayParams::new("metadata[]", &metadata); 66 | match RequestBuilder::from_route(&route).form(&form) { 67 | Ok(req) => self.http.request(req), 68 | Err(err) => ResponseFuture::failed(err), 69 | } 70 | } 71 | } 72 | 73 | #[cfg(test)] 74 | mod tests { 75 | use serde_test::{assert_ser_tokens, Token}; 76 | 77 | use super::{AddModMetadataFields, ArrayParams, MetadataMap}; 78 | 79 | #[test] 80 | pub fn serialize_fields() { 81 | let mut metadata = MetadataMap::new(); 82 | metadata.insert( 83 | String::from("aaa"), 84 | vec![String::from("bbb"), String::from("ccc")], 85 | ); 86 | metadata.insert( 87 | String::from("ddd"), 88 | vec![String::from("eee"), String::from("fff")], 89 | ); 90 | let fields = AddModMetadataFields { metadata }; 91 | let flatten = fields.flatten(); 92 | let params = ArrayParams::new("metadata[]", &flatten); 93 | 94 | assert_ser_tokens( 95 | ¶ms, 96 | &[ 97 | Token::Map { len: Some(4) }, 98 | Token::Str("metadata[]"), 99 | Token::Str("aaa:bbb"), 100 | Token::Str("metadata[]"), 101 | Token::Str("aaa:ccc"), 102 | Token::Str("metadata[]"), 103 | Token::Str("ddd:eee"), 104 | Token::Str("metadata[]"), 105 | Token::Str("ddd:fff"), 106 | Token::MapEnd, 107 | ], 108 | ); 109 | 110 | let qs = serde_urlencoded::to_string(¶ms).unwrap(); 111 | assert_eq!( 112 | "metadata%5B%5D=aaa%3Abbb&metadata%5B%5D=aaa%3Accc&metadata%5B%5D=ddd%3Aeee&metadata%5B%5D=ddd%3Afff", 113 | qs 114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/util/upload/byte_ranges.rs: -------------------------------------------------------------------------------- 1 | use std::iter::{repeat, Chain, Map, Repeat, Zip}; 2 | use std::ops::Range; 3 | 4 | use super::MULTIPART_FILE_PART_SIZE; 5 | 6 | /// The [`ByteRanges`] type implements the underlying `Iterator` for [`byte_ranges`]. 7 | /// 8 | /// # Example 9 | /// 10 | /// ``` 11 | /// # use modio::util::upload::ByteRanges; 12 | /// let mut iter = ByteRanges::new(14).chunks(5).into_iter(); 13 | /// 14 | /// assert_eq!(iter.next(), Some((0, 4))); 15 | /// assert_eq!(iter.next(), Some((5, 9))); 16 | /// assert_eq!(iter.next(), Some((10, 13))); 17 | /// assert_eq!(iter.next(), None); 18 | /// ``` 19 | pub struct ByteRanges { 20 | length: u64, 21 | chunk_size: u64, 22 | } 23 | 24 | impl ByteRanges { 25 | /// Creates a new `ByteRanges` for the give length. 26 | pub const fn new(length: u64) -> Self { 27 | Self { 28 | length, 29 | chunk_size: MULTIPART_FILE_PART_SIZE, 30 | } 31 | } 32 | 33 | /// Sets the chunk size for the iterator. Defaults to [`MULTIPART_FILE_PART_SIZE`]. 34 | pub const fn chunks(mut self, chunk_size: u64) -> Self { 35 | self.chunk_size = chunk_size; 36 | self 37 | } 38 | } 39 | 40 | impl IntoIterator for ByteRanges { 41 | type Item = (u64, u64); 42 | type IntoIter = IntoIter; 43 | 44 | fn into_iter(self) -> Self::IntoIter { 45 | fn map_fn((i, chunk_size): (u64, u64)) -> (u64, u64) { 46 | (i * chunk_size, (i + 1) * chunk_size - 1) 47 | } 48 | 49 | let ByteRanges { length, chunk_size } = self; 50 | 51 | let count = length / chunk_size; 52 | let rem = length % chunk_size; 53 | 54 | let iter = (0..count) 55 | .zip(repeat(chunk_size)) 56 | .map(map_fn as MapFn) 57 | .chain(Some((count * chunk_size, count * chunk_size + rem - 1))); 58 | 59 | IntoIter { inner: iter } 60 | } 61 | } 62 | 63 | type MapFn = fn((u64, u64)) -> (u64, u64); 64 | type Elements = Map, Repeat>, MapFn>; 65 | type Last = std::option::IntoIter<(u64, u64)>; 66 | 67 | pub struct IntoIter { 68 | inner: Chain, 69 | } 70 | 71 | impl Iterator for IntoIter { 72 | type Item = (u64, u64); 73 | 74 | fn next(&mut self) -> Option { 75 | self.inner.next() 76 | } 77 | } 78 | 79 | /// Calculates bytes ranges for the given `length` parameter and the chunk size of 50MB. 80 | /// 81 | /// # Example 82 | /// 83 | /// ``` 84 | /// # use modio::util::upload::byte_ranges; 85 | /// let mut iter = byte_ranges(52 * 1024 * 1024); 86 | /// 87 | /// assert_eq!(iter.next(), Some((0, 52428799))); 88 | /// assert_eq!(iter.next(), Some((52428800, 54525951))); 89 | /// assert_eq!(iter.next(), None); 90 | /// ``` 91 | pub fn byte_ranges(length: u64) -> impl Iterator { 92 | ByteRanges::new(length).into_iter() 93 | } 94 | 95 | #[cfg(test)] 96 | mod tests { 97 | use super::*; 98 | 99 | #[test] 100 | pub fn multiple_byte_ranges() { 101 | const SIZE: u64 = 522 * 1024 * 1024; 102 | 103 | let ranges: Vec<_> = byte_ranges(SIZE).collect(); 104 | 105 | assert_eq!( 106 | ranges, 107 | [ 108 | (0, 52428799), 109 | (52428800, 104857599), 110 | (104857600, 157286399), 111 | (157286400, 209715199), 112 | (209715200, 262143999), 113 | (262144000, 314572799), 114 | (314572800, 367001599), 115 | (367001600, 419430399), 116 | (419430400, 471859199), 117 | (471859200, 524287999), 118 | (524288000, 547356671), 119 | ] 120 | ); 121 | } 122 | 123 | #[test] 124 | pub fn single_byte_range() { 125 | const SIZE: u64 = 25 * 1024 * 1024; 126 | 127 | let ranges: Vec<_> = byte_ranges(SIZE).collect(); 128 | 129 | assert_eq!(ranges, [(0, SIZE - 1)]); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/util/download/info/get_file_by_version.rs: -------------------------------------------------------------------------------- 1 | use std::future::{Future, IntoFuture}; 2 | use std::pin::Pin; 3 | use std::task::{ready, Context, Poll}; 4 | 5 | use futures_util::future::Either; 6 | use http::StatusCode; 7 | 8 | use crate::client::Client; 9 | use crate::request::files::filters::Version; 10 | use crate::request::files::GetFiles; 11 | use crate::request::filter::prelude::*; 12 | use crate::response::{DataFuture, ResponseFuture}; 13 | use crate::types::files::File; 14 | use crate::types::id::{GameId, ModId}; 15 | use crate::types::List; 16 | use crate::util::download::{Error, ErrorKind, ResolvePolicy}; 17 | 18 | pin_project_lite::pin_project! { 19 | pub struct GetFileByVersion { 20 | game_id: GameId, 21 | mod_id: ModId, 22 | version: String, 23 | policy: ResolvePolicy, 24 | #[pin] 25 | future: Either>, DataFuture>>, 26 | } 27 | } 28 | 29 | impl GetFileByVersion { 30 | pub(crate) fn new( 31 | http: &Client, 32 | game_id: GameId, 33 | mod_id: ModId, 34 | version: String, 35 | policy: ResolvePolicy, 36 | ) -> Self { 37 | let filter = Version::eq(version.clone()) 38 | .order_by(DateAdded::desc()) 39 | .limit(2); 40 | 41 | let fut = GetFiles::new(http, game_id, mod_id) 42 | .filter(filter) 43 | .into_future(); 44 | 45 | Self { 46 | game_id, 47 | mod_id, 48 | version, 49 | policy, 50 | future: Either::Left(fut), 51 | } 52 | } 53 | } 54 | 55 | impl Future for GetFileByVersion { 56 | type Output = Result; 57 | 58 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 59 | let mut this = self.project(); 60 | loop { 61 | match this.future.as_mut().as_pin_mut() { 62 | Either::Left(fut) => { 63 | let resp = ready!(fut.poll(cx)).map_err(Error::request)?; 64 | 65 | if resp.status() == StatusCode::NOT_FOUND { 66 | let kind = ErrorKind::ModNotFound { 67 | game_id: *this.game_id, 68 | mod_id: *this.mod_id, 69 | }; 70 | return Poll::Ready(Err(Error::new(kind))); 71 | } 72 | 73 | this.future.set(Either::Right(resp.data())); 74 | } 75 | Either::Right(fut) => { 76 | let mut list = match fut.poll(cx) { 77 | Poll::Ready(Ok(list)) => list.data, 78 | Poll::Ready(Err(err)) => return Poll::Ready(Err(Error::body(err))), 79 | Poll::Pending => return Poll::Pending, 80 | }; 81 | 82 | let result = match (list.len(), &this.policy) { 83 | (1, _) | (_, ResolvePolicy::Latest) => Ok(list.remove(0)), 84 | (0, _) => Err({ 85 | let kind = ErrorKind::VersionNotFound { 86 | game_id: *this.game_id, 87 | mod_id: *this.mod_id, 88 | version: this.version.clone(), 89 | }; 90 | Error::new(kind) 91 | }), 92 | (_, ResolvePolicy::Fail) => Err({ 93 | let kind = ErrorKind::MultipleFilesFound { 94 | game_id: *this.game_id, 95 | mod_id: *this.mod_id, 96 | version: this.version.clone(), 97 | }; 98 | Error::new(kind) 99 | }), 100 | }; 101 | 102 | return Poll::Ready(result); 103 | } 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/request/files/multipart/add_multipart_upload_file.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use serde_derive::Serialize; 4 | 5 | use crate::client::Client; 6 | use crate::error; 7 | use crate::request::multipart::{Form, Part}; 8 | use crate::request::{Output, RequestBuilder, Route}; 9 | use crate::response::ResponseFuture; 10 | use crate::types::files::multipart::UploadId; 11 | use crate::types::files::File; 12 | use crate::types::id::{GameId, ModId}; 13 | 14 | pub struct AddMultipartUploadFile<'a> { 15 | http: &'a Client, 16 | game_id: GameId, 17 | mod_id: ModId, 18 | fields: AddMultipartUploadFileFields<'a>, 19 | } 20 | 21 | #[derive(Serialize)] 22 | struct AddMultipartUploadFileFields<'a> { 23 | upload_id: UploadId, 24 | #[serde(skip_serializing_if = "Option::is_none")] 25 | active: Option, 26 | #[serde(skip_serializing_if = "Option::is_none")] 27 | changelog: Option<&'a str>, 28 | #[serde(skip_serializing_if = "Option::is_none")] 29 | filehash: Option<&'a str>, 30 | #[serde(skip_serializing_if = "Option::is_none")] 31 | metadata_blob: Option<&'a str>, 32 | #[serde(skip_serializing_if = "Option::is_none")] 33 | version: Option<&'a str>, 34 | } 35 | 36 | impl<'a> AddMultipartUploadFile<'a> { 37 | pub(crate) const fn new( 38 | http: &'a Client, 39 | game_id: GameId, 40 | mod_id: ModId, 41 | upload_id: UploadId, 42 | ) -> Self { 43 | Self { 44 | http, 45 | game_id, 46 | mod_id, 47 | fields: AddMultipartUploadFileFields { 48 | upload_id, 49 | active: None, 50 | changelog: None, 51 | filehash: None, 52 | metadata_blob: None, 53 | version: None, 54 | }, 55 | } 56 | } 57 | 58 | pub const fn active(mut self, active: bool) -> Self { 59 | self.fields.active = Some(active); 60 | self 61 | } 62 | 63 | pub const fn changelog(mut self, changelog: &'a str) -> Self { 64 | self.fields.changelog = Some(changelog); 65 | self 66 | } 67 | 68 | pub const fn filehash(mut self, filehash: &'a str) -> Self { 69 | self.fields.filehash = Some(filehash); 70 | self 71 | } 72 | 73 | pub const fn metadata_blob(mut self, metadata: &'a str) -> Self { 74 | self.fields.metadata_blob = Some(metadata); 75 | self 76 | } 77 | 78 | pub const fn version(mut self, version: &'a str) -> Self { 79 | self.fields.version = Some(version); 80 | self 81 | } 82 | } 83 | 84 | impl IntoFuture for AddMultipartUploadFile<'_> { 85 | type Output = Output; 86 | type IntoFuture = ResponseFuture; 87 | 88 | fn into_future(self) -> Self::IntoFuture { 89 | let route = Route::AddFile { 90 | game_id: self.game_id, 91 | mod_id: self.mod_id, 92 | }; 93 | 94 | let mut form = Form::new(); 95 | form = form.text("upload_id", self.fields.upload_id.to_string()); 96 | 97 | if let Some(value) = self.fields.active { 98 | form = form.text("active", value.to_string()); 99 | } 100 | if let Some(value) = self.fields.changelog { 101 | form = form.text("changelog", value.to_owned()); 102 | } 103 | if let Some(value) = self.fields.metadata_blob { 104 | form = form.text("metadata_blob", value.to_owned()); 105 | } 106 | if let Some(value) = self.fields.version { 107 | form = form.text("version", value.to_owned()); 108 | } 109 | 110 | form = match serde_json::to_vec(&self.fields) { 111 | Ok(json) => form.part("input_json", Part::bytes(json.into())), 112 | Err(err) => return ResponseFuture::failed(error::builder(err)), 113 | }; 114 | 115 | match RequestBuilder::from_route(&route).multipart(form) { 116 | Ok(req) => self.http.request(req), 117 | Err(err) => ResponseFuture::failed(err), 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/request/mods/metadata/delete_mod_metadata.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::future::IntoFuture; 3 | 4 | use crate::client::Client; 5 | use crate::request::{ArrayParams, Output, RequestBuilder, Route}; 6 | use crate::response::{NoContent, ResponseFuture}; 7 | use crate::types::id::{GameId, ModId}; 8 | use crate::types::mods::MetadataMap; 9 | 10 | /// Delete key value pairs metadata defined for a mod. 11 | pub struct DeleteModMetadata<'a> { 12 | http: &'a Client, 13 | game_id: GameId, 14 | mod_id: ModId, 15 | fields: DeleteModMetadataFields, 16 | } 17 | 18 | struct DeleteModMetadataFields { 19 | metadata: MetadataMap, 20 | } 21 | 22 | impl DeleteModMetadataFields { 23 | fn flatten(&self) -> Vec { 24 | let sorted = self.metadata.iter().collect::>(); 25 | let mut metadata = Vec::new(); 26 | for (key, values) in sorted { 27 | if values.is_empty() { 28 | metadata.push(key.to_owned()); 29 | continue; 30 | } 31 | for value in values { 32 | let mut v = key.clone(); 33 | v.push(':'); 34 | v.push_str(value); 35 | metadata.push(v); 36 | } 37 | } 38 | metadata 39 | } 40 | } 41 | 42 | impl<'a> DeleteModMetadata<'a> { 43 | pub(crate) const fn new( 44 | http: &'a Client, 45 | game_id: GameId, 46 | mod_id: ModId, 47 | metadata: MetadataMap, 48 | ) -> Self { 49 | Self { 50 | http, 51 | game_id, 52 | mod_id, 53 | fields: DeleteModMetadataFields { metadata }, 54 | } 55 | } 56 | } 57 | 58 | impl IntoFuture for DeleteModMetadata<'_> { 59 | type Output = Output; 60 | type IntoFuture = ResponseFuture; 61 | 62 | fn into_future(self) -> Self::IntoFuture { 63 | let route = Route::DeleteModMetadata { 64 | game_id: self.game_id, 65 | mod_id: self.mod_id, 66 | }; 67 | let metadata = self.fields.flatten(); 68 | let form = ArrayParams::new("metadata[]", &metadata); 69 | match RequestBuilder::from_route(&route).form(&form) { 70 | Ok(req) => self.http.request(req), 71 | Err(err) => ResponseFuture::failed(err), 72 | } 73 | } 74 | } 75 | 76 | #[cfg(test)] 77 | mod tests { 78 | use serde_test::{assert_ser_tokens, Token}; 79 | 80 | use super::{ArrayParams, DeleteModMetadataFields, MetadataMap}; 81 | 82 | #[test] 83 | pub fn serialize_fields() { 84 | let mut metadata = MetadataMap::new(); 85 | metadata.insert( 86 | String::from("aaa"), 87 | vec![String::from("bbb"), String::from("ccc")], 88 | ); 89 | metadata.insert(String::from("ddd"), vec![]); 90 | metadata.insert( 91 | String::from("eee"), 92 | vec![String::from("fff"), String::from("ggg")], 93 | ); 94 | let fields = DeleteModMetadataFields { metadata }; 95 | let flatten = fields.flatten(); 96 | let params = ArrayParams::new("metadata[]", &flatten); 97 | 98 | assert_ser_tokens( 99 | ¶ms, 100 | &[ 101 | Token::Map { len: Some(5) }, 102 | Token::Str("metadata[]"), 103 | Token::Str("aaa:bbb"), 104 | Token::Str("metadata[]"), 105 | Token::Str("aaa:ccc"), 106 | Token::Str("metadata[]"), 107 | Token::Str("ddd"), 108 | Token::Str("metadata[]"), 109 | Token::Str("eee:fff"), 110 | Token::Str("metadata[]"), 111 | Token::Str("eee:ggg"), 112 | Token::MapEnd, 113 | ], 114 | ); 115 | 116 | let qs = serde_urlencoded::to_string(¶ms).unwrap(); 117 | assert_eq!( 118 | "metadata%5B%5D=aaa%3Abbb&metadata%5B%5D=aaa%3Accc&metadata%5B%5D=ddd&metadata%5B%5D=eee%3Afff&metadata%5B%5D=eee%3Aggg", 119 | qs 120 | ); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/request/mods/media/delete_mod_media.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use serde_derive::Serialize; 4 | 5 | use crate::client::Client; 6 | use crate::request::{ArrayParams, Output, RequestBuilder, Route}; 7 | use crate::response::{NoContent, ResponseFuture}; 8 | use crate::types::id::{GameId, ModId}; 9 | 10 | /// Delete images, sketchfab or youtube links from a mod profile. 11 | pub struct DeleteModMedia<'a> { 12 | http: &'a Client, 13 | game_id: GameId, 14 | mod_id: ModId, 15 | fields: DeleteModMediaFields<'a>, 16 | } 17 | 18 | #[derive(Serialize)] 19 | struct DeleteModMediaFields<'a> { 20 | #[serde(flatten, skip_serializing_if = "Option::is_none")] 21 | images: Option>, 22 | #[serde(flatten, skip_serializing_if = "Option::is_none")] 23 | youtube: Option>, 24 | #[serde(flatten, skip_serializing_if = "Option::is_none")] 25 | sketchfab: Option>, 26 | } 27 | 28 | impl<'a> DeleteModMediaFields<'a> { 29 | const fn set_images(&mut self, images: &'a [&'a str]) { 30 | self.images = Some(ArrayParams::new("images[]", images)); 31 | } 32 | 33 | const fn set_youtube(&mut self, youtube: &'a [&'a str]) { 34 | self.youtube = Some(ArrayParams::new("youtube[]", youtube)); 35 | } 36 | 37 | const fn set_sketchfab(&mut self, sketchfab: &'a [&'a str]) { 38 | self.sketchfab = Some(ArrayParams::new("sketchfab[]", sketchfab)); 39 | } 40 | } 41 | 42 | impl<'a> DeleteModMedia<'a> { 43 | pub(crate) const fn new(http: &'a Client, game_id: GameId, mod_id: ModId) -> Self { 44 | Self { 45 | http, 46 | game_id, 47 | mod_id, 48 | fields: DeleteModMediaFields { 49 | images: None, 50 | youtube: None, 51 | sketchfab: None, 52 | }, 53 | } 54 | } 55 | 56 | pub const fn images(mut self, images: &'a [&'a str]) -> Self { 57 | self.fields.set_images(images); 58 | self 59 | } 60 | 61 | pub const fn youtube(mut self, youtube: &'a [&'a str]) -> Self { 62 | self.fields.set_youtube(youtube); 63 | self 64 | } 65 | 66 | pub const fn sketchfab(mut self, sketchfab: &'a [&'a str]) -> Self { 67 | self.fields.set_sketchfab(sketchfab); 68 | self 69 | } 70 | } 71 | 72 | impl IntoFuture for DeleteModMedia<'_> { 73 | type Output = Output; 74 | type IntoFuture = ResponseFuture; 75 | 76 | fn into_future(self) -> Self::IntoFuture { 77 | let route = Route::DeleteModMedia { 78 | game_id: self.game_id, 79 | mod_id: self.mod_id, 80 | }; 81 | match RequestBuilder::from_route(&route).form(&self.fields) { 82 | Ok(req) => self.http.request(req), 83 | Err(err) => ResponseFuture::failed(err), 84 | } 85 | } 86 | } 87 | 88 | #[cfg(test)] 89 | mod tests { 90 | use serde_test::{assert_ser_tokens, Token}; 91 | 92 | use super::DeleteModMediaFields; 93 | 94 | #[test] 95 | pub fn serialize_fields() { 96 | let mut fields = DeleteModMediaFields { 97 | images: None, 98 | youtube: None, 99 | sketchfab: None, 100 | }; 101 | fields.set_images(&["aaa", "bbb"]); 102 | fields.set_sketchfab(&["ccc", "ddd"]); 103 | 104 | assert_ser_tokens( 105 | &fields, 106 | &[ 107 | Token::Map { len: None }, 108 | Token::Str("images[]"), 109 | Token::Str("aaa"), 110 | Token::Str("images[]"), 111 | Token::Str("bbb"), 112 | Token::Str("sketchfab[]"), 113 | Token::Str("ccc"), 114 | Token::Str("sketchfab[]"), 115 | Token::Str("ddd"), 116 | Token::MapEnd, 117 | ], 118 | ); 119 | 120 | let qs = serde_urlencoded::to_string(&fields).unwrap(); 121 | assert_eq!( 122 | "images%5B%5D=aaa&images%5B%5D=bbb&sketchfab%5B%5D=ccc&sketchfab%5B%5D=ddd", 123 | qs 124 | ); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/request/mods/media/reorder_mod_media.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | 3 | use serde_derive::Serialize; 4 | 5 | use crate::client::Client; 6 | use crate::request::{ArrayParams, Output, RequestBuilder, Route}; 7 | use crate::response::{NoContent, ResponseFuture}; 8 | use crate::types::id::{GameId, ModId}; 9 | 10 | /// Reorder images, sketchfab or youtube links from a mod profile. 11 | pub struct ReorderModMedia<'a> { 12 | http: &'a Client, 13 | game_id: GameId, 14 | mod_id: ModId, 15 | fields: ReorderModMediaFields<'a>, 16 | } 17 | 18 | #[derive(Serialize)] 19 | struct ReorderModMediaFields<'a> { 20 | #[serde(flatten, skip_serializing_if = "Option::is_none")] 21 | images: Option>, 22 | #[serde(flatten, skip_serializing_if = "Option::is_none")] 23 | youtube: Option>, 24 | #[serde(flatten, skip_serializing_if = "Option::is_none")] 25 | sketchfab: Option>, 26 | } 27 | 28 | impl<'a> ReorderModMediaFields<'a> { 29 | const fn set_images(&mut self, images: &'a [&'a str]) { 30 | self.images = Some(ArrayParams::new("images[]", images)); 31 | } 32 | 33 | const fn set_youtube(&mut self, youtube: &'a [&'a str]) { 34 | self.youtube = Some(ArrayParams::new("youtube[]", youtube)); 35 | } 36 | 37 | const fn set_sketchfab(&mut self, sketchfab: &'a [&'a str]) { 38 | self.sketchfab = Some(ArrayParams::new("sketchfab[]", sketchfab)); 39 | } 40 | } 41 | 42 | impl<'a> ReorderModMedia<'a> { 43 | pub(crate) const fn new(http: &'a Client, game_id: GameId, mod_id: ModId) -> Self { 44 | Self { 45 | http, 46 | game_id, 47 | mod_id, 48 | fields: ReorderModMediaFields { 49 | images: None, 50 | youtube: None, 51 | sketchfab: None, 52 | }, 53 | } 54 | } 55 | 56 | pub const fn images(mut self, images: &'a [&'a str]) -> Self { 57 | self.fields.set_images(images); 58 | self 59 | } 60 | 61 | pub const fn youtube(mut self, youtube: &'a [&'a str]) -> Self { 62 | self.fields.set_youtube(youtube); 63 | self 64 | } 65 | 66 | pub const fn sketchfab(mut self, sketchfab: &'a [&'a str]) -> Self { 67 | self.fields.set_sketchfab(sketchfab); 68 | self 69 | } 70 | } 71 | 72 | impl IntoFuture for ReorderModMedia<'_> { 73 | type Output = Output; 74 | type IntoFuture = ResponseFuture; 75 | 76 | fn into_future(self) -> Self::IntoFuture { 77 | let route = Route::ReorderModMedia { 78 | game_id: self.game_id, 79 | mod_id: self.mod_id, 80 | }; 81 | match RequestBuilder::from_route(&route).form(&self.fields) { 82 | Ok(req) => self.http.request(req), 83 | Err(err) => ResponseFuture::failed(err), 84 | } 85 | } 86 | } 87 | 88 | #[cfg(test)] 89 | mod tests { 90 | use serde_test::{assert_ser_tokens, Token}; 91 | 92 | use super::ReorderModMediaFields; 93 | 94 | #[test] 95 | pub fn serialize_fields() { 96 | let mut fields = ReorderModMediaFields { 97 | images: None, 98 | youtube: None, 99 | sketchfab: None, 100 | }; 101 | fields.set_images(&["aaa", "bbb"]); 102 | fields.set_sketchfab(&["ccc", "ddd"]); 103 | 104 | assert_ser_tokens( 105 | &fields, 106 | &[ 107 | Token::Map { len: None }, 108 | Token::Str("images[]"), 109 | Token::Str("aaa"), 110 | Token::Str("images[]"), 111 | Token::Str("bbb"), 112 | Token::Str("sketchfab[]"), 113 | Token::Str("ccc"), 114 | Token::Str("sketchfab[]"), 115 | Token::Str("ddd"), 116 | Token::MapEnd, 117 | ], 118 | ); 119 | 120 | let qs = serde_urlencoded::to_string(&fields).unwrap(); 121 | assert_eq!( 122 | "images%5B%5D=aaa&images%5B%5D=bbb&sketchfab%5B%5D=ccc&sketchfab%5B%5D=ddd", 123 | qs 124 | ); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/util/download/info/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::future::Future; 3 | use std::pin::Pin; 4 | use std::task::{ready, Context, Poll}; 5 | 6 | use pin_project_lite::pin_project; 7 | use url::Url; 8 | 9 | use crate::types::files::File; 10 | use crate::types::id::FileId; 11 | use crate::Client; 12 | 13 | use super::{DownloadAction, Error}; 14 | 15 | mod get_file; 16 | mod get_file_by_version; 17 | mod get_primary_file; 18 | 19 | use self::get_file::GetFile; 20 | use self::get_file_by_version::GetFileByVersion; 21 | use self::get_primary_file::GetPrimaryFile; 22 | 23 | #[non_exhaustive] 24 | pub struct Info { 25 | pub file_id: FileId, 26 | pub download_url: Url, 27 | pub filesize: u64, 28 | pub filesize_uncompressed: u64, 29 | pub filehash: String, 30 | } 31 | 32 | impl fmt::Debug for Info { 33 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 34 | f.debug_struct("Info") 35 | .field("file_id", &self.file_id) 36 | .field("download_url", &self.download_url.as_str()) 37 | .field("filesize", &self.filesize) 38 | .field("filesize_uncompressed", &self.filesize_uncompressed) 39 | .field("filehash", &self.filehash) 40 | .finish_non_exhaustive() 41 | } 42 | } 43 | 44 | pin_project! { 45 | pub struct GetInfo { 46 | #[pin] 47 | future: FileFuture, 48 | } 49 | } 50 | 51 | pin_project! { 52 | #[project = FileFutureProj] 53 | enum FileFuture { 54 | Primary { 55 | #[pin] 56 | future: GetPrimaryFile, 57 | }, 58 | File { 59 | #[pin] 60 | future: GetFile, 61 | }, 62 | FileObj { 63 | file: Option>, 64 | }, 65 | Version { 66 | #[pin] 67 | future: GetFileByVersion, 68 | } 69 | } 70 | } 71 | 72 | impl Future for FileFuture { 73 | type Output = Result; 74 | 75 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 76 | match self.project() { 77 | FileFutureProj::Primary { future } => future.poll(cx), 78 | FileFutureProj::File { future } => future.poll(cx), 79 | FileFutureProj::FileObj { file } => { 80 | Poll::Ready(Ok(*file.take().expect("polled after completion"))) 81 | } 82 | FileFutureProj::Version { future } => future.poll(cx), 83 | } 84 | } 85 | } 86 | 87 | impl GetInfo { 88 | pub(crate) fn new(http: &Client, action: DownloadAction) -> Self { 89 | let future = match action { 90 | DownloadAction::Primary { game_id, mod_id } => FileFuture::Primary { 91 | future: GetPrimaryFile::new(http, game_id, mod_id), 92 | }, 93 | DownloadAction::File { 94 | game_id, 95 | mod_id, 96 | file_id, 97 | } => FileFuture::File { 98 | future: GetFile::new(http, game_id, mod_id, file_id), 99 | }, 100 | DownloadAction::FileObj(file) => FileFuture::FileObj { file: Some(file) }, 101 | DownloadAction::Version { 102 | game_id, 103 | mod_id, 104 | version, 105 | policy, 106 | } => FileFuture::Version { 107 | future: GetFileByVersion::new(http, game_id, mod_id, version, policy), 108 | }, 109 | }; 110 | Self { future } 111 | } 112 | } 113 | 114 | impl Future for GetInfo { 115 | type Output = Result; 116 | 117 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 118 | let file = ready!(self.project().future.poll(cx))?; 119 | 120 | Poll::Ready(Ok(Info { 121 | file_id: file.id, 122 | download_url: file.download.binary_url, 123 | filesize: file.filesize, 124 | filesize_uncompressed: file.filesize_uncompressed, 125 | filehash: file.filehash.md5, 126 | })) 127 | } 128 | } 129 | 130 | pub fn download_info(http: &Client, action: DownloadAction) -> GetInfo { 131 | GetInfo::new(http, action) 132 | } 133 | -------------------------------------------------------------------------------- /src/request/files/add_file.rs: -------------------------------------------------------------------------------- 1 | use std::future::IntoFuture; 2 | use std::path::Path; 3 | 4 | use serde_derive::Serialize; 5 | 6 | use crate::client::Client; 7 | use crate::error; 8 | use crate::request::multipart::{Form, Part}; 9 | use crate::request::{Output, RequestBuilder, Route}; 10 | use crate::response::ResponseFuture; 11 | use crate::types::files::File; 12 | use crate::types::id::{GameId, ModId}; 13 | 14 | /// Upload a file for a mod. 15 | pub struct AddFile<'a> { 16 | http: &'a Client, 17 | game_id: GameId, 18 | mod_id: ModId, 19 | filedata: Option, 20 | fields: AddFileFields<'a>, 21 | } 22 | 23 | #[derive(Serialize)] 24 | struct AddFileFields<'a> { 25 | #[serde(skip_serializing_if = "Option::is_none")] 26 | active: Option, 27 | #[serde(skip_serializing_if = "Option::is_none")] 28 | changelog: Option<&'a str>, 29 | #[serde(skip_serializing_if = "Option::is_none")] 30 | filehash: Option<&'a str>, 31 | #[serde(skip_serializing_if = "Option::is_none")] 32 | metadata_blob: Option<&'a str>, 33 | #[serde(skip_serializing_if = "Option::is_none")] 34 | version: Option<&'a str>, 35 | } 36 | 37 | impl<'a> AddFile<'a> { 38 | pub(crate) const fn new(http: &'a Client, game_id: GameId, mod_id: ModId) -> Self { 39 | Self { 40 | http, 41 | game_id, 42 | mod_id, 43 | filedata: None, 44 | fields: AddFileFields { 45 | active: None, 46 | changelog: None, 47 | filehash: None, 48 | metadata_blob: None, 49 | version: None, 50 | }, 51 | } 52 | } 53 | 54 | pub fn file>(self, file: P) -> Self { 55 | self.file_with_name(file, "modfile.zip") 56 | } 57 | 58 | pub fn file_with_name>(mut self, file: P, filename: &str) -> Self { 59 | let part = Part::file(file, filename).mime(mime::APPLICATION_OCTET_STREAM); 60 | self.filedata = Some(part); 61 | self 62 | } 63 | 64 | pub const fn active(mut self, active: bool) -> Self { 65 | self.fields.active = Some(active); 66 | self 67 | } 68 | 69 | pub const fn changelog(mut self, changelog: &'a str) -> Self { 70 | self.fields.changelog = Some(changelog); 71 | self 72 | } 73 | 74 | pub const fn filehash(mut self, filehash: &'a str) -> Self { 75 | self.fields.filehash = Some(filehash); 76 | self 77 | } 78 | 79 | pub const fn metadata_blob(mut self, metadata: &'a str) -> Self { 80 | self.fields.metadata_blob = Some(metadata); 81 | self 82 | } 83 | 84 | pub const fn version(mut self, version: &'a str) -> Self { 85 | self.fields.version = Some(version); 86 | self 87 | } 88 | } 89 | 90 | impl IntoFuture for AddFile<'_> { 91 | type Output = Output; 92 | type IntoFuture = ResponseFuture; 93 | 94 | fn into_future(self) -> Self::IntoFuture { 95 | let route = Route::AddFile { 96 | game_id: self.game_id, 97 | mod_id: self.mod_id, 98 | }; 99 | 100 | let mut form = Form::new(); 101 | if let Some(value) = self.filedata { 102 | form = form.part("filedata", value); 103 | } 104 | if let Some(value) = self.fields.active { 105 | form = form.text("active", value.to_string()); 106 | } 107 | if let Some(value) = self.fields.changelog { 108 | form = form.text("changelog", value.to_owned()); 109 | } 110 | if let Some(value) = self.fields.metadata_blob { 111 | form = form.text("metadata_blob", value.to_owned()); 112 | } 113 | if let Some(value) = self.fields.version { 114 | form = form.text("version", value.to_owned()); 115 | } 116 | 117 | form = match serde_json::to_vec(&self.fields) { 118 | Ok(json) => form.part("input_json", Part::bytes(json.into())), 119 | Err(err) => return ResponseFuture::failed(error::builder(err)), 120 | }; 121 | 122 | match RequestBuilder::from_route(&route).multipart(form) { 123 | Ok(req) => self.http.request(req), 124 | Err(err) => ResponseFuture::failed(err), 125 | } 126 | } 127 | } 128 | --------------------------------------------------------------------------------