Skip to main content

mangadex_api/v5/scanlation_group/
get.rs

1//! Builder for the scanlation group list endpoint.
2//!
3//! <https://api.mangadex.org/docs/swagger.html#/ScanlationGroup/get-search-group>
4//!
5//! # Examples
6//!
7//! ```rust
8//! use mangadex_api_types::MangaStatus;
9//! use mangadex_api::v5::MangaDexClient;
10//!
11//! # async fn run() -> anyhow::Result<()> {
12//! let client = MangaDexClient::default();
13//!
14//! let group_res = client
15//!     .scanlation_group()
16//!     .get()
17//!     .name("mangadex")
18//!     .send()
19//!     .await?;
20//!
21//! println!("groups: {:?}", group_res);
22//! # Ok(())
23//! # }
24//! ```
25
26use derive_builder::Builder;
27use mangadex_api_schema::v5::GroupCollection;
28use serde::Serialize;
29use uuid::Uuid;
30
31use crate::HttpClientRef;
32use mangadex_api_types::{GroupSortOrder, Language, ReferenceExpansionResource};
33
34#[cfg_attr(
35    feature = "deserializable-endpoint",
36    derive(serde::Deserialize, getset::Getters, getset::Setters)
37)]
38#[derive(Debug, Serialize, Clone, Builder, Default)]
39#[serde(rename_all = "camelCase")]
40#[builder(
41    setter(into, strip_option),
42    default,
43    build_fn(error = "crate::error::BuilderError")
44)]
45#[non_exhaustive]
46pub struct ListGroup {
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    pub limit: Option<u32>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub offset: Option<u32>,
57    #[builder(setter(each = "add_group_id"))]
58    #[serde(skip_serializing_if = "Vec::is_empty")]
59    #[serde(rename = "ids")]
60    pub group_ids: Vec<Uuid>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub name: Option<String>,
63    /// Language the scanlation primarily translates or uploads works into.
64    // The corresponding response body field returns an array so this seems likely to change to accept an array of languages.
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub focused_language: Option<Language>,
67    #[builder(setter(each = "include"))]
68    #[serde(skip_serializing_if = "Vec::is_empty")]
69    pub includes: Vec<ReferenceExpansionResource>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub order: Option<GroupSortOrder>,
72}
73
74endpoint! {
75    GET "/group",
76    #[query] ListGroup,
77    #[flatten_result] crate::Result<GroupCollection>,
78    ListGroupBuilder
79}
80
81#[cfg(test)]
82mod tests {
83    use serde_json::json;
84    use time::OffsetDateTime;
85    use url::Url;
86    use uuid::Uuid;
87    use wiremock::matchers::{method, path, query_param, query_param_is_missing};
88    use wiremock::{Mock, MockServer, ResponseTemplate};
89
90    use crate::error::Error;
91    use crate::{HttpClient, MangaDexClient};
92    use mangadex_api_types::{MangaDexDateTime, ResponseType};
93
94    #[tokio::test]
95    async fn list_scanlation_groups_fires_a_request_to_base_url() -> anyhow::Result<()> {
96        let mock_server = MockServer::start().await;
97        let http_client = HttpClient::builder()
98            .base_url(Url::parse(&mock_server.uri())?)
99            .build()?;
100        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
101
102        let group_id = Uuid::new_v4();
103
104        let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
105
106        let response_body = json!({
107            "result": "ok",
108            "response": "collection",
109            "data": [
110                {
111                    "id": group_id,
112                    "type": "scanlation_group",
113                    "attributes": {
114                        "name": "Scanlation Group",
115                        "altNames": [
116                            {
117                                "en": "Alternative Name"
118                            }
119                        ],
120                        "website": "https://example.org",
121                        "ircServer": null,
122                        "ircChannel": null,
123                        "discord": null,
124                        "contactEmail": null,
125                        "description": null,
126                        "twitter": null,
127                        "focusedLanguages": ["en"],
128                        "locked": false,
129                        "official": false,
130                        "verified": false,
131                        "inactive": false,
132                        "publishDelay": "P6WT5M",
133                        "version": 1,
134                        "createdAt": datetime.to_string(),
135                        "updatedAt": datetime.to_string(),
136                    },
137                    "relationships": []
138                }
139            ],
140            "limit": 1,
141            "offset": 0,
142            "total": 1
143        });
144
145        Mock::given(method("GET"))
146            .and(path("/group"))
147            .and(query_param("limit", "1"))
148            .and(query_param_is_missing("offset"))
149            .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
150            .expect(1)
151            .mount(&mock_server)
152            .await;
153
154        let res = mangadex_client
155            .scanlation_group()
156            .get()
157            .limit(1u32)
158            .send()
159            .await?;
160
161        assert_eq!(res.response, ResponseType::Collection);
162        let group = &res.data[0];
163        assert_eq!(group.id, group_id);
164        assert_eq!(group.attributes.name, "Scanlation Group");
165        assert_eq!(
166            group.attributes.website,
167            Some("https://example.org".to_string())
168        );
169        assert_eq!(group.attributes.irc_server, None);
170        assert_eq!(group.attributes.irc_channel, None);
171        assert_eq!(group.attributes.discord, None);
172        assert_eq!(group.attributes.contact_email, None);
173        assert_eq!(group.attributes.description, None);
174        assert!(group.attributes.twitter.is_none());
175        assert!(!group.attributes.locked);
176        assert_eq!(group.attributes.version, 1);
177        assert_eq!(
178            group.attributes.created_at.to_string(),
179            datetime.to_string()
180        );
181        assert_eq!(
182            group.attributes.updated_at.to_string(),
183            datetime.to_string()
184        );
185
186        Ok(())
187    }
188
189    #[tokio::test]
190    async fn list_scanlation_groups_handles_400() -> anyhow::Result<()> {
191        let mock_server = MockServer::start().await;
192        let http_client: HttpClient = HttpClient::builder()
193            .base_url(Url::parse(&mock_server.uri())?)
194            .build()?;
195        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
196
197        let error_id = Uuid::new_v4();
198
199        let response_body = json!({
200            "result": "error",
201            "errors": [{
202                "id": error_id.to_string(),
203                "status": 400,
204                "title": "Invalid limit",
205                "detail": "Limit must be between 1 and 100"
206            }]
207        });
208
209        Mock::given(method("GET"))
210            .and(path("/group"))
211            .respond_with(ResponseTemplate::new(400).set_body_json(response_body))
212            .expect(1)
213            .mount(&mock_server)
214            .await;
215
216        let res = mangadex_client
217            .scanlation_group()
218            .get()
219            .limit(0u32)
220            .send()
221            .await
222            .expect_err("expected error");
223
224        if let Error::Api(errors) = res {
225            assert_eq!(errors.errors.len(), 1);
226
227            assert_eq!(errors.errors[0].id, error_id);
228            assert_eq!(errors.errors[0].status, 400);
229            assert_eq!(errors.errors[0].title, Some("Invalid limit".to_string()));
230            assert_eq!(
231                errors.errors[0].detail,
232                Some("Limit must be between 1 and 100".to_string())
233            );
234        }
235
236        Ok(())
237    }
238}