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 install_patterns: Option<Vec<String>>,
209}
210
211impl FromRow for InstalledPackage {
212    fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
213        let parse_provides = |idx: &str| -> rusqlite::Result<Option<Vec<PackageProvide>>> {
214            let value: Option<String> = row.get(idx)?;
215            Ok(value.and_then(|s| serde_json::from_str(&s).ok()))
216        };
217
218        let parse_install_patterns = |idx: &str| -> rusqlite::Result<Option<Vec<String>>> {
219            let value: Option<String> = row.get(idx)?;
220            Ok(value.and_then(|s| serde_json::from_str(&s).ok()))
221        };
222
223        let provides = parse_provides("provides")?;
224        let install_patterns = parse_install_patterns("install_patterns")?;
225
226        Ok(InstalledPackage {
227            id: row.get("id")?,
228            repo_name: row.get("repo_name")?,
229            pkg: row.get("pkg")?,
230            pkg_id: row.get("pkg_id")?,
231            pkg_name: row.get("pkg_name")?,
232            pkg_type: row.get("pkg_type")?,
233            version: row.get("version")?,
234            size: row.get("size")?,
235            checksum: row.get("checksum")?,
236            installed_path: row.get("installed_path")?,
237            installed_date: row.get("installed_date")?,
238            profile: row.get("profile")?,
239            pinned: row.get("pinned")?,
240            is_installed: row.get("is_installed")?,
241            with_pkg_id: row.get("with_pkg_id")?,
242            detached: row.get("detached")?,
243            unlinked: row.get("unlinked")?,
244            provides,
245            portable_path: row.get("portable_path")?,
246            portable_home: row.get("portable_home")?,
247            portable_config: row.get("portable_config")?,
248            portable_share: row.get("portable_share")?,
249            install_patterns,
250        })
251    }
252}
253
254#[derive(Deserialize)]
255#[serde(untagged)]
256enum FlexiBool {
257    Bool(bool),
258    String(String),
259}
260
261fn empty_is_none<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
262where
263    D: Deserializer<'de>,
264{
265    let s: Option<String> = Option::deserialize(deserializer)?;
266    Ok(s.filter(|s| !s.is_empty()))
267}
268
269fn optional_number<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
270where
271    D: Deserializer<'de>,
272{
273    let s: Option<String> = Option::deserialize(deserializer)?;
274    Ok(s.filter(|s| !s.is_empty())
275        .and_then(|s| s.parse::<i64>().ok())
276        .filter(|&n| n >= 0)
277        .map(|n| n as u64))
278}
279
280fn flexible_bool<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
281where
282    D: Deserializer<'de>,
283{
284    match Option::<FlexiBool>::deserialize(deserializer)? {
285        Some(FlexiBool::Bool(b)) => Ok(Some(b)),
286        Some(FlexiBool::String(s)) => match s.to_lowercase().as_str() {
287            "true" | "yes" | "1" => Ok(Some(true)),
288            "false" | "no" | "0" => Ok(Some(false)),
289            "" => Ok(None),
290            _ => Err(de::Error::invalid_value(
291                de::Unexpected::Str(&s),
292                &"a valid boolean (true/false, yes/no, 1/0)",
293            )),
294        },
295        None => Ok(None),
296    }
297}
298
299#[derive(Debug, Default, Clone, Deserialize, Serialize)]
300pub struct RemotePackage {
301    #[serde(deserialize_with = "flexible_bool", alias = "_disabled")]
302    pub disabled: Option<bool>,
303
304    #[serde(alias = "_disabled_reason")]
305    pub disabled_reason: Option<serde_json::Value>,
306
307    #[serde(default, deserialize_with = "optional_number")]
308    pub rank: Option<u64>,
309
310    #[serde(default, deserialize_with = "empty_is_none")]
311    pub pkg: Option<String>,
312    pub pkg_id: String,
313    pub pkg_name: String,
314
315    #[serde(default, deserialize_with = "empty_is_none")]
316    pub pkg_family: Option<String>,
317
318    #[serde(default, deserialize_with = "empty_is_none")]
319    pub pkg_type: Option<String>,
320
321    #[serde(default, deserialize_with = "empty_is_none")]
322    pub pkg_webpage: Option<String>,
323
324    pub description: String,
325    pub version: String,
326
327    #[serde(default, deserialize_with = "empty_is_none")]
328    pub version_upstream: Option<String>,
329
330    pub download_url: String,
331
332    #[serde(default, deserialize_with = "optional_number")]
333    pub size_raw: Option<u64>,
334
335    #[serde(default, deserialize_with = "empty_is_none")]
336    pub ghcr_pkg: Option<String>,
337
338    #[serde(default, deserialize_with = "optional_number")]
339    pub ghcr_size_raw: Option<u64>,
340
341    pub ghcr_files: Option<Vec<String>>,
342
343    #[serde(default, deserialize_with = "empty_is_none")]
344    pub ghcr_blob: Option<String>,
345
346    #[serde(default, deserialize_with = "empty_is_none")]
347    pub ghcr_url: Option<String>,
348
349    #[serde(alias = "src_url")]
350    pub src_urls: Option<Vec<String>>,
351
352    #[serde(alias = "homepage")]
353    pub homepages: Option<Vec<String>>,
354
355    #[serde(alias = "license")]
356    pub licenses: Option<Vec<String>>,
357
358    #[serde(alias = "maintainer")]
359    pub maintainers: Option<Vec<String>>,
360
361    #[serde(alias = "note")]
362    pub notes: Option<Vec<String>>,
363
364    #[serde(alias = "tag")]
365    pub tags: Option<Vec<String>>,
366
367    #[serde(default, deserialize_with = "empty_is_none")]
368    pub bsum: Option<String>,
369
370    #[serde(default, deserialize_with = "empty_is_none")]
371    pub shasum: Option<String>,
372
373    #[serde(default, deserialize_with = "empty_is_none")]
374    pub build_id: Option<String>,
375
376    #[serde(default, deserialize_with = "empty_is_none")]
377    pub build_date: Option<String>,
378
379    #[serde(default, deserialize_with = "empty_is_none", alias = "build_gha")]
380    pub build_action: Option<String>,
381
382    #[serde(default, deserialize_with = "empty_is_none")]
383    pub build_script: Option<String>,
384
385    #[serde(default, deserialize_with = "empty_is_none")]
386    pub build_log: Option<String>,
387
388    #[serde(alias = "category")]
389    pub categories: Option<Vec<String>>,
390
391    pub provides: Option<Vec<String>>,
392
393    #[serde(default, deserialize_with = "empty_is_none")]
394    pub icon: Option<String>,
395
396    #[serde(default, deserialize_with = "empty_is_none")]
397    pub desktop: Option<String>,
398
399    #[serde(default, deserialize_with = "empty_is_none")]
400    pub appstream: Option<String>,
401
402    #[serde(default, deserialize_with = "empty_is_none")]
403    pub app_id: Option<String>,
404
405    #[serde(default, deserialize_with = "optional_number")]
406    pub download_count: Option<u64>,
407
408    #[serde(default, deserialize_with = "optional_number")]
409    pub download_count_month: Option<u64>,
410
411    #[serde(default, deserialize_with = "optional_number")]
412    pub download_count_week: Option<u64>,
413
414    #[serde(default, deserialize_with = "flexible_bool")]
415    pub bundle: Option<bool>,
416
417    #[serde(default, deserialize_with = "empty_is_none")]
418    pub bundle_type: Option<String>,
419
420    #[serde(default, deserialize_with = "flexible_bool")]
421    pub soar_syms: Option<bool>,
422
423    #[serde(default, deserialize_with = "flexible_bool")]
424    pub deprecated: Option<bool>,
425
426    #[serde(default, deserialize_with = "flexible_bool")]
427    pub desktop_integration: Option<bool>,
428
429    #[serde(default, deserialize_with = "flexible_bool")]
430    pub external: Option<bool>,
431
432    #[serde(default, deserialize_with = "flexible_bool")]
433    pub installable: Option<bool>,
434
435    #[serde(default, deserialize_with = "flexible_bool")]
436    pub portable: Option<bool>,
437
438    #[serde(default, deserialize_with = "flexible_bool")]
439    pub recurse_provides: Option<bool>,
440
441    #[serde(default, deserialize_with = "flexible_bool")]
442    pub trusted: Option<bool>,
443
444    #[serde(default, deserialize_with = "empty_is_none")]
445    pub version_latest: Option<String>,
446
447    #[serde(default, deserialize_with = "flexible_bool")]
448    pub version_outdated: Option<bool>,
449
450    pub repology: Option<Vec<String>>,
451    pub snapshots: Option<Vec<String>>,
452    pub replaces: Option<Vec<String>>,
453}
454
455impl PackageExt for Package {
456    fn pkg_name(&self) -> &str {
457        &self.pkg_name
458    }
459
460    fn pkg_id(&self) -> &str {
461        &self.pkg_id
462    }
463
464    fn version(&self) -> &str {
465        &self.version
466    }
467
468    fn repo_name(&self) -> &str {
469        &self.repo_name
470    }
471}
472
473impl PackageExt for InstalledPackage {
474    fn pkg_name(&self) -> &str {
475        &self.pkg_name
476    }
477
478    fn pkg_id(&self) -> &str {
479        &self.pkg_id
480    }
481
482    fn version(&self) -> &str {
483        &self.version
484    }
485
486    fn repo_name(&self) -> &str {
487        &self.repo_name
488    }
489}