mangadex_api/v5/scanlation_group/
get.rs1use 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 #[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}