mangadex_api/v5/user/follows/manga/feed/
get.rs1use derive_builder::Builder;
43use serde::Serialize;
44use uuid::Uuid;
45
46use crate::HttpClientRef;
47use mangadex_api_schema::v5::ChapterListResponse;
48use mangadex_api_types::{
49 ContentRating, IncludeExternalUrl, IncludeFuturePages, IncludeFuturePublishAt,
50 IncludeFutureUpdates, Language, MangaDexDateTime, MangaFeedSortOrder,
51 ReferenceExpansionResource,
52};
53
54#[cfg_attr(
55 feature = "deserializable-endpoint",
56 derive(serde::Deserialize, getset::Getters, getset::Setters)
57)]
58#[derive(Debug, Serialize, Clone, Builder, Default)]
59#[serde(rename_all = "camelCase")]
60#[builder(
61 setter(into, strip_option),
62 default,
63 build_fn(error = "mangadex_api_types::error::BuilderError")
64)]
65#[cfg_attr(feature = "non_exhaustive", non_exhaustive)]
66#[cfg_attr(
67 feature = "custom_list_v2",
68 deprecated(
69 since = "3.0.0-rc.1",
70 note = "After the introduction of the Subscription system, this endpoint will be removed in v3"
71 )
72)]
73pub struct GetFollowedMangaFeed {
74 #[doc(hidden)]
76 #[serde(skip)]
77 #[builder(pattern = "immutable")]
78 #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
79 pub http_client: HttpClientRef,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub limit: Option<u32>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub offset: Option<u32>,
85 #[builder(setter(each = "add_translated_language"))]
86 #[serde(skip_serializing_if = "Vec::is_empty")]
87 pub translated_language: Vec<Language>,
88 #[builder(setter(each = "add_original_language"))]
89 #[serde(skip_serializing_if = "Vec::is_empty")]
90 pub original_language: Vec<Language>,
91 #[builder(setter(each = "exclude_original_language"))]
92 #[serde(skip_serializing_if = "Vec::is_empty")]
93 pub excluded_original_language: Vec<Language>,
94 #[builder(setter(each = "add_content_rating"))]
95 #[serde(skip_serializing_if = "Vec::is_empty")]
96 pub content_rating: Vec<ContentRating>,
97 #[builder(setter(each = "excluded_group"))]
99 #[serde(skip_serializing_if = "Vec::is_empty")]
100 pub excluded_groups: Vec<Uuid>,
101 #[builder(setter(each = "excluded_uploader"))]
103 #[serde(skip_serializing_if = "Vec::is_empty")]
104 pub excluded_uploaders: Vec<Uuid>,
105 #[serde(skip_serializing_if = "Option::is_none")]
109 pub include_future_updates: Option<IncludeFutureUpdates>,
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub created_at_since: Option<MangaDexDateTime>,
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub updated_at_since: Option<MangaDexDateTime>,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub publish_at_since: Option<MangaDexDateTime>,
119 #[serde(skip_serializing_if = "Option::is_none")]
120 pub order: Option<MangaFeedSortOrder>,
121 #[builder(setter(each = "include"))]
122 #[serde(skip_serializing_if = "Vec::is_empty")]
123 pub includes: Vec<ReferenceExpansionResource>,
124 #[builder(default)]
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub include_empty_pages: Option<IncludeFuturePages>,
127 #[builder(default)]
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub include_future_publish_at: Option<IncludeFuturePublishAt>,
130 #[builder(default)]
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub include_external_url: Option<IncludeExternalUrl>,
133}
134
135endpoint! {
136 GET "/user/follows/manga/feed",
137 #[query auth] GetFollowedMangaFeed,
138 #[flatten_result] ChapterListResponse,
139 GetFollowedMangaFeedBuilder
140}
141
142#[cfg(test)]
143mod tests {
144 use fake::faker::name::en::Name;
145 use fake::Fake;
146 use serde_json::json;
147 use time::OffsetDateTime;
148 use url::Url;
149 use uuid::Uuid;
150 use wiremock::matchers::{header, method, path_regex};
151 use wiremock::{Mock, MockServer, ResponseTemplate};
152
153 use crate::v5::AuthTokens;
154 use crate::{HttpClient, MangaDexClient};
155 use mangadex_api_types::MangaDexDateTime;
156
157 #[tokio::test]
158 async fn get_followed_manga_feed_fires_a_request_to_base_url() -> anyhow::Result<()> {
159 let mock_server = MockServer::start().await;
160 let http_client: HttpClient = HttpClient::builder()
161 .base_url(Url::parse(&mock_server.uri())?)
162 .auth_tokens(AuthTokens {
163 session: "sessiontoken".to_string(),
164 refresh: "refreshtoken".to_string(),
165 })
166 .build()?;
167 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
168
169 let chapter_id = Uuid::new_v4();
170 let uploader_id = Uuid::new_v4();
171 let chapter_title: String = Name().fake();
172
173 let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
174
175 let response_body = json!({
176 "result": "ok",
177 "response": "collection",
178 "data": [
179 {
180 "id": chapter_id,
181 "type": "chapter",
182 "attributes": {
183 "title": chapter_title,
184 "volume": "1",
185 "chapter": "1.5",
186 "pages": 4,
187 "translatedLanguage": "en",
188 "uploader": uploader_id,
189 "version": 1,
190 "createdAt": datetime.to_string(),
191 "updatedAt": datetime.to_string(),
192 "publishAt": datetime.to_string(),
193 "readableAt": datetime.to_string(),
194 },
195 "relationships": [],
196 },
197 ],
198 "limit": 1,
199 "offset": 0,
200 "total": 1
201 });
202
203 Mock::given(method("GET"))
204 .and(path_regex(r"/user/follows/manga/feed"))
205 .and(header("Authorization", "Bearer sessiontoken"))
206 .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
207 .expect(1)
208 .mount(&mock_server)
209 .await;
210
211 let _ = mangadex_client
212 .user()
213 .follows()
214 .manga()
215 .feed()
216 .get()
217 .limit(1u32)
218 .send()
219 .await?;
220
221 Ok(())
222 }
223}