mangadex_api/v5/custom_list/id/feed/
get.rs1use derive_builder::Builder;
31use serde::Serialize;
32use uuid::Uuid;
33
34use crate::HttpClientRef;
35use mangadex_api_schema::v5::ChapterListResponse;
36use mangadex_api_types::{
37 ContentRating, IncludeExternalUrl, IncludeFuturePages, IncludeFuturePublishAt,
38 IncludeFutureUpdates, Language, MangaDexDateTime, MangaFeedSortOrder,
39 ReferenceExpansionResource,
40};
41
42#[cfg_attr(
43 feature = "deserializable-endpoint",
44 derive(serde::Deserialize, getset::Getters, getset::Setters)
45)]
46#[derive(Debug, Serialize, Clone, Builder)]
47#[serde(rename_all = "camelCase")]
48#[builder(
49 setter(into, strip_option),
50 build_fn(error = "mangadex_api_types::error::BuilderError")
51)]
52#[cfg_attr(feature = "non_exhaustive", non_exhaustive)]
53pub struct CustomListMangaFeed {
54 #[doc(hidden)]
56 #[serde(skip)]
57 #[builder(pattern = "immutable")]
58 #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
59 pub http_client: HttpClientRef,
60
61 #[serde(skip_serializing)]
62 pub list_id: Uuid,
63
64 #[builder(default)]
65 #[serde(skip_serializing_if = "Option::is_none", default)]
66 pub limit: Option<u32>,
67 #[builder(default)]
68 #[serde(skip_serializing_if = "Option::is_none", default)]
69 pub offset: Option<u32>,
70 #[builder(setter(each = "add_translated_language"), default)]
71 #[serde(skip_serializing_if = "Vec::is_empty")]
72 pub translated_language: Vec<Language>,
73 #[builder(setter(each = "add_original_language"), default)]
74 #[serde(skip_serializing_if = "Vec::is_empty")]
75 pub original_language: Vec<Language>,
76 #[builder(setter(each = "exclude_original_language"), default)]
77 #[serde(skip_serializing_if = "Vec::is_empty")]
78 pub excluded_original_language: Vec<Language>,
79 #[builder(setter(each = "add_content_rating"), default)]
80 #[serde(skip_serializing_if = "Vec::is_empty")]
81 pub content_rating: Vec<ContentRating>,
82 #[builder(setter(each = "excluded_group"), default)]
84 #[serde(skip_serializing_if = "Vec::is_empty")]
85 pub excluded_groups: Vec<Uuid>,
86 #[builder(setter(each = "excluded_uploader"), default)]
88 #[serde(skip_serializing_if = "Vec::is_empty")]
89 pub excluded_uploaders: Vec<Uuid>,
90 #[builder(default)]
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub include_future_updates: Option<IncludeFutureUpdates>,
96 #[builder(default)]
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub include_empty_pages: Option<IncludeFuturePages>,
99 #[builder(default)]
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub include_future_publish_at: Option<IncludeFuturePublishAt>,
102 #[builder(default)]
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub include_external_url: Option<IncludeExternalUrl>,
105 #[builder(default)]
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub created_at_since: Option<MangaDexDateTime>,
109 #[builder(default)]
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub updated_at_since: Option<MangaDexDateTime>,
113 #[builder(default)]
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub publish_at_since: Option<MangaDexDateTime>,
117 #[builder(default)]
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub order: Option<MangaFeedSortOrder>,
120 #[builder(setter(each = "include"), default)]
121 #[serde(skip_serializing_if = "Vec::is_empty")]
122 pub includes: Vec<ReferenceExpansionResource>,
123}
124
125endpoint! {
126 GET ("/list/{}/feed", list_id),
127 #[query] CustomListMangaFeed,
128 #[flatten_result] ChapterListResponse,
129 CustomListMangaFeedBuilder
130}
131
132#[cfg(test)]
133mod tests {
134 use serde_json::json;
135 use time::OffsetDateTime;
136 use url::Url;
137 use uuid::Uuid;
138 use wiremock::matchers::{method, path_regex};
139 use wiremock::{Mock, MockServer, ResponseTemplate};
140
141 use crate::v5::AuthTokens;
142 use crate::{HttpClient, MangaDexClient};
143 use mangadex_api_types::MangaDexDateTime;
144
145 #[tokio::test]
146 async fn get_custom_list_manga_feed_fires_a_request_to_base_url() -> anyhow::Result<()> {
147 let mock_server = MockServer::start().await;
148 let http_client: HttpClient = HttpClient::builder()
149 .base_url(Url::parse(&mock_server.uri())?)
150 .auth_tokens(AuthTokens {
151 session: "sessiontoken".to_string(),
152 refresh: "refreshtoken".to_string(),
153 })
154 .build()?;
155 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
156
157 let list_id = Uuid::new_v4();
158 let chapter_id = Uuid::new_v4();
159 let uploader_id = Uuid::new_v4();
160
161 let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
162
163 let response_body = json!({
164 "result": "ok",
165 "response": "collection",
166 "data": [
167 {
168 "id": chapter_id,
169 "type": "chapter",
170 "attributes": {
171 "title": "Chapter title",
172 "volume": null,
173 "chapter": "1",
174 "pages": 4,
175 "translatedLanguage": "en",
176 "hash": "123456abcdef",
177 "data": [
178 "1.jpg"
179 ],
180 "dataSaver": [
181 "1.jpg"
182 ],
183 "uploader": uploader_id,
184 "version": 1,
185 "createdAt": datetime.to_string(),
186 "updatedAt": datetime.to_string(),
187 "publishAt": datetime.to_string(),
188 "readableAt": datetime.to_string(),
189 },
190 "relationships": []
191 }
192 ],
193 "limit": 1,
194 "offset": 0,
195 "total": 1
196 });
197
198 Mock::given(method("GET"))
199 .and(path_regex(r"/list/[0-9a-fA-F-]+/feed"))
200 .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
201 .expect(1)
202 .mount(&mock_server)
203 .await;
204
205 let _ = mangadex_client
206 .custom_list()
207 .id(list_id)
208 .feed()
209 .get()
210 .limit(1u32)
211 .send()
212 .await?;
213
214 Ok(())
215 }
216}