1#![allow(ambiguous_glob_reexports)]
2pub mod anime;
4pub use anime::*;
5pub mod animelist;
7pub use animelist::*;
8pub mod manga;
10pub use manga::*;
11pub mod mangalist;
13pub use mangalist::*;
14pub mod model;
16pub mod user;
18pub use user::*;
19
20use crate::auth::OAuth;
21use reqwest::{ClientBuilder, RequestBuilder};
22use serde::{Deserialize, Serialize};
23
24pub const API_URL: &str = "https://api.myanimelist.net/v2";
25
26#[derive(Debug)]
27pub enum Error {
28 NoAuth,
29 TimedOut,
30 Unknown,
31 NoBody,
32 ParseError(serde_json::Error),
33 QuerySerializeError(serde_urlencoded::ser::Error),
34 HttpError(reqwest::StatusCode),
35}
36
37impl From<reqwest::Error> for Error {
38 fn from(e: reqwest::Error) -> Self {
39 if e.is_timeout() {
40 Error::TimedOut
41 } else {
42 Error::Unknown
43 }
44 }
45}
46
47impl From<serde_json::Error> for Error {
48 fn from(e: serde_json::Error) -> Self {
49 Error::ParseError(e)
50 }
51}
52
53impl From<serde_urlencoded::ser::Error> for Error {
54 fn from(e: serde_urlencoded::ser::Error) -> Self {
55 Error::QuerySerializeError(e)
56 }
57}
58
59#[derive(Debug)]
60pub(crate) struct ApiResponse {
61 status: reqwest::StatusCode,
62 body: Option<String>,
63}
64
65type ApiResult<T> = Result<T, Error>;
66
67pub(crate) fn apply_headers(req: RequestBuilder, auth: &OAuth) -> ApiResult<RequestBuilder> {
68 let access_token = match auth.token() {
69 Some(token) => &token.token.access_token,
70 None => return Err(Error::NoAuth),
71 };
72 Ok(req
73 .header(reqwest::header::ACCEPT, "application/json")
74 .header(
75 reqwest::header::CONTENT_TYPE,
76 "application/x-www-form-urlencoded",
77 )
78 .header(
79 reqwest::header::AUTHORIZATION,
80 format!("Bearer {}", access_token),
81 ))
82}
83
84pub(crate) async fn send(request: RequestBuilder, auth: &OAuth) -> ApiResult<ApiResponse> {
85 let request = apply_headers(request, auth)?;
86 let response = request.send().await?;
87 let status = response.status();
88 Ok(ApiResponse {
89 status,
90 body: (response.text().await).ok(),
91 })
92}
93
94pub(crate) async fn get<U: reqwest::IntoUrl>(url: U, auth: &OAuth) -> ApiResult<ApiResponse> {
95 let request = ClientBuilder::new()
96 .user_agent(auth.user_agent())
97 .build()?
98 .get(url);
99 send(request, auth).await
100}
101
102pub(crate) async fn patch<U: reqwest::IntoUrl, B: Serialize>(
103 url: U,
104 auth: &OAuth,
105 body: &B,
106) -> ApiResult<ApiResponse> {
107 let request = ClientBuilder::new()
108 .user_agent(auth.user_agent())
109 .build()?
110 .patch(url)
111 .body(serde_urlencoded::to_string(body)?);
112 send(request, auth).await
113}
114
115pub(crate) async fn delete<U: reqwest::IntoUrl>(url: U, auth: &OAuth) -> ApiResult<ApiResponse> {
116 let request = ClientBuilder::new()
117 .user_agent(auth.user_agent())
118 .build()?
119 .delete(url);
120 send(request, auth).await
121}
122
123pub(crate) fn handle_response<'a, D: Deserialize<'a>>(res: &'a ApiResponse) -> ApiResult<D> {
124 if !res.status.is_success() {
125 return Err(Error::HttpError(res.status));
126 }
127 if let Some(body) = &res.body {
128 Ok(serde_json::from_str::<D>(body)?)
129 } else {
130 Err(Error::NoBody)
131 }
132}