1use std::fmt::Display;
4
5use serde::{Deserialize, Serialize};
6use soar_db::{models::types::PackageProvide, repository::core::InstalledPackageWithPortable};
7use soar_package::PackageExt;
8
9#[derive(Debug, Clone, Deserialize, Serialize)]
11pub struct Maintainer {
12 pub name: String,
13 pub contact: String,
14}
15
16impl Display for Maintainer {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 write!(f, "{} ({})", self.name, self.contact)
19 }
20}
21
22#[derive(Debug, Clone, Default)]
24pub struct Package {
25 pub id: u64,
26 pub repo_name: String,
27 pub disabled: Option<bool>,
28 pub disabled_reason: Option<String>,
29 pub pkg_id: String,
30 pub pkg_name: String,
31 pub pkg_family: Option<String>,
32 pub pkg_type: Option<String>,
33 pub pkg_webpage: Option<String>,
34 pub app_id: Option<String>,
35 pub description: String,
36 pub version: String,
37 pub licenses: Option<Vec<String>>,
38 pub download_url: String,
39 pub size: Option<u64>,
40 pub ghcr_pkg: Option<String>,
41 pub ghcr_size: Option<u64>,
42 pub ghcr_files: Option<Vec<String>>,
43 pub ghcr_blob: Option<String>,
44 pub ghcr_url: Option<String>,
45 pub bsum: Option<String>,
46 pub homepages: Option<Vec<String>>,
47 pub notes: Option<Vec<String>>,
48 pub source_urls: Option<Vec<String>>,
49 pub tags: Option<Vec<String>>,
50 pub categories: Option<Vec<String>>,
51 pub icon: Option<String>,
52 pub desktop: Option<String>,
53 pub appstream: Option<String>,
54 pub build_id: Option<String>,
55 pub build_date: Option<String>,
56 pub build_action: Option<String>,
57 pub build_script: Option<String>,
58 pub build_log: Option<String>,
59 pub provides: Option<Vec<PackageProvide>>,
60 pub snapshots: Option<Vec<String>>,
61 pub repology: Option<Vec<String>>,
62 pub maintainers: Option<Vec<Maintainer>>,
63 pub replaces: Option<Vec<String>>,
64 pub soar_syms: bool,
65 pub deprecated: bool,
66 pub desktop_integration: Option<bool>,
67 pub portable: Option<bool>,
68}
69
70impl PackageExt for Package {
71 fn pkg_name(&self) -> &str {
72 &self.pkg_name
73 }
74
75 fn pkg_id(&self) -> &str {
76 &self.pkg_id
77 }
78
79 fn version(&self) -> &str {
80 &self.version
81 }
82
83 fn repo_name(&self) -> &str {
84 &self.repo_name
85 }
86}
87
88fn resolve_version_placeholder(s: &str, version: &str) -> String {
90 s.replace("{{version}}", version)
91}
92
93fn resolve_version_placeholder_opt(s: Option<&str>, version: &str) -> Option<String> {
95 s.map(|s| resolve_version_placeholder(s, version))
96}
97
98impl Package {
99 pub fn has_version(&self, version: &str) -> bool {
104 if self.version == version {
105 return true;
106 }
107 self.snapshots
108 .as_ref()
109 .is_some_and(|s| s.iter().any(|v| v == version))
110 }
111
112 pub fn resolve(&self, version: Option<&str>) -> Self {
117 let ver = version.unwrap_or(&self.version);
118 let mut pkg = self.clone();
119 pkg.download_url = resolve_version_placeholder(&self.download_url, ver);
120 pkg.ghcr_pkg = resolve_version_placeholder_opt(self.ghcr_pkg.as_deref(), ver);
121 pkg.ghcr_blob = resolve_version_placeholder_opt(self.ghcr_blob.as_deref(), ver);
122 pkg.ghcr_url = resolve_version_placeholder_opt(self.ghcr_url.as_deref(), ver);
123 if version.is_some() {
124 pkg.version = ver.to_string();
125 }
126 pkg
127 }
128}
129
130#[derive(Debug, Clone)]
132pub struct InstalledPackage {
133 pub id: u64,
134 pub repo_name: String,
135 pub pkg_id: String,
136 pub pkg_name: String,
137 pub pkg_type: Option<String>,
138 pub version: String,
139 pub size: u64,
140 pub checksum: Option<String>,
141 pub installed_path: String,
142 pub installed_date: String,
143 pub profile: String,
144 pub pinned: bool,
145 pub is_installed: bool,
146 pub detached: bool,
147 pub unlinked: bool,
148 pub provides: Option<Vec<PackageProvide>>,
149 pub portable_path: Option<String>,
150 pub portable_home: Option<String>,
151 pub portable_config: Option<String>,
152 pub portable_share: Option<String>,
153 pub portable_cache: Option<String>,
154 pub install_patterns: Option<Vec<String>>,
155}
156
157impl PackageExt for InstalledPackage {
158 fn pkg_name(&self) -> &str {
159 &self.pkg_name
160 }
161
162 fn pkg_id(&self) -> &str {
163 &self.pkg_id
164 }
165
166 fn version(&self) -> &str {
167 &self.version
168 }
169
170 fn repo_name(&self) -> &str {
171 &self.repo_name
172 }
173}
174
175impl From<InstalledPackageWithPortable> for InstalledPackage {
177 fn from(pkg: InstalledPackageWithPortable) -> Self {
178 Self {
179 id: pkg.id as u64,
180 repo_name: pkg.repo_name,
181 pkg_id: pkg.pkg_id,
182 pkg_name: pkg.pkg_name,
183 pkg_type: pkg.pkg_type,
184 version: pkg.version,
185 size: pkg.size as u64,
186 checksum: pkg.checksum,
187 installed_path: pkg.installed_path,
188 installed_date: pkg.installed_date,
189 profile: pkg.profile,
190 pinned: pkg.pinned,
191 is_installed: pkg.is_installed,
192 detached: pkg.detached,
193 unlinked: pkg.unlinked,
194 provides: pkg.provides,
195 portable_path: pkg.portable_path,
196 portable_home: pkg.portable_home,
197 portable_config: pkg.portable_config,
198 portable_share: pkg.portable_share,
199 portable_cache: pkg.portable_cache,
200 install_patterns: pkg.install_patterns,
201 }
202 }
203}
204
205impl From<soar_db::repository::core::InstalledPackage> for InstalledPackage {
207 fn from(pkg: soar_db::repository::core::InstalledPackage) -> Self {
208 Self {
209 id: pkg.id as u64,
210 repo_name: pkg.repo_name,
211 pkg_id: pkg.pkg_id,
212 pkg_name: pkg.pkg_name,
213 pkg_type: pkg.pkg_type,
214 version: pkg.version,
215 size: pkg.size as u64,
216 checksum: pkg.checksum,
217 installed_path: pkg.installed_path,
218 installed_date: pkg.installed_date,
219 profile: pkg.profile,
220 pinned: pkg.pinned,
221 is_installed: pkg.is_installed,
222 detached: pkg.detached,
223 unlinked: pkg.unlinked,
224 provides: pkg.provides,
225 portable_path: None,
226 portable_home: None,
227 portable_config: None,
228 portable_share: None,
229 portable_cache: None,
230 install_patterns: pkg.install_patterns,
231 }
232 }
233}
234
235impl From<soar_db::models::metadata::Package> for Package {
237 fn from(pkg: soar_db::models::metadata::Package) -> Self {
238 Self {
239 id: pkg.id as u64,
240 repo_name: String::new(), disabled: None,
242 disabled_reason: None,
243 pkg_id: pkg.pkg_id,
244 pkg_name: pkg.pkg_name,
245 pkg_family: pkg.pkg_family,
246 pkg_type: pkg.pkg_type,
247 pkg_webpage: pkg.pkg_webpage,
248 app_id: pkg.app_id,
249 description: pkg.description.unwrap_or_default(),
250 version: pkg.version,
251 licenses: pkg.licenses,
252 download_url: pkg.download_url,
253 size: pkg.size.map(|s| s as u64),
254 ghcr_pkg: pkg.ghcr_pkg,
255 ghcr_size: pkg.ghcr_size.map(|s| s as u64),
256 ghcr_files: None,
257 ghcr_blob: pkg.ghcr_blob,
258 ghcr_url: pkg.ghcr_url,
259 bsum: pkg.bsum,
260 homepages: pkg.homepages,
261 notes: pkg.notes,
262 source_urls: pkg.source_urls,
263 tags: pkg.tags,
264 categories: pkg.categories,
265 icon: pkg.icon,
266 desktop: pkg.desktop,
267 appstream: pkg.appstream,
268 build_id: pkg.build_id,
269 build_date: pkg.build_date,
270 build_action: pkg.build_action,
271 build_script: pkg.build_script,
272 build_log: pkg.build_log,
273 provides: pkg.provides,
274 snapshots: pkg.snapshots,
275 repology: None,
276 maintainers: None,
277 replaces: pkg.replaces,
278 soar_syms: pkg.soar_syms,
279 deprecated: false,
280 desktop_integration: pkg.desktop_integration,
281 portable: pkg.portable,
282 }
283 }
284}
285
286impl From<soar_db::models::metadata::PackageWithRepo> for Package {
288 fn from(pkg_with_repo: soar_db::models::metadata::PackageWithRepo) -> Self {
289 let mut pkg: Package = pkg_with_repo.package.into();
290 pkg.repo_name = pkg_with_repo.repo_name;
291 pkg
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn test_resolve_version_placeholder() {
301 assert_eq!(
302 resolve_version_placeholder("https://example.com/pkg?tag={{version}}-x86_64", "v1.0.0"),
303 "https://example.com/pkg?tag=v1.0.0-x86_64"
304 );
305 }
306
307 #[test]
308 fn test_resolve_version_placeholder_multiple() {
309 assert_eq!(
310 resolve_version_placeholder("ghcr.io/user/pkg:{{version}}-{{version}}", "v2.0.0"),
311 "ghcr.io/user/pkg:v2.0.0-v2.0.0"
312 );
313 }
314
315 #[test]
316 fn test_resolve_version_placeholder_none() {
317 assert_eq!(
318 resolve_version_placeholder("https://example.com/static-url", "v1.0.0"),
319 "https://example.com/static-url"
320 );
321 }
322
323 #[test]
324 fn test_resolve_version_placeholder_opt() {
325 assert_eq!(
326 resolve_version_placeholder_opt(Some("ghcr.io/pkg:{{version}}"), "v1.0.0"),
327 Some("ghcr.io/pkg:v1.0.0".to_string())
328 );
329 assert_eq!(resolve_version_placeholder_opt(None, "v1.0.0"), None);
330 }
331
332 #[test]
333 fn test_package_resolve() {
334 let pkg = Package {
335 version: "v1.0.0".to_string(),
336 download_url: "https://example.com/pkg?tag={{version}}".to_string(),
337 ghcr_pkg: Some("ghcr.io/pkg:{{version}}".to_string()),
338 ..Default::default()
339 };
340
341 let resolved = pkg.resolve(None);
343 assert_eq!(resolved.download_url, "https://example.com/pkg?tag=v1.0.0");
344 assert_eq!(resolved.ghcr_pkg, Some("ghcr.io/pkg:v1.0.0".to_string()));
345 assert_eq!(resolved.version, "v1.0.0");
346
347 let resolved = pkg.resolve(Some("v0.5.0"));
349 assert_eq!(resolved.download_url, "https://example.com/pkg?tag=v0.5.0");
350 assert_eq!(resolved.ghcr_pkg, Some("ghcr.io/pkg:v0.5.0".to_string()));
351 assert_eq!(resolved.version, "v0.5.0");
352 }
353
354 #[test]
355 fn test_package_resolve_no_placeholder() {
356 let pkg = Package {
357 version: "v2.0.0".to_string(),
358 download_url: "https://api.example.com/pkg/static-url".to_string(),
359 ..Default::default()
360 };
361
362 let resolved = pkg.resolve(None);
364 assert_eq!(
365 resolved.download_url,
366 "https://api.example.com/pkg/static-url"
367 );
368 }
369
370 #[test]
371 fn test_has_version_current() {
372 let pkg = Package {
373 version: "v1.0.0".to_string(),
374 ..Default::default()
375 };
376
377 assert!(pkg.has_version("v1.0.0"));
378 assert!(!pkg.has_version("v0.9.0"));
379 }
380
381 #[test]
382 fn test_has_version_snapshot() {
383 let pkg = Package {
384 version: "v1.0.0".to_string(),
385 snapshots: Some(vec![
386 "v0.9.0".to_string(),
387 "v0.8.0".to_string(),
388 "v0.7.0".to_string(),
389 ]),
390 ..Default::default()
391 };
392
393 assert!(pkg.has_version("v1.0.0")); assert!(pkg.has_version("v0.9.0")); assert!(pkg.has_version("v0.8.0")); assert!(!pkg.has_version("v0.6.0")); }
398
399 #[test]
400 fn test_has_version_no_snapshots() {
401 let pkg = Package {
402 version: "v1.0.0".to_string(),
403 snapshots: None,
404 ..Default::default()
405 };
406
407 assert!(pkg.has_version("v1.0.0"));
408 assert!(!pkg.has_version("v0.9.0"));
409 }
410}