modio/
games.rs

1//! Games interface
2use std::ffi::OsStr;
3use std::path::Path;
4
5use mime::IMAGE_STAR;
6
7use crate::file_source::FileSource;
8use crate::mods::{ModRef, Mods};
9use crate::prelude::*;
10use crate::types::id::{GameId, ModId};
11
12pub use crate::types::games::{
13    ApiAccessOptions, CommunityOptions, CurationOption, Downloads, Game, HeaderImage, Icon,
14    MaturityOptions, OtherUrl, Platform, PresentationOption, Statistics, SubmissionOption,
15    TagOption, TagType, Theme,
16};
17pub use crate::types::Logo;
18pub use crate::types::Status;
19
20/// Interface for games.
21#[derive(Clone)]
22pub struct Games {
23    modio: Modio,
24}
25
26impl 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<Game> {
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)]
49pub struct GameRef {
50    modio: Modio,
51    id: GameId,
52}
53
54impl 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<Game> {
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<Statistics> {
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::<Message>()
96            .await?;
97        Ok(())
98    }
99}
100
101/// Interface for tag options.
102#[derive(Clone)]
103pub struct Tags {
104    modio: Modio,
105    game_id: GameId,
106}
107
108impl 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<Vec<TagOption>> {
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<impl Stream<Item = Result<TagOption>>> {
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::<Message>()
143            .await?;
144        Ok(())
145    }
146
147    /// Delete tag options. [required: token]
148    pub async fn delete(self, options: DeleteTagsOptions) -> Result<Deletion> {
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]
213pub 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
245pub struct AddTagsOptions {
246    name: String,
247    kind: TagType,
248    hidden: bool,
249    locked: bool,
250    tags: Vec<String>,
251}
252
253impl AddTagsOptions {
254    pub fn new<S: Into<String>>(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)]
280impl serde::ser::Serialize for AddTagsOptions {
281    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
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
304pub struct DeleteTagsOptions {
305    name: String,
306    tags: Option<Vec<String>>,
307}
308
309impl DeleteTagsOptions {
310    pub fn all<S: Into<String>>(name: S) -> Self {
311        Self {
312            name: name.into(),
313            tags: None,
314        }
315    }
316
317    pub fn some<S: Into<String>>(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)]
330impl serde::ser::Serialize for DeleteTagsOptions {
331    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
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)]
352pub struct EditMediaOptions {
353    logo: Option<FileSource>,
354    icon: Option<FileSource>,
355    header: Option<FileSource>,
356}
357
358impl EditMediaOptions {
359    #[must_use]
360    pub fn logo<P: AsRef<Path>>(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<P: AsRef<Path>>(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<P: AsRef<Path>>(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)]
403impl From<EditMediaOptions> 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}