client_3dsdb/
json.rs

1//! Accesses <https://github.com/hax0kartik/3dsdb> to get 3DS title data.
2//!
3//! This module uses data from a set of JSON files published by hax0kartik. A quirk of this dataset
4//! is that these are divided by region. These can be accessed individually using the [Region] enum
5//! with [get_releases] and [get_releases_async], or alternatively can all be accessed using
6//! [get_all_releases], which is the recommended approach.
7//!
8//! ```
9//! use client_3dsdb::json::get_all_releases;
10//!
11//! async fn print_releases() {
12//!     let releases = get_all_releases().await.unwrap();
13//!
14//!     for release in releases {
15//!         println!("{}", release.name);
16//!     }
17//! }
18//! ```
19//!
20//! If you know the title ID ahead of time, you can get a [HashMap] for lookups by title ID using
21//! [get_releases_map].
22//!
23//! ```
24//! use client_3dsdb::json::{get_releases_map, Region};
25//!
26//! let releases = get_releases_map(Region::GB).unwrap();
27//! let a_great_game = releases.get("0004000000030200").unwrap();
28//! assert_eq!(a_great_game.name, "Kid Icarus™: Uprising")
29//! ```
30//!
31
32use std::collections::HashMap;
33use futures::future::join_all;
34use itertools::Itertools;
35use serde::Deserialize;
36use strum_macros::{Display, EnumIter};
37use strum::IntoEnumIterator;
38use rayon::prelude::*;
39use crate::error::Error;
40
41/// A title region. Required to access region-specific title lists.
42#[derive(Display, Debug, EnumIter)]
43pub enum Region {
44    GB,
45    JP,
46    KR,
47    TW,
48    US
49}
50
51/// A 3DS title.
52#[derive(Deserialize, Eq, PartialEq, Debug)]
53pub struct Release {
54    #[serde(alias = "Name")]
55    pub name: String,
56    #[serde(alias = "UID")]
57    pub uid: String,
58    #[serde(alias = "TitleID")]
59    pub title_id: String,
60    #[serde(alias = "Version")]
61    pub version: String,
62    #[serde(alias = "Size")]
63    pub size: String,
64    #[serde(alias = "Product Code")]
65    pub product_code: String,
66    #[serde(alias = "Publisher")]
67    pub publisher: String
68}
69
70/// Gets [Release]s asynchronously for all regions
71pub async fn get_all_releases() -> Result<Vec<Release>, Error> {
72    let release_futures = Region::iter().map(|region| get_releases_async(region));
73    let releases = join_all(release_futures).await;
74    releases.into_iter().flatten_ok().collect()
75}
76
77/// Gets [Release]s asynchronously for a given region.
78pub async fn get_releases_async(region: Region) -> Result<Vec<Release>, Error> {
79    let request = reqwest::get(&format!("https://raw.githubusercontent.com/hax0kartik/3dsdb/master/jsons/list_{}.json", region)).await?;
80    match request.json().await {
81        Ok(releases) => Ok(releases),
82        Err(error) => Err(Error::from(error))
83    }
84}
85
86/// Gets [Release]s synchronously for a given region.
87pub fn get_releases(region: Region) -> Result<Vec<Release>, Error> {
88    let request = reqwest::blocking::get(&format!("https://raw.githubusercontent.com/hax0kartik/3dsdb/master/jsons/list_{}.json", region))?;
89    match request.json() {
90        Ok(releases) => Ok(releases),
91        Err(error) => Err(Error::from(error))
92    }
93}
94
95/// Gets a hash map of [Release]s with title IDs as the key.
96///
97/// ```
98/// use client_3dsdb::json::{get_releases_map, Region};
99///
100/// let releases = get_releases_map(Region::GB).unwrap();
101/// let a_great_game = releases.get("0004000000030200").unwrap();
102/// assert_eq!(a_great_game.name, "Kid Icarus™: Uprising")
103/// ```
104pub fn get_releases_map(region: Region) -> Result<HashMap<String, Release>, Error> {
105    let releases = get_releases(region)?;
106    Ok(releases.into_par_iter()
107        .map(|release| (release.title_id.clone(), release))
108        .collect())
109}
110
111#[cfg(test)]
112mod tests {
113    use std::ops::Deref;
114    use once_cell::sync::Lazy;
115    use rstest::*;
116    use crate::json::{get_all_releases, get_releases, get_releases_async, get_releases_map, Region, Release};
117
118    #[rstest]
119    #[case(Region::GB, "GB")]
120    #[case(Region::JP, "JP")]
121    #[case(Region::KR, "KR")]
122    #[case(Region::TW, "TW")]
123    #[case(Region::US, "US")]
124    fn region_to_string_outputs_correct_string(#[case] region: Region, #[case] expected: String) {
125        let actual = format!("{}", region);
126        assert_eq!(actual, expected)
127    }
128
129    static EXPECTED_RELEASE: Lazy<Release> = Lazy::new(|| Release {
130        name: "Shovel Software Insurance Claim".to_string(),
131        uid: "50010000049535".to_string(),
132        title_id: "000400000F715C00".to_string(),
133        version: "N/A".to_string(),
134        size: "25.7 MB [206 blocks]".to_string(),
135        product_code: "KTR-N-CF6P".to_string(),
136        publisher: "Batafurai".to_string()
137    });
138
139    #[rstest]
140    async fn get_all_releases_returns_valid_information() {
141        let releases = get_all_releases().await.unwrap();
142        let actual = releases.get(0).unwrap();
143        assert_eq!(actual, EXPECTED_RELEASE.deref())
144    }
145
146    #[rstest]
147    fn get_releases_returns_valid_information() {
148        let releases = get_releases(Region::GB).unwrap();
149        let actual = releases.get(0).unwrap();
150        assert_eq!(actual, EXPECTED_RELEASE.deref())
151    }
152
153    #[rstest]
154    async fn get_releases_async_returns_valid_information() {
155        let releases = get_releases_async(Region::GB).await.unwrap();
156        let actual = releases.get(0).unwrap();
157        assert_eq!(actual, EXPECTED_RELEASE.deref())
158    }
159
160    #[rstest]
161    fn get_releases_map_returns_valid_information() {
162        let releases = get_releases_map(Region::GB).unwrap();
163        let actual = releases.get(&EXPECTED_RELEASE.title_id).unwrap();
164        assert_eq!(actual, EXPECTED_RELEASE.deref())
165    }
166}