client_3dsdb/
xml.rs

1//! Accesses <http://3dsdb.com> to get 3DS title data.
2//!
3//! This module uses data from an XML file published on 3dsdb.com. There are two methods to
4//! access these being [get_releases] and [get_releases_async], which are equivalent bar the async
5//! usage.
6//!
7//! ```
8//! use client_3dsdb::xml::get_releases;
9//!
10//! fn print_releases() {
11//!     let releases = get_releases().unwrap();
12//!
13//!     for release in releases {
14//!         println!("{}", release.name);
15//!     }
16//! }
17//!
18//! ```
19//!
20//! If you know the title ID ahead of time, you can get a [HashMap] using [get_releases_map].
21//!
22//! ```
23//! use client_3dsdb::xml::get_releases_map;
24//!
25//! let releases = get_releases_map().unwrap();
26//! let a_great_game = releases.get("0004000000030200").unwrap();
27//! assert_eq!(a_great_game.name, "Kid Icarus: Uprising")
28//! ```
29//!
30
31use std::collections::HashMap;
32use std::fmt::Debug;
33use serde::Deserialize;
34use rayon::prelude::*;
35use crate::error::Error;
36
37#[derive(Deserialize)]
38struct Releases {
39    #[serde(rename = "$value")]
40    releases: Vec<Release>
41}
42
43/// A 3DS title.
44#[derive(Deserialize, Eq, PartialEq, Debug)]
45pub struct Release {
46    pub id: String,
47    pub name: String,
48    pub publisher: String,
49    pub region: String,
50    pub languages: String,
51    pub group: String,
52    #[serde(alias = "imagesize")]
53    pub image_size: u64,
54    pub serial: String,
55    #[serde(alias = "titleid")]
56    pub title_id: String,
57    #[serde(alias = "imgcrc")]
58    pub img_crc: String,
59    pub filename: String,
60    #[serde(alias = "releasename")]
61    pub release_name: String,
62    #[serde(alias = "trimmedsize")]
63    pub trimmed_size: u64,
64    pub firmware: String,
65    #[serde(alias = "type")]
66    pub _type: String,
67    pub card: String,
68}
69
70/// Gets of [Release]s asynchronously.
71pub async fn get_releases_async() -> Result<Vec<Release>, Error> {
72    let response = reqwest::get("http://3dsdb.com/xml.php").await?;
73    let text = response.text().await?;
74    let release: Releases = serde_xml_rs::from_str(&text)?;
75    Ok(release.releases)
76}
77
78/// Gets [Release]s synchronously.
79pub fn get_releases() -> Result<Vec<Release>, Error> {
80    let response = reqwest::blocking::get("http://3dsdb.com/xml.php")?;
81    let release: Releases = serde_xml_rs::from_reader(response)?;
82    Ok(release.releases)
83
84}
85
86/// Gets a hash map of [Release]s with title IDs as the key.
87///
88/// ```
89/// use client_3dsdb::xml::get_releases_map;
90///
91/// let releases = get_releases_map().unwrap();
92/// let a_great_game = releases.get("0004000000030200").unwrap();
93/// assert_eq!(a_great_game.name, "Kid Icarus: Uprising")
94/// ```
95pub fn get_releases_map() -> Result<HashMap<String, Release>, Error> {
96    let releases = get_releases()?;
97    Ok(releases.into_par_iter()
98        .map(|release| (release.title_id.clone(), release))
99        .collect())
100}
101
102impl From<serde_xml_rs::Error> for Error {
103    fn from(value: serde_xml_rs::Error) -> Self {
104        Error { message: format!("{}", value) }
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use std::ops::Deref;
111    use once_cell::sync::Lazy;
112    use rstest::rstest;
113    use super::*;
114
115    static EXPECTED_RELEASE: Lazy<Release> = Lazy::new(|| Release {
116        id: "1".to_string(),
117        name: "Tom Clancys Ghost Recon: Shadow Wars".to_string(),
118        publisher: "Ubisoft".to_string(),
119        region: "EUR".to_string(),
120        languages: "en,fr,de,it,es".to_string(),
121        group: "Legacy".to_string(),
122        image_size: 2048,
123        serial: "CTR-AGRP".to_string(),
124        title_id: "0004000000037500".to_string(),
125        img_crc: "5BD0B123".to_string(),
126        filename: "lgc-grsw".to_string(),
127        release_name: "Tom_Clancys_Ghost_Recon_Shadow_Wars_EUR_3DS-LGC".to_string(),
128        trimmed_size: 229750272,
129        firmware: "1.0.0E".to_string(),
130        _type: "1".to_string(),
131        card: "1".to_string()
132    });
133
134    #[rstest]
135    fn get_releases_gets_valid_information() {
136        let value = get_releases().unwrap();
137        let actual = value.get(0).unwrap();
138        assert_eq!(actual, EXPECTED_RELEASE.deref())
139    }
140
141    #[rstest]
142    async fn get_releases_async_gets_valid_information() {
143        let value = get_releases_async().await.unwrap();
144        let actual = value.get(0).unwrap();
145        assert_eq!(actual, EXPECTED_RELEASE.deref())
146    }
147
148    #[rstest]
149    fn get_releases_map_gets_valid_information() {
150        let releases_map = get_releases_map().unwrap();
151        let actual = releases_map.get(&EXPECTED_RELEASE.title_id).unwrap();
152        assert_eq!(actual, EXPECTED_RELEASE.deref())
153    }
154}