thermite/
api.rs

1use std::collections::{BTreeMap, HashMap};
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::{
7    error::ThermiteError,
8    model::{Mod, ModVersion},
9};
10
11#[derive(Deserialize, Serialize, Clone, Debug)]
12struct PackageListing {
13    name: String,
14    owner: String,
15    versions: Vec<PackageVersion>,
16    #[serde(flatten)]
17    _extra: HashMap<String, Value>,
18}
19
20#[derive(Deserialize, Serialize, Clone, Debug)]
21struct PackageVersion {
22    dependencies: Vec<String>,
23    description: String,
24    download_url: String,
25    file_size: u64,
26    version_number: String,
27    full_name: String,
28
29    #[serde(flatten)]
30    _extra: HashMap<String, Value>,
31}
32
33/// Get a list of available packges from `northstar.thunderstore.io`
34///
35/// # Errors
36/// * IO Errors
37/// * Unexpected response format from Thunderstore
38pub fn get_package_index() -> Result<Vec<Mod>, ThermiteError> {
39    let raw = ureq::get("https://northstar.thunderstore.io/c/northstar/api/v1/package/")
40        .header("accept", "application/json")
41        .call()?;
42    let parsed: Vec<PackageListing> = serde_json::from_reader(raw.into_body().into_reader())?;
43    let index = map_response(&parsed);
44
45    Ok(index)
46}
47
48fn map_response(res: &[PackageListing]) -> Vec<Mod> {
49    res.iter()
50        .map(|e| {
51            let versions = &e.versions;
52            let latest = versions[0].clone();
53            let mut urls = BTreeMap::new();
54
55            for v in versions {
56                urls.insert(
57                    v.version_number.clone(),
58                    ModVersion {
59                        name: e.name.clone(),
60                        full_name: v.full_name.clone(),
61                        version: v.version_number.clone(),
62                        desc: v.description.clone(),
63                        file_size: v.file_size,
64                        deps: v
65                            .dependencies
66                            .iter()
67                            .filter(|e| !e.contains("northstar-Northstar"))
68                            .cloned()
69                            .collect::<Vec<String>>(),
70                        installed: false,
71                        global: false,
72                        url: v.download_url.clone(),
73                    },
74                );
75            }
76
77            Mod {
78                name: e.name.clone(),
79                author: e.owner.clone(),
80                latest: latest.version_number,
81                versions: urls,
82                installed: false,
83                global: false,
84                upgradable: false,
85            }
86        })
87        .collect()
88}
89
90#[cfg(test)]
91mod test {
92    use std::collections::{BTreeMap, HashMap};
93
94    use crate::model::{Mod, ModVersion};
95
96    use super::{get_package_index, map_response, PackageListing, PackageVersion};
97
98    #[test]
99    fn get_packages_from_tstore() {
100        let index = get_package_index();
101        assert!(index.is_ok());
102        let index = index.unwrap();
103        assert!(!index.is_empty());
104        let mut deps = 0;
105        for f in index {
106            for d in f.versions.get(&f.latest).unwrap().deps.iter() {
107                assert_ne!(d, "northstar-Northstar");
108                deps += 1;
109            }
110        }
111
112        assert_ne!(0, deps);
113    }
114
115    #[test]
116    fn map_thunderstore_response() {
117        let test_data = [PackageListing {
118            name: "Foo".into(),
119            owner: "Bar".into(),
120            versions: vec![PackageVersion {
121                dependencies: vec!["something".into()],
122                description: "Test".into(),
123                download_url: "localhost".into(),
124                file_size: 420,
125                version_number: "0.1.0".into(),
126                full_name: "Bar-Foo-0.1.0".into(),
127                _extra: HashMap::new(),
128            }],
129            _extra: HashMap::new(),
130        }];
131
132        let expected = [Mod {
133            name: "Foo".into(),
134            author: "Bar".into(),
135            latest: "0.1.0".into(),
136            installed: false,
137            upgradable: false,
138            global: false,
139            versions: BTreeMap::from([(
140                "0.1.0".into(),
141                ModVersion {
142                    name: "Foo".into(),
143                    full_name: "Bar-Foo-0.1.0".into(),
144                    version: "0.1.0".into(),
145                    url: "localhost".into(),
146                    desc: "Test".into(),
147                    deps: vec!["something".into()],
148                    installed: false,
149                    global: false,
150                    file_size: 420,
151                },
152            )]),
153        }];
154
155        let res = map_response(&test_data);
156        assert!(!res.is_empty());
157        assert_eq!(&res[0], &expected[0]);
158    }
159}