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/// Reverse dependency analysis result.
287///
288/// Contains the list of packages that depend on the target packages, along with
289/// summary statistics for each target package.
290#[derive(Clone, Debug, Default)]
291pub struct ReverseDependencyReport {
292    /// Packages that depend on the target packages.
293    pub dependents: Vec<Dependency>,
294    /// Per-package summary statistics.
295    pub summaries: Vec<ReverseDependencySummary>,
296}
297
298/// Summary statistics for a single package's reverse dependencies.
299///
300/// Used in reverse dependency analysis to summarize how many packages depend
301/// on a given package, broken down by direct and transitive dependents.
302#[derive(Clone, Debug, Default)]
303pub struct ReverseDependencySummary {
304    /// Package name.
305    pub package: String,
306    /// Number of packages that directly depend on this package (depth 1).
307    pub direct_dependents: usize,
308    /// Number of packages that depend on this package through other packages (depth ≥ 2).
309    pub transitive_dependents: usize,
310    /// Total number of dependents (direct + transitive).
311    pub total_dependents: usize,
312}
313
314/// Parsed .SRCINFO file data.
315///
316/// Contains all dependency-related fields extracted from a .SRCINFO file,
317/// which is the machine-readable format generated from PKGBUILD files.
318#[derive(Clone, Debug, Default, Serialize, Deserialize)]
319pub struct SrcinfoData {
320    /// Package base name (may differ from pkgname for split packages).
321    pub pkgbase: String,
322    /// Package name (may differ from pkgbase for split packages).
323    pub pkgname: String,
324    /// Package version.
325    pub pkgver: String,
326    /// Package release number.
327    pub pkgrel: String,
328    /// Runtime dependencies.
329    pub depends: Vec<String>,
330    /// Build-time dependencies.
331    pub makedepends: Vec<String>,
332    /// Test dependencies.
333    pub checkdepends: Vec<String>,
334    /// Optional dependencies.
335    pub optdepends: Vec<String>,
336    /// Conflicting packages.
337    pub conflicts: Vec<String>,
338    /// Packages this package provides.
339    pub provides: Vec<String>,
340    /// Packages this package replaces.
341    pub replaces: Vec<String>,
342}
343
344/// Result of dependency resolution operation.
345///
346/// Contains all resolved dependencies along with any conflicts or missing packages
347/// discovered during the resolution process.
348#[derive(Clone, Debug, Default, Serialize, Deserialize)]
349pub struct DependencyResolution {
350    /// Resolved dependencies with status.
351    pub dependencies: Vec<Dependency>,
352    /// Packages that have conflicts.
353    pub conflicts: Vec<String>,
354    /// Packages that are missing.
355    pub missing: Vec<String>,
356}
357
358/// Configuration for dependency resolution.
359///
360/// Controls various aspects of how dependencies are resolved, including which
361/// types of dependencies to include and how deep to traverse the dependency tree.
362///
363/// Note: This struct does not implement `Clone` or `Debug` because it contains
364/// a function pointer (`pkgbuild_cache`) that cannot be cloned or debugged.
365#[allow(clippy::struct_excessive_bools, clippy::type_complexity)]
366pub struct ResolverConfig {
367    /// Whether to include optional dependencies.
368    pub include_optdepends: bool,
369    /// Whether to include make dependencies.
370    pub include_makedepends: bool,
371    /// Whether to include check dependencies.
372    pub include_checkdepends: bool,
373    /// Maximum depth for transitive dependency resolution (0 = direct only).
374    pub max_depth: usize,
375    /// Custom callback for fetching PKGBUILD from cache (optional).
376    pub pkgbuild_cache: Option<Box<dyn Fn(&str) -> Option<String> + Send + Sync>>,
377    /// Whether to check AUR for missing dependencies.
378    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, // Direct dependencies only
389            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}