1use std::ffi::OsStr;
3use std::path::Path;
4
5use mime::{APPLICATION_OCTET_STREAM, IMAGE_STAR};
6use url::Url;
7
8use crate::comments::Comments;
9use crate::file_source::FileSource;
10use crate::files::{FileRef, Files};
11use crate::metadata::Metadata;
12use crate::prelude::*;
13use crate::teams::Members;
14use crate::types::id::{FileId, GameId, ModId};
15
16pub use crate::types::mods::{
17 CommunityOptions, CreditOptions, Dependency, Event, EventType, Image, MaturityOption, Media,
18 Mod, Platform, Popularity, Ratings, Statistics, Tag, Visibility,
19};
20pub use crate::types::Logo;
21pub use crate::types::Status;
22
23#[derive(Clone)]
25pub struct Mods {
26 modio: Modio,
27 game: GameId,
28}
29
30impl Mods {
31 pub(crate) fn new(modio: Modio, game: GameId) -> Self {
32 Self { modio, game }
33 }
34
35 pub fn search(&self, filter: Filter) -> Query<Mod> {
39 let route = Route::GetMods { game_id: self.game };
40 Query::new(self.modio.clone(), route, filter)
41 }
42
43 pub fn get(&self, id: ModId) -> ModRef {
45 ModRef::new(self.modio.clone(), self.game, id)
46 }
47
48 #[allow(clippy::should_implement_trait)]
50 pub async fn add(self, options: AddModOptions) -> Result<Mod> {
51 let route = Route::AddMod { game_id: self.game };
52 self.modio
53 .request(route)
54 .multipart(Form::from(options))
55 .send()
56 .await
57 }
58
59 pub fn statistics(self, filter: Filter) -> Query<Statistics> {
63 let route = Route::GetModsStats { game_id: self.game };
64 Query::new(self.modio, route, filter)
65 }
66
67 pub fn events(self, filter: Filter) -> Query<Event> {
72 let route = Route::GetModsEvents { game_id: self.game };
73 Query::new(self.modio, route, filter)
74 }
75}
76
77#[derive(Clone)]
79pub struct ModRef {
80 modio: Modio,
81 game: GameId,
82 id: ModId,
83}
84
85impl ModRef {
86 pub(crate) fn new(modio: Modio, game: GameId, id: ModId) -> Self {
87 Self { modio, game, id }
88 }
89
90 pub async fn get(self) -> Result<Mod> {
92 let route = Route::GetMod {
93 game_id: self.game,
94 mod_id: self.id,
95 };
96 self.modio.request(route).send().await
97 }
98
99 pub fn files(&self) -> Files {
101 Files::new(self.modio.clone(), self.game, self.id)
102 }
103
104 pub fn file(&self, id: FileId) -> FileRef {
106 FileRef::new(self.modio.clone(), self.game, self.id, id)
107 }
108
109 pub fn metadata(&self) -> Metadata {
111 Metadata::new(self.modio.clone(), self.game, self.id)
112 }
113
114 pub fn tags(&self) -> Tags {
116 Tags::new(self.modio.clone(), self.game, self.id)
117 }
118
119 pub fn comments(&self) -> Comments {
121 Comments::new(self.modio.clone(), self.game, self.id)
122 }
123
124 pub fn dependencies(&self) -> Dependencies {
126 Dependencies::new(self.modio.clone(), self.game, self.id)
127 }
128
129 pub async fn statistics(self) -> Result<Statistics> {
131 let route = Route::GetModStats {
132 game_id: self.game,
133 mod_id: self.id,
134 };
135 self.modio.request(route).send().await
136 }
137
138 pub fn events(self, filter: Filter) -> Query<Event> {
142 let route = Route::GetModEvents {
143 game_id: self.game,
144 mod_id: self.id,
145 };
146 Query::new(self.modio, route, filter)
147 }
148
149 pub fn members(&self) -> Members {
151 Members::new(self.modio.clone(), self.game, self.id)
152 }
153
154 pub async fn edit(self, options: EditModOptions) -> Result<Editing<Mod>> {
156 let route = Route::EditMod {
157 game_id: self.game,
158 mod_id: self.id,
159 };
160 self.modio.request(route).form(&options).send().await
161 }
162
163 pub async fn delete(self) -> Result<()> {
165 let route = Route::DeleteMod {
166 game_id: self.game,
167 mod_id: self.id,
168 };
169 self.modio.request(route).send().await
170 }
171
172 pub async fn add_media(self, options: AddMediaOptions) -> Result<()> {
174 let route = Route::AddModMedia {
175 game_id: self.game,
176 mod_id: self.id,
177 };
178 self.modio
179 .request(route)
180 .multipart(Form::from(options))
181 .send::<Message>()
182 .await?;
183
184 Ok(())
185 }
186
187 pub async fn delete_media(self, options: DeleteMediaOptions) -> Result<Deletion> {
189 let route = Route::DeleteModMedia {
190 game_id: self.game,
191 mod_id: self.id,
192 };
193 self.modio.request(route).form(&options).send().await
194 }
195
196 pub async fn reorder_media(self, options: ReorderMediaOptions) -> Result<()> {
198 let route = Route::ReorderModMedia {
199 game_id: self.game,
200 mod_id: self.id,
201 };
202 self.modio.request(route).form(&options).send().await
203 }
204
205 pub async fn rate(self, rating: Rating) -> Result<()> {
207 let route = Route::RateMod {
208 game_id: self.game,
209 mod_id: self.id,
210 };
211 self.modio
212 .request(route)
213 .form(&rating)
214 .send::<Message>()
215 .await
216 .map(|_| ())
217 .or_else(|err| match (err.status(), err.error_ref()) {
218 (Some(StatusCode::BAD_REQUEST), Some(15028 | 15043)) => Ok(()),
219 _ => Err(err),
220 })
221 }
222
223 pub async fn subscribe(self) -> Result<()> {
225 let route = Route::SubscribeToMod {
226 game_id: self.game,
227 mod_id: self.id,
228 };
229 self.modio
230 .request(route)
231 .send::<Mod>()
232 .await
233 .map(|_| ())
234 .or_else(|err| match (err.status(), err.error_ref()) {
235 (Some(StatusCode::BAD_REQUEST), Some(15004)) => Ok(()),
236 _ => Err(err),
237 })
238 }
239
240 pub async fn unsubscribe(self) -> Result<()> {
242 let route = Route::UnsubscribeFromMod {
243 game_id: self.game,
244 mod_id: self.id,
245 };
246 self.modio.request(route).send().await.or_else(|err| {
247 match (err.status(), err.error_ref()) {
248 (Some(StatusCode::BAD_REQUEST), Some(15005)) => Ok(()),
249 _ => Err(err),
250 }
251 })
252 }
253}
254
255#[derive(Clone)]
257pub struct Dependencies {
258 modio: Modio,
259 game_id: GameId,
260 mod_id: ModId,
261}
262
263impl Dependencies {
264 fn new(modio: Modio, game_id: GameId, mod_id: ModId) -> Self {
265 Self {
266 modio,
267 game_id,
268 mod_id,
269 }
270 }
271
272 pub async fn list(self) -> Result<Vec<Dependency>> {
274 let route = Route::GetModDependencies {
275 game_id: self.game_id,
276 mod_id: self.mod_id,
277 };
278 Query::new(self.modio, route, Filter::default())
279 .collect()
280 .await
281 }
282
283 #[allow(clippy::iter_not_returning_iterator)]
285 pub async fn iter(self) -> Result<impl Stream<Item = Result<Dependency>>> {
286 let route = Route::GetModDependencies {
287 game_id: self.game_id,
288 mod_id: self.mod_id,
289 };
290 let filter = Filter::default();
291 Query::new(self.modio, route, filter).iter().await
292 }
293
294 #[allow(clippy::should_implement_trait)]
296 pub async fn add(self, options: EditDependenciesOptions) -> Result<()> {
297 let route = Route::AddModDependencies {
298 game_id: self.game_id,
299 mod_id: self.mod_id,
300 };
301 self.modio
302 .request(route)
303 .form(&options)
304 .send::<Message>()
305 .await?;
306 Ok(())
307 }
308
309 pub async fn delete(self, options: EditDependenciesOptions) -> Result<Deletion> {
311 let route = Route::DeleteModDependencies {
312 game_id: self.game_id,
313 mod_id: self.mod_id,
314 };
315 self.modio.request(route).form(&options).send().await
316 }
317}
318
319#[derive(Clone)]
321pub struct Tags {
322 modio: Modio,
323 game_id: GameId,
324 mod_id: ModId,
325}
326
327impl Tags {
328 fn new(modio: Modio, game_id: GameId, mod_id: ModId) -> Self {
329 Self {
330 modio,
331 game_id,
332 mod_id,
333 }
334 }
335
336 pub async fn list(self) -> Result<Vec<Tag>> {
338 let route = Route::GetModTags {
339 game_id: self.game_id,
340 mod_id: self.mod_id,
341 };
342 Query::new(self.modio, route, Filter::default())
343 .collect()
344 .await
345 }
346
347 #[allow(clippy::iter_not_returning_iterator)]
349 pub async fn iter(self) -> Result<impl Stream<Item = Result<Tag>>> {
350 let route = Route::GetModTags {
351 game_id: self.game_id,
352 mod_id: self.mod_id,
353 };
354 let filter = Filter::default();
355 Query::new(self.modio, route, filter).iter().await
356 }
357
358 #[allow(clippy::should_implement_trait)]
360 pub async fn add(self, options: EditTagsOptions) -> Result<()> {
361 let route = Route::AddModTags {
362 game_id: self.game_id,
363 mod_id: self.mod_id,
364 };
365 self.modio
366 .request(route)
367 .form(&options)
368 .send::<Message>()
369 .await?;
370 Ok(())
371 }
372
373 pub async fn delete(self, options: EditTagsOptions) -> Result<Deletion> {
375 let route = Route::DeleteModTags {
376 game_id: self.game_id,
377 mod_id: self.mod_id,
378 };
379 self.modio.request(route).form(&options).send().await
380 }
381}
382
383#[rustfmt::skip]
431pub mod filters {
432 #[doc(inline)]
433 pub use crate::filter::prelude::Fulltext;
434 #[doc(inline)]
435 pub use crate::filter::prelude::Id;
436 #[doc(inline)]
437 pub use crate::filter::prelude::Name;
438 #[doc(inline)]
439 pub use crate::filter::prelude::NameId;
440 #[doc(inline)]
441 pub use crate::filter::prelude::Status;
442 #[doc(inline)]
443 pub use crate::filter::prelude::DateAdded;
444 #[doc(inline)]
445 pub use crate::filter::prelude::DateUpdated;
446 #[doc(inline)]
447 pub use crate::filter::prelude::DateLive;
448 #[doc(inline)]
449 pub use crate::filter::prelude::SubmittedBy;
450
451 filter!(GameId, GAME_ID, "game_id", Eq, NotEq, In, Cmp, OrderBy);
452 filter!(Visible, VISIBLE, "visible", Eq);
453 filter!(MaturityOption, MATURITY_OPTION, "maturity_option", Eq, Cmp, Bit);
454 filter!(Summary, SUMMARY, "summary", Like);
455 filter!(Description, DESCRIPTION, "description", Like);
456 filter!(Homepage, HOMEPAGE, "homepage_url", Eq, NotEq, Like, In);
457 filter!(Modfile, MODFILE, "modfile", Eq, NotEq, In, Cmp);
458 filter!(MetadataBlob, METADATA_BLOB, "metadata_blob", Eq, NotEq, Like);
459 filter!(MetadataKVP, METADATA_KVP, "metadata_kvp", Eq, NotEq, Like);
460 filter!(Tags, TAGS, "tags", Eq, NotEq, Like, In);
461
462 filter!(Downloads, DOWNLOADS, "downloads", OrderBy);
463 filter!(Popular, POPULAR, "popular", OrderBy);
464 filter!(Ratings, RATINGS, "ratings", OrderBy);
465 filter!(Subscribers, SUBSCRIBERS, "subscribers", OrderBy);
466
467 pub mod events {
494 #[doc(inline)]
495 pub use crate::filter::prelude::Id;
496 #[doc(inline)]
497 pub use crate::filter::prelude::ModId;
498 #[doc(inline)]
499 pub use crate::filter::prelude::DateAdded;
500
501 filter!(UserId, USER_ID, "user_id", Eq, NotEq, In, Cmp, OrderBy);
502 filter!(EventType, EVENT_TYPE, "event_type", Eq, NotEq, In, OrderBy);
503 }
504
505 pub mod stats {
532 #[doc(inline)]
533 pub use crate::filter::prelude::ModId;
534
535 filter!(Popularity, POPULARITY, "popularity_rank_position", Eq, NotEq, In, Cmp, OrderBy);
536 filter!(Downloads, DOWNLOADS, "downloads_total", Eq, NotEq, In, Cmp, OrderBy);
537 filter!(Subscribers, SUBSCRIBERS, "subscribers_total", Eq, NotEq, In, Cmp, OrderBy);
538 filter!(RatingsPositive, RATINGS_POSITIVE, "ratings_positive", Eq, NotEq, In, Cmp, OrderBy);
539 filter!(RatingsNegative, RATINGS_NEGATIVE, "ratings_negative", Eq, NotEq, In, Cmp, OrderBy);
540 }
541}
542
543#[derive(Clone, Copy)]
544pub enum Rating {
545 Positive,
546 Negative,
547 None,
548}
549
550#[doc(hidden)]
551impl serde::ser::Serialize for Rating {
552 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
553 where
554 S: serde::ser::Serializer,
555 {
556 use serde::ser::SerializeMap;
557
558 let mut map = serializer.serialize_map(Some(1))?;
559 match self {
560 Rating::Negative => map.serialize_entry("rating", "-1")?,
561 Rating::Positive => map.serialize_entry("rating", "1")?,
562 Rating::None => map.serialize_entry("rating", "0")?,
563 }
564 map.end()
565 }
566}
567
568pub struct AddModOptions {
569 visible: Option<Visibility>,
570 logo: FileSource,
571 name: String,
572 name_id: Option<String>,
573 summary: String,
574 description: Option<String>,
575 homepage_url: Option<Url>,
576 stock: Option<u32>,
577 maturity_option: Option<MaturityOption>,
578 community_options: Option<CommunityOptions>,
579 credit_options: Option<CreditOptions>,
580 metadata_blob: Option<String>,
581 tags: Option<Vec<String>>,
582}
583
584impl AddModOptions {
585 pub fn new<T, P>(name: T, logo: P, summary: T) -> AddModOptions
586 where
587 T: Into<String>,
588 P: AsRef<Path>,
589 {
590 let filename = logo
591 .as_ref()
592 .file_name()
593 .and_then(OsStr::to_str)
594 .map_or_else(String::new, ToString::to_string);
595
596 let logo = FileSource::new_from_file(logo, filename, IMAGE_STAR);
597
598 AddModOptions {
599 name: name.into(),
600 logo,
601 summary: summary.into(),
602 visible: None,
603 name_id: None,
604 description: None,
605 homepage_url: None,
606 stock: None,
607 maturity_option: None,
608 community_options: None,
609 credit_options: None,
610 metadata_blob: None,
611 tags: None,
612 }
613 }
614
615 #[must_use]
616 pub fn visible(self, v: bool) -> Self {
617 Self {
618 visible: if v {
619 Some(Visibility::PUBLIC)
620 } else {
621 Some(Visibility::HIDDEN)
622 },
623 ..self
624 }
625 }
626
627 option!(name_id);
628 option!(description);
629 option!(homepage_url: Url);
630 option!(stock: u32);
631 option!(maturity_option: MaturityOption);
632 option!(community_options: CommunityOptions);
633 option!(credit_options: CreditOptions);
634 option!(metadata_blob);
635
636 #[must_use]
637 pub fn tags(self, tags: &[String]) -> Self {
638 Self {
639 tags: Some(tags.to_vec()),
640 ..self
641 }
642 }
643}
644
645#[doc(hidden)]
646impl From<AddModOptions> for Form {
647 fn from(opts: AddModOptions) -> Form {
648 let mut form = Form::new();
649
650 form = form.text("name", opts.name).text("summary", opts.summary);
651
652 if let Some(visible) = opts.visible {
653 form = form.text("visible", visible.to_string());
654 }
655 if let Some(name_id) = opts.name_id {
656 form = form.text("name_id", name_id);
657 }
658 if let Some(desc) = opts.description {
659 form = form.text("description", desc);
660 }
661 if let Some(url) = opts.homepage_url {
662 form = form.text("homepage_url", url.to_string());
663 }
664 if let Some(stock) = opts.stock {
665 form = form.text("stock", stock.to_string());
666 }
667 if let Some(maturity_option) = opts.maturity_option {
668 form = form.text("maturity_option", maturity_option.to_string());
669 }
670 if let Some(community_options) = opts.community_options {
671 form = form.text("community_options", community_options.to_string());
672 }
673 if let Some(credit_options) = opts.credit_options {
674 form = form.text("credit_options", credit_options.to_string());
675 }
676 if let Some(metadata_blob) = opts.metadata_blob {
677 form = form.text("metadata_blob", metadata_blob);
678 }
679 if let Some(tags) = opts.tags {
680 for tag in tags {
681 form = form.text("tags[]", tag);
682 }
683 }
684 form.part("logo", opts.logo.into())
685 }
686}
687
688#[derive(Debug, Default)]
689pub struct EditModOptions {
690 params: std::collections::BTreeMap<&'static str, String>,
691}
692
693impl EditModOptions {
694 option!(status: Status >> "status");
695
696 #[must_use]
697 pub fn visible(self, v: bool) -> Self {
698 let value = if v {
699 Visibility::PUBLIC
700 } else {
701 Visibility::HIDDEN
702 };
703 let mut params = self.params;
704 params.insert("visible", value.to_string());
705 Self { params }
706 }
707
708 option!(visibility: Visibility >> "visible");
709 option!(name >> "name");
710 option!(name_id >> "name_id");
711 option!(summary >> "summary");
712 option!(description >> "description");
713 option!(homepage_url: Url >> "homepage_url");
714 option!(stock >> "stock");
715 option!(maturity_option: MaturityOption >> "maturity_option");
716 option!(community_options: CommunityOptions >> "community_options");
717 option!(credit_options: CreditOptions >> "credit_options");
718 option!(metadata_blob >> "metadata_blob");
719}
720
721impl_serialize_params!(EditModOptions >> params);
722
723pub struct EditDependenciesOptions {
724 dependencies: Vec<ModId>,
725}
726
727impl EditDependenciesOptions {
728 pub fn new(dependencies: &[ModId]) -> Self {
729 Self {
730 dependencies: dependencies.to_vec(),
731 }
732 }
733
734 pub fn one(dependency: ModId) -> Self {
735 Self {
736 dependencies: vec![dependency],
737 }
738 }
739}
740
741#[doc(hidden)]
742impl serde::ser::Serialize for EditDependenciesOptions {
743 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
744 where
745 S: serde::ser::Serializer,
746 {
747 use serde::ser::SerializeMap;
748
749 let mut map = serializer.serialize_map(Some(self.dependencies.len()))?;
750 for d in &self.dependencies {
751 map.serialize_entry("dependencies[]", d)?;
752 }
753 map.end()
754 }
755}
756
757pub struct EditTagsOptions {
758 tags: Vec<String>,
759}
760
761impl EditTagsOptions {
762 pub fn new(tags: &[String]) -> Self {
763 Self {
764 tags: tags.to_vec(),
765 }
766 }
767}
768
769#[doc(hidden)]
770impl serde::ser::Serialize for EditTagsOptions {
771 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
772 where
773 S: serde::ser::Serializer,
774 {
775 use serde::ser::SerializeMap;
776
777 let mut map = serializer.serialize_map(Some(self.tags.len()))?;
778 for t in &self.tags {
779 map.serialize_entry("tags[]", t)?;
780 }
781 map.end()
782 }
783}
784
785#[derive(Default)]
786pub struct AddMediaOptions {
787 sync: Option<bool>,
788 logo: Option<FileSource>,
789 images_zip: Option<FileSource>,
790 images: Option<Vec<FileSource>>,
791 youtube: Option<Vec<String>>,
792 sketchfab: Option<Vec<String>>,
793}
794
795impl AddMediaOptions {
796 #[must_use]
797 pub fn sync(self, value: bool) -> Self {
798 Self {
799 sync: Some(value),
800 ..self
801 }
802 }
803
804 #[must_use]
805 pub fn logo<P: AsRef<Path>>(self, logo: P) -> Self {
806 let logo = logo.as_ref();
807 let filename = logo
808 .file_name()
809 .and_then(OsStr::to_str)
810 .map_or_else(String::new, ToString::to_string);
811
812 Self {
813 logo: Some(FileSource::new_from_file(logo, filename, IMAGE_STAR)),
814 ..self
815 }
816 }
817
818 #[must_use]
819 pub fn images_zip<P: AsRef<Path>>(self, images: P) -> Self {
820 Self {
821 images_zip: Some(FileSource::new_from_file(
822 images,
823 "images.zip".into(),
824 APPLICATION_OCTET_STREAM,
825 )),
826 ..self
827 }
828 }
829
830 #[must_use]
831 pub fn images<P: AsRef<Path>>(self, images: &[P]) -> Self {
832 Self {
833 images: Some(
834 images
835 .iter()
836 .map(|p| {
837 let file = p.as_ref();
838 let filename = file
839 .file_name()
840 .and_then(OsStr::to_str)
841 .map_or_else(String::new, ToString::to_string);
842
843 FileSource::new_from_file(file, filename, IMAGE_STAR)
844 })
845 .collect(),
846 ),
847 ..self
848 }
849 }
850
851 #[must_use]
852 pub fn youtube(self, urls: &[String]) -> Self {
853 Self {
854 youtube: Some(urls.to_vec()),
855 ..self
856 }
857 }
858
859 #[must_use]
860 pub fn sketchfab(self, urls: &[String]) -> Self {
861 Self {
862 sketchfab: Some(urls.to_vec()),
863 ..self
864 }
865 }
866}
867
868#[doc(hidden)]
869impl From<AddMediaOptions> for Form {
870 fn from(opts: AddMediaOptions) -> Form {
871 let mut form = Form::new();
872 if let Some(sync) = opts.sync {
873 form = form.text("sync", sync.to_string());
874 }
875 if let Some(logo) = opts.logo {
876 form = form.part("logo", logo.into());
877 }
878 if let Some(zip) = opts.images_zip {
879 form = form.part("images", zip.into());
880 }
881 if let Some(images) = opts.images {
882 for (i, image) in images.into_iter().enumerate() {
883 form = form.part(format!("image{i}"), image.into());
884 }
885 }
886 if let Some(youtube) = opts.youtube {
887 for url in youtube {
888 form = form.text("youtube[]", url);
889 }
890 }
891 if let Some(sketchfab) = opts.sketchfab {
892 for url in sketchfab {
893 form = form.text("sketchfab[]", url);
894 }
895 }
896 form
897 }
898}
899
900#[derive(Default)]
901pub struct DeleteMediaOptions {
902 images: Option<Vec<String>>,
903 youtube: Option<Vec<String>>,
904 sketchfab: Option<Vec<String>>,
905}
906
907impl DeleteMediaOptions {
908 #[must_use]
909 pub fn images(self, images: &[String]) -> Self {
910 Self {
911 images: Some(images.to_vec()),
912 ..self
913 }
914 }
915
916 #[must_use]
917 pub fn youtube(self, urls: &[String]) -> Self {
918 Self {
919 youtube: Some(urls.to_vec()),
920 ..self
921 }
922 }
923
924 #[must_use]
925 pub fn sketchfab(self, urls: &[String]) -> Self {
926 Self {
927 sketchfab: Some(urls.to_vec()),
928 ..self
929 }
930 }
931}
932
933#[doc(hidden)]
934impl serde::ser::Serialize for DeleteMediaOptions {
935 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
936 where
937 S: serde::ser::Serializer,
938 {
939 use serde::ser::SerializeMap;
940
941 let len = self.images.as_ref().map(Vec::len).unwrap_or_default()
942 + self.youtube.as_ref().map(Vec::len).unwrap_or_default()
943 + self.sketchfab.as_ref().map(Vec::len).unwrap_or_default();
944 let mut map = serializer.serialize_map(Some(len))?;
945 if let Some(ref images) = self.images {
946 for e in images {
947 map.serialize_entry("images[]", e)?;
948 }
949 }
950 if let Some(ref urls) = self.youtube {
951 for e in urls {
952 map.serialize_entry("youtube[]", e)?;
953 }
954 }
955 if let Some(ref urls) = self.sketchfab {
956 for e in urls {
957 map.serialize_entry("sketchfab[]", e)?;
958 }
959 }
960 map.end()
961 }
962}
963
964#[derive(Default)]
965pub struct ReorderMediaOptions {
966 images: Option<Vec<String>>,
967 youtube: Option<Vec<String>>,
968 sketchfab: Option<Vec<String>>,
969}
970
971impl ReorderMediaOptions {
972 #[must_use]
973 pub fn images(self, images: &[String]) -> Self {
974 Self {
975 images: Some(images.to_vec()),
976 ..self
977 }
978 }
979
980 #[must_use]
981 pub fn youtube(self, urls: &[String]) -> Self {
982 Self {
983 youtube: Some(urls.to_vec()),
984 ..self
985 }
986 }
987
988 #[must_use]
989 pub fn sketchfab(self, urls: &[String]) -> Self {
990 Self {
991 sketchfab: Some(urls.to_vec()),
992 ..self
993 }
994 }
995}
996
997#[doc(hidden)]
998impl serde::ser::Serialize for ReorderMediaOptions {
999 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
1000 where
1001 S: serde::ser::Serializer,
1002 {
1003 use serde::ser::SerializeMap;
1004
1005 let len = self.images.as_ref().map(Vec::len).unwrap_or_default()
1006 + self.youtube.as_ref().map(Vec::len).unwrap_or_default()
1007 + self.sketchfab.as_ref().map(Vec::len).unwrap_or_default();
1008 let mut map = serializer.serialize_map(Some(len))?;
1009 if let Some(ref images) = self.images {
1010 for e in images {
1011 map.serialize_entry("images[]", e)?;
1012 }
1013 }
1014 if let Some(ref urls) = self.youtube {
1015 for e in urls {
1016 map.serialize_entry("youtube[]", e)?;
1017 }
1018 }
1019 if let Some(ref urls) = self.sketchfab {
1020 for e in urls {
1021 map.serialize_entry("sketchfab[]", e)?;
1022 }
1023 }
1024 map.end()
1025 }
1026}