igdb_api_rust/
client.rs

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
29/// The IGDB API client.
30pub 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    /// Create a new client.
40    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    /// Set a custom endpoint for use with the CORS proxy or your own proxy.
50    /// ```
51    /// use igdb_api_rust::client::Client;
52    /// let mut client = Client::new("test","test").with_endpoint("https://example.com/v4");
53    /// ```
54    pub fn with_endpoint(mut self, endpoint: &str) -> Self {
55        self.endpoint = endpoint.to_string();
56        self
57    }
58
59    /// Request the IGDB API for a protobuf response.
60    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    /// Request the IGDB API for a protobuf response.
93    /// This is the raw version of the request method.
94    /// It allows you to pass a query string directly.
95    /// ```
96    /// #[tokio::main]
97    /// async fn main() {
98    ///   use igdb_api_rust::client::Client;
99    ///   let mut client = Client::new("test","test");
100    ///   let query = "fields name; limit 5;";
101    ///   let response = client.request_raw::<igdb_api_rust::igdb::Game>(query).await;
102    /// }
103    /// ```
104    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    /// Request the IGDB API for a protobuf response for the count endpoint.
112    /// ```
113    ///#[tokio::main]
114    /// async fn main() {
115    ///   use igdb_api_rust::apicalypse_builder::ApicalypseBuilder;
116    ///   use igdb_api_rust::client::Client;
117    ///   let mut client = Client::new("test","test");
118    ///   let query = ApicalypseBuilder::default().filter("id > 1337").clone();
119    ///   let response = client.request_count::<igdb_api_rust::igdb::Game>(&query).await;
120    ///   println!("{:?}", response);
121    ///
122    /// }
123    /// ```
124    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    /// Request the IGDB API for a protobuf response for the count endpoint.
133    /// This is the raw version of the request_count method.
134    /// It allows you to pass a query string directly.
135    /// ```
136    /// #[tokio::main]
137    /// async fn main() {
138    ///   use igdb_api_rust::client::Client;
139    ///   let mut client = Client::new("test","test");
140    ///   let query = "w id > 1337";
141    ///   let response = client.request_count_raw::<igdb_api_rust::igdb::Game>(query).await;
142    /// }
143    /// ```
144    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    /// Get a client with the credentials from the environment variables.
177    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        // Set the environment variables that the default method expects to read
206        env::set_var("IGDB_API_ID", "test_id_env");
207        env::set_var("IGDB_API_SECRET", "test_secret_env");
208
209        // Call the default method
210        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        // Clean up by removing the environment variables if needed
216        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        // Basic checks to make sure the client was constructed correctly
225        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}