>(file: P) -> AddFileOptions {
208 | let file = file.as_ref();
209 | let filename = file
210 | .file_name()
211 | .and_then(OsStr::to_str)
212 | .map_or_else(String::new, ToString::to_string);
213 |
214 | Self::with_file_name(file, filename)
215 | }
216 |
217 | pub fn with_file_name(file: P, filename: S) -> AddFileOptions
218 | where
219 | P: AsRef,
220 | S: Into,
221 | {
222 | let file = file.as_ref();
223 |
224 | AddFileOptions {
225 | source: FileSource::new_from_file(file, filename.into(), APPLICATION_OCTET_STREAM),
226 | version: None,
227 | changelog: None,
228 | active: None,
229 | filehash: None,
230 | metadata_blob: None,
231 | }
232 | }
233 |
234 | option!(version);
235 | option!(changelog);
236 | option!(active: bool);
237 | option!(filehash);
238 | option!(metadata_blob);
239 | }
240 |
241 | #[doc(hidden)]
242 | impl From for Form {
243 | fn from(opts: AddFileOptions) -> Form {
244 | let mut form = Form::new();
245 | if let Some(version) = opts.version {
246 | form = form.text("version", version);
247 | }
248 | if let Some(changelog) = opts.changelog {
249 | form = form.text("changelog", changelog);
250 | }
251 | if let Some(active) = opts.active {
252 | form = form.text("active", active.to_string());
253 | }
254 | if let Some(filehash) = opts.filehash {
255 | form = form.text("filehash", filehash);
256 | }
257 | if let Some(metadata_blob) = opts.metadata_blob {
258 | form = form.text("metadata_blob", metadata_blob);
259 | }
260 | form.part("filedata", opts.source.into())
261 | }
262 | }
263 |
264 | #[derive(Default)]
265 | pub struct EditFileOptions {
266 | params: std::collections::BTreeMap<&'static str, String>,
267 | }
268 |
269 | impl EditFileOptions {
270 | option!(version >> "version");
271 | option!(changelog >> "changelog");
272 | option!(active: bool >> "active");
273 | option!(metadata_blob >> "metadata_blob");
274 | }
275 |
276 | impl_serialize_params!(EditFileOptions >> params);
277 |
278 | pub struct EditPlatformStatusOptions {
279 | approved: Vec,
280 | denied: Vec,
281 | }
282 |
283 | impl EditPlatformStatusOptions {
284 | pub fn new(approved: &[TargetPlatform], denied: &[TargetPlatform]) -> Self {
285 | Self {
286 | approved: approved.to_vec(),
287 | denied: denied.to_vec(),
288 | }
289 | }
290 | }
291 |
292 | impl Serialize for EditPlatformStatusOptions {
293 | fn serialize(&self, serializer: S) -> Result {
294 | let mut s = serializer.serialize_map(Some(self.approved.len() + self.denied.len()))?;
295 | for target in &self.approved {
296 | s.serialize_entry("approved[]", target)?;
297 | }
298 | for target in &self.denied {
299 | s.serialize_entry("denied[]", target)?;
300 | }
301 | s.end()
302 | }
303 | }
304 |
--------------------------------------------------------------------------------
/src/games.rs:
--------------------------------------------------------------------------------
1 | //! Games interface
2 | use std::ffi::OsStr;
3 | use std::path::Path;
4 |
5 | use mime::IMAGE_STAR;
6 |
7 | use crate::file_source::FileSource;
8 | use crate::mods::{ModRef, Mods};
9 | use crate::prelude::*;
10 | use crate::types::id::{GameId, ModId};
11 |
12 | pub use crate::types::games::{
13 | ApiAccessOptions, CommunityOptions, CurationOption, Downloads, Game, HeaderImage, Icon,
14 | MaturityOptions, OtherUrl, Platform, PresentationOption, Statistics, SubmissionOption,
15 | TagOption, TagType, Theme,
16 | };
17 | pub use crate::types::Logo;
18 | pub use crate::types::Status;
19 |
20 | /// Interface for games.
21 | #[derive(Clone)]
22 | pub struct Games {
23 | modio: Modio,
24 | }
25 |
26 | impl Games {
27 | pub(crate) fn new(modio: Modio) -> Self {
28 | Self { modio }
29 | }
30 |
31 | /// Returns a `Query` interface to retrieve games.
32 | ///
33 | /// See [Filters and sorting](filters).
34 | pub fn search(&self, filter: Filter) -> Query {
35 | let route = Route::GetGames {
36 | show_hidden_tags: None,
37 | };
38 | Query::new(self.modio.clone(), route, filter)
39 | }
40 |
41 | /// Return a reference to a game.
42 | pub fn get(&self, id: GameId) -> GameRef {
43 | GameRef::new(self.modio.clone(), id)
44 | }
45 | }
46 |
47 | /// Reference interface of a game.
48 | #[derive(Clone)]
49 | pub struct GameRef {
50 | modio: Modio,
51 | id: GameId,
52 | }
53 |
54 | impl GameRef {
55 | pub(crate) fn new(modio: Modio, id: GameId) -> Self {
56 | Self { modio, id }
57 | }
58 |
59 | /// Get a reference to the Modio game object that this `GameRef` refers to.
60 | pub async fn get(self) -> Result {
61 | let route = Route::GetGame {
62 | id: self.id,
63 | show_hidden_tags: None,
64 | };
65 | self.modio.request(route).send().await
66 | }
67 |
68 | /// Return a reference to a mod of a game.
69 | pub fn mod_(&self, mod_id: ModId) -> ModRef {
70 | ModRef::new(self.modio.clone(), self.id, mod_id)
71 | }
72 |
73 | /// Return a reference to an interface that provides access to the mods of a game.
74 | pub fn mods(&self) -> Mods {
75 | Mods::new(self.modio.clone(), self.id)
76 | }
77 |
78 | /// Return the statistics for a game.
79 | pub async fn statistics(self) -> Result {
80 | let route = Route::GetGameStats { game_id: self.id };
81 | self.modio.request(route).send().await
82 | }
83 |
84 | /// Return a reference to an interface that provides access to the tags of a game.
85 | pub fn tags(&self) -> Tags {
86 | Tags::new(self.modio.clone(), self.id)
87 | }
88 |
89 | /// Add new media to a game. [required: token]
90 | pub async fn edit_media(self, media: EditMediaOptions) -> Result<()> {
91 | let route = Route::AddGameMedia { game_id: self.id };
92 | self.modio
93 | .request(route)
94 | .multipart(Form::from(media))
95 | .send::()
96 | .await?;
97 | Ok(())
98 | }
99 | }
100 |
101 | /// Interface for tag options.
102 | #[derive(Clone)]
103 | pub struct Tags {
104 | modio: Modio,
105 | game_id: GameId,
106 | }
107 |
108 | impl Tags {
109 | fn new(modio: Modio, game_id: GameId) -> Self {
110 | Self { modio, game_id }
111 | }
112 |
113 | /// List tag options.
114 | pub async fn list(self) -> Result> {
115 | let route = Route::GetGameTags {
116 | game_id: self.game_id,
117 | };
118 | Query::new(self.modio, route, Filter::default())
119 | .collect()
120 | .await
121 | }
122 |
123 | /// Provides a stream over all tag options.
124 | #[allow(clippy::iter_not_returning_iterator)]
125 | pub async fn iter(self) -> Result>> {
126 | let route = Route::GetGameTags {
127 | game_id: self.game_id,
128 | };
129 | let filter = Filter::default();
130 | Query::new(self.modio, route, filter).iter().await
131 | }
132 |
133 | /// Add tag options. [required: token]
134 | #[allow(clippy::should_implement_trait)]
135 | pub async fn add(self, options: AddTagsOptions) -> Result<()> {
136 | let route = Route::AddGameTags {
137 | game_id: self.game_id,
138 | };
139 | self.modio
140 | .request(route)
141 | .form(&options)
142 | .send::()
143 | .await?;
144 | Ok(())
145 | }
146 |
147 | /// Delete tag options. [required: token]
148 | pub async fn delete(self, options: DeleteTagsOptions) -> Result {
149 | let route = Route::DeleteGameTags {
150 | game_id: self.game_id,
151 | };
152 | self.modio.request(route).form(&options).send().await
153 | }
154 |
155 | /// Rename an existing tag, updating all mods in the progress. [required: token]
156 | pub async fn rename(self, from: String, to: String) -> Result<()> {
157 | let route = Route::RenameGameTags {
158 | game_id: self.game_id,
159 | };
160 | self.modio
161 | .request(route)
162 | .form(&[("from", from), ("to", to)])
163 | .send::<()>()
164 | .await?;
165 |
166 | Ok(())
167 | }
168 | }
169 |
170 | /// Game filters and sorting.
171 | ///
172 | /// # Filters
173 | /// - `Fulltext`
174 | /// - `Id`
175 | /// - `Status`
176 | /// - `SubmittedBy`
177 | /// - `DateAdded`
178 | /// - `DateUpdated`
179 | /// - `DateLive`
180 | /// - `Name`
181 | /// - `NameId`
182 | /// - `Summary`
183 | /// - `InstructionsUrl`
184 | /// - `UgcName`
185 | /// - `PresentationOption`
186 | /// - `SubmissionOption`
187 | /// - `CurationOption`
188 | /// - `CommunityOptions`
189 | /// - `RevenueOptions`
190 | /// - `ApiAccessOptions`
191 | /// - `MaturityOptions`
192 | ///
193 | /// # Sorting
194 | /// - `Id`
195 | /// - `Status`
196 | /// - `Name`
197 | /// - `NameId`
198 | /// - `DateUpdated`
199 | ///
200 | /// See [modio docs](https://docs.mod.io/restapiref/#get-games) for more information.
201 | ///
202 | /// By default this returns up to `100` items. You can limit the result by using `limit` and
203 | /// `offset`.
204 | ///
205 | /// # Example
206 | /// ```
207 | /// use modio::filter::prelude::*;
208 | /// use modio::games::filters::Id;
209 | ///
210 | /// let filter = Id::_in(vec![1, 2]).order_by(Id::desc());
211 | /// ```
212 | #[rustfmt::skip]
213 | pub mod filters {
214 | #[doc(inline)]
215 | pub use crate::filter::prelude::Fulltext;
216 | #[doc(inline)]
217 | pub use crate::filter::prelude::Id;
218 | #[doc(inline)]
219 | pub use crate::filter::prelude::Name;
220 | #[doc(inline)]
221 | pub use crate::filter::prelude::NameId;
222 | #[doc(inline)]
223 | pub use crate::filter::prelude::Status;
224 | #[doc(inline)]
225 | pub use crate::filter::prelude::DateAdded;
226 | #[doc(inline)]
227 | pub use crate::filter::prelude::DateUpdated;
228 | #[doc(inline)]
229 | pub use crate::filter::prelude::DateLive;
230 | #[doc(inline)]
231 | pub use crate::filter::prelude::SubmittedBy;
232 |
233 | filter!(Summary, SUMMARY, "summary", Eq, NotEq, Like);
234 | filter!(InstructionsUrl, INSTRUCTIONS_URL, "instructions_url", Eq, NotEq, In, Like);
235 | filter!(UgcName, UGC_NAME, "ugc_name", Eq, NotEq, In, Like);
236 | filter!(PresentationOption, PRESENTATION_OPTION, "presentation_option", Eq, NotEq, In, Cmp, Bit);
237 | filter!(SubmissionOption, SUBMISSION_OPTION, "submission_option", Eq, NotEq, In, Cmp, Bit);
238 | filter!(CurationOption, CURATION_OPTION, "curation_option", Eq, NotEq, In, Cmp, Bit);
239 | filter!(CommunityOptions, COMMUNITY_OPTIONS, "community_options", Eq, NotEq, In, Cmp, Bit);
240 | filter!(RevenueOptions, REVENUE_OPTIONS, "revenue_options", Eq, NotEq, In, Cmp, Bit);
241 | filter!(ApiAccessOptions, API_ACCESS_OPTIONS, "api_access_options", Eq, NotEq, In, Cmp, Bit);
242 | filter!(MaturityOptions, MATURITY_OPTIONS, "maturity_options", Eq, NotEq, In, Cmp, Bit);
243 | }
244 |
245 | pub struct AddTagsOptions {
246 | name: String,
247 | kind: TagType,
248 | hidden: bool,
249 | locked: bool,
250 | tags: Vec,
251 | }
252 |
253 | impl AddTagsOptions {
254 | pub fn new>(name: S, kind: TagType, tags: &[String]) -> Self {
255 | Self {
256 | name: name.into(),
257 | kind,
258 | hidden: false,
259 | locked: false,
260 | tags: tags.to_vec(),
261 | }
262 | }
263 |
264 | pub fn hidden(self, value: bool) -> Self {
265 | Self {
266 | hidden: value,
267 | ..self
268 | }
269 | }
270 |
271 | pub fn locked(self, value: bool) -> Self {
272 | Self {
273 | locked: value,
274 | ..self
275 | }
276 | }
277 | }
278 |
279 | #[doc(hidden)]
280 | impl serde::ser::Serialize for AddTagsOptions {
281 | fn serialize(&self, serializer: S) -> std::result::Result
282 | where
283 | S: serde::ser::Serializer,
284 | {
285 | use serde::ser::SerializeMap;
286 |
287 | let len = 2 + usize::from(self.hidden) + usize::from(self.locked) + self.tags.len();
288 | let mut map = serializer.serialize_map(Some(len))?;
289 | map.serialize_entry("name", &self.name)?;
290 | map.serialize_entry("type", &self.kind)?;
291 | if self.hidden {
292 | map.serialize_entry("hidden", &self.hidden)?;
293 | }
294 | if self.locked {
295 | map.serialize_entry("locked", &self.locked)?;
296 | }
297 | for t in &self.tags {
298 | map.serialize_entry("tags[]", t)?;
299 | }
300 | map.end()
301 | }
302 | }
303 |
304 | pub struct DeleteTagsOptions {
305 | name: String,
306 | tags: Option>,
307 | }
308 |
309 | impl DeleteTagsOptions {
310 | pub fn all>(name: S) -> Self {
311 | Self {
312 | name: name.into(),
313 | tags: None,
314 | }
315 | }
316 |
317 | pub fn some>(name: S, tags: &[String]) -> Self {
318 | Self {
319 | name: name.into(),
320 | tags: if tags.is_empty() {
321 | None
322 | } else {
323 | Some(tags.to_vec())
324 | },
325 | }
326 | }
327 | }
328 |
329 | #[doc(hidden)]
330 | impl serde::ser::Serialize for DeleteTagsOptions {
331 | fn serialize(&self, serializer: S) -> std::result::Result
332 | where
333 | S: serde::ser::Serializer,
334 | {
335 | use serde::ser::SerializeMap;
336 |
337 | let len = self.tags.as_ref().map_or(1, Vec::len);
338 | let mut map = serializer.serialize_map(Some(len + 1))?;
339 | map.serialize_entry("name", &self.name)?;
340 | if let Some(ref tags) = self.tags {
341 | for t in tags {
342 | map.serialize_entry("tags[]", t)?;
343 | }
344 | } else {
345 | map.serialize_entry("tags[]", "")?;
346 | }
347 | map.end()
348 | }
349 | }
350 |
351 | #[derive(Default)]
352 | pub struct EditMediaOptions {
353 | logo: Option,
354 | icon: Option,
355 | header: Option,
356 | }
357 |
358 | impl EditMediaOptions {
359 | #[must_use]
360 | pub fn logo>(self, logo: P) -> Self {
361 | let logo = logo.as_ref();
362 | let filename = logo
363 | .file_name()
364 | .and_then(OsStr::to_str)
365 | .map_or_else(String::new, ToString::to_string);
366 |
367 | Self {
368 | logo: Some(FileSource::new_from_file(logo, filename, IMAGE_STAR)),
369 | ..self
370 | }
371 | }
372 |
373 | #[must_use]
374 | pub fn icon>(self, icon: P) -> Self {
375 | let icon = icon.as_ref();
376 | let filename = icon
377 | .file_name()
378 | .and_then(OsStr::to_str)
379 | .map_or_else(String::new, ToString::to_string);
380 |
381 | Self {
382 | icon: Some(FileSource::new_from_file(icon, filename, IMAGE_STAR)),
383 | ..self
384 | }
385 | }
386 |
387 | #[must_use]
388 | pub fn header>(self, header: P) -> Self {
389 | let header = header.as_ref();
390 | let filename = header
391 | .file_name()
392 | .and_then(OsStr::to_str)
393 | .map_or_else(String::new, ToString::to_string);
394 |
395 | Self {
396 | header: Some(FileSource::new_from_file(header, filename, IMAGE_STAR)),
397 | ..self
398 | }
399 | }
400 | }
401 |
402 | #[doc(hidden)]
403 | impl From for Form {
404 | fn from(opts: EditMediaOptions) -> Form {
405 | let mut form = Form::new();
406 | if let Some(logo) = opts.logo {
407 | form = form.part("logo", logo.into());
408 | }
409 | if let Some(icon) = opts.icon {
410 | form = form.part("icon", icon.into());
411 | }
412 | if let Some(header) = opts.header {
413 | form = form.part("header", header.into());
414 | }
415 | form
416 | }
417 | }
418 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! Modio provides a set of building blocks for interacting with the [mod.io](https://mod.io) API.
2 | //!
3 | //! The client uses asynchronous I/O, backed by the `futures` and `tokio` crates, and requires both
4 | //! to be used alongside.
5 | //!
6 | //! # Authentication
7 | //!
8 | //! To access the API authentication is required and can be done via several ways:
9 | //!
10 | //! - Request an [API key (Read-only)](https://mod.io/me/access)
11 | //! - Manually create an [OAuth 2 Access Token (Read + Write)](https://mod.io/me/access#oauth)
12 | //! - [Email Authentication Flow](auth::Auth#example) to create an OAuth 2 Access Token (Read + Write)
13 | //! - [External Authentication](auth::Auth::external) to create an OAuth 2 Access Token (Read + Write)
14 | //! automatically on platforms such as Steam, GOG, itch.io, Switch, Xbox, Discord and Oculus.
15 | //!
16 | //! # Rate Limiting
17 | //!
18 | //! - API keys linked to a game have **unlimited requests**.
19 | //! - API keys linked to a user have **60 requests per minute**.
20 | //! - OAuth2 user tokens are limited to **120 requests per minute**.
21 | //!
22 | //! [`Error::is_ratelimited`] will return true
23 | //! if the rate limit associated with credentials has been exhausted.
24 | //!
25 | //! # Example: Basic setup
26 | //!
27 | //! ```no_run
28 | //! use modio::{Credentials, Modio};
29 | //!
30 | //! #[tokio::main]
31 | //! async fn main() -> Result<(), Box> {
32 | //! let modio = Modio::new(Credentials::new("user-or-game-api-key"))?;
33 | //!
34 | //! // create some tasks and execute them
35 | //! // let result = task.await?;
36 | //! Ok(())
37 | //! }
38 | //! ```
39 | //!
40 | //! For testing purposes use [`Modio::host`] to create a client for the
41 | //! mod.io [test environment](https://docs.mod.io/restapiref/#testing).
42 | //!
43 | //! # Example: Chaining api requests
44 | //!
45 | //! ```no_run
46 | //! use futures_util::future::try_join3;
47 | //! use modio::filter::Filter;
48 | //! use modio::types::id::Id;
49 | //! # #[tokio::main]
50 | //! # async fn main() -> Result<(), Box> {
51 | //! # let modio = modio::Modio::new("user-or-game-api-key")?;
52 | //!
53 | //! // OpenXcom: The X-Com Files
54 | //! let modref = modio.mod_(Id::new(51), Id::new(158));
55 | //!
56 | //! // Get mod with its dependencies and all files
57 | //! let deps = modref.dependencies().list();
58 | //! let files = modref.files().search(Filter::default()).collect();
59 | //! let mod_ = modref.get();
60 | //!
61 | //! let (m, deps, files) = try_join3(mod_, deps, files).await?;
62 | //!
63 | //! println!("{}", m.name);
64 | //! println!(
65 | //! "deps: {:?}",
66 | //! deps.into_iter().map(|d| d.mod_id).collect::>()
67 | //! );
68 | //! for file in files {
69 | //! println!("file id: {} version: {:?}", file.id, file.version);
70 | //! }
71 | //! # Ok(())
72 | //! # }
73 | //! ```
74 | //!
75 | //! # Example: Downloading mods
76 | //!
77 | //! ```no_run
78 | //! use modio::download::{DownloadAction, ResolvePolicy};
79 | //! use modio::types::id::Id;
80 | //! # #[tokio::main]
81 | //! # async fn main() -> Result<(), Box> {
82 | //! # let modio = modio::Modio::new("user-or-game-api-key")?;
83 | //!
84 | //! // Download the primary file of a mod.
85 | //! let action = DownloadAction::Primary {
86 | //! game_id: Id::new(5),
87 | //! mod_id: Id::new(19),
88 | //! };
89 | //! modio
90 | //! .download(action)
91 | //! .await?
92 | //! .save_to_file("mod.zip")
93 | //! .await?;
94 | //!
95 | //! // Download the specific file of a mod.
96 | //! let action = DownloadAction::File {
97 | //! game_id: Id::new(5),
98 | //! mod_id: Id::new(19),
99 | //! file_id: Id::new(101),
100 | //! };
101 | //! modio
102 | //! .download(action)
103 | //! .await?
104 | //! .save_to_file("mod.zip")
105 | //! .await?;
106 | //!
107 | //! // Download the specific version of a mod.
108 | //! // if multiple files are found then the latest file is downloaded.
109 | //! // Set policy to `ResolvePolicy::Fail` to return with
110 | //! // `modio::download::Error::MultipleFilesFound` as source error.
111 | //! let action = DownloadAction::Version {
112 | //! game_id: Id::new(5),
113 | //! mod_id: Id::new(19),
114 | //! version: "0.1".to_string(),
115 | //! policy: ResolvePolicy::Latest,
116 | //! };
117 | //! modio
118 | //! .download(action)
119 | //! .await?
120 | //! .save_to_file("mod.zip")
121 | //! .await?;
122 | //! # Ok(())
123 | //! # }
124 | //! ```
125 | #![doc(html_root_url = "https://docs.rs/modio/0.13.0")]
126 | #![deny(rust_2018_idioms)]
127 | #![deny(rustdoc::broken_intra_doc_links)]
128 | #![allow(clippy::upper_case_acronyms)]
129 |
130 | #[macro_use]
131 | mod macros;
132 |
133 | pub mod auth;
134 | #[macro_use]
135 | pub mod filter;
136 | pub mod comments;
137 | pub mod download;
138 | pub mod files;
139 | pub mod games;
140 | pub mod metadata;
141 | pub mod mods;
142 | pub mod reports;
143 | pub mod teams;
144 | pub mod types;
145 | pub mod user;
146 |
147 | mod client;
148 | mod error;
149 | mod file_source;
150 | mod loader;
151 | mod request;
152 | mod routing;
153 |
154 | pub use crate::auth::Credentials;
155 | pub use crate::client::{Builder, Modio};
156 | pub use crate::download::DownloadAction;
157 | pub use crate::error::{Error, Result};
158 | pub use crate::loader::{Page, Query};
159 | pub use crate::types::{Deletion, Editing, TargetPlatform, TargetPortal};
160 |
161 | mod prelude {
162 | pub use futures_util::Stream;
163 | pub use reqwest::multipart::Form;
164 | pub use reqwest::StatusCode;
165 |
166 | pub use crate::filter::Filter;
167 | pub use crate::loader::Query;
168 | pub use crate::routing::Route;
169 | pub use crate::types::Message;
170 | pub use crate::{Deletion, Editing, Modio, Result};
171 | }
172 |
173 | /// Re-exports of the used reqwest types.
174 | #[doc(hidden)]
175 | pub mod lib {
176 | pub use reqwest::header;
177 | pub use reqwest::redirect::Policy;
178 | pub use reqwest::ClientBuilder;
179 | #[cfg(feature = "__tls")]
180 | pub use reqwest::{Certificate, Identity};
181 | pub use reqwest::{Proxy, Url};
182 | }
183 |
--------------------------------------------------------------------------------
/src/loader.rs:
--------------------------------------------------------------------------------
1 | use std::marker::PhantomData;
2 | use std::pin::Pin;
3 | use std::task::{Context, Poll};
4 |
5 | use futures_util::future::Either;
6 | use futures_util::{stream, Stream, StreamExt, TryStreamExt};
7 | use pin_project_lite::pin_project;
8 | use serde::de::DeserializeOwned;
9 |
10 | use crate::filter::Filter;
11 | use crate::routing::Route;
12 | use crate::types::List;
13 | use crate::{Modio, Result};
14 |
15 | /// Interface for retrieving search results.
16 | pub struct Query {
17 | modio: Modio,
18 | route: Route,
19 | filter: Filter,
20 | phantom: PhantomData,
21 | }
22 |
23 | impl Query {
24 | pub(crate) fn new(modio: Modio, route: Route, filter: Filter) -> Self {
25 | Self {
26 | modio,
27 | route,
28 | filter,
29 | phantom: PhantomData,
30 | }
31 | }
32 | }
33 |
34 | impl Query {
35 | /// Returns the first search result.
36 | pub async fn first(mut self) -> Result