dynasty_api/
directory.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{DynastyReaderRoute, DYNASTY_READER_BASE};
4
5/// A configuration to get a [Directory]
6#[allow(missing_docs)]
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct DirectoryConfig {
9    pub kind: DirectoryKind,
10    pub page_number: u64,
11}
12
13impl DynastyReaderRoute for DirectoryConfig {
14    fn request_builder(
15        &self,
16        client: &reqwest::Client,
17        url: reqwest::Url,
18    ) -> reqwest::RequestBuilder {
19        client.get(url).query(&[("page", self.page_number)])
20    }
21
22    fn request_url(&self) -> reqwest::Url {
23        DYNASTY_READER_BASE
24            .join(&format!("{}.json", self.kind))
25            .unwrap()
26    }
27}
28
29/// A Dynasty Reader's directory types
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
31pub enum DirectoryKind {
32    /// <https://dynasty-scans.com/anthologies>
33    Anthology,
34
35    /// <https://dynasty-scans.com/doujins>
36    Doujin,
37
38    /// <https://dynasty-scans.com/issues>
39    Issue,
40
41    /// <https://dynasty-scans.com/series>
42    Series,
43
44    /// <https://dynasty-scans.com/authors>
45    Author,
46
47    /// <https://dynasty-scans.com/scanlators>
48    Scanlator,
49
50    /// <https://dynasty-scans.com/tags>
51    #[serde(alias = "General")]
52    Tag,
53
54    /// <https://dynasty-scans.com/pairings>
55    Pairing,
56}
57
58impl std::fmt::Display for DirectoryKind {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        let s = {
61            use DirectoryKind::*;
62
63            match &self {
64                Anthology => "anthologies",
65                Doujin => "doujins",
66                Issue => "issues",
67                Series => "series",
68                Author => "authors",
69                Scanlator => "scanlators",
70                Tag => "tags",
71                Pairing => "pairings",
72            }
73        };
74
75        write!(f, "{s}")
76    }
77}
78
79impl std::str::FromStr for DirectoryKind {
80    type Err = anyhow::Error;
81
82    fn from_str(s: &str) -> Result<Self, Self::Err> {
83        use DirectoryKind::*;
84
85        match s {
86            "anthologies" => Ok(Anthology),
87            "authors" => Ok(Author),
88            "doujins" => Ok(Doujin),
89            "tags" => Ok(Tag),
90            "issues" => Ok(Issue),
91            "pairings" => Ok(Pairing),
92            "scanlators" => Ok(Scanlator),
93            "series" => Ok(Series),
94            _ => Err(anyhow::anyhow!("`{}` is not a valid directory", s)),
95        }
96    }
97}
98
99#[derive(Debug, Clone, Serialize)]
100pub(crate) struct UntaggedDirectory {
101    items: Vec<UntaggedDirectoryItem>,
102    page_number: u64,
103    max_page_number: u64,
104}
105
106impl<'de> Deserialize<'de> for UntaggedDirectory {
107    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
108    where
109        D: serde::Deserializer<'de>,
110    {
111        use std::collections::HashMap;
112
113        #[derive(Deserialize)]
114        struct UntaggedDirectoryWrapper {
115            tags: Vec<HashMap<String, Vec<UntaggedDirectoryItem>>>,
116            current_page: u64,
117            total_pages: u64,
118        }
119
120        let wrapper: UntaggedDirectoryWrapper = Deserialize::deserialize(deserializer)?;
121
122        let UntaggedDirectoryWrapper {
123            tags: items,
124            current_page: page_number,
125            total_pages: max_page_number,
126        } = wrapper;
127
128        let items = items
129            .into_iter()
130            .flatten()
131            .flat_map(|(_, items)| items)
132            .collect();
133
134        Ok(UntaggedDirectory {
135            items,
136            page_number,
137            max_page_number,
138        })
139    }
140}
141
142/// A wrapper around Dynasty Reader's directory
143///
144/// # Example urls
145///
146/// - <https://dynasty-scans.com/tags>
147/// - <https://dynasty-scans.com/doujins>
148#[allow(missing_docs)]
149#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
150pub struct Directory {
151    pub items: Vec<DirectoryItem>,
152    pub page_number: u64,
153    pub max_page_number: u64,
154}
155
156impl UntaggedDirectory {
157    pub(crate) fn into_tagged(self, kind: DirectoryKind) -> Directory {
158        let items = self
159            .items
160            .into_iter()
161            .map(|item| item.into_tagged(kind))
162            .collect();
163
164        Directory {
165            items,
166            page_number: self.page_number,
167            max_page_number: self.max_page_number,
168        }
169    }
170}
171
172#[derive(Debug, Clone, Deserialize, Serialize)]
173pub(crate) struct UntaggedDirectoryItem {
174    name: String,
175    permalink: String,
176}
177
178/// A Dynasty Reader's [Directory] item
179#[allow(missing_docs)]
180#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
181pub struct DirectoryItem {
182    pub name: String,
183    pub permalink: String,
184    pub kind: DirectoryKind,
185}
186
187impl UntaggedDirectoryItem {
188    pub(crate) fn into_tagged(self, kind: DirectoryKind) -> DirectoryItem {
189        DirectoryItem {
190            kind,
191            name: self.name,
192            permalink: self.permalink,
193        }
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use anyhow::Result;
200
201    use crate::test_utils::tryhard_configs;
202
203    use super::*;
204
205    fn create_config(kind: DirectoryKind) -> DirectoryConfig {
206        DirectoryConfig {
207            kind,
208            page_number: 1,
209        }
210    }
211
212    #[tokio::test]
213    #[ignore = "requires internet"]
214    async fn response_structure() -> Result<()> {
215        let configs = {
216            use DirectoryKind::*;
217
218            [
219                Anthology, Doujin, Issue, Series, Author, Scanlator, Tag, Pairing,
220            ]
221            .map(create_config)
222        };
223
224        tryhard_configs(configs, |client, config| client.directory(config)).await?;
225
226        Ok(())
227    }
228}