mangadex_api/v5/cover/manga_id/
post.rs1use std::borrow::Cow;
42
43use derive_builder::Builder;
44use mangadex_api_schema::v5::CoverData;
45use mangadex_api_schema::Endpoint;
46use mangadex_api_schema::Limited;
47use reqwest::multipart::{Form, Part};
48use serde::Serialize;
49use uuid::Uuid;
50
51use crate::HttpClientRef;
52use mangadex_api_types::{error::Result, Language};
53
54#[cfg_attr(
60 feature = "deserializable-endpoint",
61 derive(serde::Deserialize, getset::Getters, getset::Setters)
62)]
63#[derive(Debug, Serialize, Clone, Builder, Default)]
64#[serde(rename_all = "camelCase")]
65#[builder(
66 setter(into, strip_option),
67 build_fn(error = "mangadex_api_types::error::BuilderError")
68)]
69#[cfg_attr(feature = "non_exhaustive", non_exhaustive)]
70pub struct UploadCover {
71 #[doc(hidden)]
73 #[serde(skip)]
74 #[builder(pattern = "immutable")]
75 #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
76 pub http_client: HttpClientRef,
77
78 #[serde(skip_serializing)]
80 pub manga_id: Uuid,
81
82 pub file: Cow<'static, [u8]>,
84 pub volume: String,
90 #[builder(default)]
91 pub description: String,
92 pub locale: Language,
93}
94
95impl Endpoint for UploadCover {
97 type Query = ();
98 type Body = ();
99 type Response = CoverData;
100
101 fn path(&self) -> Cow<str> {
102 Cow::Owned(format!("/cover/{}", self.manga_id))
103 }
104
105 fn method(&self) -> reqwest::Method {
106 reqwest::Method::POST
107 }
108
109 fn require_auth(&self) -> bool {
110 true
111 }
112
113 fn multipart(&self) -> Option<Form> {
114 let part = Part::bytes(self.file.clone());
115 let mut form = Form::new().part("file", part);
116
117 let volume_part = Part::text(self.volume.clone());
118 form = form.part("volume", volume_part);
119
120 form = form.part("description", Part::text(self.description.to_string()));
121
122 form = form.part("locale", Part::text(self.locale.code2().to_string()));
123
124 Some(form)
125 }
126}
127
128impl UploadCover {
129 pub async fn send(&self) -> Result<Limited<<Self as Endpoint>::Response>> {
130 #[cfg(all(
131 not(feature = "multi-thread"),
132 not(feature = "tokio-multi-thread"),
133 not(feature = "rw-multi-thread")
134 ))]
135 {
136 self.http_client
137 .try_borrow()?
138 .send_request_with_rate_limit(self)
139 .await
140 }
141 #[cfg(any(feature = "multi-thread", feature = "tokio-multi-thread"))]
142 {
143 self.http_client
144 .lock()
145 .await
146 .send_request_with_rate_limit(self)
147 .await
148 }
149 #[cfg(feature = "rw-multi-thread")]
150 {
151 self.http_client
152 .read()
153 .await
154 .send_request_with_rate_limit(self)
155 .await
156 }
157 }
158}
159
160builder_send! {
161 #[builder] UploadCoverBuilder,
162 #[rate_limited] CoverData
163}
164
165#[cfg(test)]
166mod tests {
167 use fake::faker::lorem::en::Sentence;
168 use fake::Fake;
169 use serde_json::json;
170 use time::OffsetDateTime;
171 use url::Url;
172 use uuid::Uuid;
173 use wiremock::matchers::{header, header_exists, method, path_regex};
174 use wiremock::{Mock, MockServer, ResponseTemplate};
175
176 use crate::v5::AuthTokens;
177 use crate::{HttpClient, MangaDexClient};
178 use mangadex_api_types::{Language, MangaDexDateTime};
179
180 #[tokio::test]
181 async fn upload_cover_fires_a_request_to_base_url() -> anyhow::Result<()> {
182 let mock_server = MockServer::start().await;
183 let http_client: HttpClient = HttpClient::builder()
184 .base_url(Url::parse(&mock_server.uri())?)
185 .auth_tokens(AuthTokens {
186 session: "sessiontoken".to_string(),
187 refresh: "refreshtoken".to_string(),
188 })
189 .build()?;
190 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
191
192 let manga_id = Uuid::new_v4();
193 let file_bytes = vec![0_u8];
194 let cover_id = Uuid::new_v4();
195 let description: String = Sentence(1..3).fake();
196
197 let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
198
199 let response_body = json!({
200 "result": "ok",
201 "response": "entity",
202 "data": {
203 "id": cover_id,
204 "type": "cover_art",
205 "attributes": {
206 "volume": "1",
207 "fileName": "1.jpg",
208 "description": &description,
209 "locale": "en",
210 "version": 1,
211 "createdAt": datetime.to_string(),
212 "updatedAt": datetime.to_string(),
213 },
214 "relationships": [],
215 },
216 });
217
218 Mock::given(method("POST"))
219 .and(path_regex("/cover/[0-9a-fA-F-]+"))
220 .and(header("Authorization", "Bearer sessiontoken"))
221 .and(header_exists("Content-Type"))
223 .respond_with(
224 ResponseTemplate::new(201)
225 .insert_header("x-ratelimit-retry-after", "1698723860")
226 .insert_header("x-ratelimit-limit", "40")
227 .insert_header("x-ratelimit-remaining", "39")
228 .set_body_json(response_body),
229 )
230 .expect(1)
231 .mount(&mock_server)
232 .await;
233
234 let _ = mangadex_client
235 .upload()
236 .cover(manga_id)
237 .file(file_bytes)
238 .locale(Language::English)
239 .description(description)
240 .volume("1")
241 .send()
242 .await?;
243
244 Ok(())
245 }
246}