1use std::any::type_name;
2use crate::apicalypse_builder::ApicalypseBuilder;
3use crate::client::IGDBApiError::AuthError;
4use microjson::JSONParsingError;
5use prost::DecodeError;
6use thiserror::Error;
7use crate::igdb::Count;
8
9const LIB_VERSION_HEADER: &str = concat!("igdb-api-rust v " ,env!("CARGO_PKG_VERSION"));
10
11#[derive(Error, Debug)]
12pub enum IGDBApiError {
13 #[error("Something is wrong with the auth please check the credentials: {0:?}")]
14 AuthError(JSONParsingError),
15 #[error("Cannot decode API response: {0:?}")]
16 ApiResponseDecodeError(#[from] DecodeError),
17 #[error("Cannot request server")]
18 Request(#[from] reqwest::Error),
19 #[error("unknown API error")]
20 Unknown,
21}
22
23impl From<JSONParsingError> for IGDBApiError {
24 fn from(value: JSONParsingError) -> Self {
25 AuthError(value)
26 }
27}
28
29pub struct Client {
31 client_id: String,
32 client_secret: String,
33 client: reqwest::Client,
34 client_access_token: String,
35 endpoint: String,
36}
37
38impl Client {
39 pub fn new(client_id: &str, client_secret: &str) -> Self {
41 Client {
42 client_id: client_id.to_string(),
43 client_secret: client_secret.to_string(),
44 client: reqwest::Client::new(),
45 client_access_token: String::default(),
46 endpoint: "https://api.igdb.com/v4".to_string()
47 }
48 }
49 pub fn with_endpoint(mut self, endpoint: &str) -> Self {
55 self.endpoint = endpoint.to_string();
56 self
57 }
58
59 pub async fn request<M: prost::Message + Default>(
61 &mut self,
62 query: &ApicalypseBuilder,
63 ) -> Result<M, IGDBApiError> {
64 let query_string = query.to_query();
65 self.request_raw(query_string.as_str()).await
66 }
67
68
69 async fn check_access_token(&mut self) -> Result<(), IGDBApiError> {
70 if self.client_access_token.is_empty() {
71 use microjson::JSONValue;
72 let resp = self
73 .client
74 .post("https://id.twitch.tv/oauth2/token")
75 .query(&[
76 ("client_id", self.client_id.as_str()),
77 ("client_secret", self.client_secret.as_str()),
78 ("grant_type", "client_credentials"),
79 ])
80 .send()
81 .await
82 .map(|response| response.text())?;
83
84 self.client_access_token = JSONValue::parse(resp.await?.as_str())?
85 .get_key_value("access_token")?
86 .read_string()?
87 .to_string();
88 }
89 Ok(())
90 }
91
92 pub async fn request_raw<M: prost::Message + Default>(
105 &mut self,
106 query: &str,
107 ) -> Result<M, IGDBApiError> {
108 self.request_api(query, endpoint_name::<M>()).await
109 }
110
111 pub async fn request_count<M: prost::Message + Default>(
125 &mut self,
126 query: & ApicalypseBuilder,
127 ) -> Result<Count, IGDBApiError> {
128 let query_string = query.to_query();
129 self.request_count_raw::<M>(query_string.as_str()).await
130 }
131
132 pub async fn request_count_raw<M: prost::Message + Default>(
145 &mut self,
146 query: &str,
147 ) -> Result<Count, IGDBApiError> {
148 self.request_api(query, format!("{}/count", self.endpoint_url::<M>())).await
149 }
150
151
152 fn endpoint_url<M: prost::Message + Default>(&self) -> String {
153 format!("{}/{}", self.endpoint, endpoint_name::<M>())
154 }
155
156 async fn request_api<M: prost::Message + Default>(&mut self, query: &str, url: String) -> Result<M, IGDBApiError> {
157 if let Err(error) = self.check_access_token().await {
158 return Err(error);
159 }
160 let bytes = self
161 .client
162 .post(url)
163 .body(query.to_string())
164 .bearer_auth(&self.client_access_token)
165 .header("client-id", &self.client_id)
166 .header("x-user-agent", LIB_VERSION_HEADER )
167 .send()
168 .await?
169 .bytes()
170 .await?;
171 M::decode(bytes).map_err(Into::into)
172 }
173}
174
175impl Default for Client {
176 fn default() -> Self {
178 use std::env::var;
179 Self::new(
180 &var("IGDB_API_ID").expect("for IGDB_API_ID env var to be defined"),
181 &var("IGDB_API_SECRET").expect("for IGDB_API_SECRET env var to be defined"),
182 )
183 }
184}
185
186
187fn endpoint_name<M: prost::Message + Default>() -> String {
188 let message_name = type_name::<M>().split("::").last().unwrap_or_default();
189 if message_name == "Person" {
190 "people".to_string()
191 } else {
192 use heck::AsSnekCase;
193 AsSnekCase(message_name).to_string().replace("_result", "") + "s"
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use std::env;
200 use crate::igdb::{AlternativeName, Game, GameEngineLogoResult, ThemeResult};
201 use super::*;
202
203 #[test]
204 fn test_default() {
205 env::set_var("IGDB_API_ID", "test_id_env");
207 env::set_var("IGDB_API_SECRET", "test_secret_env");
208
209 let client = Client::default();
211
212 assert_eq!(client.client_id, "test_id_env");
213 assert_eq!(client.client_secret, "test_secret_env");
214
215 env::remove_var("IGDB_API_ID");
217 env::remove_var("IGDB_API_SECRET");
218 }
219
220 #[test]
221 fn test_new() {
222 let client = Client::new("test_id", "test_secret");
223
224 assert_eq!(client.client_id, "test_id");
226 assert_eq!(client.client_secret, "test_secret");
227 assert_eq!(client.client_access_token, "");
228 }
229
230
231 #[test]
232 fn endpoint_name_games() {
233 assert_eq!(
234 "games",
235 endpoint_name::<Game>()
236 );
237 }
238
239
240 #[test]
241 fn endpoint_name_alternative_names() {
242 assert_eq!(
243 "alternative_names",
244 endpoint_name::<AlternativeName>()
245 );
246 }
247
248 #[test]
249 fn endpoint_name_game_engine_logos() {
250 assert_eq!(
251 "game_engine_logos",
252 endpoint_name::<GameEngineLogoResult>()
253 );
254 }
255
256
257 #[test]
258 fn endpoint_name_themes() {
259 assert_eq!(
260 "themes",
261 endpoint_name::<ThemeResult>()
262 );
263 }
264
265
266}