mangadex_api/v5/author/
get.rs

1//! Builder for the author list endpoint.
2//!
3//! <https://api.mangadex.org/docs/swagger.html#/Author/get-author>
4//!
5//! # Examples
6//!
7//! ```rust
8//! use mangadex_api::v5::MangaDexClient;
9//!
10//! # async fn run() -> anyhow::Result<()> {
11//! let client = MangaDexClient::default();
12//!
13//! let author_res = client
14//!     .author()
15//!     .get()
16//!     .name("carlo zen")
17//!     .send()
18//!     .await?;
19//!
20//! println!("authors: {:?}", author_res);
21//! # Ok(())
22//! # }
23//! ```
24
25use 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    /// This should never be set manually as this is only for internal use.
47    #[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}