1use serde::{Deserialize, Serialize};
4
5#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
12pub enum DependencyStatus {
13 Installed {
15 version: String,
17 },
18 ToInstall,
20 ToUpgrade {
22 current: String,
24 required: String,
26 },
27 Conflict {
29 reason: String,
31 },
32 Missing,
34}
35
36impl DependencyStatus {
37 #[must_use]
48 pub const fn is_installed(&self) -> bool {
49 matches!(self, Self::Installed { .. } | Self::ToUpgrade { .. })
50 }
51
52 #[must_use]
63 pub const fn needs_action(&self) -> bool {
64 matches!(self, Self::ToInstall | Self::ToUpgrade { .. })
65 }
66
67 #[must_use]
78 pub const fn is_conflict(&self) -> bool {
79 matches!(self, Self::Conflict { .. })
80 }
81
82 #[must_use]
93 pub const fn priority(&self) -> u8 {
94 match self {
95 Self::Conflict { .. } => 0,
96 Self::Missing => 1,
97 Self::ToInstall => 2,
98 Self::ToUpgrade { .. } => 3,
99 Self::Installed { .. } => 4,
100 }
101 }
102}
103
104impl std::fmt::Display for DependencyStatus {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 match self {
107 Self::Installed { version } => write!(f, "Installed ({version})"),
108 Self::ToInstall => write!(f, "To Install"),
109 Self::ToUpgrade { current, required } => {
110 write!(f, "To Upgrade ({current} -> {required})")
111 }
112 Self::Conflict { reason } => write!(f, "Conflict: {reason}"),
113 Self::Missing => write!(f, "Missing"),
114 }
115 }
116}
117
118#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
123pub enum DependencySource {
124 Official {
126 repo: String,
128 },
129 Aur,
131 Local,
133}
134
135impl std::fmt::Display for DependencySource {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 match self {
138 Self::Official { repo } => write!(f, "Official ({repo})"),
139 Self::Aur => write!(f, "AUR"),
140 Self::Local => write!(f, "Local"),
141 }
142 }
143}
144
145#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
150pub enum PackageSource {
151 Official {
153 repo: String,
155 arch: String,
157 },
158 Aur,
160}
161
162impl std::fmt::Display for PackageSource {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 match self {
165 Self::Official { repo, arch } => write!(f, "Official ({repo}/{arch})"),
166 Self::Aur => write!(f, "AUR"),
167 }
168 }
169}
170
171#[derive(Clone, Debug, Serialize, Deserialize)]
178pub struct Dependency {
179 pub name: String,
181 pub version_req: String,
183 pub status: DependencyStatus,
185 pub source: DependencySource,
187 pub required_by: Vec<String>,
189 pub depends_on: Vec<String>,
191 pub is_core: bool,
193 pub is_system: bool,
195}
196
197#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
202pub struct PackageRef {
203 pub name: String,
205 pub version: String,
207 pub source: PackageSource,
209}
210
211#[derive(Clone, Debug, PartialEq, Eq, Default)]
215pub struct DependencySpec {
216 pub name: String,
218 pub version_req: String,
220}
221
222impl DependencySpec {
223 #[must_use]
234 pub fn new(name: impl Into<String>) -> Self {
235 Self {
236 name: name.into(),
237 version_req: String::new(),
238 }
239 }
240
241 #[must_use]
253 pub fn with_version(name: impl Into<String>, version_req: impl Into<String>) -> Self {
254 Self {
255 name: name.into(),
256 version_req: version_req.into(),
257 }
258 }
259
260 #[must_use]
271 pub const fn has_version_req(&self) -> bool {
272 !self.version_req.is_empty()
273 }
274}
275
276impl std::fmt::Display for DependencySpec {
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 if self.version_req.is_empty() {
279 write!(f, "{}", self.name)
280 } else {
281 write!(f, "{}{}", self.name, self.version_req)
282 }
283 }
284}
285
286#[derive(Clone, Debug, Default)]
291pub struct ReverseDependencyReport {
292 pub dependents: Vec<Dependency>,
294 pub summaries: Vec<ReverseDependencySummary>,
296}
297
298#[derive(Clone, Debug, Default)]
303pub struct ReverseDependencySummary {
304 pub package: String,
306 pub direct_dependents: usize,
308 pub transitive_dependents: usize,
310 pub total_dependents: usize,
312}
313
314#[derive(Clone, Debug, Default, Serialize, Deserialize)]
319pub struct SrcinfoData {
320 pub pkgbase: String,
322 pub pkgname: String,
324 pub pkgver: String,
326 pub pkgrel: String,
328 pub depends: Vec<String>,
330 pub makedepends: Vec<String>,
332 pub checkdepends: Vec<String>,
334 pub optdepends: Vec<String>,
336 pub conflicts: Vec<String>,
338 pub provides: Vec<String>,
340 pub replaces: Vec<String>,
342}
343
344#[derive(Clone, Debug, Default, Serialize, Deserialize)]
349pub struct DependencyResolution {
350 pub dependencies: Vec<Dependency>,
352 pub conflicts: Vec<String>,
354 pub missing: Vec<String>,
356}
357
358#[allow(clippy::struct_excessive_bools, clippy::type_complexity)]
366pub struct ResolverConfig {
367 pub include_optdepends: bool,
369 pub include_makedepends: bool,
371 pub include_checkdepends: bool,
373 pub max_depth: usize,
375 pub pkgbuild_cache: Option<Box<dyn Fn(&str) -> Option<String> + Send + Sync>>,
377 pub check_aur: bool,
379}
380
381#[allow(clippy::derivable_impls)]
382impl Default for ResolverConfig {
383 fn default() -> Self {
384 Self {
385 include_optdepends: false,
386 include_makedepends: false,
387 include_checkdepends: false,
388 max_depth: 0, pkgbuild_cache: None,
390 check_aur: false,
391 }
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn dependency_status_priority_ordering() {
401 let conflict = DependencyStatus::Conflict {
402 reason: "test".to_string(),
403 };
404 let missing = DependencyStatus::Missing;
405 let to_install = DependencyStatus::ToInstall;
406 let to_upgrade = DependencyStatus::ToUpgrade {
407 current: "1.0".to_string(),
408 required: "2.0".to_string(),
409 };
410 let installed = DependencyStatus::Installed {
411 version: "1.0".to_string(),
412 };
413
414 assert!(conflict.priority() < missing.priority());
415 assert!(missing.priority() < to_install.priority());
416 assert!(to_install.priority() < to_upgrade.priority());
417 assert!(to_upgrade.priority() < installed.priority());
418 }
419
420 #[test]
421 fn dependency_status_helper_methods() {
422 let installed = DependencyStatus::Installed {
423 version: "1.0".to_string(),
424 };
425 assert!(installed.is_installed());
426 assert!(!installed.needs_action());
427 assert!(!installed.is_conflict());
428
429 let to_install = DependencyStatus::ToInstall;
430 assert!(!to_install.is_installed());
431 assert!(to_install.needs_action());
432 assert!(!to_install.is_conflict());
433
434 let conflict = DependencyStatus::Conflict {
435 reason: "test".to_string(),
436 };
437 assert!(!conflict.is_installed());
438 assert!(!conflict.needs_action());
439 assert!(conflict.is_conflict());
440 }
441
442 #[test]
443 fn dependency_spec_constructors() {
444 let spec1 = DependencySpec::new("glibc");
445 assert_eq!(spec1.name, "glibc");
446 assert!(spec1.version_req.is_empty());
447 assert!(!spec1.has_version_req());
448
449 let spec2 = DependencySpec::with_version("python", ">=3.12");
450 assert_eq!(spec2.name, "python");
451 assert_eq!(spec2.version_req, ">=3.12");
452 assert!(spec2.has_version_req());
453 }
454
455 #[test]
456 fn dependency_spec_display() {
457 let spec1 = DependencySpec::new("glibc");
458 assert_eq!(spec1.to_string(), "glibc");
459
460 let spec2 = DependencySpec::with_version("python", ">=3.12");
461 assert_eq!(spec2.to_string(), "python>=3.12");
462 }
463
464 #[test]
465 fn dependency_status_display() {
466 let installed = DependencyStatus::Installed {
467 version: "1.0".to_string(),
468 };
469 assert!(installed.to_string().contains("Installed"));
470 assert!(installed.to_string().contains("1.0"));
471
472 let to_install = DependencyStatus::ToInstall;
473 assert_eq!(to_install.to_string(), "To Install");
474
475 let to_upgrade = DependencyStatus::ToUpgrade {
476 current: "1.0".to_string(),
477 required: "2.0".to_string(),
478 };
479 assert!(to_upgrade.to_string().contains("To Upgrade"));
480 assert!(to_upgrade.to_string().contains("1.0"));
481 assert!(to_upgrade.to_string().contains("2.0"));
482
483 let conflict = DependencyStatus::Conflict {
484 reason: "test reason".to_string(),
485 };
486 assert!(conflict.to_string().contains("Conflict"));
487 assert!(conflict.to_string().contains("test reason"));
488
489 let missing = DependencyStatus::Missing;
490 assert_eq!(missing.to_string(), "Missing");
491 }
492
493 #[test]
494 fn dependency_source_display() {
495 let official = DependencySource::Official {
496 repo: "core".to_string(),
497 };
498 assert!(official.to_string().contains("Official"));
499 assert!(official.to_string().contains("core"));
500
501 let aur = DependencySource::Aur;
502 assert_eq!(aur.to_string(), "AUR");
503
504 let local = DependencySource::Local;
505 assert_eq!(local.to_string(), "Local");
506 }
507
508 #[test]
509 fn package_source_display() {
510 let official = PackageSource::Official {
511 repo: "extra".to_string(),
512 arch: "x86_64".to_string(),
513 };
514 assert!(official.to_string().contains("Official"));
515 assert!(official.to_string().contains("extra"));
516 assert!(official.to_string().contains("x86_64"));
517
518 let aur = PackageSource::Aur;
519 assert_eq!(aur.to_string(), "AUR");
520 }
521
522 #[test]
523 fn serde_roundtrip_dependency_status() {
524 let statuses = vec![
525 DependencyStatus::Installed {
526 version: "1.0.0".to_string(),
527 },
528 DependencyStatus::ToInstall,
529 DependencyStatus::ToUpgrade {
530 current: "1.0.0".to_string(),
531 required: "2.0.0".to_string(),
532 },
533 DependencyStatus::Conflict {
534 reason: "test conflict".to_string(),
535 },
536 DependencyStatus::Missing,
537 ];
538
539 for status in statuses {
540 let json = serde_json::to_string(&status).expect("serialization should succeed");
541 let deserialized: DependencyStatus =
542 serde_json::from_str(&json).expect("deserialization should succeed");
543 assert_eq!(status, deserialized);
544 }
545 }
546
547 #[test]
548 fn serde_roundtrip_dependency_source() {
549 let sources = vec![
550 DependencySource::Official {
551 repo: "core".to_string(),
552 },
553 DependencySource::Aur,
554 DependencySource::Local,
555 ];
556
557 for source in sources {
558 let json = serde_json::to_string(&source).expect("serialization should succeed");
559 let deserialized: DependencySource =
560 serde_json::from_str(&json).expect("deserialization should succeed");
561 assert_eq!(source, deserialized);
562 }
563 }
564
565 #[test]
566 fn serde_roundtrip_dependency() {
567 let dep = Dependency {
568 name: "glibc".to_string(),
569 version_req: ">=2.35".to_string(),
570 status: DependencyStatus::Installed {
571 version: "2.35".to_string(),
572 },
573 source: DependencySource::Official {
574 repo: "core".to_string(),
575 },
576 required_by: vec!["firefox".to_string(), "chromium".to_string()],
577 depends_on: vec!["linux-api-headers".to_string()],
578 is_core: true,
579 is_system: true,
580 };
581
582 let json = serde_json::to_string(&dep).expect("serialization should succeed");
583 let deserialized: Dependency =
584 serde_json::from_str(&json).expect("deserialization should succeed");
585 assert_eq!(dep.name, deserialized.name);
586 assert_eq!(dep.version_req, deserialized.version_req);
587 assert_eq!(dep.status, deserialized.status);
588 assert_eq!(dep.source, deserialized.source);
589 assert_eq!(dep.required_by, deserialized.required_by);
590 assert_eq!(dep.depends_on, deserialized.depends_on);
591 assert_eq!(dep.is_core, deserialized.is_core);
592 assert_eq!(dep.is_system, deserialized.is_system);
593 }
594
595 #[test]
596 fn serde_roundtrip_srcinfo_data() {
597 let srcinfo = SrcinfoData {
598 pkgbase: "test-package".to_string(),
599 pkgname: "test-package".to_string(),
600 pkgver: "1.0.0".to_string(),
601 pkgrel: "1".to_string(),
602 depends: vec!["glibc".to_string(), "python>=3.12".to_string()],
603 makedepends: vec!["make".to_string(), "gcc".to_string()],
604 checkdepends: vec!["check".to_string()],
605 optdepends: vec!["optional: optional-package".to_string()],
606 conflicts: vec!["conflicting-pkg".to_string()],
607 provides: vec!["provided-pkg".to_string()],
608 replaces: vec!["replaced-pkg".to_string()],
609 };
610
611 let json = serde_json::to_string(&srcinfo).expect("serialization should succeed");
612 let deserialized: SrcinfoData =
613 serde_json::from_str(&json).expect("deserialization should succeed");
614 assert_eq!(srcinfo.pkgbase, deserialized.pkgbase);
615 assert_eq!(srcinfo.pkgname, deserialized.pkgname);
616 assert_eq!(srcinfo.pkgver, deserialized.pkgver);
617 assert_eq!(srcinfo.pkgrel, deserialized.pkgrel);
618 assert_eq!(srcinfo.depends, deserialized.depends);
619 assert_eq!(srcinfo.makedepends, deserialized.makedepends);
620 assert_eq!(srcinfo.checkdepends, deserialized.checkdepends);
621 assert_eq!(srcinfo.optdepends, deserialized.optdepends);
622 assert_eq!(srcinfo.conflicts, deserialized.conflicts);
623 assert_eq!(srcinfo.provides, deserialized.provides);
624 assert_eq!(srcinfo.replaces, deserialized.replaces);
625 }
626
627 #[test]
628 fn serde_roundtrip_package_ref() {
629 let pkg_ref = PackageRef {
630 name: "firefox".to_string(),
631 version: "121.0".to_string(),
632 source: PackageSource::Official {
633 repo: "extra".to_string(),
634 arch: "x86_64".to_string(),
635 },
636 };
637
638 let json = serde_json::to_string(&pkg_ref).expect("serialization should succeed");
639 let deserialized: PackageRef =
640 serde_json::from_str(&json).expect("deserialization should succeed");
641 assert_eq!(pkg_ref, deserialized);
642 }
643}