mangadex_api/v5/author/
post.rs1use derive_builder::Builder;
43use serde::Serialize;
44use url::Url;
45
46use crate::HttpClientRef;
47use mangadex_api_schema::v5::{AuthorData, LocalizedString};
48
49#[cfg_attr(
50 feature = "deserializable-endpoint",
51 derive(serde::Deserialize, getset::Getters, getset::Setters)
52)]
53#[derive(Debug, Serialize, Clone, Builder)]
54#[serde(rename_all = "camelCase")]
55#[builder(
56 setter(into, strip_option),
57 build_fn(error = "mangadex_api_types::error::BuilderError")
58)]
59#[cfg_attr(feature = "non_exhaustive", non_exhaustive)]
60pub struct CreateAuthor {
61 #[doc(hidden)]
63 #[serde(skip)]
64 #[builder(pattern = "immutable")]
65 #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
66 pub http_client: HttpClientRef,
67
68 pub name: String,
69
70 #[serde(skip_serializing_if = "Option::is_none")]
71 #[builder(default)]
72 pub biography: Option<LocalizedString>,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
78 #[builder(default)]
79 pub twitter: Option<Option<Url>>,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
85 #[builder(default)]
86 pub pixiv: Option<Option<Url>>,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
92 #[builder(default)]
93 pub melon_book: Option<Option<Url>>,
94
95 #[serde(skip_serializing_if = "Option::is_none")]
99 #[builder(default)]
100 pub fan_box: Option<Option<Url>>,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
106 #[builder(default)]
107 pub booth: Option<Option<Url>>,
108
109 #[serde(skip_serializing_if = "Option::is_none")]
113 #[builder(default)]
114 pub nico_video: Option<Option<Url>>,
115
116 #[serde(skip_serializing_if = "Option::is_none")]
120 #[builder(default)]
121 pub skeb: Option<Option<Url>>,
122
123 #[serde(skip_serializing_if = "Option::is_none")]
127 #[builder(default)]
128 pub fantia: Option<Option<Url>>,
129
130 #[serde(skip_serializing_if = "Option::is_none")]
134 #[builder(default)]
135 pub tumblr: Option<Option<Url>>,
136
137 #[serde(skip_serializing_if = "Option::is_none")]
141 #[builder(default)]
142 pub youtube: Option<Option<Url>>,
143
144 #[serde(skip_serializing_if = "Option::is_none")]
150 #[builder(default)]
151 pub weibo: Option<Option<Url>>,
152
153 #[serde(skip_serializing_if = "Option::is_none")]
157 #[builder(default)]
158 pub naver: Option<Option<Url>>,
159
160 #[serde(skip_serializing_if = "Option::is_none")]
162 #[builder(default)]
163 pub website: Option<Option<Url>>,
164
165 #[builder(default = "CreateAuthor::default_version()")]
166 pub version: u16,
167}
168
169impl CreateAuthor {
170 fn default_version() -> u16 {
171 1
172 }
173}
174
175endpoint! {
176 POST ("/author"),
177 #[body auth] CreateAuthor,
178 #[rate_limited] AuthorData,
179 CreateAuthorBuilder
180}
181
182#[cfg(test)]
183mod tests {
184 use fake::faker::lorem::en::Sentence;
185 use fake::faker::name::en::Name;
186 use fake::Fake;
187 use serde_json::json;
188 use time::OffsetDateTime;
189 use url::Url;
190 use uuid::Uuid;
191 use wiremock::matchers::{body_json, header, method, path};
192 use wiremock::{Mock, MockServer, ResponseTemplate};
193
194 use crate::v5::AuthTokens;
195 use crate::{HttpClient, MangaDexClient};
196 use mangadex_api_types::MangaDexDateTime;
197
198 #[tokio::test]
199 async fn create_author_fires_a_request_to_base_url() -> anyhow::Result<()> {
200 let mock_server = MockServer::start().await;
201 let http_client = HttpClient::builder()
202 .base_url(Url::parse(&mock_server.uri())?)
203 .auth_tokens(AuthTokens {
204 session: "sessiontoken".to_string(),
205 refresh: "refreshtoken".to_string(),
206 })
207 .build()?;
208 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
209
210 let author_id = Uuid::new_v4();
211 let author_name: String = Name().fake();
212 let author_biography: String = Sentence(1..2).fake();
213
214 let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
215
216 let expected_body = json!({
217 "name": author_name,
218 "version": 1
219 });
220
221 let response_body = json!({
222 "result": "ok",
223 "response": "entity",
224 "data": {
225 "id": author_id,
226 "type": "author",
227 "attributes": {
228 "name": author_name,
229 "imageUrl": "",
230 "biography": {
231 "en": author_biography,
232 },
233 "twitter": null,
234 "pixiv": null,
235 "melonBook": null,
236 "fanBox": null,
237 "booth": null,
238 "nicoVideo": null,
239 "skeb": null,
240 "fantia": null,
241 "tumblr": null,
242 "youtube": null,
243 "weibo": null,
244 "naver": null,
245 "website": null,
246 "version": 2,
247 "createdAt": datetime.to_string(),
248 "updatedAt": datetime.to_string(),
249
250 },
251 "relationships": [],
252 }
253 });
254
255 Mock::given(method("POST"))
256 .and(path("/author"))
257 .and(header("Authorization", "Bearer sessiontoken"))
258 .and(header("Content-Type", "application/json"))
259 .and(body_json(expected_body))
260 .respond_with(
261 ResponseTemplate::new(200)
262 .insert_header("x-ratelimit-retry-after", "1698723860")
263 .insert_header("x-ratelimit-limit", "40")
264 .insert_header("x-ratelimit-remaining", "39")
265 .set_body_json(response_body),
266 )
267 .expect(1)
268 .mount(&mock_server)
269 .await;
270
271 let _ = mangadex_client
272 .author()
273 .post()
274 .name(author_name.as_str())
275 .send()
276 .await?;
277
278 Ok(())
279 }
280}