mangadex_api/v5/manga/draft/
get.rs

1//! Builder for getting a list of Manga Drafts.
2//!
3//! This endpoint requires authentication.
4//!
5//! <https://api.mangadex.org/docs/swagger.html#/Manga/get-manga-drafts>
6//!
7//! # Examples
8//!
9//! ```rust
10//! use mangadex_api_types::{MangaState, MangaStatus};
11//! use mangadex_api::v5::MangaDexClient;
12//! // use mangadex_api_types::{Password, Username};
13//!
14//! # async fn run() -> anyhow::Result<()> {
15//! let client = MangaDexClient::default();
16//!
17//! /*
18//!     let _login_res = client
19//!         .auth()
20//!         .login()
21//!         .post()    
22//!         .username(Username::parse("myusername")?)
23//!         .password(Password::parse("hunter23")?)
24//!         .build()?
25//!         .send()
26//!         .await?;
27//! */
28//! let manga_res = client
29//!     .manga()
30//!     .draft()
31//!     .get()
32//!     .state(MangaState::Draft)
33//!     .send()
34//!     .await?;
35//!
36//! println!("manga: {:?}", manga_res);
37//! # Ok(())
38//! # }
39//! ```
40
41use derive_builder::Builder;
42use serde::Serialize;
43use uuid::Uuid;
44
45use crate::HttpClientRef;
46use mangadex_api_schema::v5::MangaListResponse;
47use mangadex_api_types::{MangaDraftsSortOrder, MangaState, ReferenceExpansionResource};
48
49#[cfg_attr(
50    feature = "deserializable-endpoint",
51    derive(serde::Deserialize, getset::Getters, getset::Setters)
52)]
53#[derive(Debug, Serialize, Clone, Builder, Default)]
54#[serde(rename_all = "camelCase")]
55#[builder(
56    setter(into, strip_option),
57    default,
58    build_fn(error = "mangadex_api_types::error::BuilderError")
59)]
60#[cfg_attr(feature = "non_exhaustive", non_exhaustive)]
61pub struct ListMangaDrafts {
62    #[doc(hidden)]
63    #[serde(skip)]
64    #[builder(pattern = "immutable")]
65    #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
66    pub http_client: HttpClientRef,
67
68    /// Minimum: 1
69    ///
70    /// Maximum: 100
71    ///
72    /// Default: 10 (if not specified)
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub limit: Option<u32>,
75    /// >= 0
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub offset: Option<u32>,
78    #[deprecated(since = "1.2.1", note = "MangaDex removed this in 5.4.9 of their API")]
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub user: Option<Uuid>,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub state: Option<MangaState>,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub order: Option<MangaDraftsSortOrder>,
85    #[serde(skip_serializing_if = "Vec::is_empty")]
86    #[builder(setter(each = "include"))]
87    pub includes: Vec<ReferenceExpansionResource>,
88}
89
90endpoint! {
91    GET "/manga/draft",
92    #[query auth] ListMangaDrafts,
93    #[flatten_result] MangaListResponse,
94    ListMangaDraftsBuilder
95}
96
97#[cfg(test)]
98mod tests {
99    use serde_json::json;
100    use time::OffsetDateTime;
101    use url::Url;
102    use uuid::Uuid;
103    use wiremock::matchers::{header, method, path};
104    use wiremock::{Mock, MockServer, ResponseTemplate};
105
106    use crate::v5::AuthTokens;
107    use crate::{HttpClient, MangaDexClient};
108    use mangadex_api_types::error::Error;
109    use mangadex_api_types::{
110        ContentRating, Demographic, Language, MangaDexDateTime, MangaStatus, ResponseType,
111    };
112
113    #[tokio::test]
114    async fn list_manga_drafts_fires_a_request_to_base_url() -> anyhow::Result<()> {
115        let mock_server = MockServer::start().await;
116        let http_client: HttpClient = HttpClient::builder()
117            .base_url(Url::parse(&mock_server.uri())?)
118            .auth_tokens(AuthTokens {
119                session: "sessiontoken".to_string(),
120                refresh: "refreshtoken".to_string(),
121            })
122            .build()?;
123        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
124
125        let manga_id = Uuid::new_v4();
126        let manga_title = "Test Manga".to_string();
127
128        let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
129
130        let response_body = json!({
131            "result": "ok",
132            "response": "collection",
133            "data": [
134                {
135                    "id": manga_id,
136                    "type": "manga",
137                    "attributes": {
138                        "title": {
139                            "en": manga_title
140                        },
141                        "altTitles": [],
142                        "description": {},
143                        "isLocked": false,
144                        "links": null,
145                        "originalLanguage": "ja",
146                        "lastVolume": null,
147                        "lastChapter": null,
148                        "publicationDemographic": "shoujo",
149                        "status": "ongoing",
150                        "year": null,
151                        "contentRating": "safe",
152                        "chapterNumbersResetOnNewVolume": true,
153                        "availableTranslatedLanguages": ["en"],
154                        "tags": [],
155                        "state": "draft",
156                        "createdAt": datetime.to_string(),
157                        "updatedAt": datetime.to_string(),
158
159                        "version": 1
160                    },
161                    "relationships": []
162                }
163            ],
164            "limit": 1,
165            "offset": 0,
166            "total": 1
167        });
168
169        Mock::given(method("GET"))
170            .and(path("/manga/draft"))
171            .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
172            .expect(1)
173            .mount(&mock_server)
174            .await;
175
176        let res = mangadex_client
177            .manga()
178            .draft()
179            .get()
180            .limit(1u32)
181            .send()
182            .await?;
183
184        assert_eq!(res.response, ResponseType::Collection);
185        let manga = &res.data[0];
186        assert_eq!(manga.id, manga_id);
187        assert_eq!(
188            manga.attributes.title.get(&Language::English).unwrap(),
189            &manga_title
190        );
191        assert!(manga.attributes.alt_titles.is_empty());
192        assert!(manga.attributes.description.is_empty());
193        assert!(!manga.attributes.is_locked);
194        assert_eq!(manga.attributes.links, None);
195        assert_eq!(manga.attributes.original_language, Language::Japanese);
196        assert_eq!(manga.attributes.last_volume, None);
197        assert_eq!(manga.attributes.last_chapter, None);
198        assert_eq!(
199            manga.attributes.publication_demographic.unwrap(),
200            Demographic::Shoujo
201        );
202        assert_eq!(manga.attributes.status, MangaStatus::Ongoing);
203        assert_eq!(manga.attributes.year, None);
204        assert_eq!(
205            manga.attributes.content_rating.unwrap(),
206            ContentRating::Safe
207        );
208        assert!(manga.attributes.tags.is_empty());
209        assert_eq!(
210            manga.attributes.created_at.to_string(),
211            datetime.to_string()
212        );
213        assert_eq!(
214            manga.attributes.updated_at.as_ref().unwrap().to_string(),
215            datetime.to_string()
216        );
217        assert_eq!(manga.attributes.version, 1);
218
219        Ok(())
220    }
221
222    #[tokio::test]
223    async fn list_manga_drafts_handles_400() -> anyhow::Result<()> {
224        let mock_server = MockServer::start().await;
225        let http_client: HttpClient = HttpClient::builder()
226            .base_url(Url::parse(&mock_server.uri())?)
227            .auth_tokens(AuthTokens {
228                session: "sessiontoken".to_string(),
229                refresh: "refreshtoken".to_string(),
230            })
231            .build()?;
232        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
233
234        let error_id = Uuid::new_v4();
235
236        let response_body = json!({
237            "result": "error",
238            "errors": [{
239                "id": error_id.to_string(),
240                "status": 400,
241                "title": "Invalid limit",
242                "detail": "Limit must be between 1 and 100"
243            }]
244        });
245
246        Mock::given(method("GET"))
247            .and(path("/manga/draft"))
248            .and(header("Authorization", "Bearer sessiontoken"))
249            .respond_with(ResponseTemplate::new(400).set_body_json(response_body))
250            .expect(1)
251            .mount(&mock_server)
252            .await;
253
254        let res = mangadex_client
255            .manga()
256            .draft()
257            .get()
258            .limit(0u32)
259            .send()
260            .await
261            .expect_err("expected error");
262
263        if let Error::Api(errors) = res {
264            assert_eq!(errors.errors.len(), 1);
265
266            assert_eq!(errors.errors[0].id, error_id);
267            assert_eq!(errors.errors[0].status, 400);
268            assert_eq!(errors.errors[0].title, Some("Invalid limit".to_string()));
269            assert_eq!(
270                errors.errors[0].detail,
271                Some("Limit must be between 1 and 100".to_string())
272            );
273        }
274
275        Ok(())
276    }
277
278    #[tokio::test]
279    async fn list_manga_drafts_requires_auth() -> anyhow::Result<()> {
280        let mock_server = MockServer::start().await;
281        let http_client: HttpClient = HttpClient::builder()
282            .base_url(Url::parse(&mock_server.uri())?)
283            .build()?;
284        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
285
286        let error_id = Uuid::new_v4();
287
288        let response_body = json!({
289            "result": "error",
290            "errors": [{
291                "id": error_id.to_string(),
292                "status": 403,
293                "title": "Forbidden",
294                "detail": "You must be logged in to continue."
295            }]
296        });
297
298        Mock::given(method("GET"))
299            .and(path("/manga/draft"))
300            .respond_with(ResponseTemplate::new(403).set_body_json(response_body))
301            .expect(0)
302            .mount(&mock_server)
303            .await;
304
305        let res = mangadex_client
306            .manga()
307            .draft()
308            .get()
309            .limit(0u32)
310            .send()
311            .await
312            .expect_err("expected error");
313
314        match res {
315            Error::MissingTokens => {}
316            _ => panic!("unexpected error: {:#?}", res),
317        }
318
319        Ok(())
320    }
321}