mangadex_api/v5/upload/upload_session_id/commit/
post.rs1use mangadex_api_schema::v5::ChapterData;
49use serde::Serialize;
50use url::Url;
51use uuid::Uuid;
52
53use crate::HttpClientRef;
54use crate::{error::Error, Result};
55use mangadex_api_types::{Language, MangaDexDateTime};
56
57#[cfg_attr(
58 feature = "deserializable-endpoint",
59 derive(serde::Deserialize, getset::Getters, getset::Setters)
60)]
61#[derive(Debug, Serialize, Clone)]
62#[serde(rename_all = "camelCase")]
63#[non_exhaustive]
64pub struct CommitUploadSession {
65 #[serde(skip)]
67 #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
68 pub http_client: HttpClientRef,
69
70 #[serde(skip_serializing)]
71 pub session_id: Uuid,
72
73 #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
74 chapter_draft: ChapterDraft,
75 pub page_order: Vec<Uuid>,
79 pub terms_accepted: bool,
80}
81
82#[cfg_attr(feature = "deserializable-endpoint", derive(serde::Deserialize))]
83#[derive(Debug, Serialize, Clone)]
84#[serde(rename_all = "camelCase")]
85#[non_exhaustive]
86pub struct ChapterDraft {
87 pub volume: Option<String>,
89 pub chapter: Option<String>,
91 pub title: Option<String>,
93 pub translated_language: Language,
94 #[serde(skip_serializing_if = "Option::is_none")]
98 pub external_url: Option<Url>,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub publish_at: Option<MangaDexDateTime>,
101}
102
103#[cfg_attr(feature = "deserializable-endpoint", derive(serde::Deserialize))]
104#[derive(Debug, Serialize, Clone, Default)]
106#[non_exhaustive]
107pub struct CommitUploadSessionBuilder {
108 #[serde(skip)]
109 pub http_client: Option<HttpClientRef>,
110
111 pub session_id: Option<Uuid>,
112 pub page_order: Vec<Uuid>,
114
115 pub volume: Option<String>,
117 pub chapter: Option<String>,
119 pub title: Option<String>,
121 pub translated_language: Option<Language>,
122 pub external_url: Option<Url>,
126 pub publish_at: Option<MangaDexDateTime>,
127 pub terms_accepted: bool,
128}
129
130impl CommitUploadSessionBuilder {
131 pub fn new(http_client: HttpClientRef) -> Self {
132 Self {
133 http_client: Some(http_client),
134 ..Default::default()
135 }
136 }
137
138 pub fn http_client(mut self, http_client: HttpClientRef) -> Self {
139 self.http_client = Some(http_client);
140 self
141 }
142
143 pub fn session_id(mut self, session_id: Uuid) -> Self {
145 self.session_id = Some(session_id);
146 self
147 }
148
149 pub fn page_order(mut self, page_order: Vec<Uuid>) -> Self {
151 self.page_order = page_order;
152 self
153 }
154
155 pub fn add_page(mut self, page: Uuid) -> Self {
157 self.page_order.push(page);
158 self
159 }
160
161 pub fn volume(mut self, volume: Option<String>) -> Self {
165 self.volume = volume;
166 self
167 }
168
169 pub fn chapter(mut self, chapter: Option<String>) -> Self {
173 self.chapter = chapter;
174 self
175 }
176
177 pub fn title(mut self, title: Option<String>) -> Self {
181 self.title = title;
182 self
183 }
184
185 pub fn translated_language(mut self, translated_language: Language) -> Self {
189 self.translated_language = Some(translated_language);
190 self
191 }
192
193 pub fn external_url(mut self, external_url: Option<Url>) -> Self {
199 self.external_url = external_url;
200 self
201 }
202
203 pub fn publish_at<DT: Into<MangaDexDateTime>>(mut self, publish_at: DT) -> Self {
205 self.publish_at = Some(publish_at.into());
206 self
207 }
208
209 pub fn terms_accepted(mut self, accepted: bool) -> Self {
210 self.terms_accepted = accepted;
211 self
212 }
213
214 fn validate(&self) -> std::result::Result<(), String> {
216 if self.session_id.is_none() {
217 return Err("session_id cannot be None".to_string());
218 }
219
220 if self.translated_language.is_none() {
221 return Err("translated_language cannot be None".to_string());
222 }
223
224 Ok(())
225 }
226
227 pub fn build(&self) -> Result<CommitUploadSession> {
230 if let Err(error) = self.validate() {
231 return Err(Error::RequestBuilderError(error));
232 }
233
234 let session_id = self
235 .session_id
236 .ok_or(Error::RequestBuilderError(String::from(
237 "session_id must be provided",
238 )))?;
239 let translated_language =
240 self.translated_language
241 .ok_or(Error::RequestBuilderError(String::from(
242 "translated_language must be provided",
243 )))?;
244
245 let chapter_draft = ChapterDraft {
246 volume: self.volume.to_owned(),
247 chapter: self.chapter.to_owned(),
248 title: self.title.to_owned(),
249 translated_language,
250 external_url: self.external_url.to_owned(),
251 publish_at: self.publish_at,
252 };
253 Ok(CommitUploadSession {
254 http_client: self
255 .http_client
256 .to_owned()
257 .ok_or(Error::RequestBuilderError(String::from(
258 "http_client must be provided",
259 )))?,
260
261 session_id,
262 chapter_draft,
263 page_order: self.page_order.to_owned(),
264 terms_accepted: self.terms_accepted,
265 })
266 }
267}
268
269endpoint! {
270 POST ("/upload/{}/commit", session_id),
271 #[body auth] CommitUploadSession,
272 #[rate_limited] ChapterData,
273 CommitUploadSessionBuilder
274}
275
276#[cfg(test)]
277mod tests {
278 use fake::faker::name::en::Name;
279 use fake::Fake;
280 use serde_json::json;
281 use time::OffsetDateTime;
282 use url::Url;
283 use uuid::Uuid;
284 use wiremock::matchers::{body_json, header, method, path_regex};
285 use wiremock::{Mock, MockServer, ResponseTemplate};
286
287 use crate::v5::upload::upload_session_id::commit::post::ChapterDraft;
288 use crate::v5::AuthTokens;
289 use crate::{HttpClient, MangaDexClient};
290 use mangadex_api_types::{Language, MangaDexDateTime, RelationshipType};
291
292 use serde::Serialize;
293
294 #[derive(Clone, Serialize, Debug)]
295 #[serde(rename_all = "camelCase")]
296 struct ExceptedBody {
297 chapter_draft: ChapterDraft,
298 page_order: Vec<Uuid>,
302 terms_accepted: bool,
303 }
304
305 #[tokio::test]
306 async fn commit_upload_session_fires_a_request_to_base_url() -> anyhow::Result<()> {
307 let mock_server = MockServer::start().await;
308 let http_client = HttpClient::builder()
309 .base_url(Url::parse(&mock_server.uri())?)
310 .auth_tokens(non_exhaustive::non_exhaustive!(AuthTokens {
311 session: "sessiontoken".to_string(),
312 refresh: "refreshtoken".to_string(),
313 }))
314 .build()?;
315 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
316
317 let session_id = Uuid::new_v4();
318 let session_file_id = Uuid::new_v4();
319 let chapter_id = Uuid::new_v4();
320 let uploader_id = Uuid::new_v4();
321 let chapter_title: String = Name().fake();
322
323 let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
324
325 let expected_body = ExceptedBody {
326 chapter_draft: ChapterDraft {
327 volume: Some(String::from("1")),
328 chapter: Some(String::from("2.5")),
329 title: Some(chapter_title.clone()),
330 translated_language: Language::English,
331 external_url: None,
332 publish_at: None,
333 },
334 page_order: vec![session_file_id],
335 terms_accepted: true,
336 };
337
338 let response_body = json!({
339 "result": "ok",
340 "response": "entity",
341 "data": {
342 "id": chapter_id,
343 "type": "chapter",
344 "attributes": {
345 "title": chapter_title,
346 "volume": "1",
347 "chapter": "2.5",
348 "pages": 4,
349 "translatedLanguage": "en",
350 "uploader": uploader_id,
351 "version": 1,
352 "createdAt": datetime.to_string(),
353 "updatedAt": datetime.to_string(),
354 "publishAt": datetime.to_string(),
355 "readableAt": datetime.to_string(),
356 },
357 "relationships": [],
358 }
359
360 });
361 Mock::given(method("POST"))
362 .and(path_regex(r"/upload/[0-9a-fA-F-]+/commit"))
363 .and(header("authorization", "Bearer sessiontoken"))
364 .and(header("content-type", "application/json"))
365 .and(body_json(expected_body))
366 .respond_with(
367 ResponseTemplate::new(200)
368 .insert_header("x-ratelimit-retry-after", "1698723860")
369 .insert_header("x-ratelimit-limit", "40")
370 .insert_header("x-ratelimit-remaining", "39")
371 .set_body_json(response_body),
372 )
373 .expect(1)
374 .mount(&mock_server)
375 .await;
376
377 let res = mangadex_client
378 .upload()
379 .upload_session_id(session_id)
380 .commit()
381 .post()
382 .volume(Some("1".to_string()))
383 .chapter(Some("2.5".to_string()))
384 .title(Some(chapter_title.clone()))
385 .translated_language(Language::English)
386 .page_order(vec![session_file_id])
387 .terms_accepted(true)
388 .send()
389 .await?;
390
391 let res = &res.data;
392
393 assert_eq!(res.id, chapter_id);
394 assert_eq!(res.type_, RelationshipType::Chapter);
395 assert_eq!(res.attributes.title, Some(chapter_title.clone()));
396 assert_eq!(res.attributes.volume, Some("1".to_string()));
397 assert_eq!(res.attributes.chapter, Some("2.5".to_string()));
398 assert_eq!(res.attributes.pages, 4);
399 assert_eq!(res.attributes.translated_language, Language::English);
400 assert_eq!(res.attributes.external_url, None);
401 assert_eq!(res.attributes.version, 1);
402 assert_eq!(res.attributes.created_at.to_string(), datetime.to_string());
403 assert_eq!(
404 res.attributes.updated_at.as_ref().unwrap().to_string(),
405 datetime.to_string()
406 );
407 assert_eq!(
408 res.attributes.publish_at.unwrap().to_string(),
409 datetime.to_string()
410 );
411 assert_eq!(
412 res.attributes.readable_at.unwrap().to_string(),
413 datetime.to_string()
414 );
415
416 Ok(())
417 }
418}