arch_toolkit/types/
dependency.rs

1//! Dependency-related data types for dependency resolution operations.
2
3use serde::{Deserialize, Serialize};
4
5// === Enums ===
6
7/// Status of a dependency relative to the current system state.
8///
9/// This enum represents the installation status and requirements for a dependency,
10/// used throughout the dependency resolution process to track what actions are needed.
11#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
12pub enum DependencyStatus {
13    /// Already installed and version matches requirement.
14    Installed {
15        /// Installed version of the package.
16        version: String,
17    },
18    /// Not installed, needs to be installed.
19    ToInstall,
20    /// Installed but outdated, needs upgrade.
21    ToUpgrade {
22        /// Current installed version.
23        current: String,
24        /// Required version for upgrade.
25        required: String,
26    },
27    /// Conflicts with existing packages.
28    Conflict {
29        /// Reason for the conflict.
30        reason: String,
31    },
32    /// Cannot be found in configured repositories or AUR.
33    Missing,
34}
35
36impl DependencyStatus {
37    /// What: Check if the dependency is already installed.
38    ///
39    /// Inputs:
40    /// - `self`: The dependency status to check.
41    ///
42    /// Output:
43    /// - Returns `true` if the dependency is installed (regardless of version).
44    ///
45    /// Details:
46    /// - Returns `true` for both `Installed` and `ToUpgrade` variants.
47    #[must_use]
48    pub const fn is_installed(&self) -> bool {
49        matches!(self, Self::Installed { .. } | Self::ToUpgrade { .. })
50    }
51
52    /// What: Check if the dependency needs action (install or upgrade).
53    ///
54    /// Inputs:
55    /// - `self`: The dependency status to check.
56    ///
57    /// Output:
58    /// - Returns `true` if the dependency needs to be installed or upgraded.
59    ///
60    /// Details:
61    /// - Returns `true` for `ToInstall` and `ToUpgrade` variants.
62    #[must_use]
63    pub const fn needs_action(&self) -> bool {
64        matches!(self, Self::ToInstall | Self::ToUpgrade { .. })
65    }
66
67    /// What: Check if there's a conflict with this dependency.
68    ///
69    /// Inputs:
70    /// - `self`: The dependency status to check.
71    ///
72    /// Output:
73    /// - Returns `true` if the dependency has a conflict.
74    ///
75    /// Details:
76    /// - Returns `true` only for the `Conflict` variant.
77    #[must_use]
78    pub const fn is_conflict(&self) -> bool {
79        matches!(self, Self::Conflict { .. })
80    }
81
82    /// What: Get a priority value for sorting (lower = more urgent).
83    ///
84    /// Inputs:
85    /// - `self`: The dependency status to get priority for.
86    ///
87    /// Output:
88    /// - Returns a numeric priority where lower numbers indicate higher urgency.
89    ///
90    /// Details:
91    /// - Priority order: Conflict (0) < Missing (1) < `ToInstall` (2) < `ToUpgrade` (3) < Installed (4).
92    #[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/// Source of a dependency package.
119///
120/// Indicates where a dependency package comes from, which affects how it's resolved
121/// and installed.
122#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
123pub enum DependencySource {
124    /// Official repository package.
125    Official {
126        /// Repository name (e.g., "core", "extra", "community").
127        repo: String,
128    },
129    /// AUR package.
130    Aur,
131    /// Local package (not in repos).
132    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/// Package source for dependency resolution input.
146///
147/// Used when specifying packages to resolve dependencies for, indicating whether
148/// the package is from an official repository or AUR.
149#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
150pub enum PackageSource {
151    /// Official repository.
152    Official {
153        /// Repository name (e.g., "core", "extra", "community").
154        repo: String,
155        /// Target architecture (e.g., `"x86_64"`).
156        arch: String,
157    },
158    /// AUR package.
159    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// === Core Structs ===
172
173/// Information about a single dependency.
174///
175/// Contains all metadata about a dependency including its status, source, and
176/// relationships to other packages.
177#[derive(Clone, Debug, Serialize, Deserialize)]
178pub struct Dependency {
179    /// Package name.
180    pub name: String,
181    /// Required version constraint (e.g., ">=1.2.3" or empty if no constraint).
182    pub version_req: String,
183    /// Current status of this dependency.
184    pub status: DependencyStatus,
185    /// Source repository or origin.
186    pub source: DependencySource,
187    /// Packages that require this dependency.
188    pub required_by: Vec<String>,
189    /// Packages that this dependency depends on (transitive dependencies).
190    pub depends_on: Vec<String>,
191    /// Whether this is a core repository package.
192    pub is_core: bool,
193    /// Whether this is a critical system package.
194    pub is_system: bool,
195}
196
197/// Package reference for dependency resolution input.
198///
199/// Used to specify packages for which dependencies should be resolved.
200/// This is a simplified representation compared to full package details.
201#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
202pub struct PackageRef {
203    /// Package name.
204    pub name: String,
205    /// Package version.
206    pub version: String,
207    /// Package source (official or AUR).
208    pub source: PackageSource,
209}
210
211/// Parsed dependency specification (name with optional version requirement).
212///
213/// Result of parsing a dependency string like "python>=3.12" or "glibc".
214#[derive(Clone, Debug, PartialEq, Eq, Default)]
215pub struct DependencySpec {
216    /// Package name.
217    pub name: String,
218    /// Version constraint (may be empty if no constraint specified).
219    pub version_req: String,
220}
221
222impl DependencySpec {
223    /// What: Create a new dependency spec with just a name.
224    ///
225    /// Inputs:
226    /// - `name`: Package name (will be converted to String).
227    ///
228    /// Output:
229    /// - Returns a new `DependencySpec` with empty version requirement.
230    ///
231    /// Details:
232    /// - Convenience constructor for dependencies without version constraints.
233    #[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    /// What: Create a new dependency spec with name and version requirement.
242    ///
243    /// Inputs:
244    /// - `name`: Package name (will be converted to String).
245    /// - `version_req`: Version requirement string (e.g., ">=1.2.3").
246    ///
247    /// Output:
248    /// - Returns a new `DependencySpec` with both name and version requirement.
249    ///
250    /// Details:
251    /// - Convenience constructor for dependencies with version constraints.
252    #[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    /// What: Check if this spec has a version requirement.
261    ///
262    /// Inputs:
263    /// - `self`: The dependency spec to check.
264    ///
265    /// Output:
266    /// - Returns `true` if a version requirement is specified.
267    ///
268    /// Details:
269    /// - Checks if `version_req` is non-empty.
270    #[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/// Summary statistics for a single package's reverse dependencies.
287///
288/// Used in reverse dependency analysis to summarize how many packages depend
289/// on a given package, broken down by direct and transitive dependents.
290#[derive(Clone, Debug, Default)]
291pub struct ReverseDependencySummary {
292    /// Package name.
293    pub package: String,
294    /// Number of packages that directly depend on this package (depth 1).
295    pub direct_dependents: usize,
296    /// Number of packages that depend on this package through other packages (depth ≥ 2).
297    pub transitive_dependents: usize,
298    /// Total number of dependents (direct + transitive).
299    pub total_dependents: usize,
300}
301
302/// Parsed .SRCINFO file data.
303///
304/// Contains all dependency-related fields extracted from a .SRCINFO file,
305/// which is the machine-readable format generated from PKGBUILD files.
306#[derive(Clone, Debug, Default, Serialize, Deserialize)]
307pub struct SrcinfoData {
308    /// Package base name (may differ from pkgname for split packages).
309    pub pkgbase: String,
310    /// Package name (may differ from pkgbase for split packages).
311    pub pkgname: String,
312    /// Package version.
313    pub pkgver: String,
314    /// Package release number.
315    pub pkgrel: String,
316    /// Runtime dependencies.
317    pub depends: Vec<String>,
318    /// Build-time dependencies.
319    pub makedepends: Vec<String>,
320    /// Test dependencies.
321    pub checkdepends: Vec<String>,
322    /// Optional dependencies.
323    pub optdepends: Vec<String>,
324    /// Conflicting packages.
325    pub conflicts: Vec<String>,
326    /// Packages this package provides.
327    pub provides: Vec<String>,
328    /// Packages this package replaces.
329    pub replaces: Vec<String>,
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    #[test]
337    fn dependency_status_priority_ordering() {
338        let conflict = DependencyStatus::Conflict {
339            reason: "test".to_string(),
340        };
341        let missing = DependencyStatus::Missing;
342        let to_install = DependencyStatus::ToInstall;
343        let to_upgrade = DependencyStatus::ToUpgrade {
344            current: "1.0".to_string(),
345            required: "2.0".to_string(),
346        };
347        let installed = DependencyStatus::Installed {
348            version: "1.0".to_string(),
349        };
350
351        assert!(conflict.priority() < missing.priority());
352        assert!(missing.priority() < to_install.priority());
353        assert!(to_install.priority() < to_upgrade.priority());
354        assert!(to_upgrade.priority() < installed.priority());
355    }
356
357    #[test]
358    fn dependency_status_helper_methods() {
359        let installed = DependencyStatus::Installed {
360            version: "1.0".to_string(),
361        };
362        assert!(installed.is_installed());
363        assert!(!installed.needs_action());
364        assert!(!installed.is_conflict());
365
366        let to_install = DependencyStatus::ToInstall;
367        assert!(!to_install.is_installed());
368        assert!(to_install.needs_action());
369        assert!(!to_install.is_conflict());
370
371        let conflict = DependencyStatus::Conflict {
372            reason: "test".to_string(),
373        };
374        assert!(!conflict.is_installed());
375        assert!(!conflict.needs_action());
376        assert!(conflict.is_conflict());
377    }
378
379    #[test]
380    fn dependency_spec_constructors() {
381        let spec1 = DependencySpec::new("glibc");
382        assert_eq!(spec1.name, "glibc");
383        assert!(spec1.version_req.is_empty());
384        assert!(!spec1.has_version_req());
385
386        let spec2 = DependencySpec::with_version("python", ">=3.12");
387        assert_eq!(spec2.name, "python");
388        assert_eq!(spec2.version_req, ">=3.12");
389        assert!(spec2.has_version_req());
390    }
391
392    #[test]
393    fn dependency_spec_display() {
394        let spec1 = DependencySpec::new("glibc");
395        assert_eq!(spec1.to_string(), "glibc");
396
397        let spec2 = DependencySpec::with_version("python", ">=3.12");
398        assert_eq!(spec2.to_string(), "python>=3.12");
399    }
400
401    #[test]
402    fn dependency_status_display() {
403        let installed = DependencyStatus::Installed {
404            version: "1.0".to_string(),
405        };
406        assert!(installed.to_string().contains("Installed"));
407        assert!(installed.to_string().contains("1.0"));
408
409        let to_install = DependencyStatus::ToInstall;
410        assert_eq!(to_install.to_string(), "To Install");
411
412        let to_upgrade = DependencyStatus::ToUpgrade {
413            current: "1.0".to_string(),
414            required: "2.0".to_string(),
415        };
416        assert!(to_upgrade.to_string().contains("To Upgrade"));
417        assert!(to_upgrade.to_string().contains("1.0"));
418        assert!(to_upgrade.to_string().contains("2.0"));
419
420        let conflict = DependencyStatus::Conflict {
421            reason: "test reason".to_string(),
422        };
423        assert!(conflict.to_string().contains("Conflict"));
424        assert!(conflict.to_string().contains("test reason"));
425
426        let missing = DependencyStatus::Missing;
427        assert_eq!(missing.to_string(), "Missing");
428    }
429
430    #[test]
431    fn dependency_source_display() {
432        let official = DependencySource::Official {
433            repo: "core".to_string(),
434        };
435        assert!(official.to_string().contains("Official"));
436        assert!(official.to_string().contains("core"));
437
438        let aur = DependencySource::Aur;
439        assert_eq!(aur.to_string(), "AUR");
440
441        let local = DependencySource::Local;
442        assert_eq!(local.to_string(), "Local");
443    }
444
445    #[test]
446    fn package_source_display() {
447        let official = PackageSource::Official {
448            repo: "extra".to_string(),
449            arch: "x86_64".to_string(),
450        };
451        assert!(official.to_string().contains("Official"));
452        assert!(official.to_string().contains("extra"));
453        assert!(official.to_string().contains("x86_64"));
454
455        let aur = PackageSource::Aur;
456        assert_eq!(aur.to_string(), "AUR");
457    }
458
459    #[test]
460    fn serde_roundtrip_dependency_status() {
461        let statuses = vec![
462            DependencyStatus::Installed {
463                version: "1.0.0".to_string(),
464            },
465            DependencyStatus::ToInstall,
466            DependencyStatus::ToUpgrade {
467                current: "1.0.0".to_string(),
468                required: "2.0.0".to_string(),
469            },
470            DependencyStatus::Conflict {
471                reason: "test conflict".to_string(),
472            },
473            DependencyStatus::Missing,
474        ];
475
476        for status in statuses {
477            let json = serde_json::to_string(&status).expect("serialization should succeed");
478            let deserialized: DependencyStatus =
479                serde_json::from_str(&json).expect("deserialization should succeed");
480            assert_eq!(status, deserialized);
481        }
482    }
483
484    #[test]
485    fn serde_roundtrip_dependency_source() {
486        let sources = vec![
487            DependencySource::Official {
488                repo: "core".to_string(),
489            },
490            DependencySource::Aur,
491            DependencySource::Local,
492        ];
493
494        for source in sources {
495            let json = serde_json::to_string(&source).expect("serialization should succeed");
496            let deserialized: DependencySource =
497                serde_json::from_str(&json).expect("deserialization should succeed");
498            assert_eq!(source, deserialized);
499        }
500    }
501
502    #[test]
503    fn serde_roundtrip_dependency() {
504        let dep = Dependency {
505            name: "glibc".to_string(),
506            version_req: ">=2.35".to_string(),
507            status: DependencyStatus::Installed {
508                version: "2.35".to_string(),
509            },
510            source: DependencySource::Official {
511                repo: "core".to_string(),
512            },
513            required_by: vec!["firefox".to_string(), "chromium".to_string()],
514            depends_on: vec!["linux-api-headers".to_string()],
515            is_core: true,
516            is_system: true,
517        };
518
519        let json = serde_json::to_string(&dep).expect("serialization should succeed");
520        let deserialized: Dependency =
521            serde_json::from_str(&json).expect("deserialization should succeed");
522        assert_eq!(dep.name, deserialized.name);
523        assert_eq!(dep.version_req, deserialized.version_req);
524        assert_eq!(dep.status, deserialized.status);
525        assert_eq!(dep.source, deserialized.source);
526        assert_eq!(dep.required_by, deserialized.required_by);
527        assert_eq!(dep.depends_on, deserialized.depends_on);
528        assert_eq!(dep.is_core, deserialized.is_core);
529        assert_eq!(dep.is_system, deserialized.is_system);
530    }
531
532    #[test]
533    fn serde_roundtrip_srcinfo_data() {
534        let srcinfo = SrcinfoData {
535            pkgbase: "test-package".to_string(),
536            pkgname: "test-package".to_string(),
537            pkgver: "1.0.0".to_string(),
538            pkgrel: "1".to_string(),
539            depends: vec!["glibc".to_string(), "python>=3.12".to_string()],
540            makedepends: vec!["make".to_string(), "gcc".to_string()],
541            checkdepends: vec!["check".to_string()],
542            optdepends: vec!["optional: optional-package".to_string()],
543            conflicts: vec!["conflicting-pkg".to_string()],
544            provides: vec!["provided-pkg".to_string()],
545            replaces: vec!["replaced-pkg".to_string()],
546        };
547
548        let json = serde_json::to_string(&srcinfo).expect("serialization should succeed");
549        let deserialized: SrcinfoData =
550            serde_json::from_str(&json).expect("deserialization should succeed");
551        assert_eq!(srcinfo.pkgbase, deserialized.pkgbase);
552        assert_eq!(srcinfo.pkgname, deserialized.pkgname);
553        assert_eq!(srcinfo.pkgver, deserialized.pkgver);
554        assert_eq!(srcinfo.pkgrel, deserialized.pkgrel);
555        assert_eq!(srcinfo.depends, deserialized.depends);
556        assert_eq!(srcinfo.makedepends, deserialized.makedepends);
557        assert_eq!(srcinfo.checkdepends, deserialized.checkdepends);
558        assert_eq!(srcinfo.optdepends, deserialized.optdepends);
559        assert_eq!(srcinfo.conflicts, deserialized.conflicts);
560        assert_eq!(srcinfo.provides, deserialized.provides);
561        assert_eq!(srcinfo.replaces, deserialized.replaces);
562    }
563
564    #[test]
565    fn serde_roundtrip_package_ref() {
566        let pkg_ref = PackageRef {
567            name: "firefox".to_string(),
568            version: "121.0".to_string(),
569            source: PackageSource::Official {
570                repo: "extra".to_string(),
571                arch: "x86_64".to_string(),
572            },
573        };
574
575        let json = serde_json::to_string(&pkg_ref).expect("serialization should succeed");
576        let deserialized: PackageRef =
577            serde_json::from_str(&json).expect("deserialization should succeed");
578        assert_eq!(pkg_ref, deserialized);
579    }
580}