mangadex_api/v5/chapter/id/
put.rs

1//! Builder for the chapter update endpoint.
2//!
3//! <https://api.mangadex.org/swagger.html#/Chapter/put-chapter-id>
4//!
5//! # Examples
6//!
7//! ```rust
8//! use uuid::Uuid;
9//!
10//! use mangadex_api::v5::MangaDexClient;
11//! // use mangadex_api_types::{Password, Username};
12//!
13//! # async fn run() -> anyhow::Result<()> {
14//! let client = MangaDexClient::default();
15//!
16//! /*
17//! let _login_res = client
18//!     .auth()
19//!     .login()
20//!     .post()
21//!     .username(Username::parse("myusername")?)
22//!     .password(Password::parse("hunter23")?)
23//!     .send()
24//!     .await?;
25//! */
26//!
27//! let chapter_id = Uuid::new_v4();
28//! let res = client
29//!     .chapter()
30//!     .id(chapter_id)
31//!     .put()
32//!     .title("Updated Chapter Title")
33//!     .version(2u32)
34//!     .send()
35//!     .await?;
36//!
37//! println!("update: {:?}", res);
38//! # Ok(())
39//! # }
40//! ```
41
42use derive_builder::Builder;
43use serde::Serialize;
44use uuid::Uuid;
45
46use crate::HttpClientRef;
47use mangadex_api_schema::v5::ChapterData;
48use mangadex_api_types::Language;
49
50#[cfg_attr(
51    feature = "deserializable-endpoint",
52    derive(serde::Deserialize, getset::Getters, getset::Setters)
53)]
54#[derive(Debug, Serialize, Clone, Builder)]
55#[serde(rename_all = "camelCase")]
56#[builder(
57    setter(into, strip_option),
58    build_fn(error = "mangadex_api_types::error::BuilderError")
59)]
60#[cfg_attr(feature = "non_exhaustive", non_exhaustive)]
61pub struct UpdateChapter {
62    /// This should never be set manually as this is only for internal use.
63    #[doc(hidden)]
64    #[serde(skip)]
65    #[builder(pattern = "immutable")]
66    #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
67    pub http_client: HttpClientRef,
68
69    #[serde(skip_serializing)]
70    pub chapter_id: Uuid,
71
72    /// <= 255 characters in length.
73    ///
74    /// Nullable.
75    #[serde(skip_serializing_if = "Option::is_none")]
76    #[builder(default)]
77    pub title: Option<String>,
78    /// Volume number.
79    ///
80    /// Nullable.
81    #[serde(skip_serializing_if = "Option::is_none")]
82    #[builder(default)]
83    pub volume: Option<String>,
84    /// Chapter number.
85    ///
86    /// <= 8 characters in length.
87    ///
88    /// Nullable.
89    #[serde(skip_serializing_if = "Option::is_none")]
90    #[builder(default)]
91    pub chapter: Option<Option<String>>,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    #[builder(default)]
94    pub translated_language: Option<Language>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    #[builder(default)]
97    pub groups: Option<Vec<Uuid>>,
98    /// >= 1
99    pub version: u32,
100}
101
102endpoint! {
103    PUT ("/chapter/{}", chapter_id),
104    #[body auth] UpdateChapter,
105    #[rate_limited] ChapterData,
106    UpdateChapterBuilder
107}
108
109#[cfg(test)]
110mod tests {
111    use fake::faker::name::en::Name;
112    use fake::Fake;
113    use serde_json::json;
114    use time::OffsetDateTime;
115    use url::Url;
116    use uuid::Uuid;
117    use wiremock::matchers::{body_json, header, method, path_regex};
118    use wiremock::{Mock, MockServer, ResponseTemplate};
119
120    use crate::v5::AuthTokens;
121    use crate::{HttpClient, MangaDexClient};
122    use mangadex_api_types::MangaDexDateTime;
123
124    #[tokio::test]
125    async fn update_chapter_fires_a_request_to_base_url() -> anyhow::Result<()> {
126        let mock_server = MockServer::start().await;
127        let http_client = HttpClient::builder()
128            .base_url(Url::parse(&mock_server.uri())?)
129            .auth_tokens(AuthTokens {
130                session: "sessiontoken".to_string(),
131                refresh: "refreshtoken".to_string(),
132            })
133            .build()?;
134        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
135
136        let chapter_id = Uuid::new_v4();
137        let uploader_id = Uuid::new_v4();
138        let chapter_title: String = Name().fake();
139
140        let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
141
142        let expected_body = json!({
143            "version": 2
144        });
145        let response_body = json!({
146            "result": "ok",
147            "response": "entity",
148            "data": {
149                "id": chapter_id,
150                "type": "chapter",
151                "attributes": {
152                    "title": chapter_title,
153                    "volume": "1",
154                    "chapter": "1.5",
155                    "pages": 4,
156                    "translatedLanguage": "en",
157                    "uploader": uploader_id,
158                    "version": 1,
159                    "createdAt": datetime.to_string(),
160                    "updatedAt": datetime.to_string(),
161                    "publishAt": datetime.to_string(),
162                    "readableAt": datetime.to_string(),
163                },
164                "relationships": []
165            }
166        });
167
168        Mock::given(method("PUT"))
169            .and(path_regex(r"/chapter/[0-9a-fA-F-]+"))
170            .and(header("Authorization", "Bearer sessiontoken"))
171            .and(header("Content-Type", "application/json"))
172            .and(body_json(expected_body))
173            .respond_with(
174                ResponseTemplate::new(200)
175                    .insert_header("x-ratelimit-retry-after", "1698723860")
176                    .insert_header("x-ratelimit-limit", "40")
177                    .insert_header("x-ratelimit-remaining", "39")
178                    .set_body_json(response_body),
179            )
180            .expect(1)
181            .mount(&mock_server)
182            .await;
183
184        let _ = mangadex_client
185            .chapter()
186            .id(chapter_id)
187            .put()
188            .version(2_u32)
189            .send()
190            .await?;
191
192        Ok(())
193    }
194
195    #[tokio::test]
196    async fn update_chapter_does_not_include_title_when_not_used() -> anyhow::Result<()> {
197        let mock_server = MockServer::start().await;
198        let http_client = HttpClient::builder()
199            .base_url(Url::parse(&mock_server.uri())?)
200            .auth_tokens(AuthTokens {
201                session: "sessiontoken".to_string(),
202                refresh: "refreshtoken".to_string(),
203            })
204            .build()?;
205        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
206
207        let chapter_id = Uuid::new_v4();
208        let uploader_id = Uuid::new_v4();
209        let chapter_title: String = Name().fake();
210
211        let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
212
213        let expected_body = json!({
214            "version": 2
215        });
216        let response_body = json!({
217            "result": "ok",
218            "response": "entity",
219            "data": {
220                "id": chapter_id,
221                "type": "chapter",
222                "attributes": {
223                    "title": chapter_title,
224                    "volume": "1",
225                    "chapter": "1.5",
226                    "pages": 4,
227                    "translatedLanguage": "en",
228                    "uploader": uploader_id,
229                    "version": 1,
230                    "createdAt": datetime.to_string(),
231                    "updatedAt": datetime.to_string(),
232                    "publishAt": datetime.to_string(),
233                    "readableAt": datetime.to_string(),
234                },
235                "relationships": []
236            }
237        });
238
239        Mock::given(method("PUT"))
240            .and(path_regex(r"/chapter/[0-9a-fA-F-]+"))
241            .and(header("Authorization", "Bearer sessiontoken"))
242            .and(header("Content-Type", "application/json"))
243            .and(body_json(expected_body))
244            .respond_with(
245                ResponseTemplate::new(200)
246                    .insert_header("x-ratelimit-retry-after", "1698723860")
247                    .insert_header("x-ratelimit-limit", "40")
248                    .insert_header("x-ratelimit-remaining", "39")
249                    .set_body_json(response_body),
250            )
251            .expect(1)
252            .mount(&mock_server)
253            .await;
254
255        let _ = mangadex_client
256            .chapter()
257            .id(chapter_id)
258            .put()
259            .version(2_u32)
260            .send()
261            .await?;
262
263        Ok(())
264    }
265
266    #[tokio::test]
267    async fn update_chapter_sends_null_title() -> anyhow::Result<()> {
268        let mock_server = MockServer::start().await;
269        let http_client = HttpClient::builder()
270            .base_url(Url::parse(&mock_server.uri())?)
271            .auth_tokens(AuthTokens {
272                session: "sessiontoken".to_string(),
273                refresh: "refreshtoken".to_string(),
274            })
275            .build()?;
276        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
277
278        let chapter_id = Uuid::new_v4();
279        let uploader_id = Uuid::new_v4();
280
281        let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
282
283        let expected_body = json!({
284            //"title": null,
285            "version": 2
286        });
287        let response_body = json!({
288            "result": "ok",
289            "response": "entity",
290            "data": {
291                "id": chapter_id,
292                "type": "chapter",
293                "attributes": {
294                    "title": null,
295                    "volume": "1",
296                    "chapter": "1.5",
297                    "pages": 4,
298                    "translatedLanguage": "en",
299                    "uploader": uploader_id,
300                    "version": 1,
301                    "createdAt": datetime.to_string(),
302                    "updatedAt": datetime.to_string(),
303                    "publishAt": datetime.to_string(),
304                    "readableAt": datetime.to_string(),
305                },
306                "relationships": []
307            }
308        });
309
310        Mock::given(method("PUT"))
311            .and(path_regex(r"/chapter/[0-9a-fA-F-]+"))
312            .and(header("Authorization", "Bearer sessiontoken"))
313            .and(header("Content-Type", "application/json"))
314            .and(body_json(expected_body))
315            .respond_with(
316                ResponseTemplate::new(200)
317                    .insert_header("x-ratelimit-retry-after", "1698723860")
318                    .insert_header("x-ratelimit-limit", "40")
319                    .insert_header("x-ratelimit-remaining", "39")
320                    .set_body_json(response_body),
321            )
322            .expect(1)
323            .mount(&mock_server)
324            .await;
325
326        let _ = mangadex_client
327            .chapter()
328            .id(chapter_id)
329            .put()
330            .version(2_u32)
331            .send()
332            .await?;
333
334        Ok(())
335    }
336
337    #[tokio::test]
338    async fn update_chapter_sends_title_with_value() -> anyhow::Result<()> {
339        let mock_server = MockServer::start().await;
340        let http_client = HttpClient::builder()
341            .base_url(Url::parse(&mock_server.uri())?)
342            .auth_tokens(AuthTokens {
343                session: "sessiontoken".to_string(),
344                refresh: "refreshtoken".to_string(),
345            })
346            .build()?;
347        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
348
349        let chapter_id = Uuid::new_v4();
350        let uploader_id = Uuid::new_v4();
351        let chapter_title: String = Name().fake();
352
353        let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
354
355        let expected_body = json!({
356            "title": chapter_title,
357            "version": 2
358        });
359        let response_body = json!({
360            "result": "ok",
361            "response": "entity",
362            "data": {
363                "id": chapter_id,
364                "type": "chapter",
365                "attributes": {
366                    "title": chapter_title,
367                    "volume": "1",
368                    "chapter": "1.5",
369                    "pages": 4,
370                    "translatedLanguage": "en",
371                    "uploader": uploader_id,
372                    "version": 1,
373                    "createdAt": datetime.to_string(),
374                    "updatedAt": datetime.to_string(),
375                    "publishAt": datetime.to_string(),
376                    "readableAt": datetime.to_string(),
377                },
378                "relationships": []
379            }
380        });
381
382        Mock::given(method("PUT"))
383            .and(path_regex(r"/chapter/[0-9a-fA-F-]+"))
384            .and(header("Authorization", "Bearer sessiontoken"))
385            .and(header("Content-Type", "application/json"))
386            .and(body_json(expected_body))
387            .respond_with(
388                ResponseTemplate::new(200)
389                    .insert_header("x-ratelimit-retry-after", "1698723860")
390                    .insert_header("x-ratelimit-limit", "40")
391                    .insert_header("x-ratelimit-remaining", "39")
392                    .set_body_json(response_body),
393            )
394            .expect(1)
395            .mount(&mock_server)
396            .await;
397
398        let _ = mangadex_client
399            .chapter()
400            .id(chapter_id)
401            .put()
402            .title(chapter_title.as_str())
403            .version(2_u32)
404            .send()
405            .await?;
406
407        Ok(())
408    }
409}