Skip to main content

mangadex_api/v5/api_client/
get.rs

1//! Builder for the client list endpoint.
2//!
3//! <https://api.mangadex.org/docs/swagger.html#/ApiClient/get-list-apiclients>
4//! <https://api.mangadex.org/docs/redoc.html#tag/ApiClient/operation/get-list-apiclients>
5//!
6//! # Examples
7//!
8//! ```rust
9//! use mangadex_api::v5::MangaDexClient;
10//!
11//! # async fn run() -> anyhow::Result<()> {
12//! let client = MangaDexClient::default();
13//!
14//! let client_res = client
15//!     .client()
16//!     .get()
17//!     .send()
18//!     .await?;
19//!
20//! println!("Clients : {:?}", client_res);
21//! # Ok(())
22//! # }
23//! ```
24
25use derive_builder::Builder;
26use serde::Serialize;
27
28use crate::HttpClientRef;
29use mangadex_api_schema::v5::Results;
30use mangadex_api_types::{ApiClientState, ReferenceExpansionResource};
31
32type ApiClientListResponse = crate::Result<Results<mangadex_api_schema::v5::ApiClientObject>>;
33
34// Make a request to `GET /client`
35#[cfg_attr(
36    feature = "deserializable-endpoint",
37    derive(serde::Deserialize, getset::Getters, getset::Setters)
38)]
39#[derive(Debug, Serialize, Clone, Builder, Default)]
40#[serde(rename_all = "camelCase")]
41#[builder(
42    setter(into, strip_option),
43    default,
44    build_fn(error = "crate::error::BuilderError")
45)]
46#[non_exhaustive]
47pub struct ListClients {
48    #[doc(hidden)]
49    #[serde(skip)]
50    #[builder(pattern = "immutable")]
51    #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
52    pub http_client: HttpClientRef,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    #[builder(default)]
55    pub limit: Option<u32>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    #[builder(default)]
58    pub offset: Option<u32>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    #[builder(default)]
61    pub state: Option<ApiClientState>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    #[builder(default)]
64    pub name: Option<String>,
65    #[serde(skip_serializing_if = "Vec::is_empty")]
66    #[builder(default)]
67    pub includes: Vec<ReferenceExpansionResource>,
68}
69
70endpoint! {
71    GET "/client",
72    #[query auth] ListClients,
73    #[flatten_result] ApiClientListResponse,
74    ListClientsBuilder
75}
76
77#[cfg(test)]
78mod tests {
79    use mangadex_api_schema::v5::AuthTokens;
80    use serde_json::json;
81    use time::OffsetDateTime;
82    use url::Url;
83    use uuid::Uuid;
84    use wiremock::matchers::{header, method, path};
85    use wiremock::{Mock, MockServer, ResponseTemplate};
86
87    use crate::error::Error;
88    use crate::{HttpClient, MangaDexClient};
89    use mangadex_api_types::{
90        ApiClientProfile, ApiClientState, MangaDexDateTime, RelationshipType, ResponseType,
91    };
92
93    #[tokio::test]
94    async fn list_client_fires_a_request_to_base_url() -> anyhow::Result<()> {
95        let mock_server = MockServer::start().await;
96        let http_client = HttpClient::builder()
97            .base_url(Url::parse(&mock_server.uri())?)
98            .auth_tokens(non_exhaustive::non_exhaustive!(AuthTokens {
99                session: "myToken".to_string(),
100                refresh: "myRefreshToken".to_string(),
101            }))
102            .build()?;
103        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
104
105        let client_id = Uuid::new_v4();
106        let client_name = "Test Client".to_string();
107        let client_description = "A local test client for the Mangadex API".to_string();
108        let state = ApiClientState::Requested;
109        let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
110
111        let response_body = json!({
112            "result": "ok",
113            "response": "collection",
114            "data": [
115                {
116                    "id": client_id,
117                    "type": "api_client",
118                    "attributes": {
119                        "name": client_name,
120                        "description": client_description,
121                        "profile": "personal",
122                        "externalClientId": null,
123                        "isActive": false,
124                        "state": state,
125                        "createdAt": datetime.to_string(),
126                        "updatedAt": datetime.to_string(),
127                        "version": 1
128                    },
129                    "relationships": []
130                }
131            ],
132            "limit": 1,
133            "offset": 0,
134            "total": 1
135        });
136
137        Mock::given(method("GET"))
138            .and(path("/client"))
139            .and(header("Authorization", "Bearer myToken"))
140            .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
141            .expect(1)
142            .mount(&mock_server)
143            .await;
144
145        let res = mangadex_client.client().get().limit(1u32).send().await?;
146
147        assert_eq!(res.response, ResponseType::Collection);
148        let client: &mangadex_api_schema::ApiObject<mangadex_api_schema::v5::ApiClientAttributes> =
149            &res.data[0];
150        assert_eq!(client.id, client_id);
151        assert_eq!(client.type_, RelationshipType::ApiClient);
152        assert_eq!(client.attributes.name, client_name);
153        assert_eq!(client.attributes.description, Some(client_description));
154        assert_eq!(client.attributes.profile, ApiClientProfile::Personal);
155        assert_eq!(client.attributes.external_client_id, None);
156        assert!(!client.attributes.is_active);
157        assert_eq!(client.attributes.state, state);
158        assert_eq!(
159            client.attributes.created_at.to_string(),
160            datetime.to_string()
161        );
162        assert_eq!(
163            client.attributes.updated_at.to_string(),
164            datetime.to_string()
165        );
166        assert_eq!(client.attributes.version, 1);
167        Ok(())
168    }
169
170    #[tokio::test]
171    async fn list_client_handles_400() -> anyhow::Result<()> {
172        let mock_server = MockServer::start().await;
173        let http_client: HttpClient = HttpClient::builder()
174            .base_url(Url::parse(&mock_server.uri())?)
175            .auth_tokens(non_exhaustive::non_exhaustive!(AuthTokens {
176                session: "myToken".to_string(),
177                refresh: "myRefreshToken".to_string(),
178            }))
179            .build()?;
180        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
181
182        let error_id = Uuid::new_v4();
183
184        let response_body = json!({
185            "result": "error",
186            "errors": [{
187                "id": error_id.to_string(),
188                "status": 400,
189                "title": "Invalid limit",
190                "detail": "Limit must be between 1 and 100"
191            }]
192        });
193
194        Mock::given(method("GET"))
195            .and(path("/client"))
196            .respond_with(ResponseTemplate::new(400).set_body_json(response_body))
197            .expect(1)
198            .mount(&mock_server)
199            .await;
200
201        let res = mangadex_client
202            .client()
203            .get()
204            .limit(0u32)
205            .send()
206            .await
207            .expect_err("expected error");
208
209        if let Error::Api(errors) = res {
210            assert_eq!(errors.errors.len(), 1);
211
212            assert_eq!(errors.errors[0].id, error_id);
213            assert_eq!(errors.errors[0].status, 400);
214            assert_eq!(errors.errors[0].title, Some("Invalid limit".to_string()));
215            assert_eq!(
216                errors.errors[0].detail,
217                Some("Limit must be between 1 and 100".to_string())
218            );
219        }
220
221        Ok(())
222    }
223}