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 mangadex_api_types::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")]
63pub struct CommitUploadSession {
64 #[serde(skip)]
66 #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
67 pub http_client: HttpClientRef,
68
69 #[serde(skip_serializing)]
70 pub session_id: Uuid,
71
72 #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
73 chapter_draft: ChapterDraft,
74 pub page_order: Vec<Uuid>,
78}
79
80#[cfg_attr(feature = "deserializable-endpoint", derive(serde::Deserialize))]
81#[derive(Debug, Serialize, Clone)]
82#[serde(rename_all = "camelCase")]
83pub struct ChapterDraft {
84 pub volume: Option<String>,
86 pub chapter: Option<String>,
88 pub title: Option<String>,
90 pub translated_language: Language,
91 #[serde(skip_serializing_if = "Option::is_none")]
95 pub external_url: Option<Url>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub publish_at: Option<MangaDexDateTime>,
98}
99
100#[cfg_attr(
101 feature = "deserializable-endpoint",
102 derive(serde::Deserialize, getset::Getters, getset::Setters)
103)]
104#[derive(Debug, Serialize, Clone, Default)]
106pub struct CommitUploadSessionBuilder {
107 #[serde(skip)]
108 #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
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}
128
129impl CommitUploadSessionBuilder {
130 pub fn new(http_client: HttpClientRef) -> Self {
131 Self {
132 http_client: Some(http_client),
133 ..Default::default()
134 }
135 }
136
137 #[doc(hidden)]
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 fn validate(&self) -> std::result::Result<(), String> {
211 if self.session_id.is_none() {
212 return Err("session_id cannot be None".to_string());
213 }
214
215 if self.translated_language.is_none() {
216 return Err("translated_language cannot be None".to_string());
217 }
218
219 Ok(())
220 }
221
222 pub fn build(&self) -> Result<CommitUploadSession> {
224 if let Err(error) = self.validate() {
225 return Err(Error::RequestBuilderError(error));
226 }
227
228 let session_id = self
229 .session_id
230 .ok_or(Error::RequestBuilderError(String::from(
231 "session_id must be provided",
232 )))?;
233 let translated_language =
234 self.translated_language
235 .ok_or(Error::RequestBuilderError(String::from(
236 "translated_language must be provided",
237 )))?;
238
239 Ok(CommitUploadSession {
240 http_client: self
241 .http_client
242 .to_owned()
243 .ok_or(Error::RequestBuilderError(String::from(
244 "http_client must be provided",
245 )))?,
246
247 session_id,
248 chapter_draft: ChapterDraft {
249 volume: self.volume.to_owned(),
250 chapter: self.chapter.to_owned(),
251 title: self.title.to_owned(),
252 translated_language,
253 external_url: self.external_url.to_owned(),
254 publish_at: self.publish_at,
255 },
256 page_order: self.page_order.to_owned(),
257 })
258 }
259}
260
261endpoint! {
262 POST ("/upload/{}/commit", session_id),
263 #[body auth] CommitUploadSession,
264 #[rate_limited] ChapterData,
265 CommitUploadSessionBuilder
266}
267
268#[cfg(test)]
269mod tests {
270 use fake::faker::name::en::Name;
271 use fake::Fake;
272 use serde_json::json;
273 use time::OffsetDateTime;
274 use url::Url;
275 use uuid::Uuid;
276 use wiremock::matchers::{body_json, header, method, path_regex};
277 use wiremock::{Mock, MockServer, ResponseTemplate};
278
279 use crate::v5::upload::upload_session_id::commit::post::ChapterDraft;
280 use crate::v5::AuthTokens;
281 use crate::{HttpClient, MangaDexClient};
282 use mangadex_api_types::{Language, MangaDexDateTime, RelationshipType};
283
284 use serde::Serialize;
285
286 #[derive(Clone, Serialize, Debug)]
287 #[serde(rename_all = "camelCase")]
288 struct ExceptedBody {
289 chapter_draft: ChapterDraft,
290 page_order: Vec<Uuid>,
294 }
295
296 #[tokio::test]
297 async fn commit_upload_session_fires_a_request_to_base_url() -> anyhow::Result<()> {
298 let mock_server = MockServer::start().await;
299 let http_client = HttpClient::builder()
300 .base_url(Url::parse(&mock_server.uri())?)
301 .auth_tokens(AuthTokens {
302 session: "sessiontoken".to_string(),
303 refresh: "refreshtoken".to_string(),
304 })
305 .build()?;
306 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
307
308 let session_id = Uuid::new_v4();
309 let session_file_id = Uuid::new_v4();
310 let chapter_id = Uuid::new_v4();
311 let uploader_id = Uuid::new_v4();
312 let chapter_title: String = Name().fake();
313
314 let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
315
316 let expected_body = ExceptedBody {
317 chapter_draft: ChapterDraft {
318 volume: Some(String::from("1")),
319 chapter: Some(String::from("2.5")),
320 title: Some(chapter_title.clone()),
321 translated_language: Language::English,
322 external_url: None,
323 publish_at: None,
324 },
325 page_order: vec![session_file_id],
326 };
327
328 let response_body = json!({
329 "result": "ok",
330 "response": "entity",
331 "data": {
332 "id": chapter_id,
333 "type": "chapter",
334 "attributes": {
335 "title": chapter_title,
336 "volume": "1",
337 "chapter": "2.5",
338 "pages": 4,
339 "translatedLanguage": "en",
340 "uploader": uploader_id,
341 "version": 1,
342 "createdAt": datetime.to_string(),
343 "updatedAt": datetime.to_string(),
344 "publishAt": datetime.to_string(),
345 "readableAt": datetime.to_string(),
346 },
347 "relationships": [],
348 }
349
350 });
351 Mock::given(method("POST"))
352 .and(path_regex(r"/upload/[0-9a-fA-F-]+/commit"))
353 .and(header("authorization", "Bearer sessiontoken"))
354 .and(header("content-type", "application/json"))
355 .and(body_json(expected_body))
356 .respond_with(
357 ResponseTemplate::new(200)
358 .insert_header("x-ratelimit-retry-after", "1698723860")
359 .insert_header("x-ratelimit-limit", "40")
360 .insert_header("x-ratelimit-remaining", "39")
361 .set_body_json(response_body),
362 )
363 .expect(1)
364 .mount(&mock_server)
365 .await;
366
367 let res = mangadex_client
368 .upload()
369 .upload_session_id(session_id)
370 .commit()
371 .post()
372 .volume(Some("1".to_string()))
373 .chapter(Some("2.5".to_string()))
374 .title(Some(chapter_title.clone()))
375 .translated_language(Language::English)
376 .page_order(vec![session_file_id])
377 .send()
378 .await?;
379
380 let res = &res.data;
381
382 assert_eq!(res.id, chapter_id);
383 assert_eq!(res.type_, RelationshipType::Chapter);
384 assert_eq!(res.attributes.title, Some(chapter_title.clone()));
385 assert_eq!(res.attributes.volume, Some("1".to_string()));
386 assert_eq!(res.attributes.chapter, Some("2.5".to_string()));
387 assert_eq!(res.attributes.pages, 4);
388 assert_eq!(res.attributes.translated_language, Language::English);
389 assert_eq!(res.attributes.external_url, None);
390 assert_eq!(res.attributes.version, 1);
391 assert_eq!(res.attributes.created_at.to_string(), datetime.to_string());
392 assert_eq!(
393 res.attributes.updated_at.as_ref().unwrap().to_string(),
394 datetime.to_string()
395 );
396 assert_eq!(
397 res.attributes.publish_at.unwrap().to_string(),
398 datetime.to_string()
399 );
400 assert_eq!(
401 res.attributes.readable_at.unwrap().to_string(),
402 datetime.to_string()
403 );
404
405 Ok(())
406 }
407}