use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum DependencyStatus {
Installed {
version: String,
},
ToInstall,
ToUpgrade {
current: String,
required: String,
},
Conflict {
reason: String,
},
Missing,
}
impl DependencyStatus {
#[must_use]
pub const fn is_installed(&self) -> bool {
matches!(self, Self::Installed { .. } | Self::ToUpgrade { .. })
}
#[must_use]
pub const fn needs_action(&self) -> bool {
matches!(self, Self::ToInstall | Self::ToUpgrade { .. })
}
#[must_use]
pub const fn is_conflict(&self) -> bool {
matches!(self, Self::Conflict { .. })
}
#[must_use]
pub const fn priority(&self) -> u8 {
match self {
Self::Conflict { .. } => 0,
Self::Missing => 1,
Self::ToInstall => 2,
Self::ToUpgrade { .. } => 3,
Self::Installed { .. } => 4,
}
}
}
impl std::fmt::Display for DependencyStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Installed { version } => write!(f, "Installed ({version})"),
Self::ToInstall => write!(f, "To Install"),
Self::ToUpgrade { current, required } => {
write!(f, "To Upgrade ({current} -> {required})")
}
Self::Conflict { reason } => write!(f, "Conflict: {reason}"),
Self::Missing => write!(f, "Missing"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum DependencySource {
Official {
repo: String,
},
Aur,
Local,
}
impl std::fmt::Display for DependencySource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Official { repo } => write!(f, "Official ({repo})"),
Self::Aur => write!(f, "AUR"),
Self::Local => write!(f, "Local"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum PackageSource {
Official {
repo: String,
arch: String,
},
Aur,
}
impl std::fmt::Display for PackageSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Official { repo, arch } => write!(f, "Official ({repo}/{arch})"),
Self::Aur => write!(f, "AUR"),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Dependency {
pub name: String,
pub version_req: String,
pub status: DependencyStatus,
pub source: DependencySource,
pub required_by: Vec<String>,
pub depends_on: Vec<String>,
pub is_core: bool,
pub is_system: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageRef {
pub name: String,
pub version: String,
pub source: PackageSource,
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct DependencySpec {
pub name: String,
pub version_req: String,
}
impl DependencySpec {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
version_req: String::new(),
}
}
#[must_use]
pub fn with_version(name: impl Into<String>, version_req: impl Into<String>) -> Self {
Self {
name: name.into(),
version_req: version_req.into(),
}
}
#[must_use]
pub const fn has_version_req(&self) -> bool {
!self.version_req.is_empty()
}
}
impl std::fmt::Display for DependencySpec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.version_req.is_empty() {
write!(f, "{}", self.name)
} else {
write!(f, "{}{}", self.name, self.version_req)
}
}
}
#[derive(Clone, Debug, Default)]
pub struct ReverseDependencyReport {
pub dependents: Vec<Dependency>,
pub summaries: Vec<ReverseDependencySummary>,
}
#[derive(Clone, Debug, Default)]
pub struct ReverseDependencySummary {
pub package: String,
pub direct_dependents: usize,
pub transitive_dependents: usize,
pub total_dependents: usize,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct SrcinfoData {
pub pkgbase: String,
pub pkgname: String,
pub pkgver: String,
pub pkgrel: String,
pub depends: Vec<String>,
pub makedepends: Vec<String>,
pub checkdepends: Vec<String>,
pub optdepends: Vec<String>,
pub conflicts: Vec<String>,
pub provides: Vec<String>,
pub replaces: Vec<String>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct DependencyResolution {
pub dependencies: Vec<Dependency>,
pub conflicts: Vec<String>,
pub missing: Vec<String>,
}
#[allow(clippy::struct_excessive_bools, clippy::type_complexity)]
pub struct ResolverConfig {
pub include_optdepends: bool,
pub include_makedepends: bool,
pub include_checkdepends: bool,
pub max_depth: usize,
pub pkgbuild_cache: Option<Box<dyn Fn(&str) -> Option<String> + Send + Sync>>,
pub check_aur: bool,
}
#[allow(clippy::derivable_impls)]
impl Default for ResolverConfig {
fn default() -> Self {
Self {
include_optdepends: false,
include_makedepends: false,
include_checkdepends: false,
max_depth: 0, pkgbuild_cache: None,
check_aur: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dependency_status_priority_ordering() {
let conflict = DependencyStatus::Conflict {
reason: "test".to_string(),
};
let missing = DependencyStatus::Missing;
let to_install = DependencyStatus::ToInstall;
let to_upgrade = DependencyStatus::ToUpgrade {
current: "1.0".to_string(),
required: "2.0".to_string(),
};
let installed = DependencyStatus::Installed {
version: "1.0".to_string(),
};
assert!(conflict.priority() < missing.priority());
assert!(missing.priority() < to_install.priority());
assert!(to_install.priority() < to_upgrade.priority());
assert!(to_upgrade.priority() < installed.priority());
}
#[test]
fn dependency_status_helper_methods() {
let installed = DependencyStatus::Installed {
version: "1.0".to_string(),
};
assert!(installed.is_installed());
assert!(!installed.needs_action());
assert!(!installed.is_conflict());
let to_install = DependencyStatus::ToInstall;
assert!(!to_install.is_installed());
assert!(to_install.needs_action());
assert!(!to_install.is_conflict());
let conflict = DependencyStatus::Conflict {
reason: "test".to_string(),
};
assert!(!conflict.is_installed());
assert!(!conflict.needs_action());
assert!(conflict.is_conflict());
}
#[test]
fn dependency_spec_constructors() {
let spec1 = DependencySpec::new("glibc");
assert_eq!(spec1.name, "glibc");
assert!(spec1.version_req.is_empty());
assert!(!spec1.has_version_req());
let spec2 = DependencySpec::with_version("python", ">=3.12");
assert_eq!(spec2.name, "python");
assert_eq!(spec2.version_req, ">=3.12");
assert!(spec2.has_version_req());
}
#[test]
fn dependency_spec_display() {
let spec1 = DependencySpec::new("glibc");
assert_eq!(spec1.to_string(), "glibc");
let spec2 = DependencySpec::with_version("python", ">=3.12");
assert_eq!(spec2.to_string(), "python>=3.12");
}
#[test]
fn dependency_status_display() {
let installed = DependencyStatus::Installed {
version: "1.0".to_string(),
};
assert!(installed.to_string().contains("Installed"));
assert!(installed.to_string().contains("1.0"));
let to_install = DependencyStatus::ToInstall;
assert_eq!(to_install.to_string(), "To Install");
let to_upgrade = DependencyStatus::ToUpgrade {
current: "1.0".to_string(),
required: "2.0".to_string(),
};
assert!(to_upgrade.to_string().contains("To Upgrade"));
assert!(to_upgrade.to_string().contains("1.0"));
assert!(to_upgrade.to_string().contains("2.0"));
let conflict = DependencyStatus::Conflict {
reason: "test reason".to_string(),
};
assert!(conflict.to_string().contains("Conflict"));
assert!(conflict.to_string().contains("test reason"));
let missing = DependencyStatus::Missing;
assert_eq!(missing.to_string(), "Missing");
}
#[test]
fn dependency_source_display() {
let official = DependencySource::Official {
repo: "core".to_string(),
};
assert!(official.to_string().contains("Official"));
assert!(official.to_string().contains("core"));
let aur = DependencySource::Aur;
assert_eq!(aur.to_string(), "AUR");
let local = DependencySource::Local;
assert_eq!(local.to_string(), "Local");
}
#[test]
fn package_source_display() {
let official = PackageSource::Official {
repo: "extra".to_string(),
arch: "x86_64".to_string(),
};
assert!(official.to_string().contains("Official"));
assert!(official.to_string().contains("extra"));
assert!(official.to_string().contains("x86_64"));
let aur = PackageSource::Aur;
assert_eq!(aur.to_string(), "AUR");
}
#[test]
fn serde_roundtrip_dependency_status() {
let statuses = vec![
DependencyStatus::Installed {
version: "1.0.0".to_string(),
},
DependencyStatus::ToInstall,
DependencyStatus::ToUpgrade {
current: "1.0.0".to_string(),
required: "2.0.0".to_string(),
},
DependencyStatus::Conflict {
reason: "test conflict".to_string(),
},
DependencyStatus::Missing,
];
for status in statuses {
let json = serde_json::to_string(&status).expect("serialization should succeed");
let deserialized: DependencyStatus =
serde_json::from_str(&json).expect("deserialization should succeed");
assert_eq!(status, deserialized);
}
}
#[test]
fn serde_roundtrip_dependency_source() {
let sources = vec![
DependencySource::Official {
repo: "core".to_string(),
},
DependencySource::Aur,
DependencySource::Local,
];
for source in sources {
let json = serde_json::to_string(&source).expect("serialization should succeed");
let deserialized: DependencySource =
serde_json::from_str(&json).expect("deserialization should succeed");
assert_eq!(source, deserialized);
}
}
#[test]
fn serde_roundtrip_dependency() {
let dep = Dependency {
name: "glibc".to_string(),
version_req: ">=2.35".to_string(),
status: DependencyStatus::Installed {
version: "2.35".to_string(),
},
source: DependencySource::Official {
repo: "core".to_string(),
},
required_by: vec!["firefox".to_string(), "chromium".to_string()],
depends_on: vec!["linux-api-headers".to_string()],
is_core: true,
is_system: true,
};
let json = serde_json::to_string(&dep).expect("serialization should succeed");
let deserialized: Dependency =
serde_json::from_str(&json).expect("deserialization should succeed");
assert_eq!(dep.name, deserialized.name);
assert_eq!(dep.version_req, deserialized.version_req);
assert_eq!(dep.status, deserialized.status);
assert_eq!(dep.source, deserialized.source);
assert_eq!(dep.required_by, deserialized.required_by);
assert_eq!(dep.depends_on, deserialized.depends_on);
assert_eq!(dep.is_core, deserialized.is_core);
assert_eq!(dep.is_system, deserialized.is_system);
}
#[test]
fn serde_roundtrip_srcinfo_data() {
let srcinfo = SrcinfoData {
pkgbase: "test-package".to_string(),
pkgname: "test-package".to_string(),
pkgver: "1.0.0".to_string(),
pkgrel: "1".to_string(),
depends: vec!["glibc".to_string(), "python>=3.12".to_string()],
makedepends: vec!["make".to_string(), "gcc".to_string()],
checkdepends: vec!["check".to_string()],
optdepends: vec!["optional: optional-package".to_string()],
conflicts: vec!["conflicting-pkg".to_string()],
provides: vec!["provided-pkg".to_string()],
replaces: vec!["replaced-pkg".to_string()],
};
let json = serde_json::to_string(&srcinfo).expect("serialization should succeed");
let deserialized: SrcinfoData =
serde_json::from_str(&json).expect("deserialization should succeed");
assert_eq!(srcinfo.pkgbase, deserialized.pkgbase);
assert_eq!(srcinfo.pkgname, deserialized.pkgname);
assert_eq!(srcinfo.pkgver, deserialized.pkgver);
assert_eq!(srcinfo.pkgrel, deserialized.pkgrel);
assert_eq!(srcinfo.depends, deserialized.depends);
assert_eq!(srcinfo.makedepends, deserialized.makedepends);
assert_eq!(srcinfo.checkdepends, deserialized.checkdepends);
assert_eq!(srcinfo.optdepends, deserialized.optdepends);
assert_eq!(srcinfo.conflicts, deserialized.conflicts);
assert_eq!(srcinfo.provides, deserialized.provides);
assert_eq!(srcinfo.replaces, deserialized.replaces);
}
#[test]
fn serde_roundtrip_package_ref() {
let pkg_ref = PackageRef {
name: "firefox".to_string(),
version: "121.0".to_string(),
source: PackageSource::Official {
repo: "extra".to_string(),
arch: "x86_64".to_string(),
},
};
let json = serde_json::to_string(&pkg_ref).expect("serialization should succeed");
let deserialized: PackageRef =
serde_json::from_str(&json).expect("deserialization should succeed");
assert_eq!(pkg_ref, deserialized);
}
}