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}