Skip to main content

deps_core/
yaml_walk.rs

1//! Generic frame-stack mechanics shared by every `yaml-rust2`
2//! `MarkedEventReceiver`-based parser in this workspace (`deps-dart`,
3//! `deps-github-actions`, `deps-gitlab-ci`).
4//!
5//! # What is generic vs. what stays per-crate
6//!
7//! Before this module existed, all three receivers independently reimplemented the
8//! same state-machine mechanics: opening/closing `Mapping`/`Sequence` frames, tracking
9//! whether the current frame is awaiting a key or a value, resetting that flag once a
10//! value is consumed, and (only in `deps-dart`) recognizing an explicit complex YAML
11//! key (`? <mapping>` / `? <sequence>`) so its subtree doesn't desync the enclosing
12//! mapping's key/value alternation. [`FrameStack`] owns exactly that, and nothing
13//! else — the role vocabulary, which child role a given parent/key combination
14//! produces, per-role field capture, alias-resolution *policy* (a scalar-anchor value
15//! table's own mechanics are shared instead, via [`crate::yaml_anchor::ScalarAnchorTable`]),
16//! dependency budgets, and multi-document handling all stay in each crate's own receiver,
17//! threaded through the stack's generic `payload: P`.
18//!
19//! A trait-based visitor was considered and rejected: the three crates' role
20//! vocabularies, capture logic, and finalization steps share nothing beyond the
21//! mechanics above, so a trait would either force a lowest-common-denominator shape
22//! (losing each crate's own capture idiom) or grow enough associated types/methods to
23//! be no simpler than driving a shared, monomorphised generic directly. `FrameStack`
24//! is monomorphised per crate (no `dyn`, no boxing) — driving it costs nothing over the
25//! hand-rolled version it replaces.
26//!
27//! # Complex-key handling
28//!
29//! [`FrameStack::push`] checks, before pushing, whether the *current* top frame is a
30//! `Mapping` still awaiting a key ([`FrameStack::is_complex_key_position`]) — this is
31//! exactly the position an explicit `?`-prefixed complex key's subtree occupies. When
32//! it is, the parent's `awaiting_key`/pending-key state is left untouched (it stays
33//! "awaiting a key", since the complex key hasn't finished yet) and the new frame is
34//! marked internally so [`FrameStack::pop`] knows to flip the parent from "awaiting a
35//! key" to "awaiting this entry's value" once the subtree closes — exactly what a
36//! plain scalar key does, and the only way to avoid desyncing the rest of the
37//! mapping's key/value alternation. Outside a complex-key position, `push`/`pop`
38//! behave as every crate's own `consume_pending_value` did: reset `awaiting_key` to
39//! `true` and `pending_key` to its default.
40//!
41//! Because a parent's `pending_key` is still at its default while it awaits a key, a
42//! caller's own child-role computation (keyed off `pending_key`) naturally falls
43//! through to whatever "irrelevant" role it already uses for an unrecognized key —
44//! no crate-side special-casing is needed to get complex-key support: it falls out of
45//! driving [`FrameStack`] directly.
46//!
47//! # Examples
48//!
49//! ```
50//! use deps_core::yaml_walk::{FrameKind, FrameStack, ScalarPosition};
51//!
52//! #[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
53//! enum Key {
54//!     #[default]
55//!     None,
56//!     Name,
57//! }
58//!
59//! let mut stack: FrameStack<(), Key, ()> = FrameStack::new();
60//! stack.push(FrameKind::Mapping, (), ());
61//! assert_eq!(stack.scalar_position(), ScalarPosition::Key);
62//!
63//! stack.observe_key(Key::Name);
64//! assert_eq!(stack.scalar_position(), ScalarPosition::Value);
65//! assert_eq!(*stack.top().unwrap().pending_key(), Key::Name);
66//!
67//! stack.consume_value();
68//! assert_eq!(stack.scalar_position(), ScalarPosition::Key);
69//! ```
70
71/// Which container kind a [`Frame`] represents.
72///
73/// Mirrors `yaml-rust2`'s `Event::MappingStart`/`Event::SequenceStart` split, which
74/// every driving crate already matches on before calling [`FrameStack::push`].
75///
76/// Deliberately **not** `#[non_exhaustive]`: YAML has exactly two container kinds, a
77/// closed grammar concept this crate has no reason to ever extend.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum FrameKind {
80    /// A YAML mapping (`{}` or block-style `key: value` pairs).
81    Mapping,
82    /// A YAML sequence (`[]` or block-style `- item` entries).
83    Sequence,
84}
85
86/// Where the next scalar event sits relative to the open frame stack.
87///
88/// Deliberately **not** `#[non_exhaustive]`: this exhaustively covers every position a
89/// scalar can occupy relative to a frame stack (key, value, or neither) — a closed
90/// concept with no fourth case to ever add.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum ScalarPosition {
93    /// The top frame is a `Mapping` currently awaiting a key.
94    Key,
95    /// The top frame is a `Mapping` currently awaiting a value.
96    Value,
97    /// The stack is empty, or the top frame is a `Sequence` — a scalar here has no
98    /// key/value meaning of its own (a bare sequence item, or a document-level bare
99    /// scalar).
100    Outside,
101}
102
103/// One open container frame.
104///
105/// Holds its structural kind, the driving crate's own `role` (`R`) and
106/// `pending_key` (`K`) vocabulary, and an arbitrary per-frame `payload` (`P`) the
107/// driving crate accumulates fields into.
108///
109/// `kind`/`awaiting_key`/`pending_key`/the complex-key flag are private and reachable
110/// only through [`FrameStack`]'s invariant-preserving methods; `role` is read-only via
111/// [`Frame::role`]. `payload` is `pub` — it is exactly the state a driving crate's own
112/// per-role capture logic needs to read and mutate directly (e.g. `top_mut().payload`)
113/// between calls to [`FrameStack::observe_key`]/[`FrameStack::consume_value`].
114pub struct Frame<R, K, P> {
115    kind: FrameKind,
116    role: R,
117    awaiting_key: bool,
118    pending_key: K,
119    /// Set by [`FrameStack::push`] when this frame occupies a complex-key position
120    /// (see the module docs) — read back by [`FrameStack::pop`] to decide how the
121    /// parent's key/value state transitions once this frame closes.
122    is_complex_key: bool,
123    /// The driving crate's own per-frame accumulated state.
124    pub payload: P,
125}
126
127impl<R, K, P> Frame<R, K, P> {
128    /// Whether this frame is a `Mapping` or a `Sequence`.
129    #[must_use]
130    pub const fn kind(&self) -> FrameKind {
131        self.kind
132    }
133
134    /// The driving crate's role for this frame (e.g. "this is the `include:` value").
135    #[must_use]
136    pub const fn role(&self) -> &R {
137        &self.role
138    }
139
140    /// The key this frame is currently holding a value for, if it is a `Mapping` past
141    /// its key scalar — meaningless while the frame is still awaiting that key (see
142    /// [`FrameStack::scalar_position`]).
143    #[must_use]
144    pub const fn pending_key(&self) -> &K {
145        &self.pending_key
146    }
147}
148
149/// The generic frame-stack mechanics described in the [module docs](self).
150///
151/// `K` must be `Clone + Default` (not `Copy`) — [`FrameStack::consume_value`] resets
152/// the top frame's pending key to `K::default()`, and [`Frame::pending_key`] hands
153/// back a reference rather than forcing every driving crate's key enum to be `Copy`.
154pub struct FrameStack<R, K, P> {
155    frames: Vec<Frame<R, K, P>>,
156}
157
158impl<R, K, P> Default for FrameStack<R, K, P>
159where
160    K: Clone + Default,
161{
162    fn default() -> Self {
163        Self::new()
164    }
165}
166
167impl<R, K, P> FrameStack<R, K, P>
168where
169    K: Clone + Default,
170{
171    /// Creates an empty stack.
172    #[must_use]
173    pub const fn new() -> Self {
174        Self { frames: Vec::new() }
175    }
176
177    /// Number of currently open frames — `0` at the document root before any
178    /// `MappingStart`/`SequenceStart` has been pushed.
179    #[must_use]
180    pub fn depth(&self) -> usize {
181        self.frames.len()
182    }
183
184    /// The innermost open frame, if any.
185    #[must_use]
186    pub fn top(&self) -> Option<&Frame<R, K, P>> {
187        self.frames.last()
188    }
189
190    /// Mutable access to the innermost open frame, if any.
191    #[must_use]
192    pub fn top_mut(&mut self) -> Option<&mut Frame<R, K, P>> {
193        self.frames.last_mut()
194    }
195
196    /// The top frame's role, or `default` if the stack is empty.
197    ///
198    /// Convenience for `self.top().map_or(default, |f| *f.role())` — a pattern every
199    /// driving crate's own child-role computation and key-scalar handling repeats at
200    /// each call site, since a fresh role must always be read before deciding how to
201    /// route a new key or child container.
202    ///
203    /// # Examples
204    ///
205    /// ```
206    /// use deps_core::yaml_walk::{FrameKind, FrameStack};
207    ///
208    /// #[derive(Clone, Copy, PartialEq, Eq, Debug)]
209    /// enum Role {
210    ///     Irrelevant,
211    ///     Root,
212    /// }
213    ///
214    /// let mut stack: FrameStack<Role, (), ()> = FrameStack::new();
215    /// assert_eq!(stack.top_role_or(Role::Irrelevant), Role::Irrelevant);
216    ///
217    /// stack.push(FrameKind::Mapping, Role::Root, ());
218    /// assert_eq!(stack.top_role_or(Role::Irrelevant), Role::Root);
219    /// ```
220    #[must_use]
221    pub fn top_role_or(&self, default: R) -> R
222    where
223        R: Copy,
224    {
225        self.top().map_or(default, |frame| *frame.role())
226    }
227
228    /// Closes every open frame without running any [`FrameStack::pop`] transition —
229    /// for a multi-document YAML stream, called on `Event::DocumentStart`/
230    /// `Event::DocumentEnd` so one document's nesting never mis-scopes another's.
231    pub fn clear(&mut self) {
232        self.frames.clear();
233    }
234
235    /// Where the next scalar event sits: awaiting a key, awaiting a value, or outside
236    /// any key/value-bearing context (an empty stack, or a `Sequence` top).
237    #[must_use]
238    pub fn scalar_position(&self) -> ScalarPosition {
239        match self.frames.last() {
240            None => ScalarPosition::Outside,
241            Some(frame) if frame.kind == FrameKind::Sequence => ScalarPosition::Outside,
242            Some(frame) if frame.awaiting_key => ScalarPosition::Key,
243            Some(_) => ScalarPosition::Value,
244        }
245    }
246
247    /// Whether a container about to be pushed would occupy a complex YAML key's
248    /// position (`? <mapping>` / `? <sequence>`): the top frame is a `Mapping` still
249    /// awaiting a key. Exposed so a driving crate's own `on_alias`-replay path (which
250    /// calls [`FrameStack::push`]/[`FrameStack::pop`] out of band, not from a live key
251    /// position) can assert it is never mistaken for a complex key.
252    #[must_use]
253    pub fn is_complex_key_position(&self) -> bool {
254        self.frames
255            .last()
256            .is_some_and(|frame| frame.kind == FrameKind::Mapping && frame.awaiting_key)
257    }
258
259    /// Records that a key scalar (or an alias resolving to one) was just read: the top
260    /// frame transitions from awaiting a key to awaiting `key`'s value. A no-op if the
261    /// stack is empty.
262    pub fn observe_key(&mut self, key: K)
263    where
264        K: PartialEq,
265    {
266        if let Some(top) = self.frames.last_mut() {
267            debug_assert!(
268                !top.awaiting_key || top.pending_key == K::default(),
269                "observe_key: pending_key was not at its default while awaiting_key was \
270                 true — the pending_key/awaiting_key invariant this module relies on \
271                 throughout has been violated"
272            );
273            top.pending_key = key;
274            top.awaiting_key = false;
275        }
276    }
277
278    /// Records that a value was just consumed (a scalar, alias, or a just-closed
279    /// container): the top frame transitions back to awaiting a key, and its pending
280    /// key resets to `K::default()`. A no-op if the stack is empty.
281    ///
282    /// Like [`FrameStack::push`]/[`FrameStack::pop`], this mutates `awaiting_key`/
283    /// `pending_key` unconditionally, even when the top frame is a `Sequence` (which
284    /// has no key/value structure of its own). This is harmless as long as every
285    /// caller reads a frame's key/value state only through
286    /// [`FrameStack::scalar_position`]/[`FrameStack::is_complex_key_position`] — both
287    /// already treat a `Sequence` top as [`ScalarPosition::Outside`]/never a complex
288    /// key — rather than reading [`Frame::pending_key`] directly without first
289    /// checking the frame's [`FrameKind`].
290    pub fn consume_value(&mut self) {
291        if let Some(top) = self.frames.last_mut() {
292            top.awaiting_key = true;
293            top.pending_key = K::default();
294        }
295    }
296
297    /// Opens a new frame of kind `kind`, role `role`, and initial `payload`.
298    ///
299    /// If the current top frame occupies a complex-key position (see the [module
300    /// docs](self)), the parent is left untouched (still awaiting its key) and the
301    /// new frame is marked so [`FrameStack::pop`] applies the matching transition
302    /// instead of the ordinary one; otherwise this is exactly
303    /// [`FrameStack::consume_value`] on the parent followed by pushing the new frame.
304    ///
305    /// # Examples
306    ///
307    /// Pushing a container onto a `Mapping` still awaiting its key (`? <mapping>:
308    /// value`, a complex key's own subtree) is recognized via
309    /// [`FrameStack::is_complex_key_position`] *before* the push — that recognition is
310    /// exactly what makes the matching [`FrameStack::pop`] transition possible once the
311    /// subtree closes (see that method's own example for the visible effect):
312    ///
313    /// ```
314    /// use deps_core::yaml_walk::{FrameKind, FrameStack, ScalarPosition};
315    ///
316    /// let mut stack: FrameStack<(), (), ()> = FrameStack::new();
317    /// stack.push(FrameKind::Mapping, (), ()); // the enclosing mapping
318    /// assert_eq!(stack.scalar_position(), ScalarPosition::Key);
319    ///
320    /// assert!(stack.is_complex_key_position());
321    /// stack.push(FrameKind::Mapping, (), ()); // `? <mapping>`'s own subtree
322    /// assert_eq!(stack.depth(), 2);
323    /// ```
324    pub fn push(&mut self, kind: FrameKind, role: R, payload: P) {
325        let is_complex_key = self.is_complex_key_position();
326        if !is_complex_key {
327            self.consume_value();
328        }
329        self.frames.push(Frame {
330            kind,
331            role,
332            awaiting_key: true,
333            pending_key: K::default(),
334            is_complex_key,
335            payload,
336        });
337    }
338
339    /// Closes the innermost open frame and returns it, or `None` if the stack was
340    /// already empty.
341    ///
342    /// If the closed frame was pushed at a complex-key position, the new top (the
343    /// former parent) transitions directly to "awaiting this entry's value" — the
344    /// same transition a plain scalar key produces — rather than the ordinary
345    /// [`FrameStack::consume_value`] reset, which would leave it stuck awaiting
346    /// another key and desync the rest of the mapping's key/value alternation.
347    ///
348    /// # Examples
349    ///
350    /// Closing a complex key's subtree (`? <mapping>: value`) flips the parent
351    /// straight to "awaiting a value", exactly as a plain scalar key would — not back
352    /// to "awaiting a key", which is what would happen for an ordinary (non-complex-key)
353    /// child:
354    ///
355    /// ```
356    /// use deps_core::yaml_walk::{FrameKind, FrameStack, ScalarPosition};
357    ///
358    /// let mut stack: FrameStack<(), (), ()> = FrameStack::new();
359    /// stack.push(FrameKind::Mapping, (), ()); // the enclosing mapping
360    /// stack.push(FrameKind::Mapping, (), ()); // `? <mapping>`'s own subtree
361    /// stack.pop(); // closes the complex key
362    ///
363    /// // The parent now awaits this entry's *value* — not another key.
364    /// assert_eq!(stack.scalar_position(), ScalarPosition::Value);
365    /// ```
366    pub fn pop(&mut self) -> Option<Frame<R, K, P>> {
367        let frame = self.frames.pop()?;
368        if frame.is_complex_key {
369            if let Some(parent) = self.frames.last_mut() {
370                parent.awaiting_key = false;
371                parent.pending_key = K::default();
372            }
373        } else {
374            self.consume_value();
375        }
376        Some(frame)
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::{FrameKind, FrameStack, ScalarPosition};
383
384    #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
385    enum Key {
386        #[default]
387        None,
388        Name,
389    }
390
391    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
392    enum Role {
393        Root,
394        Irrelevant,
395    }
396
397    #[test]
398    fn test_key_value_toggling() {
399        let mut stack: FrameStack<Role, Key, ()> = FrameStack::new();
400        stack.push(FrameKind::Mapping, Role::Root, ());
401        assert_eq!(stack.scalar_position(), ScalarPosition::Key);
402
403        stack.observe_key(Key::Name);
404        assert_eq!(stack.scalar_position(), ScalarPosition::Value);
405        assert_eq!(*stack.top().unwrap().pending_key(), Key::Name);
406
407        stack.consume_value();
408        assert_eq!(stack.scalar_position(), ScalarPosition::Key);
409        assert_eq!(*stack.top().unwrap().pending_key(), Key::None);
410    }
411
412    #[test]
413    fn test_sequence_parent_scalar_position_is_outside() {
414        let mut stack: FrameStack<Role, Key, ()> = FrameStack::new();
415        stack.push(FrameKind::Sequence, Role::Irrelevant, ());
416        assert_eq!(stack.scalar_position(), ScalarPosition::Outside);
417        // A sequence frame never tracks a key at all.
418        stack.observe_key(Key::Name);
419        assert_eq!(*stack.top().unwrap().pending_key(), Key::Name);
420        assert_eq!(stack.scalar_position(), ScalarPosition::Outside);
421    }
422
423    #[test]
424    fn test_empty_stack_scalar_position_is_outside() {
425        let stack: FrameStack<Role, Key, ()> = FrameStack::new();
426        assert_eq!(stack.scalar_position(), ScalarPosition::Outside);
427        assert!(stack.top().is_none());
428    }
429
430    #[test]
431    fn test_alias_in_key_position_toggles_same_as_a_scalar_key() {
432        // A resolved YAML alias in key position (`*anchor: value`) is driven through
433        // exactly the same `observe_key`/`consume_value` calls a plain scalar key
434        // would use — the walker has no separate "alias" concept at all.
435        let mut stack: FrameStack<Role, Key, ()> = FrameStack::new();
436        stack.push(FrameKind::Mapping, Role::Root, ());
437        assert_eq!(stack.scalar_position(), ScalarPosition::Key);
438        stack.observe_key(Key::Name);
439        assert_eq!(stack.scalar_position(), ScalarPosition::Value);
440        stack.consume_value();
441        assert_eq!(stack.scalar_position(), ScalarPosition::Key);
442    }
443
444    #[test]
445    fn test_complex_key_subtree_does_not_disturb_parent_then_flips_to_awaiting_value() {
446        // `? <mapping>: value` — the complex key's own subtree is a Mapping pushed
447        // while the parent is still awaiting a key.
448        let mut stack: FrameStack<Role, Key, ()> = FrameStack::new();
449        stack.push(FrameKind::Mapping, Role::Root, ());
450        assert!(stack.is_complex_key_position());
451
452        stack.push(FrameKind::Mapping, Role::Irrelevant, ());
453        // Pushing into a complex-key position must not touch the parent at all —
454        // there is no parent-visible effect until the subtree closes.
455        assert_eq!(stack.depth(), 2);
456
457        let closed = stack.pop().unwrap();
458        assert_eq!(closed.kind(), FrameKind::Mapping);
459        assert_eq!(stack.depth(), 1);
460        // The parent now behaves exactly as if a plain scalar key had just been read:
461        // awaiting this entry's value, not another key.
462        assert_eq!(stack.scalar_position(), ScalarPosition::Value);
463        assert_eq!(*stack.top().unwrap().pending_key(), Key::None);
464    }
465
466    #[test]
467    fn test_nested_complex_key_sequence_form() {
468        // `? <sequence>: value` — the sequence variant of a complex key.
469        let mut stack: FrameStack<Role, Key, ()> = FrameStack::new();
470        stack.push(FrameKind::Mapping, Role::Root, ());
471        stack.push(FrameKind::Sequence, Role::Irrelevant, ());
472        assert_eq!(stack.top().unwrap().kind(), FrameKind::Sequence);
473        stack.pop();
474        assert_eq!(stack.scalar_position(), ScalarPosition::Value);
475    }
476
477    #[test]
478    fn test_ordinary_mapping_value_push_pop_resets_parent_to_awaiting_key() {
479        let mut stack: FrameStack<Role, Key, ()> = FrameStack::new();
480        stack.push(FrameKind::Mapping, Role::Root, ());
481        stack.observe_key(Key::Name);
482        // The value is itself a nested mapping — an ordinary (non-complex-key) push,
483        // since the parent is awaiting a *value*, not a key, at this point.
484        assert!(!stack.is_complex_key_position());
485        stack.push(FrameKind::Mapping, Role::Irrelevant, ());
486        stack.pop();
487        assert_eq!(stack.scalar_position(), ScalarPosition::Key);
488        assert_eq!(*stack.top().unwrap().pending_key(), Key::None);
489    }
490
491    #[test]
492    fn test_depth_tracks_open_frames() {
493        let mut stack: FrameStack<Role, Key, ()> = FrameStack::new();
494        assert_eq!(stack.depth(), 0);
495        stack.push(FrameKind::Mapping, Role::Root, ());
496        assert_eq!(stack.depth(), 1);
497        stack.push(FrameKind::Sequence, Role::Irrelevant, ());
498        assert_eq!(stack.depth(), 2);
499        stack.pop();
500        assert_eq!(stack.depth(), 1);
501    }
502
503    #[test]
504    fn test_clear_drops_every_open_frame() {
505        let mut stack: FrameStack<Role, Key, ()> = FrameStack::new();
506        stack.push(FrameKind::Mapping, Role::Root, ());
507        stack.push(FrameKind::Sequence, Role::Irrelevant, ());
508        stack.clear();
509        assert_eq!(stack.depth(), 0);
510        assert_eq!(stack.scalar_position(), ScalarPosition::Outside);
511    }
512
513    #[test]
514    fn test_payload_is_directly_mutable_between_key_and_value() {
515        let mut stack: FrameStack<Role, Key, Vec<u32>> = FrameStack::new();
516        stack.push(FrameKind::Mapping, Role::Root, Vec::new());
517        stack.top_mut().unwrap().payload.push(1);
518        stack.observe_key(Key::Name);
519        stack.top_mut().unwrap().payload.push(2);
520        assert_eq!(stack.top().unwrap().payload, vec![1, 2]);
521    }
522}