itchio-api 0.3.0

Easily interact with the itch.io server-side API
Documentation
use chrono::{DateTime, Utc};
use serde::Deserialize;
use url::Url;

use crate::{Embed, Itchio, ItchioError, SmallUser, parsers::{date_from_str, option_date_from_str, empty_object_as_empty_array, small_date_from_str}};

/// The original response this crate gets from the server, useless to crate users.
#[derive(Clone, Debug, Deserialize)]
struct Games {
  games: Vec<Game>,
}

/// A representation of one of yours Games on the itch.io website.
#[derive(Clone, Debug, Deserialize)]
pub struct Game {
  pub id: u32,
  pub title: String,
  pub short_text: Option<String>,
  pub url: Url,
  pub cover_url: Option<Url>,
  pub still_cover_url: Option<Url>,
  pub r#type: String,
  pub classification: String,
  pub p_linux: bool,
  pub p_android: bool,
  pub p_windows: bool,
  pub p_osx: bool,
  #[serde(deserialize_with = "date_from_str")]
  pub created_at: DateTime<Utc>,
  pub min_price: u32,
  pub can_be_bought: bool,
  pub published: bool,
  // Note to self: When using a custom deserializer function, if the property is an Option because it might be missing in the JSON,
  // "default" needs to be added to cover the missing field case
  #[serde(default, deserialize_with = "option_date_from_str")]
  pub published_at: Option<DateTime<Utc>>,
  pub has_demo: bool,
  pub embed: Option<Embed>,
  pub user: SmallUser,
  pub views_count: u64,
  pub purchases_count: u64,
  pub downloads_count: u64,
  pub in_press_system: bool,
  pub earnings: Option<Vec<Earning>>,
}

/// How much money a Game has earned its developers.
#[derive(Clone, Debug, Deserialize)]
pub struct Earning {
  pub currency: String,
  pub amount_formatted: String,
  pub amount: u64,
}

/// Recent stats about a Game.
#[derive(Clone, Debug, Deserialize)]
pub struct Graphs {
  #[serde(deserialize_with = "empty_object_as_empty_array")]
  pub downloads: Vec<Download>,
  #[serde(deserialize_with = "empty_object_as_empty_array")]
  pub purchases: Vec<Purchase>,
  #[serde(deserialize_with = "empty_object_as_empty_array")]
  pub views: Vec<View>,
}

/// TODO no idea what this is supposed to be
#[derive(Clone, Debug, Deserialize)]
pub struct Download {}

/// TODO no idea what this is supposed to be
#[derive(Clone, Debug, Deserialize)]
pub struct Purchase {}

/// A representation of how many times a Game has been viewed in one day.
#[derive(Clone, Debug, Deserialize)]
pub struct View {
  #[serde(deserialize_with = "small_date_from_str")]
  pub date: DateTime<Utc>,
  pub count: u64,
}

impl Itchio {
  /// Get the games you've uploaded or have edit access to: <https://itch.io/docs/api/serverside#reference/profilegames-httpsitchioapi1keymy-games>
  pub async fn get_my_games(&self) -> Result<Vec<Game>, ItchioError> {
    let response = self.request::<Games>("my-games".to_string()).await?;
    Ok(response.games)
  }

  /// Get the views, downloads, and purchases related to your games.
  pub async fn get_my_games_graphs(&self) -> Result<Graphs, ItchioError> {
    Ok(self.request::<Graphs>("my-games/graphs".to_string()).await?)
  }
}

#[cfg(test)]
mod tests {
  use super::*;
  use std::env;
  use dotenv::dotenv;

  #[tokio::test]
  async fn good_games() {
    dotenv().ok();
    let client_secret = env::var("KEY").expect("KEY has to be set");
    let api = Itchio::new(client_secret).unwrap();
    let games = api.get_my_games().await.inspect_err(|err| eprintln!("Error spotted: {}", err));
    assert!(games.is_ok())
  }

  #[tokio::test]
  async fn bad_key_games() {
    let api = Itchio::new("bad_key".to_string()).unwrap();
    let games = api.get_my_games().await;
    assert!(games.is_err_and(|err| matches!(err, ItchioError::BadKey)))
  }

  #[tokio::test]
  async fn good_graphs() {
    dotenv().ok();
    let client_secret = env::var("KEY").expect("KEY has to be set");
    let api = Itchio::new(client_secret).unwrap();
    let graphs = api.get_my_games_graphs().await.inspect_err(|err| eprintln!("Error spotted: {}", err));
    assert!(graphs.is_ok())
  }

  #[tokio::test]
  async fn bad_key_graphs() {
    let api = Itchio::new("bad_key".to_string()).unwrap();
    let graphs = api.get_my_games_graphs().await.inspect_err(|err| eprintln!("Error spotted: {}", err));
    assert!(graphs.is_err_and(|err| matches!(err, ItchioError::BadKey)))
  }
}