mangadex_api/v5/custom_list/id/feed/
get.rs

1//! Builder for the custom list manga feed endpoint to get a list of new chapters for a given list.
2//!
3//! <https://api.mangadex.org/swagger.html#/CustomList/get-list-id-feed>
4//!
5//! # Examples
6//!
7//! ```rust
8//! use uuid::Uuid;
9//!
10//! use mangadex_api::v5::MangaDexClient;
11//!
12//! # async fn run() -> anyhow::Result<()> {
13//! let client = MangaDexClient::default();
14//!
15//! let list_id = Uuid::new_v4();
16//! let res = client
17//!     .custom_list()
18//!     .id(list_id)
19//!     .feed()
20//!     .get()
21//!     .limit(1_u32)
22//!     .send()
23//!     .await?;
24//!
25//! println!("Manga feed: {:?}", res);
26//! # Ok(())
27//! # }
28//! ```
29
30use 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    /// This should never be set manually as this is only for internal use.
55    #[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    /// Groups to exclude from the results.
83    #[builder(setter(each = "excluded_group"), default)]
84    #[serde(skip_serializing_if = "Vec::is_empty")]
85    pub excluded_groups: Vec<Uuid>,
86    /// Uploaders to exclude from the results.
87    #[builder(setter(each = "excluded_uploader"), default)]
88    #[serde(skip_serializing_if = "Vec::is_empty")]
89    pub excluded_uploaders: Vec<Uuid>,
90    /// Flag to include future chapter updates in the results.
91    ///
92    /// Default: `IncludeFutureUpdates::Include` (1)
93    #[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    /// DateTime string with following format: `YYYY-MM-DDTHH:MM:SS`.
106    #[builder(default)]
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub created_at_since: Option<MangaDexDateTime>,
109    /// DateTime string with following format: `YYYY-MM-DDTHH:MM:SS`.
110    #[builder(default)]
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub updated_at_since: Option<MangaDexDateTime>,
113    /// DateTime string with following format: `YYYY-MM-DDTHH:MM:SS`.
114    #[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}