Skip to main content

itchio_api/
lib.rs

1use reqwest::{Client, StatusCode};
2use thiserror::Error;
3use serde::{Deserialize, de::DeserializeOwned};
4
5mod parsers;
6pub mod endpoints;
7
8/// What you need to make requests, it stores and uses your API key.
9pub struct Itchio {
10  client: Client,
11  key: String,
12}
13
14impl Itchio {
15  /// Create a new Itchio client using your API key, which you can find there: <https://itch.io/user/settings/api-keys>
16  pub fn new(key: String) -> Self {
17    Self {
18      client: Client::new(),
19      key,
20    }
21  }
22
23  /// Make your own requests by specifying an endpoint and struct to deserialize!
24  pub async fn request<T: DeserializeOwned>(&self, endpoint: String) -> Result<T, ItchioError> {
25    let url = format!("https://itch.io/api/1/key/{}", endpoint);
26    let response = self
27      .client
28      .get(&url)
29      .header("Authorization", format!("Bearer {}", self.key))
30      .header("User-Agent", "itchio-api (https://codeberg.org/Taevas/itchio-api)")
31      .send()
32      .await
33      .map_err(|err| {
34        match err.status() {
35          Some(status) => match status {
36            StatusCode::NOT_FOUND => ItchioError::BadEndpoint,
37            StatusCode::TOO_MANY_REQUESTS => ItchioError::RateLimited,
38            _ => ItchioError::RequestFailed(err),
39          }
40          None => ItchioError::RequestFailed(err),
41        }
42      })?;
43
44    // We cannot consume the response twice, so instead of trying to call .json() twice,
45    // let's use serde_json twice to consume references to a String containing what we want from the Response!
46    let text = response
47      .text()
48      .await?;
49
50    if let Ok(bad_response) = serde_json::from_str::<ErrorResponseWrapped>(&text) {
51      let error = match bad_response.errors.zero.as_str() {
52        "invalid key" => ItchioError::BadKey,
53        "missing authorization header" => ItchioError::BadKey,
54        "invalid api endpoint" => ItchioError::BadEndpoint, // should have already been handled by 404 status
55        other => ItchioError::InternalError(other.to_string()),
56      };
57      return Err(error);
58    }
59
60    Ok(serde_json::from_str::<T>(&text)?)
61  }
62}
63
64#[derive(Deserialize)]
65struct ErrorResponseWrapped {
66  errors: ErrorResponse
67}
68
69#[derive(Deserialize)]
70struct ErrorResponse {
71  #[serde(rename = "0")]
72  zero: String
73}
74
75// TODO Remove RequestFailed, add several other Errors, created from map_err on a request.send
76#[derive(Error, Debug)]
77pub enum ItchioError {
78  /// A generic error about the request itself failing.
79  #[error("HTTP request failed: {0}")]
80  RequestFailed(#[from] reqwest::Error),
81  /// Happens if the API gives us an unexpected object, likely means there's a mistake with this crate (or a bad struct given to request()).
82  #[error("Failed to parse JSON: {0}")]
83  JsonParseError(#[from] serde_json::Error),
84  /// Due to certain circumstances, no other error can be used.
85  #[error("An unexpected error happened: {0}")]
86  InternalError(String),
87  /// The key that was used was rejected by the server.
88  #[error("Authentication failed.")]
89  BadKey,
90  /// The crate received a 404 status, meaning the endpoint doesn't exist or that there is a mistake with the URL.
91  #[error("The requested endpoint doesn't exist.")]
92  BadEndpoint,
93  /// The crate received a 429 status, meaning we sent too many requests in a given amount of time.
94  #[error("The server is rate limiting us.")]
95  RateLimited,
96}