mangadex_api/v5/manga/manga_id/relation/
post.rs

1//! Builder for creating a Manga relation.
2//!
3//! This endpoint requires authentication.
4//!
5//! <https://api.mangadex.org/docs/swagger.html#/Manga/post-manga-relation>
6//!
7//! # Examples
8//!
9//! ```rust
10//! use uuid::Uuid;
11//!
12//! use mangadex_api_types::MangaRelation;
13//! use mangadex_api::v5::MangaDexClient;
14//! // use mangadex_api_types::{Password, Username};
15//!
16//! # async fn run() -> anyhow::Result<()> {
17//! let client = MangaDexClient::default();
18//!
19//! /*
20//!
21//!     let _login_res = client
22//!         .auth()
23//!         .login()
24//!         .username(Username::parse("myusername")?)
25//!         .password(Password::parse("hunter23")?)
26//!         .send()
27//!         .await?;
28//!
29//!  */
30//!
31//! let manga_id = Uuid::new_v4();
32//! let target_manga_id = Uuid::new_v4();
33//! let res = client
34//!     .manga()
35//!     .manga_id(manga_id)
36//!     .relation()
37//!     .post()
38//!     .target_manga(target_manga_id)
39//!     .relation(MangaRelation::SpinOff)
40//!     .send()
41//!     .await?;
42//!
43//! println!("created manga relation: {:?}", res);
44//! # Ok(())
45//! # }
46//! ```
47
48use derive_builder::Builder;
49use serde::Serialize;
50use uuid::Uuid;
51
52use crate::HttpClientRef;
53use mangadex_api_schema::v5::MangaRelationListResponse;
54use mangadex_api_types::MangaRelation;
55
56#[cfg_attr(
57    feature = "deserializable-endpoint",
58    derive(serde::Deserialize, getset::Getters, getset::Setters)
59)]
60#[derive(Debug, Serialize, Clone, Builder, Default)]
61#[serde(rename_all = "camelCase")]
62#[builder(
63    setter(into),
64    build_fn(error = "mangadex_api_types::error::BuilderError")
65)]
66pub struct CreateMangaRelation {
67    /// This should never be set manually as this is only for internal use.
68    #[doc(hidden)]
69    #[serde(skip)]
70    #[builder(pattern = "immutable")]
71    #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
72    pub http_client: HttpClientRef,
73
74    #[serde(skip_serializing)]
75    pub manga_id: Uuid,
76    pub target_manga: Uuid,
77    pub relation: MangaRelation,
78}
79
80endpoint! {
81    POST ("/manga/{}/relation", manga_id),
82    #[body auth] CreateMangaRelation,
83    #[flatten_result] MangaRelationListResponse,
84    CreateMangaRelationBuilder
85}
86
87#[cfg(test)]
88mod tests {
89    use serde_json::json;
90    use url::Url;
91    use uuid::Uuid;
92    use wiremock::matchers::{body_json, header, method, path_regex};
93    use wiremock::{Mock, MockServer, ResponseTemplate};
94
95    use crate::v5::AuthTokens;
96    use crate::{HttpClient, MangaDexClient};
97    use mangadex_api_types::error::Error;
98    use mangadex_api_types::{MangaRelation, RelationshipType, ResponseType};
99
100    #[tokio::test]
101    async fn create_manga_relation_fires_a_request_to_base_url() -> anyhow::Result<()> {
102        let mock_server = MockServer::start().await;
103        let http_client: HttpClient = HttpClient::builder()
104            .base_url(Url::parse(&mock_server.uri())?)
105            .auth_tokens(AuthTokens {
106                session: "sessiontoken".to_string(),
107                refresh: "refreshtoken".to_string(),
108            })
109            .build()?;
110        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
111
112        let manga_id = Uuid::new_v4();
113        let target_manga_id = Uuid::new_v4();
114        let expected_body = json!({
115            "targetManga": target_manga_id,
116            "relation": "spin_off"
117        });
118        let response_body = json!({
119            "result": "ok",
120            "response": "collection",
121            "data": [
122                {
123                    "id": "0b92f446-4ee0-4c15-9e5c-6ae1211e785b",
124                    "type": "manga_relation",
125                    "attributes": {
126                        "relation": "doujinshi",
127                        "version": 1
128                    },
129                    "relationships": [
130                        {
131                            "id": "7944cc53-86e6-4135-898f-47c5c8d0647c",
132                            "type": "manga"
133                        }
134                    ]
135                },
136                {
137                    "id": "31b831b7-aac5-4797-b3eb-a41575cd4399",
138                    "type": "manga_relation",
139                    "attributes": {
140                        "relation": "doujinshi",
141                        "version": 1
142                    },
143                    "relationships": [
144                        {
145                            "id": "119327ab-9b32-4841-9068-02264c15e118",
146                            "type": "manga"
147                        }
148                    ]
149                },
150                {
151                    "id": "53815c02-b357-4e23-b8e7-0d6d114ea420",
152                    "type": "manga_relation",
153                    "attributes": {
154                        "relation": "doujinshi",
155                        "version": 1
156                    },
157                    "relationships": [
158                        {
159                            "id": "25e26436-7eb7-4505-8711-6e014bb16fd7",
160                            "type": "manga"
161                        }
162                    ]
163                },
164                {
165                    "id": "6958767b-54c5-4b4c-8f0f-579a36389f68",
166                    "type": "manga_relation",
167                    "attributes": {
168                        "relation": "doujinshi",
169                        "version": 1
170                    },
171                    "relationships": [
172                        {
173                            "id": "0736a46a-1a34-4411-b665-a1e45ebf54a9",
174                            "type": "manga"
175                        }
176                    ]
177                },
178                {
179                    "id": "b358b2f5-beab-484a-9daf-880ad6085225",
180                    "type": "manga_relation",
181                    "attributes": {
182                        "relation": "spin_off",
183                        "version": 1
184                    },
185                    "relationships": [
186                        {
187                            "id": "1e4deefe-9eb8-4183-837a-f24002adb318",
188                            "type": "manga"
189                        }
190                    ]
191                }
192            ],
193            "limit": 5,
194            "offset": 0,
195            "total": 5
196        });
197
198        Mock::given(method("POST"))
199            .and(path_regex(r"/manga/[0-9a-fA-F-]+/relation"))
200            .and(header("Authorization", "Bearer sessiontoken"))
201            .and(header("Content-Type", "application/json"))
202            .and(body_json(expected_body))
203            .respond_with(ResponseTemplate::new(201).set_body_json(response_body))
204            .expect(1)
205            .mount(&mock_server)
206            .await;
207
208        let res = mangadex_client
209            .manga()
210            .manga_id(manga_id)
211            .relation()
212            .post()
213            .target_manga(target_manga_id)
214            .relation(MangaRelation::SpinOff)
215            .send()
216            .await?;
217
218        let related = &res.data[0];
219        assert_eq!(res.response, ResponseType::Collection);
220        assert_eq!(related.type_, RelationshipType::MangaRelation);
221        assert_eq!(related.attributes.relation, MangaRelation::Doujinshi);
222        assert_eq!(related.attributes.version, 1);
223        assert_eq!(related.relationships[0].type_, RelationshipType::Manga);
224        assert!(related.relationships[0].related.is_none());
225        assert!(related.relationships[0].attributes.is_none());
226
227        Ok(())
228    }
229
230    #[tokio::test]
231    async fn create_manga_relation_requires_auth() -> anyhow::Result<()> {
232        let mock_server = MockServer::start().await;
233        let http_client: HttpClient = HttpClient::builder()
234            .base_url(Url::parse(&mock_server.uri())?)
235            .build()?;
236        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
237
238        let manga_id = Uuid::new_v4();
239        let target_manga_id = Uuid::new_v4();
240        let error_id = Uuid::new_v4();
241        let expected_body = json!({
242            "targetManga": target_manga_id,
243            "relation": "sequel"
244        });
245        let response_body = json!({
246            "result": "error",
247            "errors": [{
248                "id": error_id.to_string(),
249                "status": 403,
250                "title": "Forbidden",
251                "detail": "You must be logged in to continue."
252            }]
253        });
254
255        Mock::given(method("POST"))
256            .and(path_regex(r"/manga/[0-9a-fA-F-]+/relation"))
257            .and(header("Content-Type", "application/json"))
258            .and(body_json(expected_body))
259            .respond_with(ResponseTemplate::new(403).set_body_json(response_body))
260            .expect(0)
261            .mount(&mock_server)
262            .await;
263
264        let res = mangadex_client
265            .manga()
266            .manga_id(manga_id)
267            .relation()
268            .post()
269            .target_manga(target_manga_id)
270            .relation(MangaRelation::Sequel)
271            .send()
272            .await
273            .expect_err("expected error");
274
275        match res {
276            Error::MissingTokens => {}
277            _ => panic!("unexpected error: {:#?}", res),
278        }
279
280        Ok(())
281    }
282}