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(crate) mod child_path;
31pub mod cycle;
32pub mod depends_on;
33pub mod dup_symlink;
34
35pub(crate) use child_path::{ChildPathValidator, DupChildPathValidator};
36pub use cycle::CycleValidator;
37pub use depends_on::DependsOnValidator;
38pub use dup_symlink::DuplicateSymlinkValidator;
39
40/// Errors raised by plan-phase validators.
41///
42/// Marked `#[non_exhaustive]` so future slices (slices 3–6) can add variants
43/// without breaking downstream `match` arms.
44#[non_exhaustive]
45#[derive(Debug, Clone, PartialEq, Eq, Error)]
46pub enum PackValidationError {
47    /// Two `symlink` actions within the same pack resolve to the same
48    /// literal `dst` string. `first` and `second` are indices in the
49    /// flattened action-walk order (see
50    /// [`PackManifest::iter_all_symlinks`]).
51    #[error("duplicate symlink dst `{dst}` (actions at indices {first} and {second})")]
52    DuplicateSymlinkDst {
53        /// Literal `dst` string (pre-expansion).
54        dst: String,
55        /// Global index of the earlier action.
56        first: usize,
57        /// Global index of the later action.
58        second: usize,
59    },
60
61    /// A cycle was detected in the assembled pack graph. `chain` lists the
62    /// pack names from the outermost node down to the recurrence.
63    #[error("cycle detected in pack graph: {chain:?}")]
64    GraphCycle {
65        /// Ordered chain of pack names that forms the cycle.
66        chain: Vec<String>,
67    },
68
69    /// A `depends_on` entry could not be resolved against any node in the
70    /// walked graph.
71    #[error("pack `{pack}` depends on `{required}` but no such pack exists in the graph")]
72    DependsOnUnsatisfied {
73        /// Name of the pack that declared the dependency.
74        pack: String,
75        /// The unresolved `depends_on` entry (a pack name or url).
76        required: String,
77    },
78
79    /// A `children[].path` value violates the bare-name rule
80    /// (`^[a-z][a-z0-9-]*$`, no separators, no `.` / `..`, no empty).
81    /// Enforced since v1.1.0 — see the `child_path` module's
82    /// `ChildPathValidator` (internal).
83    #[error("pack child `{child_name}` has invalid path `{path}`: {reason}")]
84    ChildPathInvalid {
85        /// Label of the offending child (its `path` field, or `url` as
86        /// fallback).
87        child_name: String,
88        /// The rejected literal `path` value.
89        path: String,
90        /// One-line explanation of which sub-rule failed.
91        reason: String,
92    },
93
94    /// Two or more `children[]` entries within the same parent
95    /// resolve to the same `effective_path()`. Without this gate the
96    /// second clone would silently overwrite the first's working
97    /// tree, or — once both have a `.git` — collide on the
98    /// dest-already-exists fast path and skip-fetch the wrong upstream.
99    /// Enforced since v1.1.0 — see the `child_path` module's
100    /// `DupChildPathValidator` (internal).
101    #[error("pack has duplicate children resolving to `{path}`: {urls:?}")]
102    ChildPathDuplicate {
103        /// The shared resolved path that two or more children claim.
104        path: String,
105        /// URLs of every colliding child, in declaration order.
106        urls: Vec<String>,
107    },
108}
109
110/// A single plan-phase validator.
111///
112/// Implementations run against a fully parsed manifest and return every
113/// problem they observe — never `Result`, because aggregation across
114/// validators is the point.
115pub trait Validator {
116    /// Stable human-readable identifier for diagnostics / allowlisting.
117    fn name(&self) -> &'static str;
118
119    /// Inspect `pack` and emit zero or more errors.
120    fn check(&self, pack: &PackManifest) -> Vec<PackValidationError>;
121}
122
123/// Run every default validator against `pack`, concatenating their
124/// findings.
125///
126/// The current default set is:
127///
128/// 1. [`DuplicateSymlinkValidator`] — two symlinks with the same literal
129///    `dst`.
130/// 2. `ChildPathValidator` (internal) — every `children[].path` matches
131///    the bare-name regex (since v1.1.0).
132/// 3. `DupChildPathValidator` (internal) — no two `children[]` entries
133///    within the same parent share an `effective_path()` (since v1.1.0).
134///
135/// Later slices extend this list; callers should prefer
136/// [`PackManifest::validate_plan`] over instantiating validators manually,
137/// so the default set stays discoverable.
138#[must_use]
139pub fn run_all(pack: &PackManifest) -> Vec<PackValidationError> {
140    let validators: [&dyn Validator; 3] =
141        [&DuplicateSymlinkValidator, &ChildPathValidator, &DupChildPathValidator];
142    let mut errs = Vec::new();
143    for v in validators {
144        errs.extend(v.check(pack));
145    }
146    errs
147}
148
149/// Plan-phase validator that operates on an assembled [`PackGraph`].
150///
151/// Separate trait from [`Validator`] on purpose: graph-level checks need
152/// the full graph, not a single manifest, and mixing the two into one
153/// trait would force every per-manifest validator to accept a graph it
154/// doesn't need. Two traits keep each call site's surface minimal and
155/// type-safe.
156pub trait GraphValidator {
157    /// Stable human-readable identifier.
158    fn name(&self) -> &'static str;
159
160    /// Inspect `graph` and emit zero or more errors.
161    fn check(&self, graph: &PackGraph) -> Vec<PackValidationError>;
162}
163
164/// Run every default [`GraphValidator`] against `graph`, concatenating
165/// their findings.
166///
167/// Current default set:
168///
169/// 1. [`CycleValidator`] — belt-and-suspenders for cycles the walker
170///    should have caught.
171/// 2. [`DependsOnValidator`] — verify every `depends_on` entry resolves.
172#[must_use]
173pub fn run_all_graph(graph: &PackGraph) -> Vec<PackValidationError> {
174    let validators: [&dyn GraphValidator; 2] = [&CycleValidator, &DependsOnValidator];
175    let mut errs = Vec::new();
176    for v in validators {
177        errs.extend(v.check(graph));
178    }
179    errs
180}
181
182impl PackGraph {
183    /// Run the default graph-validator set over `self`.
184    ///
185    /// Mirrors [`PackManifest::validate_plan`] at the graph surface. Kept
186    /// here (rather than in `tree::graph`) so the `tree` module does not
187    /// depend on `pack::validate`; the dependency direction stays
188    /// `validate -> tree` only.
189    ///
190    /// # Errors
191    ///
192    /// Returns the aggregated error list when any graph validator
193    /// flags a problem.
194    pub fn validate(&self) -> Result<(), Vec<PackValidationError>> {
195        let errs = run_all_graph(self);
196        if errs.is_empty() {
197            Ok(())
198        } else {
199            Err(errs)
200        }
201    }
202}