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