protontweaks_api/
lib.rs

1use app::App;
2use log::trace;
3use reqwest::{StatusCode, Url};
4use serde::{de::DeserializeOwned, Deserialize, Serialize};
5
6pub mod app;
7pub mod system;
8
9pub struct Protontweaks {
10    url: &'static str,
11}
12
13#[derive(Debug, Deserialize, Serialize)]
14pub struct AppsList {
15    pub sha: String,
16    pub short_sha: String,
17    pub apps: Vec<MicroApp>,
18}
19
20#[derive(Debug, Deserialize, Serialize)]
21pub struct MicroApp {
22    pub id: String,
23    pub name: String,
24}
25
26impl Protontweaks {
27    pub const fn new() -> Self {
28        Self {
29            url: "https://api.protontweaks.com/v4",
30        }
31    }
32
33    pub const fn new_with_url(url: &'static str) -> Self {
34        Self { url: url }
35    }
36
37    pub fn to_url(&self, endpoint: &str) -> Url {
38        let url = Url::parse(&self.url).unwrap();
39
40        return url.join(endpoint).unwrap();
41    }
42
43    async fn get<T: DeserializeOwned>(&self, url: &str) -> Result<T, String> {
44        let url = self.to_url(url);
45
46        trace!("Requesting apps from '{url}'...");
47
48        let response = reqwest::get(url.clone())
49            .await
50            .map_err(|error| error.to_string())?;
51
52        if response.status().is_success() {
53            response
54                .json::<T>()
55                .await
56                .map_err(|_| "Failed to parse apps.json".to_string())
57        } else {
58            match response.status() {
59                StatusCode::NOT_FOUND => {
60                    Err(format!("Unable to locate file at '{}'.", url.to_string()))
61                }
62                _ => Err(response.error_for_status().unwrap_err().to_string()),
63            }
64        }
65    }
66
67    pub async fn try_apps_list(&self) -> Result<AppsList, String> {
68        self.get::<AppsList>("apps.json").await
69    }
70
71    pub async fn apps_list(&self) -> AppsList {
72        self.try_apps_list().await.unwrap()
73    }
74
75    pub async fn try_apps(&self) -> Result<Vec<MicroApp>, String> {
76        self.try_apps_list().await.map(|apps_list| apps_list.apps)
77    }
78
79    pub async fn apps(&self) -> Vec<MicroApp> {
80        self.try_apps().await.unwrap()
81    }
82
83    pub async fn try_app_ids(&self) -> Result<Vec<String>, String> {
84        self.try_apps()
85            .await
86            .map(|apps| apps.iter().map(|app| app.id.clone()).collect())
87    }
88
89    pub async fn app_ids(&self) -> Vec<String> {
90        self.try_app_ids().await.unwrap()
91    }
92
93    pub async fn try_app(&self, app_id: &str) -> Result<App, String> {
94        self.get::<App>(&format!("{app_id}.json")).await
95    }
96
97    pub async fn app(&self, app_id: &str) -> App {
98        self.try_app(app_id).await.unwrap()
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[tokio::test]
107    async fn apps_list() {
108        let api = Protontweaks::new();
109
110        assert!(
111            api.try_apps_list().await.unwrap().apps.len() > 0,
112            "Should contain a list of apps"
113        );
114        assert!(
115            api.apps_list().await.apps.len() > 0,
116            "Should contain a list of apps"
117        );
118    }
119
120    #[tokio::test]
121    async fn apps() {
122        let api = Protontweaks::new();
123
124        assert!(
125            api.try_apps().await.unwrap().len() > 0,
126            "Should be a list of apps"
127        );
128        assert!(api.apps().await.len() > 0, "Should be a list of apps");
129    }
130
131    #[tokio::test]
132    async fn app_ids() {
133        let api = Protontweaks::new();
134
135        assert!(
136            api.try_app_ids().await.unwrap().len() > 0,
137            "Should be a list of app ids"
138        );
139        assert!(api.app_ids().await.len() > 0, "Should be a list of app ids");
140    }
141
142    #[tokio::test]
143    async fn try_app() {
144        let expected_id = "644930";
145        let api = Protontweaks::new();
146
147        let app = api.try_app(expected_id).await.unwrap();
148
149        assert_eq!(app.id, expected_id);
150        assert_eq!(app.issues.len(), 1);
151        assert_eq!(app.tweaks.tricks.len(), 1);
152        assert_eq!(app.tweaks.env.len(), 0);
153        assert_eq!(app.tweaks.settings.gamemode, Some(true));
154        assert_eq!(app.tweaks.settings.mangohud, Some(true));
155    }
156
157    #[tokio::test]
158    async fn app() {
159        let expected_id = "644930";
160        let api = Protontweaks::new();
161
162        let app = api.app(expected_id).await;
163
164        assert_eq!(app.id, expected_id);
165        assert_eq!(app.issues.len(), 1);
166        assert_eq!(app.tweaks.tricks.len(), 1);
167        assert_eq!(app.tweaks.env.len(), 0);
168        assert_eq!(app.tweaks.settings.gamemode, Some(true));
169        assert_eq!(app.tweaks.settings.mangohud, Some(true));
170    }
171}