Skip to main content

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
211impl PackageRef {
212    /// What: Create a reference to an official repository package.
213    ///
214    /// Inputs:
215    /// - `name`: Package name.
216    /// - `version`: Package version.
217    /// - `repo`: Repository name (e.g., "core", "extra").
218    /// - `arch`: Target architecture (e.g., `x86_64`, `any`).
219    ///
220    /// Output:
221    /// - `PackageRef` with `PackageSource::Official`.
222    ///
223    /// Details:
224    /// - Convenience constructor for resolution and install-planning inputs.
225    #[must_use]
226    pub fn official(
227        name: impl Into<String>,
228        version: impl Into<String>,
229        repo: impl Into<String>,
230        arch: impl Into<String>,
231    ) -> Self {
232        Self {
233            name: name.into(),
234            version: version.into(),
235            source: PackageSource::Official {
236                repo: repo.into(),
237                arch: arch.into(),
238            },
239        }
240    }
241
242    /// What: Create a reference to an AUR package.
243    ///
244    /// Inputs:
245    /// - `name`: Package name.
246    /// - `version`: Package version.
247    ///
248    /// Output:
249    /// - `PackageRef` with `PackageSource::Aur`.
250    ///
251    /// Details:
252    /// - Convenience constructor for resolution and install-planning inputs.
253    #[must_use]
254    pub fn aur(name: impl Into<String>, version: impl Into<String>) -> Self {
255        Self {
256            name: name.into(),
257            version: version.into(),
258            source: PackageSource::Aur,
259        }
260    }
261}
262
263/// Parsed dependency specification (name with optional version requirement).
264///
265/// Result of parsing a dependency string like "python>=3.12" or "glibc".
266#[derive(Clone, Debug, PartialEq, Eq, Default)]
267pub struct DependencySpec {
268    /// Package name.
269    pub name: String,
270    /// Version constraint (may be empty if no constraint specified).
271    pub version_req: String,
272}
273
274impl DependencySpec {
275    /// What: Create a new dependency spec with just a name.
276    ///
277    /// Inputs:
278    /// - `name`: Package name (will be converted to String).
279    ///
280    /// Output:
281    /// - Returns a new `DependencySpec` with empty version requirement.
282    ///
283    /// Details:
284    /// - Convenience constructor for dependencies without version constraints.
285    #[must_use]
286    pub fn new(name: impl Into<String>) -> Self {
287        Self {
288            name: name.into(),
289            version_req: String::new(),
290        }
291    }
292
293    /// What: Create a new dependency spec with name and version requirement.
294    ///
295    /// Inputs:
296    /// - `name`: Package name (will be converted to String).
297    /// - `version_req`: Version requirement string (e.g., ">=1.2.3").
298    ///
299    /// Output:
300    /// - Returns a new `DependencySpec` with both name and version requirement.
301    ///
302    /// Details:
303    /// - Convenience constructor for dependencies with version constraints.
304    #[must_use]
305    pub fn with_version(name: impl Into<String>, version_req: impl Into<String>) -> Self {
306        Self {
307            name: name.into(),
308            version_req: version_req.into(),
309        }
310    }
311
312    /// What: Check if this spec has a version requirement.
313    ///
314    /// Inputs:
315    /// - `self`: The dependency spec to check.
316    ///
317    /// Output:
318    /// - Returns `true` if a version requirement is specified.
319    ///
320    /// Details:
321    /// - Checks if `version_req` is non-empty.
322    #[must_use]
323    pub const fn has_version_req(&self) -> bool {
324        !self.version_req.is_empty()
325    }
326}
327
328impl std::fmt::Display for DependencySpec {
329    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330        if self.version_req.is_empty() {
331            write!(f, "{}", self.name)
332        } else {
333            write!(f, "{}{}", self.name, self.version_req)
334        }
335    }
336}
337
338/// Reverse dependency analysis result.
339///
340/// Contains the list of packages that depend on the target packages, along with
341/// summary statistics for each target package.
342#[derive(Clone, Debug, Default)]
343pub struct ReverseDependencyReport {
344    /// Packages that depend on the target packages.
345    pub dependents: Vec<Dependency>,
346    /// Per-package summary statistics.
347    pub summaries: Vec<ReverseDependencySummary>,
348}
349
350/// Summary statistics for a single package's reverse dependencies.
351///
352/// Used in reverse dependency analysis to summarize how many packages depend
353/// on a given package, broken down by direct and transitive dependents.
354#[derive(Clone, Debug, Default)]
355pub struct ReverseDependencySummary {
356    /// Package name.
357    pub package: String,
358    /// Number of packages that directly depend on this package (depth 1).
359    pub direct_dependents: usize,
360    /// Number of packages that depend on this package through other packages (depth ≥ 2).
361    pub transitive_dependents: usize,
362    /// Total number of dependents (direct + transitive).
363    pub total_dependents: usize,
364}
365
366/// Parsed .SRCINFO file data.
367///
368/// Contains all dependency-related fields extracted from a .SRCINFO file,
369/// which is the machine-readable format generated from PKGBUILD files.
370#[derive(Clone, Debug, Default, Serialize, Deserialize)]
371pub struct SrcinfoData {
372    /// Package base name (may differ from pkgname for split packages).
373    pub pkgbase: String,
374    /// Package name (may differ from pkgbase for split packages).
375    pub pkgname: String,
376    /// Package version.
377    pub pkgver: String,
378    /// Package release number.
379    pub pkgrel: String,
380    /// Runtime dependencies.
381    pub depends: Vec<String>,
382    /// Build-time dependencies.
383    pub makedepends: Vec<String>,
384    /// Test dependencies.
385    pub checkdepends: Vec<String>,
386    /// Optional dependencies.
387    pub optdepends: Vec<String>,
388    /// Conflicting packages.
389    pub conflicts: Vec<String>,
390    /// Packages this package provides.
391    pub provides: Vec<String>,
392    /// Packages this package replaces.
393    pub replaces: Vec<String>,
394}
395
396/// What: Carry raw `.SRCINFO` metadata returned by an injected graph metadata provider.
397///
398/// Inputs:
399/// - Requested name, selected actual package, verified source, and raw `.SRCINFO` text.
400///
401/// Output:
402/// - Supplies enough information to resolve direct and virtual dependencies without a crate-level
403///   AUR or HTTP dependency.
404///
405/// Details:
406/// - `package_name` must name the selected split-package output. If it differs from
407///   `requested_name`, the resolver verifies that the selected output provides the requested name.
408#[derive(Clone, Debug, PartialEq, Eq)]
409pub struct DependencyMetadata {
410    /// Package or virtual name queried from the provider.
411    pub requested_name: String,
412    /// Actual selected package name, including a split-package output when applicable.
413    pub package_name: String,
414    /// Verified source supplied by the metadata provider.
415    pub source: DependencySource,
416    /// Raw `.SRCINFO` text for the containing package base.
417    pub srcinfo: String,
418}
419
420impl DependencyMetadata {
421    /// What: Construct injected raw metadata for one requested package or provider.
422    ///
423    /// Inputs:
424    /// - `requested_name`: Queried package or virtual name.
425    /// - `package_name`: Selected actual package output.
426    /// - `source`: Verified source of the selected package.
427    /// - `srcinfo`: Raw `.SRCINFO` package-base metadata.
428    ///
429    /// Output:
430    /// - Returns a metadata record suitable for `DependencyMetadataProvider`.
431    ///
432    /// Details:
433    /// - This constructor performs no I/O or parsing so deterministic fixtures can use it directly.
434    #[must_use]
435    pub fn new(
436        requested_name: impl Into<String>,
437        package_name: impl Into<String>,
438        source: DependencySource,
439        srcinfo: impl Into<String>,
440    ) -> Self {
441        Self {
442            requested_name: requested_name.into(),
443            package_name: package_name.into(),
444            source,
445            srcinfo: srcinfo.into(),
446        }
447    }
448}
449
450/// What: Describe a batched injected metadata-provider result.
451///
452/// Inputs:
453/// - A requested name and either returned metadata, an absence reason, or a retrieval failure.
454///
455/// Output:
456/// - Lets graph resolution retain partial results and report actionable diagnostics.
457///
458/// Details:
459/// - Provider failures are non-fatal for sibling branches and never cause fallback AUR inference.
460#[derive(Clone, Debug, PartialEq, Eq)]
461pub enum DependencyMetadataResponse {
462    /// Verified metadata was returned for a requested package or virtual dependency.
463    Found(DependencyMetadata),
464    /// No verified metadata exists for the requested name.
465    Missing {
466        /// Requested package or virtual name.
467        requested_name: String,
468        /// Actionable absence reason.
469        reason: String,
470    },
471    /// Metadata retrieval failed through a network, helper, or provider-specific error.
472    Failure {
473        /// Requested package or virtual name.
474        requested_name: String,
475        /// Actionable provider error message.
476        message: String,
477    },
478}
479
480/// What: Identify the source and requested/provider identity behind a graph node.
481///
482/// Inputs:
483/// - The requested dependency name, optional verified source, and optional selected provider.
484///
485/// Output:
486/// - Lets callers distinguish direct packages, virtual providers, and unresolved names.
487///
488/// Details:
489/// - `source` is `None` only when metadata is absent or failed. The resolver never infers AUR
490///   provenance merely because an unknown name was not found elsewhere.
491#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
492pub struct DependencyProvenance {
493    /// Original dependency name requested by the parent package.
494    pub requested_name: String,
495    /// Verified source reported by the metadata provider, if metadata was available.
496    pub source: Option<DependencySource>,
497    /// Actual package selected to satisfy a virtual request, if different from the request.
498    pub provider: Option<String>,
499}
500
501/// What: Represent one inclusive or exclusive edge of an intersected version range.
502///
503/// Inputs:
504/// - A version string and whether equality is permitted at that edge.
505///
506/// Output:
507/// - Supplies a lower or upper bound for `DependencyConstraintRange`.
508///
509/// Details:
510/// - Version ordering uses the dependency resolver's epoch/pkgver/pkgrel comparator.
511#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
512pub struct DependencyVersionBound {
513    /// Bound version including epoch and pkgrel when present.
514    pub version: String,
515    /// Whether the bound includes its version value.
516    pub inclusive: bool,
517}
518
519/// What: Store the deterministic intersection of dependency version requirements.
520///
521/// Inputs:
522/// - Zero or more `=`, `>`, `>=`, `<`, or `<=` requirements for one resolved package.
523///
524/// Output:
525/// - Exposes the most restrictive compatible lower and upper bounds.
526///
527/// Details:
528/// - Equal requirements are represented by equal inclusive lower and upper bounds. An absent
529///   bound means no requirement on that side of the interval.
530#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
531pub struct DependencyConstraintRange {
532    /// Most restrictive compatible lower bound, if any.
533    pub lower: Option<DependencyVersionBound>,
534    /// Most restrictive compatible upper bound, if any.
535    pub upper: Option<DependencyVersionBound>,
536}
537
538/// What: Describe the resolution state of a graph node.
539///
540/// Inputs:
541/// - Metadata, provider, and conflict observations made during one graph run.
542///
543/// Output:
544/// - Lets callers identify resolved, missing, and conflicting graph nodes.
545///
546/// Details:
547/// - Missing nodes retain a `DependencyProvenance` with no source rather than being labelled AUR.
548#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
549pub enum DependencyGraphNodeStatus {
550    /// Metadata was parsed and the package can participate in dependency traversal.
551    #[default]
552    Resolved,
553    /// Metadata was unavailable or failed validation for the requested package.
554    Missing,
555    /// The package conflicts with another resolved graph node.
556    Conflicting,
557}
558
559/// What: Represent one deterministic dependency graph node.
560///
561/// Inputs:
562/// - Verified metadata and merged requirements for one actual package.
563///
564/// Output:
565/// - Stores stable node identity, source provenance, split-package base, and selected metadata.
566///
567/// Details:
568/// - `name` is the actual selected package. `provenance.requested_name` retains the virtual or
569///   direct dependency name that selected it.
570#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
571pub struct DependencyGraphNode {
572    /// Stable actual package identity used by graph edges.
573    pub name: String,
574    /// Package-base name retained from `.SRCINFO`.
575    pub pkgbase: Option<String>,
576    /// Combined `epoch:pkgver-pkgrel` version, if metadata was parsed.
577    pub version: Option<String>,
578    /// Verified source and provider provenance.
579    pub provenance: DependencyProvenance,
580    /// Current graph node state.
581    pub status: DependencyGraphNodeStatus,
582    /// Intersected requirements targeting this node.
583    pub constraints: DependencyConstraintRange,
584    /// Virtual packages supplied by this node.
585    pub provides: Vec<String>,
586    /// Declared package or virtual conflicts for this node.
587    pub conflicts: Vec<String>,
588    /// Minimum lexical traversal depth at which this node was encountered.
589    pub depth: usize,
590}
591
592/// What: Represent one directed dependency requirement between graph nodes.
593///
594/// Inputs:
595/// - Parent and selected child package names, requested dependency name, and version requirement.
596///
597/// Output:
598/// - Preserves dependency and virtual-provider provenance independently of rendering.
599///
600/// Details:
601/// - Edges are sorted lexically by the resolver and can be rendered without triggering metadata
602///   lookup or changing the resolution result.
603#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
604pub struct DependencyGraphEdge {
605    /// Actual package that declares the dependency.
606    pub from: String,
607    /// Actual selected package, or the missing requested name when metadata is absent.
608    pub to: String,
609    /// Dependency or virtual package name written by the parent.
610    pub requested_name: String,
611    /// Version requirement written by the parent, if present.
612    pub version_req: String,
613}
614
615/// What: Categorize non-fatal graph-resolution diagnostics.
616///
617/// Inputs:
618/// - Metadata, bound, graph, and conflict events observed during resolution.
619///
620/// Output:
621/// - Provides stable categories for actionable caller diagnostics.
622///
623/// Details:
624/// - Diagnostics preserve partial graph results instead of silently omitting failed branches.
625#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
626pub enum DependencyGraphDiagnosticKind {
627    /// Metadata was not available for a requested package or virtual dependency.
628    MissingMetadata,
629    /// The injected provider returned a transport, helper, or other retrieval failure.
630    MetadataFailure,
631    /// Returned `.SRCINFO` text did not contain the selected requested package output.
632    MalformedSrcinfo,
633    /// A dependency path returned to a package already in the active traversal path.
634    Cycle,
635    /// A child exceeded the configured transitive depth.
636    DepthLimit,
637    /// Adding a node exceeded the configured per-run node limit.
638    NodeLimit,
639    /// The provider exceeded the configured metadata timeout.
640    Timeout,
641    /// A dependency requirement used an unsupported operator or omitted its version.
642    MalformedConstraint,
643    /// Multiple valid requirements for one selected package have an empty intersection.
644    IncompatibleConstraints,
645    /// A declared package or virtual conflict matched another resolved node.
646    Conflict,
647    /// A provider returned no response or a response for an unrequested package.
648    MetadataProtocol,
649}
650
651/// What: Record one actionable non-fatal graph-resolution event.
652///
653/// Inputs:
654/// - A diagnostic kind, affected package, optional related package, and message.
655///
656/// Output:
657/// - Lets callers surface partial-resolution limitations without parsing log output.
658///
659/// Details:
660/// - Entries are sorted deterministically by kind, package, related package, and message.
661#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
662pub struct DependencyGraphDiagnostic {
663    /// Stable category for the event.
664    pub kind: DependencyGraphDiagnosticKind,
665    /// Affected package or requested dependency name.
666    pub package: String,
667    /// Related package when the event concerns an edge, cycle, or conflict.
668    pub related_package: Option<String>,
669    /// Actionable detail suitable for caller display.
670    pub message: String,
671}
672
673/// What: Return the bounded, deterministic output of one metadata graph-resolution run.
674///
675/// Inputs:
676/// - Root package references, injected metadata, and graph resolution bounds.
677///
678/// Output:
679/// - Contains lexical roots, nodes, edges, and structured diagnostics.
680///
681/// Details:
682/// - The graph is independent from tree rendering and remains useful when metadata failures leave
683///   partial branches unresolved.
684#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
685pub struct DependencyGraphResolution {
686    /// Requested root package names in lexical order.
687    pub roots: Vec<String>,
688    /// Resolved and missing nodes in lexical order by actual package name.
689    pub nodes: Vec<DependencyGraphNode>,
690    /// Directed edges in lexical order.
691    pub edges: Vec<DependencyGraphEdge>,
692    /// Non-fatal diagnostic events in lexical order.
693    pub diagnostics: Vec<DependencyGraphDiagnostic>,
694}
695
696/// What: Configure bounded metadata graph resolution.
697///
698/// Inputs:
699/// - Maximum transitive depth, graph-node count, metadata timeout, and provider batch concurrency.
700///
701/// Output:
702/// - Limits resource use for one graph-resolution run.
703///
704/// Details:
705/// - Defaults are depth 8, 256 nodes, 10-second metadata timeout, and one provider batch at a
706///   time. The synchronous resolver passes the timeout to the injected provider and keeps only one
707///   batch in flight; providers must honor the timeout for preemptive I/O cancellation.
708#[derive(Clone, Copy, Debug, PartialEq, Eq)]
709pub struct DependencyGraphConfig {
710    /// Maximum edges from a root to a traversed child; zero resolves root metadata only.
711    pub max_depth: usize,
712    /// Maximum unique graph nodes, including roots and missing nodes.
713    pub max_nodes: usize,
714    /// Maximum duration supplied to each metadata provider batch.
715    pub metadata_timeout: std::time::Duration,
716    /// Maximum names supplied in one provider batch; the synchronous resolver runs batches serially.
717    pub max_concurrency: usize,
718}
719
720impl Default for DependencyGraphConfig {
721    /// What: Construct conservative bounds for graph resolution.
722    ///
723    /// Inputs:
724    /// - None.
725    ///
726    /// Output:
727    /// - Returns depth 8, 256 nodes, a 10-second timeout, and serial provider batching.
728    ///
729    /// Details:
730    /// - These defaults constrain fixture and production providers without changing the legacy
731    ///   direct-only `DependencyResolver::resolve` entry point.
732    fn default() -> Self {
733        Self {
734            max_depth: 8,
735            max_nodes: 256,
736            metadata_timeout: std::time::Duration::from_secs(10),
737            max_concurrency: 1,
738        }
739    }
740}
741
742/// Result of dependency resolution operation.
743///
744/// Contains all resolved dependencies along with any conflicts or missing packages
745/// discovered during the resolution process.
746#[derive(Clone, Debug, Default, Serialize, Deserialize)]
747pub struct DependencyResolution {
748    /// Resolved dependencies with status.
749    pub dependencies: Vec<Dependency>,
750    /// Packages that have conflicts.
751    pub conflicts: Vec<String>,
752    /// Packages that are missing.
753    pub missing: Vec<String>,
754}
755
756/// Configuration for dependency resolution.
757///
758/// Controls various aspects of how dependencies are resolved, including which
759/// types of dependencies to include and how deep to traverse the dependency tree.
760///
761/// Note: This struct does not implement `Clone` or `Debug` because it contains
762/// a function pointer (`pkgbuild_cache`) that cannot be cloned or debugged.
763#[allow(clippy::struct_excessive_bools, clippy::type_complexity)]
764pub struct ResolverConfig {
765    /// Whether to include optional dependencies.
766    pub include_optdepends: bool,
767    /// Whether to include make dependencies.
768    pub include_makedepends: bool,
769    /// Whether to include check dependencies.
770    pub include_checkdepends: bool,
771    /// Maximum depth for transitive dependency resolution (0 = direct only).
772    pub max_depth: usize,
773    /// Custom callback for fetching PKGBUILD from cache (optional).
774    pub pkgbuild_cache: Option<Box<dyn Fn(&str) -> Option<String> + Send + Sync>>,
775    /// Whether to check AUR for missing dependencies.
776    pub check_aur: bool,
777}
778
779#[allow(clippy::derivable_impls)]
780impl Default for ResolverConfig {
781    fn default() -> Self {
782        Self {
783            include_optdepends: false,
784            include_makedepends: false,
785            include_checkdepends: false,
786            max_depth: 0, // Direct dependencies only
787            pkgbuild_cache: None,
788            check_aur: false,
789        }
790    }
791}
792
793#[cfg(test)]
794mod tests {
795    use super::*;
796
797    #[test]
798    fn dependency_status_priority_ordering() {
799        let conflict = DependencyStatus::Conflict {
800            reason: "test".to_string(),
801        };
802        let missing = DependencyStatus::Missing;
803        let to_install = DependencyStatus::ToInstall;
804        let to_upgrade = DependencyStatus::ToUpgrade {
805            current: "1.0".to_string(),
806            required: "2.0".to_string(),
807        };
808        let installed = DependencyStatus::Installed {
809            version: "1.0".to_string(),
810        };
811
812        assert!(conflict.priority() < missing.priority());
813        assert!(missing.priority() < to_install.priority());
814        assert!(to_install.priority() < to_upgrade.priority());
815        assert!(to_upgrade.priority() < installed.priority());
816    }
817
818    #[test]
819    fn dependency_status_helper_methods() {
820        let installed = DependencyStatus::Installed {
821            version: "1.0".to_string(),
822        };
823        assert!(installed.is_installed());
824        assert!(!installed.needs_action());
825        assert!(!installed.is_conflict());
826
827        let to_install = DependencyStatus::ToInstall;
828        assert!(!to_install.is_installed());
829        assert!(to_install.needs_action());
830        assert!(!to_install.is_conflict());
831
832        let conflict = DependencyStatus::Conflict {
833            reason: "test".to_string(),
834        };
835        assert!(!conflict.is_installed());
836        assert!(!conflict.needs_action());
837        assert!(conflict.is_conflict());
838    }
839
840    #[test]
841    fn dependency_spec_constructors() {
842        let spec1 = DependencySpec::new("glibc");
843        assert_eq!(spec1.name, "glibc");
844        assert!(spec1.version_req.is_empty());
845        assert!(!spec1.has_version_req());
846
847        let spec2 = DependencySpec::with_version("python", ">=3.12");
848        assert_eq!(spec2.name, "python");
849        assert_eq!(spec2.version_req, ">=3.12");
850        assert!(spec2.has_version_req());
851    }
852
853    #[test]
854    fn dependency_spec_display() {
855        let spec1 = DependencySpec::new("glibc");
856        assert_eq!(spec1.to_string(), "glibc");
857
858        let spec2 = DependencySpec::with_version("python", ">=3.12");
859        assert_eq!(spec2.to_string(), "python>=3.12");
860    }
861
862    #[test]
863    fn dependency_status_display() {
864        let installed = DependencyStatus::Installed {
865            version: "1.0".to_string(),
866        };
867        assert!(installed.to_string().contains("Installed"));
868        assert!(installed.to_string().contains("1.0"));
869
870        let to_install = DependencyStatus::ToInstall;
871        assert_eq!(to_install.to_string(), "To Install");
872
873        let to_upgrade = DependencyStatus::ToUpgrade {
874            current: "1.0".to_string(),
875            required: "2.0".to_string(),
876        };
877        assert!(to_upgrade.to_string().contains("To Upgrade"));
878        assert!(to_upgrade.to_string().contains("1.0"));
879        assert!(to_upgrade.to_string().contains("2.0"));
880
881        let conflict = DependencyStatus::Conflict {
882            reason: "test reason".to_string(),
883        };
884        assert!(conflict.to_string().contains("Conflict"));
885        assert!(conflict.to_string().contains("test reason"));
886
887        let missing = DependencyStatus::Missing;
888        assert_eq!(missing.to_string(), "Missing");
889    }
890
891    #[test]
892    fn dependency_source_display() {
893        let official = DependencySource::Official {
894            repo: "core".to_string(),
895        };
896        assert!(official.to_string().contains("Official"));
897        assert!(official.to_string().contains("core"));
898
899        let aur = DependencySource::Aur;
900        assert_eq!(aur.to_string(), "AUR");
901
902        let local = DependencySource::Local;
903        assert_eq!(local.to_string(), "Local");
904    }
905
906    #[test]
907    fn package_source_display() {
908        let official = PackageSource::Official {
909            repo: "extra".to_string(),
910            arch: "x86_64".to_string(),
911        };
912        assert!(official.to_string().contains("Official"));
913        assert!(official.to_string().contains("extra"));
914        assert!(official.to_string().contains("x86_64"));
915
916        let aur = PackageSource::Aur;
917        assert_eq!(aur.to_string(), "AUR");
918    }
919
920    #[test]
921    fn serde_roundtrip_dependency_status() {
922        let statuses = vec![
923            DependencyStatus::Installed {
924                version: "1.0.0".to_string(),
925            },
926            DependencyStatus::ToInstall,
927            DependencyStatus::ToUpgrade {
928                current: "1.0.0".to_string(),
929                required: "2.0.0".to_string(),
930            },
931            DependencyStatus::Conflict {
932                reason: "test conflict".to_string(),
933            },
934            DependencyStatus::Missing,
935        ];
936
937        for status in statuses {
938            let json = serde_json::to_string(&status).expect("serialization should succeed");
939            let deserialized: DependencyStatus =
940                serde_json::from_str(&json).expect("deserialization should succeed");
941            assert_eq!(status, deserialized);
942        }
943    }
944
945    #[test]
946    fn serde_roundtrip_dependency_source() {
947        let sources = vec![
948            DependencySource::Official {
949                repo: "core".to_string(),
950            },
951            DependencySource::Aur,
952            DependencySource::Local,
953        ];
954
955        for source in sources {
956            let json = serde_json::to_string(&source).expect("serialization should succeed");
957            let deserialized: DependencySource =
958                serde_json::from_str(&json).expect("deserialization should succeed");
959            assert_eq!(source, deserialized);
960        }
961    }
962
963    #[test]
964    fn serde_roundtrip_dependency() {
965        let dep = Dependency {
966            name: "glibc".to_string(),
967            version_req: ">=2.35".to_string(),
968            status: DependencyStatus::Installed {
969                version: "2.35".to_string(),
970            },
971            source: DependencySource::Official {
972                repo: "core".to_string(),
973            },
974            required_by: vec!["firefox".to_string(), "chromium".to_string()],
975            depends_on: vec!["linux-api-headers".to_string()],
976            is_core: true,
977            is_system: true,
978        };
979
980        let json = serde_json::to_string(&dep).expect("serialization should succeed");
981        let deserialized: Dependency =
982            serde_json::from_str(&json).expect("deserialization should succeed");
983        assert_eq!(dep.name, deserialized.name);
984        assert_eq!(dep.version_req, deserialized.version_req);
985        assert_eq!(dep.status, deserialized.status);
986        assert_eq!(dep.source, deserialized.source);
987        assert_eq!(dep.required_by, deserialized.required_by);
988        assert_eq!(dep.depends_on, deserialized.depends_on);
989        assert_eq!(dep.is_core, deserialized.is_core);
990        assert_eq!(dep.is_system, deserialized.is_system);
991    }
992
993    #[test]
994    fn serde_roundtrip_srcinfo_data() {
995        let srcinfo = SrcinfoData {
996            pkgbase: "test-package".to_string(),
997            pkgname: "test-package".to_string(),
998            pkgver: "1.0.0".to_string(),
999            pkgrel: "1".to_string(),
1000            depends: vec!["glibc".to_string(), "python>=3.12".to_string()],
1001            makedepends: vec!["make".to_string(), "gcc".to_string()],
1002            checkdepends: vec!["check".to_string()],
1003            optdepends: vec!["optional: optional-package".to_string()],
1004            conflicts: vec!["conflicting-pkg".to_string()],
1005            provides: vec!["provided-pkg".to_string()],
1006            replaces: vec!["replaced-pkg".to_string()],
1007        };
1008
1009        let json = serde_json::to_string(&srcinfo).expect("serialization should succeed");
1010        let deserialized: SrcinfoData =
1011            serde_json::from_str(&json).expect("deserialization should succeed");
1012        assert_eq!(srcinfo.pkgbase, deserialized.pkgbase);
1013        assert_eq!(srcinfo.pkgname, deserialized.pkgname);
1014        assert_eq!(srcinfo.pkgver, deserialized.pkgver);
1015        assert_eq!(srcinfo.pkgrel, deserialized.pkgrel);
1016        assert_eq!(srcinfo.depends, deserialized.depends);
1017        assert_eq!(srcinfo.makedepends, deserialized.makedepends);
1018        assert_eq!(srcinfo.checkdepends, deserialized.checkdepends);
1019        assert_eq!(srcinfo.optdepends, deserialized.optdepends);
1020        assert_eq!(srcinfo.conflicts, deserialized.conflicts);
1021        assert_eq!(srcinfo.provides, deserialized.provides);
1022        assert_eq!(srcinfo.replaces, deserialized.replaces);
1023    }
1024
1025    #[test]
1026    fn serde_roundtrip_package_ref() {
1027        let pkg_ref = PackageRef {
1028            name: "firefox".to_string(),
1029            version: "121.0".to_string(),
1030            source: PackageSource::Official {
1031                repo: "extra".to_string(),
1032                arch: "x86_64".to_string(),
1033            },
1034        };
1035
1036        let json = serde_json::to_string(&pkg_ref).expect("serialization should succeed");
1037        let deserialized: PackageRef =
1038            serde_json::from_str(&json).expect("deserialization should succeed");
1039        assert_eq!(pkg_ref, deserialized);
1040    }
1041}