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}