mangadex_api/v5/user/follows/list/id/
get.rs

1//! Builder for checking if the logged-in user follows a custom list.
2//!
3//! <https://api.mangadex.org/swagger.html#/Follows/get-user-follows-list-id>
4//!
5//! # Examples
6//!
7//! ```rust
8//! use uuid::Uuid;
9//!
10//! // use mangadex_api_types::{Password, Username};
11//! use mangadex_api::MangaDexClient;
12//!
13//! # async fn run() -> anyhow::Result<()> {
14//! let client = MangaDexClient::default();
15//!
16//! /*
17//!
18//!     let _login_res = client
19//!         .auth()
20//!         .login()
21//!         .post()
22//!         .username(Username::parse("myusername")?)
23//!         .password(Password::parse("hunter23")?)
24//!         .send()
25//!         .await?;
26//!
27//!  */
28//!
29//! let custom_list_id = Uuid::new_v4();
30//!
31//! let res = client
32//!     .user()
33//!     .follows()
34//!     .list()
35//!     .id(custom_list_id)
36//!     .get()
37//!     .send()
38//!     .await?;
39//!
40//! println!("check: {:?}", res);
41//! # Ok(())
42//! # }
43//! ```
44
45use derive_builder::Builder;
46use mangadex_api_schema::v5::IsFollowingResponse;
47use mangadex_api_schema::{FromResponse, NoData};
48use serde::Serialize;
49use uuid::Uuid;
50
51use crate::HttpClientRef;
52use mangadex_api_types::error::{Error, Result};
53
54/// Check if the logged-in user follows a custom list.
55///
56/// Makes a request to `GET /user/follows/list/{id}`.
57#[cfg_attr(
58    feature = "deserializable-endpoint",
59    derive(serde::Deserialize, getset::Getters, getset::Setters)
60)]
61#[derive(Debug, Builder, Serialize, Clone)]
62#[serde(rename_all = "camelCase")]
63#[builder(
64    setter(into, strip_option),
65    build_fn(error = "mangadex_api_types::error::BuilderError")
66)]
67#[cfg_attr(
68    feature = "custom_list_v2",
69    deprecated(
70        since = "3.0.0-rc.1",
71        note = "After the introduction of the Subscription system, this endpoint will be removed in v3"
72    )
73)]
74pub struct IsFollowingCustomList {
75    /// This should never be set manually as this is only for internal use.
76    #[doc(hidden)]
77    #[serde(skip)]
78    #[builder(pattern = "immutable")]
79    #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
80    pub http_client: HttpClientRef,
81
82    pub list_id: Uuid,
83}
84
85impl IsFollowingCustomList {
86    pub async fn send(&mut self) -> Result<IsFollowingResponse> {
87        #[cfg(all(
88            not(feature = "multi-thread"),
89            not(feature = "tokio-multi-thread"),
90            not(feature = "rw-multi-thread")
91        ))]
92        let res = self
93            .http_client
94            .try_borrow()?
95            .send_request_without_deserializing(self)
96            .await?;
97        #[cfg(any(feature = "multi-thread", feature = "tokio-multi-thread"))]
98        let res = self
99            .http_client
100            .lock()
101            .await
102            .send_request_without_deserializing(self)
103            .await?;
104        #[cfg(feature = "rw-multi-thread")]
105        let res = self
106            .http_client
107            .read()
108            .await
109            .send_request_without_deserializing(self)
110            .await?;
111
112        match res.status() {
113            reqwest::StatusCode::OK => Ok(IsFollowingResponse { is_following: true }),
114            reqwest::StatusCode::NOT_FOUND => {
115                let result = res
116                    .json::<<Result<NoData> as FromResponse>::Response>()
117                    .await?;
118                match result.into_result() {
119                    Ok(_) => Ok(IsFollowingResponse {
120                        is_following: false,
121                    }),
122                    Err(err) => Err(Error::Api(err)),
123                }
124            }
125            other_status => Err(Error::ServerError(other_status.as_u16(), res.text().await?)),
126        }
127    }
128}
129
130endpoint! {
131    GET ("/user/follows/list/{}", list_id),
132    #[no_data auth] IsFollowingCustomList,
133    #[no_send] Result<IsFollowingResponse>
134}
135
136builder_send! {
137    #[builder] IsFollowingCustomListBuilder,
138    IsFollowingResponse
139}
140
141#[cfg(test)]
142mod tests {
143    use mangadex_api_types::error::Error;
144    use serde_json::json;
145    use url::Url;
146    use uuid::Uuid;
147    use wiremock::matchers::{header, method, path_regex};
148    use wiremock::{Mock, MockServer, ResponseTemplate};
149
150    use crate::v5::AuthTokens;
151    use crate::{HttpClient, MangaDexClient};
152
153    #[tokio::test]
154    async fn user_follows_custom_list() -> anyhow::Result<()> {
155        let mock_server = MockServer::start().await;
156        let http_client: HttpClient = HttpClient::builder()
157            .base_url(Url::parse(&mock_server.uri())?)
158            .auth_tokens(AuthTokens {
159                session: "sessiontoken".to_string(),
160                refresh: "refreshtoken".to_string(),
161            })
162            .build()?;
163        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
164
165        let list_id = Uuid::new_v4();
166        let response_body = json!({
167            "result": "ok"
168        });
169
170        Mock::given(method("GET"))
171            .and(path_regex(r"/user/follows/list/[0-9a-fA-F-]+"))
172            .and(header("Authorization", "Bearer sessiontoken"))
173            .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
174            .expect(1)
175            .mount(&mock_server)
176            .await;
177
178        let res = mangadex_client
179            .user()
180            .follows()
181            .list()
182            .id(list_id)
183            .get()
184            .send()
185            .await?;
186
187        assert!(res.is_following);
188
189        Ok(())
190    }
191
192    #[tokio::test]
193    async fn user_does_not_follow_custom_list() -> anyhow::Result<()> {
194        let mock_server = MockServer::start().await;
195        let http_client: HttpClient = HttpClient::builder()
196            .base_url(Url::parse(&mock_server.uri())?)
197            .auth_tokens(AuthTokens {
198                session: "sessiontoken".to_string(),
199                refresh: "refreshtoken".to_string(),
200            })
201            .build()?;
202        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
203
204        let list_id = Uuid::new_v4();
205        let response_body = json!({
206            "result": "ok"
207        });
208
209        Mock::given(method("GET"))
210            .and(path_regex(r"/user/follows/list/[0-9a-fA-F-]+"))
211            .and(header("Authorization", "Bearer sessiontoken"))
212            .respond_with(ResponseTemplate::new(404).set_body_json(response_body))
213            .expect(1)
214            .mount(&mock_server)
215            .await;
216
217        let res = mangadex_client
218            .user()
219            .follows()
220            .list()
221            .id(list_id)
222            .get()
223            .send()
224            .await?;
225
226        assert!(!res.is_following);
227
228        Ok(())
229    }
230
231    #[tokio::test]
232    async fn custom_list_does_not_exist() -> anyhow::Result<()> {
233        let mock_server = MockServer::start().await;
234        let http_client: HttpClient = HttpClient::builder()
235            .base_url(Url::parse(&mock_server.uri())?)
236            .auth_tokens(AuthTokens {
237                session: "sessiontoken".to_string(),
238                refresh: "refreshtoken".to_string(),
239            })
240            .build()?;
241        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
242
243        let list_id = Uuid::new_v4();
244        let error_id = Uuid::new_v4();
245        let response_body = json!({
246            "result": "error",
247            "errors": [{
248                "id": error_id.to_string(),
249                "status": 404,
250                "title": "Custom list does not exist",
251                "detail": "The provided custom list does not exist or has been deleted"
252            }]
253        });
254
255        Mock::given(method("GET"))
256            .and(path_regex(r"/user/follows/list/[0-9a-fA-F-]+"))
257            .and(header("Authorization", "Bearer sessiontoken"))
258            .respond_with(ResponseTemplate::new(404).set_body_json(response_body))
259            .expect(1)
260            .mount(&mock_server)
261            .await;
262
263        let res = mangadex_client
264            .user()
265            .follows()
266            .list()
267            .id(list_id)
268            .get()
269            .send()
270            .await
271            .unwrap_err();
272
273        match res {
274            Error::Api(error_res) => {
275                assert_eq!(error_res.errors.len(), 1);
276                assert_eq!(error_res.errors[0].status, 404);
277                assert_eq!(
278                    error_res.errors[0].title.as_ref().unwrap(),
279                    "Custom list does not exist"
280                );
281            }
282            _ => panic!("did not get Error::Api"),
283        }
284
285        Ok(())
286    }
287}