mal/api/
mod.rs

1#![allow(ambiguous_glob_reexports)]
2/// Anime API endpoints
3pub mod anime;
4pub use anime::*;
5/// User animelist API endpoints
6pub mod animelist;
7pub use animelist::*;
8/// manga API endpoints
9pub mod manga;
10pub use manga::*;
11/// User mangalist API endpoints
12pub mod mangalist;
13pub use mangalist::*;
14/// API objects
15pub mod model;
16/// User API endpoints
17pub 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}