Skip to main content

grex_core/pack/validate/
mod.rs

1//! Plan-phase validators for [`PackManifest`].
2//!
3//! Stage B of M3 introduces **plan-phase** validation — checks that run
4//! after [`crate::pack::parse`] succeeds but before any execute-time work
5//! (variable expansion, filesystem touches, child-pack traversal). The
6//! validators here operate on the already-parsed manifest in its
7//! pre-expansion, literal form.
8//!
9//! # Framework shape
10//!
11//! A [`Validator`] receives an immutable [`PackManifest`] and returns a
12//! `Vec<PackValidationError>` — never fail-first. [`run_all`] composes the
13//! default validator set and concatenates their findings so callers see
14//! the full diagnostic set in one pass. This slice ships one validator
15//! ([`DuplicateSymlinkValidator`]); subsequent M3 slices (cycle detect,
16//! cross-pack conflict, `depends_on` verification) plug into the same
17//! surface without touching orchestrator code.
18//!
19//! # Non-goals for this slice
20//!
21//! * No filesystem IO, no git, no platform probing.
22//! * No variable expansion — validators compare literal `dst` strings.
23//! * No cross-pack reasoning (later slices).
24
25use thiserror::Error;
26
27use super::PackManifest;
28use crate::tree::PackGraph;
29
30pub mod cycle;
31pub mod depends_on;
32pub mod dup_symlink;
33
34pub use cycle::CycleValidator;
35pub use depends_on::DependsOnValidator;
36pub use dup_symlink::DuplicateSymlinkValidator;
37
38/// Errors raised by plan-phase validators.
39///
40/// Marked `#[non_exhaustive]` so future slices (slices 3–6) can add variants
41/// without breaking downstream `match` arms.
42#[non_exhaustive]
43#[derive(Debug, Clone, PartialEq, Eq, Error)]
44pub enum PackValidationError {
45    /// Two `symlink` actions within the same pack resolve to the same
46    /// literal `dst` string. `first` and `second` are indices in the
47    /// flattened action-walk order (see
48    /// [`PackManifest::iter_all_symlinks`]).
49    #[error("duplicate symlink dst `{dst}` (actions at indices {first} and {second})")]
50    DuplicateSymlinkDst {
51        /// Literal `dst` string (pre-expansion).
52        dst: String,
53        /// Global index of the earlier action.
54        first: usize,
55        /// Global index of the later action.
56        second: usize,
57    },
58
59    /// A cycle was detected in the assembled pack graph. `chain` lists the
60    /// pack names from the outermost node down to the recurrence.
61    #[error("cycle detected in pack graph: {chain:?}")]
62    GraphCycle {
63        /// Ordered chain of pack names that forms the cycle.
64        chain: Vec<String>,
65    },
66
67    /// A `depends_on` entry could not be resolved against any node in the
68    /// walked graph.
69    #[error("pack `{pack}` depends on `{required}` but no such pack exists in the graph")]
70    DependsOnUnsatisfied {
71        /// Name of the pack that declared the dependency.
72        pack: String,
73        /// The unresolved `depends_on` entry (a pack name or url).
74        required: String,
75    },
76}
77
78/// A single plan-phase validator.
79///
80/// Implementations run against a fully parsed manifest and return every
81/// problem they observe — never `Result`, because aggregation across
82/// validators is the point.
83pub trait Validator {
84    /// Stable human-readable identifier for diagnostics / allowlisting.
85    fn name(&self) -> &'static str;
86
87    /// Inspect `pack` and emit zero or more errors.
88    fn check(&self, pack: &PackManifest) -> Vec<PackValidationError>;
89}
90
91/// Run every default validator against `pack`, concatenating their
92/// findings.
93///
94/// The current default set is:
95///
96/// 1. [`DuplicateSymlinkValidator`] — two symlinks with the same literal
97///    `dst`.
98///
99/// Later slices extend this list; callers should prefer
100/// [`PackManifest::validate_plan`] over instantiating validators manually,
101/// so the default set stays discoverable.
102#[must_use]
103pub fn run_all(pack: &PackManifest) -> Vec<PackValidationError> {
104    let validators: [&dyn Validator; 1] = [&DuplicateSymlinkValidator];
105    let mut errs = Vec::new();
106    for v in validators {
107        errs.extend(v.check(pack));
108    }
109    errs
110}
111
112/// Plan-phase validator that operates on an assembled [`PackGraph`].
113///
114/// Separate trait from [`Validator`] on purpose: graph-level checks need
115/// the full graph, not a single manifest, and mixing the two into one
116/// trait would force every per-manifest validator to accept a graph it
117/// doesn't need. Two traits keep each call site's surface minimal and
118/// type-safe.
119pub trait GraphValidator {
120    /// Stable human-readable identifier.
121    fn name(&self) -> &'static str;
122
123    /// Inspect `graph` and emit zero or more errors.
124    fn check(&self, graph: &PackGraph) -> Vec<PackValidationError>;
125}
126
127/// Run every default [`GraphValidator`] against `graph`, concatenating
128/// their findings.
129///
130/// Current default set:
131///
132/// 1. [`CycleValidator`] — belt-and-suspenders for cycles the walker
133///    should have caught.
134/// 2. [`DependsOnValidator`] — verify every `depends_on` entry resolves.
135#[must_use]
136pub fn run_all_graph(graph: &PackGraph) -> Vec<PackValidationError> {
137    let validators: [&dyn GraphValidator; 2] = [&CycleValidator, &DependsOnValidator];
138    let mut errs = Vec::new();
139    for v in validators {
140        errs.extend(v.check(graph));
141    }
142    errs
143}
144
145impl PackGraph {
146    /// Run the default graph-validator set over `self`.
147    ///
148    /// Mirrors [`PackManifest::validate_plan`] at the graph surface. Kept
149    /// here (rather than in `tree::graph`) so the `tree` module does not
150    /// depend on `pack::validate`; the dependency direction stays
151    /// `validate -> tree` only.
152    ///
153    /// # Errors
154    ///
155    /// Returns the aggregated error list when any graph validator
156    /// flags a problem.
157    pub fn validate(&self) -> Result<(), Vec<PackValidationError>> {
158        let errs = run_all_graph(self);
159        if errs.is_empty() {
160            Ok(())
161        } else {
162            Err(errs)
163        }
164    }
165}