1use std::collections::HashMap;
2
3use reqwest::{
4 Client, ClientBuilder, Method, RequestBuilder, StatusCode,
5 header::{HeaderMap, HeaderValue},
6};
7
8use crate::{
9 VERSION,
10 err::{self, delete, get, post, validate},
11 nexus_joiner,
12 request::{
13 CategoryName, Changelog, Endorsements, GameId, GameMod, ModFile, ModFiles, ModId,
14 ModUpdated, RateLimiting, TimePeriod, TrackedModsRaw, Validate,
15 },
16};
17
18pub struct Api {
22 key: String,
23 client: Client,
24}
25
26impl Api {
27 pub fn new<S: Into<String>>(key: S) -> Self {
41 let key = key.into();
42 let client = ClientBuilder::new().default_headers({
43 let mut h = HeaderMap::new();
44 h.insert("apikey", key.parse().unwrap());
45 h.insert("accept", HeaderValue::from_static("application/json"));
46 h
47 });
48 Self {
49 key,
50 client: client.build().expect("oops"),
51 }
52 }
53
54 pub(crate) fn key(&self) -> &str {
55 &self.key
56 }
57
58 fn build(
59 &self,
60 method: Method,
61 ver: &str,
62 slugs: &[&str],
63 params: &[(&'static str, &str)],
64 ) -> RequestBuilder {
65 self.client
66 .request(method, nexus_joiner!(ver, slugs))
67 .query(params)
68 }
69
70 }
72
73impl Api {
83 pub async fn validate(&self) -> Result<Validate, validate::ValidateError> {
98 let response = self
99 .build(Method::GET, VERSION, &["users", "validate"], &[])
100 .send()
101 .await?;
102
103 match response.status() {
104 StatusCode::OK => response
105 .json()
106 .await
107 .map_err(validate::ValidateError::Reqwest),
108 StatusCode::UNAUTHORIZED => Err(validate::ValidateError::InvalidAPIKey(
109 response.json().await?,
110 )),
111 StatusCode::UNPROCESSABLE_ENTITY => {
112 unimplemented!(
113 "I have not yet encountered this return code but it is listed as a valid return code"
114 );
115 }
116 _ => unreachable!("The only three documented return codes are 200, 404 (401), and 422"),
117 }
118 }
119
120 pub async fn tracked_mods(&self) -> Result<TrackedModsRaw, validate::ValidateError> {
125 let response = self
126 .build(Method::GET, VERSION, &["user", "tracked_mods"], &[])
127 .send()
128 .await?;
129
130 match response.status() {
131 StatusCode::OK => response
132 .json()
133 .await
134 .map_err(validate::ValidateError::Reqwest),
135 StatusCode::UNAUTHORIZED => Err(validate::ValidateError::InvalidAPIKey(
136 response.json().await?,
137 )),
138 StatusCode::UNPROCESSABLE_ENTITY => {
139 unimplemented!(
140 "I have not yet encountered this return code but it is listed as a valid return code"
141 );
142 }
143 _ => unreachable!("The only three documented return codes are 200, 404 (401), and 422"),
144 }
145 }
146
147 pub async fn track_mod<T: Into<u64>>(
149 &self,
150 game: &str,
151 id: T,
152 ) -> Result<post::PostModStatus, post::TrackModError> {
153 let id = id.into();
154 let response = self
155 .build(Method::POST, VERSION, &["user", "tracked_mods"], &[])
156 .query(&[("domain_name", game)])
157 .form(&HashMap::from([("mod_id", id)]))
158 .send()
159 .await?;
160
161 match response.status() {
162 StatusCode::OK => Ok(post::PostModStatus::AlreadyTracking(ModId::from_u64(id))),
163 StatusCode::CREATED => Ok(post::PostModStatus::SuccessfullyTracked(ModId::from_u64(
164 id,
165 ))),
166 StatusCode::UNAUTHORIZED => {
167 Err(response.json::<err::InvalidAPIKeyError>().await?.into())
168 }
169 StatusCode::NOT_FOUND => Err(response.json::<err::ModNotFoundError>().await?.into()),
170 _ => unreachable!("The only four documented return codes are 200, 201, 404, and 401"),
171 }
172 }
173
174 pub async fn untrack_mod<T: Into<ModId>>(
180 &self,
181 game: &str,
182 id: T,
183 ) -> Result<(), delete::DeleteModError> {
184 let id = id.into();
185 let response = self
186 .build(Method::DELETE, VERSION, &["user", "tracked_mods"], &[])
187 .query(&[("domain_name", game)])
188 .form(&HashMap::from([("mod_id", id)]))
189 .send()
190 .await?;
191
192 match response.status() {
193 StatusCode::OK => Ok(()),
194 StatusCode::NOT_FOUND => {
195 Err(response.json::<err::UntrackedOrInvalidMod>().await?.into())
196 }
197 _ => unreachable!("The only two documented return codes are 200 and 404"),
198 }
199 }
200
201 pub async fn endorsements(&self) -> Result<Endorsements, validate::ValidateError> {
203 let response = self
204 .build(Method::GET, VERSION, &["user", "endorsements"], &[])
205 .send()
206 .await?;
207
208 match response.status() {
209 StatusCode::OK => response
210 .json()
211 .await
212 .map_err(validate::ValidateError::Reqwest),
213 StatusCode::UNAUTHORIZED => Err(validate::ValidateError::InvalidAPIKey(
214 response.json().await?,
215 )),
216 StatusCode::UNPROCESSABLE_ENTITY => {
217 unimplemented!(
218 "I have not yet encountered this return code but it is listed as a valid return code"
219 );
220 }
221 _ => unreachable!("The only three documented return codes are 200, 404 (401), and 422"),
222 }
223 }
224}
225
226impl Api {
238 pub async fn updated_during(
240 &self,
241 game: &str,
242 time: TimePeriod,
243 ) -> Result<Vec<ModUpdated>, get::GameModError> {
244 let response = self
245 .build(
246 Method::GET,
247 VERSION,
248 &["games", game, "mods", "updated"],
249 &[("period", time.as_str())],
250 )
251 .send()
252 .await?;
253
254 match response.status() {
255 StatusCode::OK => response.json().await.map_err(get::GameModError::Reqwest),
256 StatusCode::NOT_FOUND => Err(response.json::<err::InvalidAPIKeyError>().await?.into()),
257 StatusCode::UNPROCESSABLE_ENTITY => {
258 unimplemented!(
259 "I have not yet encountered this return code but it is listed as a valid return code"
260 );
261 }
262 _ => unreachable!("The only three documented return codes are 200, 404, and 422"),
263 }
264 }
265
266 pub async fn changelogs<T: Into<ModId>>(
268 &self,
269 game: &str,
270 id: T,
271 ) -> Result<Changelog, get::GameModError> {
272 let id = id.into();
273 let response = self
274 .build(
275 Method::GET,
276 VERSION,
277 &["games", game, "mods", id.to_string().as_str(), "changelogs"],
278 &[],
279 )
280 .send()
281 .await?;
282
283 match response.status() {
284 StatusCode::OK => response.json().await.map_err(get::GameModError::Reqwest),
285 StatusCode::NOT_FOUND => Err(response.json::<err::InvalidAPIKeyError>().await?.into()),
286 StatusCode::UNPROCESSABLE_ENTITY => {
287 unimplemented!(
288 "I have not yet encountered this return code but it is listed as a valid return code"
289 );
290 }
291 _ => unreachable!("The only three documented return codes are 200, 404, and 422"),
292 }
293 }
294
295 pub async fn mod_info<T: Into<ModId>>(
297 &self,
298 game: &str,
299 id: T,
300 ) -> Result<GameMod, get::GameModError> {
301 let id = id.into();
302 let response = self
303 .build(
304 Method::GET,
305 VERSION,
306 &["games", game, "mods", id.to_string().as_str()],
307 &[],
308 )
309 .send()
310 .await?;
311
312 match response.status() {
313 StatusCode::OK => response.json().await.map_err(get::GameModError::Reqwest),
314 StatusCode::NOT_FOUND => Err(response.json::<err::InvalidAPIKeyError>().await?.into()),
315 StatusCode::UNPROCESSABLE_ENTITY => {
316 unimplemented!(
317 "I have not yet encountered this return code but it is listed as a valid return code"
318 );
319 }
320 _ => unreachable!("The only three documented return codes are 200, 404, and 422"),
321 }
322 }
323}
324
325impl Api {
330 pub async fn games(&self) -> Result<Vec<GameId>, get::GameModError> {
332 let response = self
333 .build(Method::GET, VERSION, &["games"], &[])
334 .send()
335 .await?;
336
337 match response.status() {
338 StatusCode::OK => response.json().await.map_err(get::GameModError::Reqwest),
339 StatusCode::NOT_FOUND => Err(response.json::<err::InvalidAPIKeyError>().await?.into()),
340 StatusCode::UNPROCESSABLE_ENTITY => {
341 unimplemented!(
342 "I have not yet encountered this return code but it is listed as a valid return code"
343 );
344 }
345 _ => unreachable!("The only three documented return codes are 200, 404, and 422"),
346 }
347 }
348
349 pub async fn game(&self, game: &str) -> Result<GameId, get::GameModError> {
351 let response = self
352 .build(Method::GET, VERSION, &["games", game], &[])
353 .send()
354 .await?;
355
356 match response.status() {
357 StatusCode::OK => response.json().await.map_err(get::GameModError::Reqwest),
358 StatusCode::NOT_FOUND => Err(response.json::<err::InvalidAPIKeyError>().await?.into()),
359 StatusCode::UNPROCESSABLE_ENTITY => {
360 unimplemented!(
361 "I have not yet encountered this return code but it is listed as a valid return code"
362 );
363 }
364 _ => unreachable!("The only three documented return codes are 200, 404, and 422"),
365 }
366 }
367}
368
369impl Api {
375 pub async fn mod_files<S: Into<ModId>>(
377 &self,
378 game: &str,
379 mod_id: S,
380 category: Option<CategoryName>,
381 ) -> Result<ModFiles, get::GameModError> {
382 let mod_id = mod_id.into();
383 let response = self
384 .build(
385 Method::GET,
386 VERSION,
387 &["games", game, "mods", mod_id.to_string().as_str(), "files"],
388 &category
389 .iter()
390 .map(|c| ("category", c.to_header_str()))
391 .collect::<Vec<_>>(),
392 )
393 .send()
394 .await?;
395
396 match response.status() {
397 StatusCode::OK => response.json().await.map_err(get::GameModError::Reqwest),
398 StatusCode::NOT_FOUND => Err(response.json::<err::InvalidAPIKeyError>().await?.into()),
399 StatusCode::UNPROCESSABLE_ENTITY => {
400 unimplemented!(
401 "I have not yet encountered this return code but it is listed as a valid return code"
402 );
403 }
404 _ => unreachable!("The only three documented return codes are 200, 404, and 422"),
405 }
406 }
407
408 pub async fn mod_file<S: Into<ModId>>(
409 &self,
410 game: &str,
411 mod_id: S,
412 file_id: u64,
413 ) -> Result<ModFile, get::GameModError> {
414 let mod_id = mod_id.into();
415 let response = self
416 .build(
417 Method::GET,
418 VERSION,
419 &[
420 "games",
421 game,
422 "mods",
423 mod_id.to_string().as_str(),
424 "files",
425 file_id.to_string().as_str(),
426 ],
427 &[],
428 )
429 .send()
430 .await?;
431
432 match response.status() {
433 StatusCode::OK => response.json().await.map_err(get::GameModError::Reqwest),
434 StatusCode::NOT_FOUND => Err(response.json::<err::InvalidAPIKeyError>().await?.into()),
435 StatusCode::UNPROCESSABLE_ENTITY => {
436 unimplemented!(
437 "I have not yet encountered this return code but it is listed as a valid return code"
438 );
439 }
440 _ => unreachable!("The only three documented return codes are 200, 404, and 422"),
441 }
442 }
443}