mangadex_api_schema_rust/
v5.rs

1pub mod api_client;
2pub mod at_home_server;
3pub mod auth_tokens;
4pub mod author;
5pub mod chapter;
6pub mod check_token_response;
7pub mod check_username_available;
8pub mod cover;
9pub mod custom_list;
10pub mod error;
11mod exports_types;
12pub mod forum_thread;
13pub mod is_following_response;
14pub mod legacy_id_mapping;
15pub mod login_response;
16pub mod manga;
17pub mod manga_aggregate;
18pub mod manga_links;
19pub mod manga_read_markers;
20pub mod manga_reading_status;
21pub mod manga_reading_statuses;
22pub mod manga_relation;
23pub mod oauth;
24pub mod ratings;
25pub mod refresh_token_response;
26pub mod report;
27pub mod scanlation_group;
28pub mod settings_template;
29pub mod statistics;
30pub mod tag;
31pub mod upload_required_approval;
32pub mod upload_session;
33pub mod upload_session_file;
34pub mod user;
35pub mod user_history;
36pub mod user_report;
37pub mod user_settings;
38
39pub use self::exports_types::*;
40use std::collections::HashMap;
41
42use mangadex_api_types as types;
43use serde::Deserialize;
44use uuid::Uuid;
45
46use types::{
47    Language, MangaDexDateTime, MangaRelation, RelationshipType, ResponseType, ResultType,
48};
49
50use crate::FromResponse;
51pub(crate) use crate::{ApiObject, ApiObjectNoRelationships};
52use types::error::RelationshipConversionError;
53
54// TODO: Find a way to reduce the boilerplate for this.
55// `struct-variant` (https://docs.rs/struct-variant) is a potential candidate for this.
56#[derive(Debug, Deserialize, Clone)]
57#[allow(clippy::large_enum_variant)]
58#[serde(untagged)]
59#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
60#[cfg_attr(feature = "specta", derive(specta::Type))]
61pub enum RelatedAttributes {
62    /// Manga resource.
63    Manga(MangaAttributes),
64    /// Chapter resource.
65    Chapter(ChapterAttributes),
66    /// A Cover Art for a manga.
67    ///
68    /// On manga resources, only one cover art resource relation is returned,
69    /// marking the primary cover if there are more than one. By default, this will be the latest
70    /// volume's cover art. To see all the covers for a given manga, use the cover search endpoint.
71    CoverArt(CoverAttributes),
72    /// Author resource.
73    Author(AuthorAttributes),
74    /// ScanlationGroup resource.
75    ScanlationGroup(ScanlationGroupAttributes),
76    /// Tag resource.
77    Tag(TagAttributes),
78    /// User resource.
79    User(UserAttributes),
80    /// CustomList resource.
81    CustomList(CustomListAttributes),
82}
83
84impl TryFrom<Relationship> for ApiObjectNoRelationships<MangaAttributes> {
85    type Error = RelationshipConversionError;
86
87    fn try_from(value: Relationship) -> Result<Self, Self::Error> {
88        if value.type_ != RelationshipType::Manga {
89            return Err(RelationshipConversionError::InvalidInputRelationshipType {
90                input: RelationshipType::Manga,
91                inner: value.type_,
92            });
93        }
94        if let Some(RelatedAttributes::Manga(attributes)) = value.attributes {
95            Ok(Self {
96                id: value.id,
97                type_: RelationshipType::Manga,
98                attributes,
99            })
100        } else {
101            Err(RelationshipConversionError::AttributesNotFound(
102                RelationshipType::Manga,
103            ))
104        }
105    }
106}
107
108impl TryFrom<Relationship> for ApiObjectNoRelationships<ChapterAttributes> {
109    type Error = RelationshipConversionError;
110
111    fn try_from(value: Relationship) -> Result<Self, Self::Error> {
112        if value.type_ != RelationshipType::Chapter {
113            return Err(RelationshipConversionError::InvalidInputRelationshipType {
114                input: RelationshipType::Chapter,
115                inner: value.type_,
116            });
117        }
118        if let Some(RelatedAttributes::Chapter(attributes)) = value.attributes {
119            Ok(Self {
120                id: value.id,
121                type_: RelationshipType::Chapter,
122                attributes,
123            })
124        } else {
125            Err(RelationshipConversionError::AttributesNotFound(
126                RelationshipType::Chapter,
127            ))
128        }
129    }
130}
131
132impl TryFrom<Relationship> for ApiObjectNoRelationships<CoverAttributes> {
133    type Error = RelationshipConversionError;
134
135    fn try_from(value: Relationship) -> Result<Self, Self::Error> {
136        if value.type_ != RelationshipType::CoverArt {
137            return Err(RelationshipConversionError::InvalidInputRelationshipType {
138                input: RelationshipType::CoverArt,
139                inner: value.type_,
140            });
141        }
142        if let Some(RelatedAttributes::CoverArt(attributes)) = value.attributes {
143            Ok(Self {
144                id: value.id,
145                type_: RelationshipType::CustomList,
146                attributes,
147            })
148        } else {
149            Err(RelationshipConversionError::AttributesNotFound(
150                RelationshipType::CoverArt,
151            ))
152        }
153    }
154}
155
156impl TryFrom<Relationship> for ApiObjectNoRelationships<AuthorAttributes> {
157    type Error = RelationshipConversionError;
158
159    fn try_from(value: Relationship) -> Result<Self, Self::Error> {
160        if !(value.type_ == RelationshipType::Author || value.type_ == RelationshipType::Artist) {
161            return Err(RelationshipConversionError::InvalidInputRelationshipType {
162                input: RelationshipType::Author,
163                inner: value.type_,
164            });
165        }
166        if let Some(RelatedAttributes::Author(attributes)) = value.attributes {
167            Ok(Self {
168                id: value.id,
169                type_: RelationshipType::Author,
170                attributes,
171            })
172        } else {
173            Err(RelationshipConversionError::AttributesNotFound(
174                RelationshipType::Author,
175            ))
176        }
177    }
178}
179
180impl TryFrom<Relationship> for ApiObjectNoRelationships<ScanlationGroupAttributes> {
181    type Error = RelationshipConversionError;
182
183    fn try_from(value: Relationship) -> Result<Self, Self::Error> {
184        if value.type_ != RelationshipType::ScanlationGroup {
185            return Err(RelationshipConversionError::InvalidInputRelationshipType {
186                input: RelationshipType::ScanlationGroup,
187                inner: value.type_,
188            });
189        }
190        if let Some(RelatedAttributes::ScanlationGroup(attributes)) = value.attributes {
191            Ok(Self {
192                id: value.id,
193                type_: RelationshipType::ScanlationGroup,
194                attributes,
195            })
196        } else {
197            Err(RelationshipConversionError::AttributesNotFound(
198                RelationshipType::ScanlationGroup,
199            ))
200        }
201    }
202}
203
204impl TryFrom<Relationship> for ApiObjectNoRelationships<TagAttributes> {
205    type Error = RelationshipConversionError;
206
207    fn try_from(value: Relationship) -> Result<Self, Self::Error> {
208        if value.type_ != RelationshipType::Tag {
209            return Err(RelationshipConversionError::InvalidInputRelationshipType {
210                input: RelationshipType::Tag,
211                inner: value.type_,
212            });
213        }
214        if let Some(RelatedAttributes::Tag(attributes)) = value.attributes {
215            Ok(Self {
216                id: value.id,
217                type_: RelationshipType::Tag,
218                attributes,
219            })
220        } else {
221            Err(RelationshipConversionError::AttributesNotFound(
222                RelationshipType::Tag,
223            ))
224        }
225    }
226}
227
228impl TryFrom<Relationship> for ApiObjectNoRelationships<UserAttributes> {
229    type Error = RelationshipConversionError;
230
231    fn try_from(value: Relationship) -> Result<Self, Self::Error> {
232        if !(value.type_ == RelationshipType::User
233            || value.type_ == RelationshipType::Member
234            || value.type_ == RelationshipType::Leader)
235        {
236            return Err(RelationshipConversionError::InvalidInputRelationshipType {
237                input: RelationshipType::User,
238                inner: value.type_,
239            });
240        }
241        if let Some(RelatedAttributes::User(attributes)) = value.attributes {
242            Ok(Self {
243                id: value.id,
244                type_: RelationshipType::User,
245                attributes,
246            })
247        } else {
248            Err(RelationshipConversionError::AttributesNotFound(
249                RelationshipType::User,
250            ))
251        }
252    }
253}
254
255impl TryFrom<Relationship> for ApiObjectNoRelationships<CustomListAttributes> {
256    type Error = RelationshipConversionError;
257
258    fn try_from(value: Relationship) -> Result<Self, Self::Error> {
259        if value.type_ != RelationshipType::CustomList {
260            return Err(RelationshipConversionError::InvalidInputRelationshipType {
261                input: RelationshipType::CustomList,
262                inner: value.type_,
263            });
264        }
265        if let Some(RelatedAttributes::CustomList(attributes)) = value.attributes {
266            Ok(Self {
267                id: value.id,
268                type_: RelationshipType::CustomList,
269                attributes,
270            })
271        } else {
272            Err(RelationshipConversionError::AttributesNotFound(
273                RelationshipType::CustomList,
274            ))
275        }
276    }
277}
278
279#[derive(Debug, Deserialize, Clone)]
280#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
281#[cfg_attr(feature = "specta", derive(specta::Type))]
282pub struct Relationship {
283    pub id: Uuid,
284    #[serde(rename = "type")]
285    pub type_: RelationshipType,
286    /// Related Manga type.
287    ///
288    /// <https://api.mangadex.org/docs/static-data/#manga-related-enum>
289    ///
290    /// This is only present for a Manga entity and a Manga relationship.
291    #[serde(skip_serializing_if = "Option::is_none")]
292    #[serde(default)]
293    pub related: Option<MangaRelation>,
294    /// Contains object attributes for the type.
295    ///
296    /// Present if [Reference Expansion](https://api.mangadex.org/docs/reference-expansion/) is applied.
297    #[serde(skip_serializing_if = "Option::is_none")]
298    #[serde(default)]
299    pub attributes: Option<RelatedAttributes>,
300}
301
302#[derive(Debug, Deserialize, Clone)]
303#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
304#[cfg_attr(feature = "specta", derive(specta::Type))]
305pub struct Results<T> {
306    #[serde(default)]
307    pub result: ResultType,
308    pub response: ResponseType,
309    pub data: Vec<T>,
310    pub limit: u32,
311    pub offset: u32,
312    pub total: u32,
313}
314
315impl<T> FromResponse for Results<T> {
316    type Response = Self;
317    fn from_response(res: Self::Response) -> Self {
318        res
319    }
320}
321
322pub type LocalizedString = HashMap<Language, String>;
323
324/// Originally a Deserializer helper to handle JSON array or object types.
325///
326/// MangaDex currently returns an empty array when the localized string field isn't present.
327///
328/// The Serializer was added in 0.2.0 for pratical and necessities reason
329pub(crate) mod localizedstring_array_or_map {
330    use std::collections::HashMap;
331
332    use super::LocalizedString;
333    use serde::de::{Deserialize, Deserializer, MapAccess, SeqAccess, Visitor};
334    #[cfg(feature = "serialize")]
335    use serde::ser::{Serialize, Serializer};
336
337    pub fn deserialize<'de, D>(deserializer: D) -> Result<LocalizedString, D::Error>
338    where
339        D: Deserializer<'de>,
340    {
341        struct V;
342
343        impl<'de> Visitor<'de> for V {
344            type Value = LocalizedString;
345
346            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
347                formatter.write_str("array or object")
348            }
349
350            fn visit_seq<A>(self, mut _seq: A) -> Result<Self::Value, A::Error>
351            where
352                A: SeqAccess<'de>,
353            {
354                Ok(HashMap::new())
355            }
356
357            fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
358            where
359                A: MapAccess<'de>,
360            {
361                let de = serde::de::value::MapAccessDeserializer::new(map);
362                let helper = LocalizedString::deserialize(de)?;
363                Ok(helper)
364            }
365        }
366
367        deserializer.deserialize_any(V)
368    }
369    #[cfg(feature = "serialize")]
370    pub fn serialize<S>(to_use: &LocalizedString, serializer: S) -> Result<S::Ok, S::Error>
371    where
372        S: Serializer,
373    {
374        to_use.serialize(serializer)
375    }
376}
377
378/// Originally a Deserializer helper to handle JSON array or object types.
379///
380/// MangaDex sometimes returns an array instead of a JSON object for the volume aggregate field.
381///
382/// The Serializer was added in 0.2.0 for pratical and necessities reason
383pub(crate) mod volume_aggregate_array_or_map {
384    use super::manga_aggregate::VolumeAggregate;
385    use serde::de::{Deserializer, MapAccess, SeqAccess, Visitor};
386    #[cfg(feature = "serialize")]
387    use serde::ser::Serializer;
388    #[cfg(feature = "serialize")]
389    use serde::Serialize;
390    use std::collections::BTreeMap;
391    #[cfg(feature = "serialize")]
392    use std::collections::HashMap;
393
394    type VolumeAggregateCollection = Vec<VolumeAggregate>;
395
396    const PAD_WIDTH: usize = 5;
397
398    pub fn deserialize<'de, D>(deserializer: D) -> Result<VolumeAggregateCollection, D::Error>
399    where
400        D: Deserializer<'de>,
401    {
402        struct V;
403
404        impl<'de> Visitor<'de> for V {
405            type Value = VolumeAggregateCollection;
406
407            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
408                formatter.write_str("array or object")
409            }
410
411            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
412            where
413                A: SeqAccess<'de>,
414            {
415                let mut volumes = Vec::new();
416
417                while let Some(volume) = seq.next_element::<VolumeAggregate>()? {
418                    volumes.push(volume);
419                }
420
421                Ok(volumes)
422            }
423
424            fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
425            where
426                M: MapAccess<'de>,
427            {
428                // Temporary collection to sort the results because serde doesn't seem to iterate
429                // through the map in the order they appear.
430                let mut sorting_map = BTreeMap::new();
431
432                while let Some((volume_number, volume)) =
433                    map.next_entry::<String, VolumeAggregate>()?
434                {
435                    let volume_number = if volume_number.contains('.') {
436                        match volume_number.parse::<f64>() {
437                            Ok(_) => {
438                                //
439                                let (i, f) = volume_number.split_once('.').unwrap();
440                                let i = i.parse::<i32>().unwrap();
441                                // Pad the whole number part so that it is sorted correctly with the
442                                // other keys.
443                                format!("{i:0PAD_WIDTH$}.{f}")
444                            }
445                            Err(_) => volume_number,
446                        }
447                    } else {
448                        match volume_number.parse::<i32>() {
449                            Ok(n) => format!("{n:0PAD_WIDTH$}"),
450                            Err(_) => volume_number,
451                        }
452                    };
453                    sorting_map.insert(volume_number, volume);
454                }
455
456                Ok(sorting_map.values().cloned().collect())
457            }
458        }
459
460        deserializer.deserialize_any(V)
461    }
462    #[cfg(feature = "serialize")]
463    #[allow(dead_code)]
464    pub fn serialize<S>(
465        to_use: &VolumeAggregateCollection,
466        serializer: S,
467    ) -> Result<S::Ok, S::Error>
468    where
469        S: Serializer,
470    {
471        use super::manga_aggregate::VolumeAggregateSer;
472
473        let mut volumes: HashMap<String, VolumeAggregateSer> = HashMap::new();
474        for volume in to_use {
475            volumes.insert(volume.volume.clone(), Into::into(volume.clone()));
476        }
477        volumes.serialize(serializer)
478    }
479}
480
481/// Originally a Deserializer helper to handle JSON array or object types.
482///
483/// MangaDex sometimes returns an array instead of a JSON object for the chapter aggregate field.
484///
485/// The Serializer was added in 0.2.0 for pratical and necessities reason
486pub(crate) mod chapter_aggregate_array_or_map {
487    use serde::de::{Deserializer, MapAccess, SeqAccess, Visitor};
488    #[cfg(feature = "serialize")]
489    use serde::ser::Serializer;
490    #[cfg(feature = "serialize")]
491    use serde::Serialize;
492    use std::collections::BTreeMap;
493
494    use super::manga_aggregate::ChapterAggregate;
495
496    const PAD_WIDTH: usize = 5;
497
498    type ChapterAggregateCollection = Vec<ChapterAggregate>;
499
500    pub fn deserialize<'de, D>(deserializer: D) -> Result<ChapterAggregateCollection, D::Error>
501    where
502        D: Deserializer<'de>,
503    {
504        struct V;
505
506        impl<'de> Visitor<'de> for V {
507            type Value = ChapterAggregateCollection;
508
509            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
510                formatter.write_str("array or object")
511            }
512
513            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
514            where
515                A: SeqAccess<'de>,
516            {
517                let mut chapters = Vec::new();
518
519                while let Some(chapter) = seq.next_element::<ChapterAggregate>()? {
520                    chapters.push(chapter);
521                }
522
523                Ok(chapters)
524            }
525
526            fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
527            where
528                M: MapAccess<'de>,
529            {
530                // Temporary collection to sort the results because serde doesn't seem to iterate
531                // through the map in the order they appear.
532                let mut sorting_map = BTreeMap::new();
533
534                while let Some((chapter_number, chapter)) =
535                    map.next_entry::<String, ChapterAggregate>()?
536                {
537                    let chapter_number = if chapter_number.contains('.') {
538                        match chapter_number.parse::<f64>() {
539                            Ok(_) => {
540                                //
541                                let (i, f) = chapter_number.split_once('.').unwrap();
542                                let i = i.parse::<i32>().unwrap();
543                                // Pad the whole number part so that it is sorted correctly with the
544                                // other keys.
545                                format!("{i:0PAD_WIDTH$}.{f}")
546                            }
547                            Err(_) => chapter_number,
548                        }
549                    } else {
550                        match chapter_number.parse::<i32>() {
551                            Ok(n) => format!("{n:0PAD_WIDTH$}"),
552                            Err(_) => chapter_number,
553                        }
554                    };
555                    sorting_map.insert(chapter_number, chapter);
556                }
557
558                Ok(sorting_map.values().cloned().collect())
559            }
560        }
561
562        deserializer.deserialize_any(V)
563    }
564    #[cfg(feature = "serialize")]
565    pub fn serialize<S>(
566        to_use: &ChapterAggregateCollection,
567        serializer: S,
568    ) -> Result<S::Ok, S::Error>
569    where
570        S: Serializer,
571    {
572        use std::collections::HashMap;
573
574        let mut chapters: HashMap<String, ChapterAggregate> = HashMap::new();
575        for chapter in to_use {
576            chapters.insert(chapter.chapter.clone(), chapter.clone());
577        }
578        chapters.serialize(serializer)
579    }
580}
581
582/// Originally a Deserializer helper to handle JSON array or object types.
583///
584/// MangaDex sometimes returns an array instead of a JSON object for the `links` field for `MangaAttributes`.
585///
586/// The Serializer was added in 0.2.0 for pratical and necessities reason
587pub(crate) mod manga_links_array_or_struct {
588    use crate::v5::MangaLinks;
589    use serde::de::{Deserialize, Deserializer, MapAccess, SeqAccess, Visitor};
590    #[cfg(feature = "serialize")]
591    use serde::ser::Serializer;
592    #[cfg(feature = "serialize")]
593    use serde::Serialize;
594
595    /// Deserialize a `MangaLinks` from a JSON value or none.
596    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<MangaLinks>, D::Error>
597    where
598        D: Deserializer<'de>,
599    {
600        struct OptionMangaLinksVisitor;
601
602        impl<'de> Visitor<'de> for OptionMangaLinksVisitor {
603            type Value = Option<MangaLinks>;
604
605            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
606                formatter.write_str("some or none")
607            }
608
609            /// Deserialize a `MangaLinks` from none.
610            fn visit_none<E>(self) -> Result<Self::Value, E>
611            where
612                E: serde::de::Error,
613            {
614                Ok(None)
615            }
616
617            /// Deserialize a `MangaLinks` from a JSON value.
618            fn visit_some<D>(self, d: D) -> Result<Self::Value, D::Error>
619            where
620                D: Deserializer<'de>,
621            {
622                let manga_links = d.deserialize_any(MangaLinksVisitor)?;
623
624                let manga_links = if manga_links.has_no_links() {
625                    None
626                } else {
627                    Some(manga_links)
628                };
629
630                Ok(manga_links)
631            }
632
633            /// Deserialize a `MangaLinks` from none (`null`).
634            fn visit_unit<E>(self) -> Result<Self::Value, E>
635            where
636                E: serde::de::Error,
637            {
638                Ok(None)
639            }
640        }
641
642        struct MangaLinksVisitor;
643
644        impl<'de> Visitor<'de> for MangaLinksVisitor {
645            type Value = MangaLinks;
646
647            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
648                formatter.write_str("array or map")
649            }
650
651            /// Deserialize a `MangaLinks` from a sequence (array).
652            fn visit_seq<A>(self, mut _seq: A) -> Result<Self::Value, A::Error>
653            where
654                A: SeqAccess<'de>,
655            {
656                Ok(Self::Value::default())
657            }
658
659            /// Deserialize a `MangaLinks` from a map (JSON object).
660            fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
661            where
662                M: MapAccess<'de>,
663            {
664                // `MapAccessDeserializer` is a wrapper that turns a `MapAccess`
665                // into a `Deserializer`, allowing it to be used as the input to T's
666                // `Deserialize` implementation. T then deserializes itself using
667                // the entries from the map visitor.
668                Deserialize::deserialize(serde::de::value::MapAccessDeserializer::new(map))
669            }
670        }
671
672        deserializer.deserialize_option(OptionMangaLinksVisitor)
673    }
674    #[cfg(feature = "serialize")]
675    pub fn serialize<S>(to_use: &Option<MangaLinks>, serializer: S) -> Result<S::Ok, S::Error>
676    where
677        S: Serializer,
678    {
679        match to_use {
680            None => serializer.serialize_none(),
681            Some(data) => data.serialize(serializer),
682        }
683    }
684}
685
686/// Originally a Deserializer for an array of languages, discarding elements that are `null`.
687///
688/// The Serializer was added in 0.2.0 for pratical and necessities reason
689pub(crate) mod language_array_or_skip_null {
690    use mangadex_api_types::Language;
691    use serde::de::{Deserializer, SeqAccess, Visitor};
692    #[cfg(feature = "serialize")]
693    use serde::ser::Serializer;
694    #[cfg(feature = "serialize")]
695    use serde::Serialize;
696    /// Deserialize a `Vec<Language>` from an array of JSON values.
697    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Language>, D::Error>
698    where
699        D: Deserializer<'de>,
700    {
701        struct V;
702
703        impl<'de> Visitor<'de> for V {
704            type Value = Vec<Language>;
705
706            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
707                formatter.write_str("a sequence of languages")
708            }
709
710            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
711            where
712                A: SeqAccess<'de>,
713            {
714                let mut languages = Vec::new();
715
716                // Skip over invalid or `null` languages.
717                while let Some(language) = seq.next_element::<Option<Language>>()? {
718                    // `language` will be `None` if an element value is `null` from JSON.
719                    if let Some(language) = language {
720                        languages.push(language);
721                    }
722                }
723
724                Ok(languages)
725            }
726        }
727
728        deserializer.deserialize_seq(V)
729    }
730    #[cfg(feature = "serialize")]
731    pub fn serialize<S>(to_use: &Vec<Language>, serializer: S) -> Result<S::Ok, S::Error>
732    where
733        S: Serializer,
734    {
735        to_use.serialize(serializer)
736    }
737}
738
739pub fn mangadex_datetime_serialize<S>(
740    datetime: &MangaDexDateTime,
741    serializer: S,
742) -> Result<S::Ok, S::Error>
743where
744    S: serde::Serializer,
745{
746    serializer.serialize_str(datetime.to_string().as_str())
747}
748
749pub fn mangadex_datetime_serialize_option<S>(
750    datetime: &Option<MangaDexDateTime>,
751    serializer: S,
752) -> Result<S::Ok, S::Error>
753where
754    S: serde::Serializer,
755{
756    if let Some(d) = datetime {
757        serializer.serialize_str(d.to_string().as_str())
758    } else {
759        serializer.serialize_none()
760    }
761}
762
763#[cfg(test)]
764mod test {
765    use serde::{Deserialize, Serialize};
766
767    use mangadex_api_types::MangaDexDateTime;
768
769    #[derive(Serialize, Deserialize, Default)]
770    struct TestStruct {
771        #[serde(serialize_with = "crate::v5::mangadex_datetime_serialize")]
772        date: MangaDexDateTime,
773    }
774
775    #[tokio::test]
776    async fn mangadex_datetime_serialize_test() {
777        let test: TestStruct = Default::default();
778        println!("{}", serde_json::to_string_pretty(&test).unwrap());
779    }
780}