Skip to main content

mangadex_api/v5/upload/
commit_session.rs

1//! Builder for committing an active upload session.
2//!
3//! <https://api.mangadex.org/swagger.html#/Upload/commit-upload-session>
4//!
5//! # Examples
6//!
7//! ```rust
8//! use uuid::Uuid;
9//!
10//! use mangadex_api::types::Language;
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//! let _login_res = client
18//!     .auth()
19//!     .login()
20//!     .username(Username::parse("myusername")?)
21//!     .password(Password::parse("hunter23")?)
22//!     .build()?
23//!     .send()
24//!     .await?;
25//!
26//! let session_id = Uuid::new_v4();
27//! let res = client
28//!     .upload()
29//!     .commit_session()
30//!     .session_id(&session_id)
31//!     .volume(Some("1"))
32//!     .chapter(Some("1"))
33//!     .title(Some("Chapter Title"))
34//!     .translated_language(Language::English)
35//!     .build()?
36//!     .send()
37//!     .await?;
38//!
39//! println!("commit upload session: {:?}", res);
40//! # Ok(())
41//! # }
42//! ```
43
44use mangadex_api_schema::v5::ChapterObject;
45use serde::Serialize;
46use url::Url;
47use uuid::Uuid;
48
49use crate::HttpClientRef;
50use mangadex_api_types::error::{Error, Result};
51use mangadex_api_types::{Language, MangaDexDateTime};
52
53#[derive(Debug, Serialize, Clone)]
54#[serde(rename_all = "camelCase")]
55pub struct CommitUploadSession<'a> {
56    /// This should never be set manually as this is only for internal use.
57    #[serde(skip)]
58    pub(crate) http_client: HttpClientRef,
59
60    #[serde(skip)]
61    pub session_id: &'a Uuid,
62
63    chapter_draft: ChapterDraft<'a>,
64    /// Ordered list of Upload Session File IDs.
65    ///
66    /// Any uploaded files that are not included in this list will be deleted.
67    pub page_order: Vec<Uuid>,
68}
69
70#[derive(Debug, Serialize, Clone)]
71#[serde(rename_all = "camelCase")]
72pub struct ChapterDraft<'a> {
73    /// Nullable
74    pub volume: Option<&'a str>,
75    /// Nullable
76    pub chapter: Option<&'a str>,
77    /// Nullable
78    pub title: Option<&'a str>,
79    pub translated_language: Language,
80    /// Must be a URL with "http(s)://".
81    ///
82    /// Nullable
83    pub external_url: Option<&'a Url>,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub publish_at: Option<MangaDexDateTime>,
86}
87
88/// Custom request builder to handle nested struct.
89#[derive(Debug, Serialize, Clone, Default)]
90pub struct CommitUploadSessionBuilder<'a> {
91    #[serde(skip)]
92    pub(crate) http_client: HttpClientRef,
93
94    pub session_id: Option<&'a Uuid>,
95    /// Ordered list of Upload Session File IDs.
96    pub page_order: Vec<Uuid>,
97
98    /// Nullable
99    pub volume: Option<&'a str>,
100    /// Nullable
101    pub chapter: Option<&'a str>,
102    /// Nullable
103    pub title: Option<&'a str>,
104    pub translated_language: Option<Language>,
105    /// Must be a URL with "http(s)://".
106    ///
107    /// Nullable
108    pub external_url: Option<&'a Url>,
109    pub publish_at: Option<MangaDexDateTime>,
110}
111
112impl<'a> CommitUploadSessionBuilder<'a> {
113    pub fn new(http_client: HttpClientRef) -> Self {
114        Self {
115            http_client,
116            ..Default::default()
117        }
118    }
119
120    /// Specify the upload session ID to commit.
121    pub fn session_id(mut self, session_id: &'a Uuid) -> Self {
122        self.session_id = Some(session_id);
123        self
124    }
125
126    /// Specify the Upload Session File IDs to commit, ordered.
127    pub fn page_order(mut self, page_order: Vec<Uuid>) -> Self {
128        self.page_order = page_order;
129        self
130    }
131
132    /// Add an Upload Session File ID to commit, adds to the end of the `pageOrder` list.
133    pub fn add_page(mut self, page: Uuid) -> Self {
134        self.page_order.push(page);
135        self
136    }
137
138    /// Specify the volume the chapter belongs to.
139    ///
140    /// Nullable
141    pub fn volume(mut self, volume: Option<&'a str>) -> Self {
142        self.volume = volume;
143        self
144    }
145
146    /// Specify the chapter number the session is for.
147    ///
148    /// Nullable
149    pub fn chapter(mut self, chapter: Option<&'a str>) -> Self {
150        self.chapter = chapter;
151        self
152    }
153
154    /// Specify the title for the chapter.
155    ///
156    /// Nullable
157    pub fn title(mut self, title: Option<&'a str>) -> Self {
158        self.title = title;
159        self
160    }
161
162    /// Specify the chapter number the session is for.
163    ///
164    /// Nullable
165    pub fn translated_language(mut self, translated_language: Language) -> Self {
166        self.translated_language = Some(translated_language);
167        self
168    }
169
170    /// Specify the URL where the chapter can be found.
171    ///
172    /// Nullable
173    ///
174    /// This should not be used if chapter has images uploaded to MangaDex.
175    pub fn external_url(mut self, external_url: Option<&'a Url>) -> Self {
176        self.external_url = external_url;
177        self
178    }
179
180    /// Specify the date and time the chapter was originally published at.
181    pub fn publish_at<DT: Into<MangaDexDateTime>>(mut self, publish_at: DT) -> Self {
182        self.publish_at = Some(publish_at.into());
183        self
184    }
185
186    /// Validate the field values. Use this before building.
187    fn validate(&self) -> std::result::Result<(), String> {
188        if self.session_id.is_none() {
189            return Err("session_id cannot be None".to_string());
190        }
191
192        if self.translated_language.is_none() {
193            return Err("translated_language cannot be None".to_string());
194        }
195
196        Ok(())
197    }
198
199    /// Finalize the changes to the request struct and return the new struct.
200    pub fn build(self) -> Result<CommitUploadSession<'a>> {
201        if let Err(error) = self.validate() {
202            return Err(Error::RequestBuilderError(error));
203        }
204
205        let session_id = self.session_id.unwrap();
206        let translated_language = self.translated_language.unwrap();
207
208        Ok(CommitUploadSession {
209            http_client: self.http_client,
210
211            session_id,
212            chapter_draft: ChapterDraft {
213                volume: self.volume,
214                chapter: self.chapter,
215                title: self.title,
216                translated_language,
217                external_url: self.external_url,
218                publish_at: self.publish_at,
219            },
220            page_order: self.page_order,
221        })
222    }
223}
224
225endpoint! {
226    PUT ("/upload/{}/commit", session_id),
227    #[body auth] CommitUploadSession<'_>,
228    ChapterObject
229}
230
231#[cfg(test)]
232mod tests {
233    use fake::faker::name::en::Name;
234    use fake::Fake;
235    use serde_json::json;
236    use time::OffsetDateTime;
237    use url::Url;
238    use uuid::Uuid;
239    use wiremock::matchers::{header, method, path_regex};
240    use wiremock::{Mock, MockServer, ResponseTemplate};
241
242    use crate::v5::AuthTokens;
243    use crate::{HttpClient, MangaDexClient};
244    use mangadex_api_types::{Language, MangaDexDateTime, RelationshipType};
245
246    #[tokio::test]
247    async fn commit_upload_session_fires_a_request_to_base_url() -> anyhow::Result<()> {
248        let mock_server = MockServer::start().await;
249        let http_client = HttpClient::builder()
250            .base_url(Url::parse(&mock_server.uri())?)
251            .auth_tokens(AuthTokens {
252                session: "sessiontoken".to_string(),
253                refresh: "refreshtoken".to_string(),
254            })
255            .build()?;
256        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
257
258        let session_id = Uuid::new_v4();
259        let session_file_id = Uuid::new_v4();
260        let chapter_id = Uuid::new_v4();
261        let uploader_id = Uuid::new_v4();
262        let chapter_title: String = Name().fake();
263
264        let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
265
266        let _expected_body = json!({
267            "chapterDraft": {
268                "volume": "1",
269                "chapter": "2.5",
270                "title": chapter_title,
271                "translatedLanguage": "en",
272                "externalUrl": null
273            },
274            "pageOrder": [
275                session_file_id
276            ]
277        });
278        let response_body = json!({
279            "id": chapter_id,
280            "type": "chapter",
281            "attributes": {
282                "title": chapter_title,
283                "volume": "1",
284                "chapter": "2.5",
285                "pages": 4,
286                "translatedLanguage": "en",
287                "uploader": uploader_id,
288                "version": 1,
289                "createdAt": datetime.to_string(),
290                "updatedAt": datetime.to_string(),
291                "publishAt": datetime.to_string(),
292                "readableAt": datetime.to_string(),
293            },
294            "relationships": [],
295        });
296
297        Mock::given(method("PUT"))
298            .and(path_regex(r"/upload/[0-9a-fA-F-]+/commit"))
299            .and(header("Authorization", "Bearer sessiontoken"))
300            .and(header("Content-Type", "application/json"))
301            // TODO: Make the request body check work.
302            // .and(body_json(expected_body))
303            .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
304            .expect(1)
305            .mount(&mock_server)
306            .await;
307
308        let res = mangadex_client
309            .upload()
310            .commit_session()
311            .session_id(&session_id)
312            .volume(Some("1"))
313            .chapter(Some("2.5"))
314            .title(Some(&chapter_title))
315            .translated_language(Language::English)
316            .page_order(vec![session_file_id])
317            .build()?
318            .send()
319            .await?;
320
321        assert_eq!(res.id, chapter_id);
322        assert_eq!(res.type_, RelationshipType::Chapter);
323        assert_eq!(res.attributes.title, chapter_title);
324        assert_eq!(res.attributes.volume, Some("1".to_string()));
325        assert_eq!(res.attributes.chapter, Some("2.5".to_string()));
326        assert_eq!(res.attributes.pages, 4);
327        assert_eq!(res.attributes.translated_language, Language::English);
328        assert_eq!(res.attributes.uploader, Some(uploader_id));
329        assert_eq!(res.attributes.external_url, None);
330        assert_eq!(res.attributes.version, 1);
331        assert_eq!(res.attributes.created_at.to_string(), datetime.to_string());
332        assert_eq!(
333            res.attributes.updated_at.as_ref().unwrap().to_string(),
334            datetime.to_string()
335        );
336        assert_eq!(res.attributes.publish_at.to_string(), datetime.to_string());
337        assert_eq!(res.attributes.readable_at.to_string(), datetime.to_string());
338
339        Ok(())
340    }
341}