1
use app::App;
2
use log::trace;
3
use reqwest::{StatusCode, Url};
4
use serde::{de::DeserializeOwned, Deserialize, Serialize};
5

            
6
pub mod app;
7
pub mod system;
8

            
9
pub struct Protontweaks {
10
    url: &'static str,
11
}
12

            
13
#[derive(Debug, Deserialize, Serialize)]
14
pub struct AppsList {
15
    pub sha: String,
16
    pub short_sha: String,
17
    pub apps: Vec<MicroApp>,
18
}
19

            
20
#[derive(Debug, Deserialize, Serialize)]
21
pub struct MicroApp {
22
    pub id: String,
23
    pub name: String,
24
}
25

            
26
impl Protontweaks {
27
18
    pub const fn new() -> Self {
28
18
        Self {
29
18
            url: "https://api.protontweaks.com/v4",
30
18
        }
31
18
    }
32

            
33
    pub const fn new_with_url(url: &'static str) -> Self {
34
        Self { url: url }
35
    }
36

            
37
27
    pub fn to_url(&self, endpoint: &str) -> Url {
38
27
        let url = Url::parse(&self.url).unwrap();
39

            
40
27
        return url.join(endpoint).unwrap();
41
27
    }
42

            
43
27
    async fn get<T: DeserializeOwned>(&self, url: &str) -> Result<T, String> {
44
27
        let url = self.to_url(url);
45

            
46
27
        trace!("Requesting apps from '{url}'...");
47

            
48
27
        let response = reqwest::get(url.clone())
49
27
            .await
50
27
            .map_err(|error| error.to_string())?;
51

            
52
27
        if response.status().is_success() {
53
27
            response
54
27
                .json::<T>()
55
27
                .await
56
27
                .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
27
    }
66

            
67
18
    pub async fn try_apps_list(&self) -> Result<AppsList, String> {
68
18
        self.get::<AppsList>("apps.json").await
69
18
    }
70

            
71
3
    pub async fn apps_list(&self) -> AppsList {
72
3
        self.try_apps_list().await.unwrap()
73
3
    }
74

            
75
12
    pub async fn try_apps(&self) -> Result<Vec<MicroApp>, String> {
76
12
        self.try_apps_list().await.map(|apps_list| apps_list.apps)
77
12
    }
78

            
79
3
    pub async fn apps(&self) -> Vec<MicroApp> {
80
3
        self.try_apps().await.unwrap()
81
3
    }
82

            
83
6
    pub async fn try_app_ids(&self) -> Result<Vec<String>, String> {
84
6
        self.try_apps()
85
6
            .await
86
1224
            .map(|apps| apps.iter().map(|app| app.id.clone()).collect())
87
6
    }
88

            
89
3
    pub async fn app_ids(&self) -> Vec<String> {
90
3
        self.try_app_ids().await.unwrap()
91
3
    }
92

            
93
9
    pub async fn try_app(&self, app_id: &str) -> Result<App, String> {
94
9
        self.get::<App>(&format!("{app_id}.json")).await
95
9
    }
96

            
97
6
    pub async fn app(&self, app_id: &str) -> App {
98
6
        self.try_app(app_id).await.unwrap()
99
6
    }
100
}
101

            
102
#[cfg(test)]
103
mod tests {
104
    use super::*;
105

            
106
    #[tokio::test]
107
3
    async fn apps_list() {
108
3
        let api = Protontweaks::new();
109

            
110
3
        assert!(
111
3
            api.try_apps_list().await.unwrap().apps.len() > 0,
112
            "Should contain a list of apps"
113
        );
114
3
        assert!(
115
3
            api.apps_list().await.apps.len() > 0,
116
3
            "Should contain a list of apps"
117
3
        );
118
3
    }
119

            
120
    #[tokio::test]
121
3
    async fn apps() {
122
3
        let api = Protontweaks::new();
123

            
124
3
        assert!(
125
3
            api.try_apps().await.unwrap().len() > 0,
126
            "Should be a list of apps"
127
        );
128
3
        assert!(api.apps().await.len() > 0, "Should be a list of apps");
129
3
    }
130

            
131
    #[tokio::test]
132
3
    async fn app_ids() {
133
3
        let api = Protontweaks::new();
134

            
135
3
        assert!(
136
3
            api.try_app_ids().await.unwrap().len() > 0,
137
            "Should be a list of app ids"
138
        );
139
3
        assert!(api.app_ids().await.len() > 0, "Should be a list of app ids");
140
3
    }
141

            
142
    #[tokio::test]
143
3
    async fn try_app() {
144
3
        let expected_id = "644930";
145
3
        let api = Protontweaks::new();
146

            
147
3
        let app = api.try_app(expected_id).await.unwrap();
148

            
149
3
        assert_eq!(app.id, expected_id);
150
3
        assert_eq!(app.issues.len(), 1);
151
3
        assert_eq!(app.tweaks.tricks.len(), 1);
152
3
        assert_eq!(app.tweaks.env.len(), 0);
153
3
        assert_eq!(app.tweaks.settings.gamemode, Some(true));
154
3
        assert_eq!(app.tweaks.settings.mangohud, Some(true));
155
3
    }
156

            
157
    #[tokio::test]
158
3
    async fn app() {
159
3
        let expected_id = "644930";
160
3
        let api = Protontweaks::new();
161

            
162
3
        let app = api.app(expected_id).await;
163

            
164
3
        assert_eq!(app.id, expected_id);
165
3
        assert_eq!(app.issues.len(), 1);
166
3
        assert_eq!(app.tweaks.tricks.len(), 1);
167
3
        assert_eq!(app.tweaks.env.len(), 0);
168
3
        assert_eq!(app.tweaks.settings.gamemode, Some(true));
169
3
        assert_eq!(app.tweaks.settings.mangohud, Some(true));
170
3
    }
171
}