mangadex_api/v5/author/
get.rs1use derive_builder::Builder;
26use serde::Serialize;
27use uuid::Uuid;
28
29use crate::HttpClientRef;
30use mangadex_api_schema::v5::AuthorListResponse;
31use mangadex_api_types::{AuthorSortOrder, ReferenceExpansionResource};
32
33#[cfg_attr(
34 feature = "deserializable-endpoint",
35 derive(serde::Deserialize, getset::Getters, getset::Setters)
36)]
37#[derive(Debug, Serialize, Clone, Builder, Default)]
38#[serde(rename_all = "camelCase")]
39#[builder(
40 setter(into, strip_option),
41 default,
42 build_fn(error = "mangadex_api_types::error::BuilderError")
43)]
44#[cfg_attr(feature = "non_exhaustive", non_exhaustive)]
45pub struct ListAuthor {
46 #[doc(hidden)]
48 #[serde(skip)]
49 #[builder(pattern = "immutable")]
50 #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
51 pub http_client: HttpClientRef,
52
53 #[serde(skip_serializing_if = "Option::is_none")]
54 #[builder(default)]
55 pub limit: Option<u32>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 #[builder(default)]
58 pub offset: Option<u32>,
59 #[serde(rename = "ids")]
60 #[builder(default)]
61 #[builder(setter(each = "add_author"))]
62 #[serde(skip_serializing_if = "Vec::is_empty")]
63 pub author_ids: Vec<Uuid>,
64 #[builder(default)]
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub name: Option<String>,
67 #[builder(default)]
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub order: Option<AuthorSortOrder>,
70 #[builder(default)]
71 #[builder(setter(each = "include"))]
72 #[serde(skip_serializing_if = "Vec::is_empty")]
73 pub includes: Vec<ReferenceExpansionResource>,
74}
75
76endpoint! {
77 GET "/author",
78 #[query] ListAuthor,
79 #[flatten_result] AuthorListResponse,
80 ListAuthorBuilder
81}
82
83#[cfg(test)]
84mod tests {
85 use fake::faker::lorem::en::Sentence;
86 use fake::faker::name::en::Name;
87 use fake::Fake;
88 use serde_json::json;
89 use time::OffsetDateTime;
90 use url::Url;
91 use uuid::Uuid;
92 use wiremock::matchers::{method, path};
93 use wiremock::{Mock, MockServer, ResponseTemplate};
94
95 use crate::{HttpClient, MangaDexClient};
96 use mangadex_api_types::error::Error;
97 use mangadex_api_types::{Language, MangaDexDateTime, ResponseType};
98
99 #[tokio::test]
100 async fn list_author_fires_a_request_to_base_url() -> anyhow::Result<()> {
101 let mock_server = MockServer::start().await;
102 let http_client = HttpClient::builder()
103 .base_url(Url::parse(&mock_server.uri())?)
104 .build()?;
105 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
106
107 let author_id = Uuid::new_v4();
108 let author_name: String = Name().fake();
109 let author_biography: String = Sentence(1..2).fake();
110
111 let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
112
113 let response_body = json!({
114 "result": "ok",
115 "response": "collection",
116 "data": [
117 {
118 "id": author_id,
119 "type": "author",
120 "attributes": {
121 "name": author_name,
122 "imageUrl": "",
123 "biography": {
124 "en": author_biography,
125 },
126 "twitter": null,
127 "pixiv": null,
128 "melonBook": null,
129 "fanBox": null,
130 "booth": null,
131 "nicoVideo": null,
132 "skeb": null,
133 "fantia": null,
134 "tumblr": null,
135 "youtube": null,
136 "weibo": null,
137 "naver": null,
138 "website": null,
139 "version": 1,
140 "createdAt": datetime.to_string(),
141 "updatedAt": datetime.to_string(),
142 },
143 "relationships": []
144 }
145 ],
146 "limit": 1,
147 "offset": 0,
148 "total": 1
149 });
150
151 Mock::given(method("GET"))
152 .and(path("/author"))
153 .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
154 .expect(1)
155 .mount(&mock_server)
156 .await;
157
158 let res = mangadex_client.author().get().limit(1u32).send().await?;
159
160 assert_eq!(res.response, ResponseType::Collection);
161 let author = &res.data[0];
162 assert_eq!(author.id, author_id);
163 assert_eq!(author.attributes.name, author_name);
164 assert_eq!(author.attributes.image_url, Some("".to_string()));
165 assert_eq!(
166 author.attributes.biography.get(&Language::English),
167 Some(&author_biography)
168 );
169 assert_eq!(author.attributes.version, 1);
170 assert_eq!(
171 author.attributes.created_at.to_string(),
172 datetime.to_string()
173 );
174 assert_eq!(
175 author.attributes.updated_at.as_ref().unwrap().to_string(),
176 datetime.to_string()
177 );
178
179 Ok(())
180 }
181
182 #[tokio::test]
183 async fn list_author_handles_400() -> anyhow::Result<()> {
184 let mock_server = MockServer::start().await;
185 let http_client: HttpClient = HttpClient::builder()
186 .base_url(Url::parse(&mock_server.uri())?)
187 .build()?;
188 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
189
190 let error_id = Uuid::new_v4();
191
192 let response_body = json!({
193 "result": "error",
194 "errors": [{
195 "id": error_id.to_string(),
196 "status": 400,
197 "title": "Invalid limit",
198 "detail": "Limit must be between 1 and 100"
199 }]
200 });
201
202 Mock::given(method("GET"))
203 .and(path(r"/author"))
204 .respond_with(ResponseTemplate::new(400).set_body_json(response_body))
205 .expect(1)
206 .mount(&mock_server)
207 .await;
208
209 let res = mangadex_client
210 .author()
211 .get()
212 .limit(0u32)
213 .send()
214 .await
215 .expect_err("expected error");
216
217 if let Error::Api(errors) = res {
218 assert_eq!(errors.errors.len(), 1);
219
220 assert_eq!(errors.errors[0].id, error_id);
221 assert_eq!(errors.errors[0].status, 400);
222 assert_eq!(errors.errors[0].title, Some("Invalid limit".to_string()));
223 assert_eq!(
224 errors.errors[0].detail,
225 Some("Limit must be between 1 and 100".to_string())
226 );
227 }
228
229 Ok(())
230 }
231}