use serde::Deserialize;
use std::time::Duration;
use crate::{
models::{
Anime, Character, Cover, Format, Image, Manga, MediaType, Person, Status, Title, User,
},
Error, Result,
};
#[derive(Clone, Debug, PartialEq)]
pub struct Client {
api_token: Option<String>,
timeout: Duration,
}
impl Client {
pub fn with_timeout(duration: Duration) -> Self {
Self {
api_token: None,
timeout: duration,
}
}
pub fn with_token(token: &str) -> Self {
Self {
api_token: Some(token.to_string()),
timeout: Duration::from_secs(20),
}
}
pub fn timeout(mut self, duration: Duration) -> Self {
self.timeout = duration;
self
}
pub fn token(mut self, token: &str) -> Self {
self.api_token = Some(token.to_string());
self
}
pub async fn get_anime(&self, id: i64) -> Result<Anime> {
let data = self
.request(
MediaType::Anime,
Action::Get,
serde_json::json!({ "id": id }),
)
.await
.map_err(|e| Error::ApiError(e.to_string()))?;
match serde_json::from_str::<Anime>(&data["data"]["Media"].to_string()) {
Ok(mut anime) => {
anime.client = self.clone();
anime.is_full_loaded = true;
Ok(anime)
}
Err(e) => Err(crate::Error::ApiError(e.to_string())),
}
}
pub async fn get_manga(&self, id: i64) -> Result<Manga> {
let data = self
.request(
MediaType::Manga,
Action::Get,
serde_json::json!({ "id": id }),
)
.await
.map_err(|e| Error::ApiError(e.to_string()))?;
match serde_json::from_str::<Manga>(&data["data"]["Media"].to_string()) {
Ok(mut manga) => {
manga.client = self.clone();
manga.is_full_loaded = true;
Ok(manga)
}
Err(e) => Err(crate::Error::ApiError(e.to_string())),
}
}
pub async fn get_character(&self, id: i64) -> Result<Character> {
let data = self
.request(
MediaType::Character,
Action::Get,
serde_json::json!({ "id": id }),
)
.await
.map_err(|e| Error::ApiError(e.to_string()))?;
match serde_json::from_str::<Character>(&data["data"]["Character"].to_string()) {
Ok(mut character) => {
character.client = self.clone();
character.is_full_loaded = true;
Ok(character)
}
Err(e) => Err(crate::Error::ApiError(e.to_string())),
}
}
pub async fn get_char(&self, id: i64) -> Result<Character> {
self.get_character(id).await
}
pub async fn get_user(&self, id: i32) -> Result<User> {
let data = self
.request(
MediaType::User,
Action::Get,
serde_json::json!({ "id": id }),
)
.await
.map_err(|e| Error::ApiError(e.to_string()))?;
match serde_json::from_str::<User>(&data["data"]["User"].to_string()) {
Ok(user) => Ok(user),
Err(e) => Err(crate::Error::ApiError(e.to_string())),
}
}
pub async fn get_user_by_name<N: ToString>(&self, name: N) -> Result<User> {
let name = name.to_string();
let data = self
.request(
MediaType::User,
Action::Get,
serde_json::json!({ "name": name }),
)
.await
.map_err(|e| Error::ApiError(e.to_string()))?;
match serde_json::from_str::<User>(&data["data"]["User"].to_string()) {
Ok(mut user) => {
user.client = self.clone();
user.is_full_loaded = true;
Ok(user)
}
Err(e) => Err(crate::Error::ApiError(e.to_string())),
}
}
pub async fn get_person(&self, id: i64) -> Result<Person> {
let data = self
.request(
MediaType::Person,
Action::Get,
serde_json::json!({ "id": id }),
)
.await
.map_err(|e| Error::ApiError(e.to_string()))?;
match serde_json::from_str::<Person>(&data["data"]["Staff"].to_string()) {
Ok(mut person) => {
person.client = self.clone();
person.is_full_loaded = true;
Ok(person)
}
Err(e) => Err(crate::Error::ApiError(e.to_string())),
}
}
pub async fn search_anime(&self, title: &str, page: u16, limit: u16) -> Option<Vec<Anime>> {
let result = self
.request(
MediaType::Anime,
Action::Search,
serde_json::json!({ "search": title, "page": page, "per_page": limit, }),
)
.await
.map_err(|e| Error::ApiError(e.to_string()))
.unwrap();
if let Some(medias) = result["data"]["Page"]["media"].as_array() {
let mut animes = Vec::new();
for media in medias.iter() {
animes.push(Anime {
id: media["id"].as_i64().unwrap(),
id_mal: media["idMal"].as_i64(),
title: Title::deserialize(&media["title"]).unwrap(),
format: Format::deserialize(&media["format"]).unwrap(),
status: Status::deserialize(&media["status"]).unwrap(),
description: media["description"].as_str().unwrap().to_string(),
cover: Cover::deserialize(&media["coverImage"]).unwrap(),
banner: media["bannerImage"].as_str().map(String::from),
average_score: media["averageScore"].as_u64().map(|x| x as u8),
mean_score: media["meanScore"].as_u64().map(|x| x as u8),
is_adult: media["isAdult"].as_bool().unwrap(),
url: media["siteUrl"].as_str().unwrap().to_string(),
client: self.clone(),
..Default::default()
});
}
return Some(animes);
}
None
}
pub async fn search_manga(&self, title: &str, page: u16, limit: u16) -> Option<Vec<Manga>> {
let result = self
.request(
MediaType::Manga,
Action::Search,
serde_json::json!({ "search": title, "page": page, "per_page": limit, }),
)
.await
.map_err(|e| Error::ApiError(e.to_string()))
.unwrap();
if let Some(medias) = result["data"]["Page"]["media"].as_array() {
let mut mangas = Vec::new();
for media in medias.iter() {
mangas.push(Manga {
id: media["id"].as_i64().unwrap(),
id_mal: media["idMal"].as_i64(),
title: Title::deserialize(&media["title"]).unwrap(),
format: Format::deserialize(&media["format"]).unwrap(),
status: Status::deserialize(&media["status"]).unwrap(),
description: media["description"].as_str().unwrap().to_string(),
cover: Cover::deserialize(&media["coverImage"]).unwrap(),
banner: media["bannerImage"].as_str().map(String::from),
average_score: media["averageScore"].as_u64().map(|x| x as u8),
mean_score: media["meanScore"].as_u64().map(|x| x as u8),
is_adult: media["isAdult"].as_bool().unwrap(),
url: media["siteUrl"].as_str().unwrap().to_string(),
client: self.clone(),
..Default::default()
});
}
return Some(mangas);
}
None
}
pub async fn search_user(&self, name: &str, page: u16, limit: u16) -> Option<Vec<User>> {
let result = self
.request(
MediaType::User,
Action::Search,
serde_json::json!({ "search": name, "page": page, "per_page": limit, }),
)
.await
.map_err(|e| Error::ApiError(e.to_string()))
.unwrap();
if let Some(users) = result["data"]["Page"]["users"].as_array() {
let mut vec = Vec::new();
for user in users.iter() {
vec.push(User {
id: user["id"].as_i64().unwrap() as i32,
name: user["name"].as_str().unwrap().to_string(),
about: user["about"].as_str().map(String::from),
avatar: Image::deserialize(&user["avatar"]).ok(),
banner: user["bannerImage"].as_str().map(String::from),
client: self.clone(),
..Default::default()
});
}
return Some(vec);
}
None
}
async fn request(
&self,
media_type: MediaType,
action: Action,
variables: serde_json::Value,
) -> std::result::Result<serde_json::Value, reqwest::Error> {
let query = Client::get_query(media_type, action).unwrap();
let json = serde_json::json!({"query": query, "variables": variables});
let mut body = reqwest::Client::new()
.post("https://graphql.anilist.co/")
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.timeout(self.timeout)
.body(json.to_string());
if let Some(token) = &self.api_token {
body = body.bearer_auth(token);
}
let response = body.send().await?.text().await?;
let result = serde_json::from_str::<serde_json::Value>(&response).unwrap();
Ok(result)
}
fn get_query(media_type: MediaType, action: Action) -> Result<String> {
let graphql_query = match action {
Action::Get => {
match media_type {
MediaType::Anime => include_str!("../queries/get_anime.graphql").to_string(),
MediaType::Manga => include_str!("../queries/get_manga.graphql").to_string(),
MediaType::Character => {
include_str!("../queries/get_character.graphql").to_string()
}
MediaType::User => include_str!("../queries/get_user.graphql").to_string(),
MediaType::Person => include_str!("../queries/get_person.graphql").to_string(),
_ => unimplemented!(),
}
}
Action::Search => {
match media_type {
MediaType::Anime => include_str!("../queries/search_anime.graphql").to_string(),
MediaType::Manga => include_str!("../queries/search_manga.graphql").to_string(),
MediaType::User => include_str!("../queries/search_user.graphql").to_string(),
_ => unimplemented!(),
}
}
};
Ok(graphql_query)
}
}
impl Default for Client {
fn default() -> Self {
Client {
api_token: None,
timeout: Duration::from_secs(20),
}
}
}
enum Action {
Get,
Search,
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
#[test]
fn test_with_timeout() {
let duration = Duration::from_secs(30);
let client = Client::with_timeout(duration);
assert_eq!(client.timeout, duration);
assert!(client.api_token.is_none());
}
#[test]
fn test_with_token() {
let api_token = "test_token";
let client = Client::with_token(api_token);
assert_eq!(client.timeout, Duration::from_secs(20));
assert_eq!(client.api_token, Some(api_token.to_string()));
}
#[test]
fn test_timeout() {
let initial_duration = Duration::from_secs(30);
let new_duration = Duration::from_secs(60);
let client = Client::with_timeout(initial_duration).timeout(new_duration);
assert_eq!(client.timeout, new_duration);
}
#[test]
fn test_token() {
let initial_token = "initial_token";
let new_token = "new_token";
let client = Client::with_token(initial_token).token(new_token);
assert_eq!(client.api_token, Some(new_token.to_string()));
}
}