soar_core/database/
models.rs

1use std::fmt::Display;
2
3use rusqlite::types::Value;
4use serde::{de, Deserialize, Deserializer, Serialize};
5
6use super::packages::PackageProvide;
7
8#[derive(Debug, Clone, Deserialize, Serialize)]
9pub struct Maintainer {
10    pub name: String,
11    pub contact: String,
12}
13
14impl Display for Maintainer {
15    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16        write!(f, "{} ({})", self.name, self.contact)
17    }
18}
19
20pub trait PackageExt {
21    fn pkg_name(&self) -> &str;
22    fn pkg_id(&self) -> &str;
23    fn version(&self) -> &str;
24    fn repo_name(&self) -> &str;
25}
26
27pub trait FromRow: Sized {
28    fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self>;
29}
30
31#[derive(Debug, Clone, Default)]
32pub struct Package {
33    pub id: u64,
34    pub repo_name: String,
35    pub disabled: Option<bool>,
36    pub disabled_reason: Option<Value>,
37    pub rank: Option<u64>,
38    pub pkg: Option<String>,
39    pub pkg_id: String,
40    pub pkg_name: String,
41    pub pkg_family: Option<String>,
42    pub pkg_type: Option<String>,
43    pub pkg_webpage: Option<String>,
44    pub app_id: Option<String>,
45    pub description: String,
46    pub version: String,
47    pub version_upstream: Option<String>,
48    pub licenses: Option<Vec<String>>,
49    pub download_url: String,
50    pub size: Option<u64>,
51    pub ghcr_pkg: Option<String>,
52    pub ghcr_size: Option<u64>,
53    pub ghcr_files: Option<Vec<String>>,
54    pub ghcr_blob: Option<String>,
55    pub ghcr_url: Option<String>,
56    pub bsum: Option<String>,
57    pub shasum: Option<String>,
58    pub homepages: Option<Vec<String>>,
59    pub notes: Option<Vec<String>>,
60    pub source_urls: Option<Vec<String>>,
61    pub tags: Option<Vec<String>>,
62    pub categories: Option<Vec<String>>,
63    pub icon: Option<String>,
64    pub desktop: Option<String>,
65    pub appstream: Option<String>,
66    pub build_id: Option<String>,
67    pub build_date: Option<String>,
68    pub build_action: Option<String>,
69    pub build_script: Option<String>,
70    pub build_log: Option<String>,
71    pub provides: Option<Vec<PackageProvide>>,
72    pub snapshots: Option<Vec<String>>,
73    pub repology: Option<Vec<String>>,
74    pub download_count: Option<u64>,
75    pub download_count_month: Option<u64>,
76    pub download_count_week: Option<u64>,
77    pub maintainers: Option<Vec<Maintainer>>,
78    pub replaces: Option<Vec<String>>,
79    pub bundle: bool,
80    pub bundle_type: Option<String>,
81    pub soar_syms: bool,
82    pub deprecated: bool,
83    pub desktop_integration: Option<bool>,
84    pub external: Option<bool>,
85    pub installable: Option<bool>,
86    pub portable: Option<bool>,
87    pub trusted: Option<bool>,
88    pub version_latest: Option<String>,
89    pub version_outdated: Option<bool>,
90}
91
92impl FromRow for Package {
93    fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
94        let parse_json_vec = |idx: &str| -> rusqlite::Result<Option<Vec<String>>> {
95            Ok(row
96                .get::<_, Option<String>>(idx)?
97                .and_then(|json| serde_json::from_str(&json).ok()))
98        };
99
100        let parse_provides = |idx: &str| -> rusqlite::Result<Option<Vec<PackageProvide>>> {
101            Ok(row
102                .get::<_, Option<String>>(idx)?
103                .and_then(|json| serde_json::from_str(&json).ok()))
104        };
105
106        let maintainers: Option<Vec<Maintainer>> = row
107            .get::<_, Option<String>>("maintainers")?
108            .and_then(|json| serde_json::from_str(&json).ok());
109
110        let licenses = parse_json_vec("licenses")?;
111        let ghcr_files = parse_json_vec("ghcr_files")?;
112        let homepages = parse_json_vec("homepages")?;
113        let notes = parse_json_vec("notes")?;
114        let source_urls = parse_json_vec("source_urls")?;
115        let tags = parse_json_vec("tags")?;
116        let categories = parse_json_vec("categories")?;
117        let provides = parse_provides("provides")?;
118        let snapshots = parse_json_vec("snapshots")?;
119        let repology = parse_json_vec("repology")?;
120        let replaces = parse_json_vec("replaces")?;
121
122        Ok(Package {
123            id: row.get("id")?,
124            disabled: row.get("disabled")?,
125            disabled_reason: row.get("disabled_reason")?,
126            rank: row.get("rank")?,
127            pkg: row.get("pkg")?,
128            pkg_id: row.get("pkg_id")?,
129            pkg_name: row.get("pkg_name")?,
130            pkg_family: row.get("pkg_family")?,
131            pkg_type: row.get("pkg_type")?,
132            pkg_webpage: row.get("pkg_webpage")?,
133            app_id: row.get("app_id")?,
134            description: row.get("description")?,
135            version: row.get("version")?,
136            version_upstream: row.get("version_upstream")?,
137            licenses,
138            download_url: row.get("download_url")?,
139            size: row.get("size")?,
140            ghcr_pkg: row.get("ghcr_pkg")?,
141            ghcr_size: row.get("ghcr_size")?,
142            ghcr_files,
143            ghcr_blob: row.get("ghcr_blob")?,
144            ghcr_url: row.get("ghcr_url")?,
145            bsum: row.get("bsum")?,
146            shasum: row.get("shasum")?,
147            icon: row.get("icon")?,
148            desktop: row.get("desktop")?,
149            appstream: row.get("appstream")?,
150            homepages,
151            notes,
152            source_urls,
153            tags,
154            categories,
155            build_id: row.get("build_id")?,
156            build_date: row.get("build_date")?,
157            build_action: row.get("build_action")?,
158            build_script: row.get("build_script")?,
159            build_log: row.get("build_log")?,
160            provides,
161            snapshots,
162            repology,
163            download_count: row.get("download_count")?,
164            download_count_week: row.get("download_count_week")?,
165            download_count_month: row.get("download_count_month")?,
166            repo_name: row.get("repo_name")?,
167            maintainers,
168            replaces,
169            bundle: row.get("bundle")?,
170            bundle_type: row.get("bundle_type")?,
171            soar_syms: row.get("soar_syms")?,
172            deprecated: row.get("deprecated")?,
173            desktop_integration: row.get("desktop_integration")?,
174            external: row.get("external")?,
175            installable: row.get("installable")?,
176            portable: row.get("portable")?,
177            trusted: row.get("trusted")?,
178            version_latest: row.get("version_latest")?,
179            version_outdated: row.get("version_outdated")?,
180        })
181    }
182}
183
184#[derive(Debug, Clone)]
185pub struct InstalledPackage {
186    pub id: u64,
187    pub repo_name: String,
188    pub pkg: Option<String>,
189    pub pkg_id: String,
190    pub pkg_name: String,
191    pub pkg_type: Option<String>,
192    pub version: String,
193    pub size: u64,
194    pub checksum: Option<String>,
195    pub installed_path: String,
196    pub installed_date: String,
197    pub profile: String,
198    pub pinned: bool,
199    pub is_installed: bool,
200    pub with_pkg_id: bool,
201    pub detached: bool,
202    pub unlinked: bool,
203    pub provides: Option<Vec<PackageProvide>>,
204    pub portable_path: Option<String>,
205    pub portable_home: Option<String>,
206    pub portable_config: Option<String>,
207    pub portable_share: Option<String>,
208    pub portable_cache: Option<String>,
209    pub install_patterns: Option<Vec<String>>,
210}
211
212impl FromRow for InstalledPackage {
213    fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
214        let parse_provides = |idx: &str| -> rusqlite::Result<Option<Vec<PackageProvide>>> {
215            let value: Option<String> = row.get(idx)?;
216            Ok(value.and_then(|s| serde_json::from_str(&s).ok()))
217        };
218
219        let parse_install_patterns = |idx: &str| -> rusqlite::Result<Option<Vec<String>>> {
220            let value: Option<String> = row.get(idx)?;
221            Ok(value.and_then(|s| serde_json::from_str(&s).ok()))
222        };
223
224        let provides = parse_provides("provides")?;
225        let install_patterns = parse_install_patterns("install_patterns")?;
226
227        Ok(InstalledPackage {
228            id: row.get("id")?,
229            repo_name: row.get("repo_name")?,
230            pkg: row.get("pkg")?,
231            pkg_id: row.get("pkg_id")?,
232            pkg_name: row.get("pkg_name")?,
233            pkg_type: row.get("pkg_type")?,
234            version: row.get("version")?,
235            size: row.get("size")?,
236            checksum: row.get("checksum")?,
237            installed_path: row.get("installed_path")?,
238            installed_date: row.get("installed_date")?,
239            profile: row.get("profile")?,
240            pinned: row.get("pinned")?,
241            is_installed: row.get("is_installed")?,
242            with_pkg_id: row.get("with_pkg_id")?,
243            detached: row.get("detached")?,
244            unlinked: row.get("unlinked")?,
245            provides,
246            portable_path: row.get("portable_path")?,
247            portable_home: row.get("portable_home")?,
248            portable_config: row.get("portable_config")?,
249            portable_share: row.get("portable_share")?,
250            portable_cache: row.get("portable_cache")?,
251            install_patterns,
252        })
253    }
254}
255
256#[derive(Deserialize)]
257#[serde(untagged)]
258enum FlexiBool {
259    Bool(bool),
260    String(String),
261}
262
263fn empty_is_none<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
264where
265    D: Deserializer<'de>,
266{
267    let s: Option<String> = Option::deserialize(deserializer)?;
268    Ok(s.filter(|s| !s.is_empty()))
269}
270
271fn optional_number<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
272where
273    D: Deserializer<'de>,
274{
275    let s: Option<String> = Option::deserialize(deserializer)?;
276    Ok(s.filter(|s| !s.is_empty())
277        .and_then(|s| s.parse::<i64>().ok())
278        .filter(|&n| n >= 0)
279        .map(|n| n as u64))
280}
281
282fn flexible_bool<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
283where
284    D: Deserializer<'de>,
285{
286    match Option::<FlexiBool>::deserialize(deserializer)? {
287        Some(FlexiBool::Bool(b)) => Ok(Some(b)),
288        Some(FlexiBool::String(s)) => match s.to_lowercase().as_str() {
289            "true" | "yes" | "1" => Ok(Some(true)),
290            "false" | "no" | "0" => Ok(Some(false)),
291            "" => Ok(None),
292            _ => Err(de::Error::invalid_value(
293                de::Unexpected::Str(&s),
294                &"a valid boolean (true/false, yes/no, 1/0)",
295            )),
296        },
297        None => Ok(None),
298    }
299}
300
301#[derive(Debug, Default, Clone, Deserialize, Serialize)]
302pub struct RemotePackage {
303    #[serde(deserialize_with = "flexible_bool", alias = "_disabled")]
304    pub disabled: Option<bool>,
305
306    #[serde(alias = "_disabled_reason")]
307    pub disabled_reason: Option<serde_json::Value>,
308
309    #[serde(default, deserialize_with = "optional_number")]
310    pub rank: Option<u64>,
311
312    #[serde(default, deserialize_with = "empty_is_none")]
313    pub pkg: Option<String>,
314    pub pkg_id: String,
315    pub pkg_name: String,
316
317    #[serde(default, deserialize_with = "empty_is_none")]
318    pub pkg_family: Option<String>,
319
320    #[serde(default, deserialize_with = "empty_is_none")]
321    pub pkg_type: Option<String>,
322
323    #[serde(default, deserialize_with = "empty_is_none")]
324    pub pkg_webpage: Option<String>,
325
326    pub description: String,
327    pub version: String,
328
329    #[serde(default, deserialize_with = "empty_is_none")]
330    pub version_upstream: Option<String>,
331
332    pub download_url: String,
333
334    #[serde(default, deserialize_with = "optional_number")]
335    pub size_raw: Option<u64>,
336
337    #[serde(default, deserialize_with = "empty_is_none")]
338    pub ghcr_pkg: Option<String>,
339
340    #[serde(default, deserialize_with = "optional_number")]
341    pub ghcr_size_raw: Option<u64>,
342
343    pub ghcr_files: Option<Vec<String>>,
344
345    #[serde(default, deserialize_with = "empty_is_none")]
346    pub ghcr_blob: Option<String>,
347
348    #[serde(default, deserialize_with = "empty_is_none")]
349    pub ghcr_url: Option<String>,
350
351    #[serde(alias = "src_url")]
352    pub src_urls: Option<Vec<String>>,
353
354    #[serde(alias = "homepage")]
355    pub homepages: Option<Vec<String>>,
356
357    #[serde(alias = "license")]
358    pub licenses: Option<Vec<String>>,
359
360    #[serde(alias = "maintainer")]
361    pub maintainers: Option<Vec<String>>,
362
363    #[serde(alias = "note")]
364    pub notes: Option<Vec<String>>,
365
366    #[serde(alias = "tag")]
367    pub tags: Option<Vec<String>>,
368
369    #[serde(default, deserialize_with = "empty_is_none")]
370    pub bsum: Option<String>,
371
372    #[serde(default, deserialize_with = "empty_is_none")]
373    pub shasum: Option<String>,
374
375    #[serde(default, deserialize_with = "empty_is_none")]
376    pub build_id: Option<String>,
377
378    #[serde(default, deserialize_with = "empty_is_none")]
379    pub build_date: Option<String>,
380
381    #[serde(default, deserialize_with = "empty_is_none", alias = "build_gha")]
382    pub build_action: Option<String>,
383
384    #[serde(default, deserialize_with = "empty_is_none")]
385    pub build_script: Option<String>,
386
387    #[serde(default, deserialize_with = "empty_is_none")]
388    pub build_log: Option<String>,
389
390    #[serde(alias = "category")]
391    pub categories: Option<Vec<String>>,
392
393    pub provides: Option<Vec<String>>,
394
395    #[serde(default, deserialize_with = "empty_is_none")]
396    pub icon: Option<String>,
397
398    #[serde(default, deserialize_with = "empty_is_none")]
399    pub desktop: Option<String>,
400
401    #[serde(default, deserialize_with = "empty_is_none")]
402    pub appstream: Option<String>,
403
404    #[serde(default, deserialize_with = "empty_is_none")]
405    pub app_id: Option<String>,
406
407    #[serde(default, deserialize_with = "optional_number")]
408    pub download_count: Option<u64>,
409
410    #[serde(default, deserialize_with = "optional_number")]
411    pub download_count_month: Option<u64>,
412
413    #[serde(default, deserialize_with = "optional_number")]
414    pub download_count_week: Option<u64>,
415
416    #[serde(default, deserialize_with = "flexible_bool")]
417    pub bundle: Option<bool>,
418
419    #[serde(default, deserialize_with = "empty_is_none")]
420    pub bundle_type: Option<String>,
421
422    #[serde(default, deserialize_with = "flexible_bool")]
423    pub soar_syms: Option<bool>,
424
425    #[serde(default, deserialize_with = "flexible_bool")]
426    pub deprecated: Option<bool>,
427
428    #[serde(default, deserialize_with = "flexible_bool")]
429    pub desktop_integration: Option<bool>,
430
431    #[serde(default, deserialize_with = "flexible_bool")]
432    pub external: Option<bool>,
433
434    #[serde(default, deserialize_with = "flexible_bool")]
435    pub installable: Option<bool>,
436
437    #[serde(default, deserialize_with = "flexible_bool")]
438    pub portable: Option<bool>,
439
440    #[serde(default, deserialize_with = "flexible_bool")]
441    pub recurse_provides: Option<bool>,
442
443    #[serde(default, deserialize_with = "flexible_bool")]
444    pub trusted: Option<bool>,
445
446    #[serde(default, deserialize_with = "empty_is_none")]
447    pub version_latest: Option<String>,
448
449    #[serde(default, deserialize_with = "flexible_bool")]
450    pub version_outdated: Option<bool>,
451
452    pub repology: Option<Vec<String>>,
453    pub snapshots: Option<Vec<String>>,
454    pub replaces: Option<Vec<String>>,
455}
456
457impl PackageExt for Package {
458    fn pkg_name(&self) -> &str {
459        &self.pkg_name
460    }
461
462    fn pkg_id(&self) -> &str {
463        &self.pkg_id
464    }
465
466    fn version(&self) -> &str {
467        &self.version
468    }
469
470    fn repo_name(&self) -> &str {
471        &self.repo_name
472    }
473}
474
475impl PackageExt for InstalledPackage {
476    fn pkg_name(&self) -> &str {
477        &self.pkg_name
478    }
479
480    fn pkg_id(&self) -> &str {
481        &self.pkg_id
482    }
483
484    fn version(&self) -> &str {
485        &self.version
486    }
487
488    fn repo_name(&self) -> &str {
489        &self.repo_name
490    }
491}