1use serde::{Deserialize, Serialize};
2
3use crate::{DynastyReaderRoute, DYNASTY_READER_BASE};
4
5#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
31pub enum DirectoryKind {
32 Anthology,
34
35 Doujin,
37
38 Issue,
40
41 Series,
43
44 Author,
46
47 Scanlator,
49
50 #[serde(alias = "General")]
52 Tag,
53
54 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#[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#[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}