Skip to main content

aam_core/pipeline/
scope_manager.rs

1//! Scope management for tracking syntactic nesting and context during parsing.
2//!
3//! `ScopeManager` tracks brace/bracket depth, current block type, and accumulated
4//! content for multi-line directives like `@schema { ... }`.
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum BlockType {
8    /// Not inside any block
9    None,
10    /// Inside `{ ... }` object literal
11    Object,
12    /// Inside `[ ... ]` list literal
13    List,
14    /// Inside a directive block like `@schema { ... }`
15    DirectiveBlock,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum ScopeError {
20    UnbalancedExit,
21}
22
23/// Tracks syntactic nesting and block context during parsing.
24#[derive(Debug)]
25pub struct ScopeManager {
26    /// Current brace/bracket nesting depth
27    nesting_depth: i32,
28    /// Current block type
29    current_block: BlockType,
30    /// Stack of block types for nested structures
31    block_stack: Vec<BlockType>,
32    /// Accumulated content for multi-line directives
33    accumulated_content: String,
34}
35
36impl ScopeManager {
37    /// Creates a new, empty scope manager
38    #[must_use]
39    pub const fn new() -> Self {
40        Self {
41            nesting_depth: 0,
42            current_block: BlockType::None,
43            block_stack: Vec::new(),
44            accumulated_content: String::new(),
45        }
46    }
47
48    /// Returns the current nesting depth
49    #[must_use]
50    pub const fn nesting_depth(&self) -> i32 {
51        self.nesting_depth
52    }
53
54    /// Returns the current block type
55    #[must_use]
56    pub const fn current_block(&self) -> BlockType {
57        self.current_block
58    }
59
60    /// Returns true if we're currently inside any nested structure
61    #[must_use]
62    pub const fn in_nested_context(&self) -> bool {
63        self.nesting_depth > 0
64    }
65
66    /// Enters a new block (opening brace or bracket)
67    pub fn enter_block(&mut self, block_type: BlockType) {
68        self.block_stack.push(self.current_block);
69        self.current_block = block_type;
70        self.nesting_depth += 1;
71    }
72
73    /// Exits a block (closing brace or bracket)
74    ///
75    /// Returns `Ok(())` if the block nesting is balanced, or `Err(())` if
76    /// we're trying to exit without having entered.
77    ///
78    /// # Errors
79    ///
80    /// Returns `Err(ScopeError::UnbalancedExit)` when an exit is attempted
81    /// while the nesting depth is already zero.
82    pub fn exit_block(&mut self) -> Result<(), ScopeError> {
83        if self.nesting_depth <= 0 {
84            return Err(ScopeError::UnbalancedExit);
85        }
86        self.nesting_depth -= 1;
87        self.current_block = self.block_stack.pop().unwrap_or(BlockType::None);
88        Ok(())
89    }
90
91    /// Returns the accumulated content for multi-line directives
92    #[must_use]
93    pub fn accumulated_content(&self) -> &str {
94        &self.accumulated_content
95    }
96
97    /// Appends text to the accumulated content
98    pub fn accumulate(&mut self, text: &str) {
99        if !self.accumulated_content.is_empty() {
100            self.accumulated_content.push(' ');
101        }
102        self.accumulated_content.push_str(text);
103    }
104
105    /// Clears the accumulated content
106    pub fn clear_accumulated(&mut self) {
107        self.accumulated_content.clear();
108    }
109
110    /// Returns true if a multi-line block has been completed (balanced braces)
111    #[must_use]
112    pub const fn block_is_complete(&self) -> bool {
113        self.nesting_depth == 0 && !self.accumulated_content.is_empty()
114    }
115
116    /// Resets the scope manager to the initial state
117    pub fn reset(&mut self) {
118        self.nesting_depth = 0;
119        self.current_block = BlockType::None;
120        self.block_stack.clear();
121        self.accumulated_content.clear();
122    }
123}
124
125impl Default for ScopeManager {
126    fn default() -> Self {
127        Self::new()
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn test_basic_nesting() {
137        let mut scope = ScopeManager::new();
138        assert_eq!(scope.nesting_depth(), 0);
139        assert!(!scope.in_nested_context());
140
141        scope.enter_block(BlockType::Object);
142        assert_eq!(scope.nesting_depth(), 1);
143        assert!(scope.in_nested_context());
144        assert_eq!(scope.current_block(), BlockType::Object);
145
146        scope.exit_block().unwrap();
147        assert_eq!(scope.nesting_depth(), 0);
148        assert!(!scope.in_nested_context());
149    }
150
151    #[test]
152    fn test_nested_objects() {
153        let mut scope = ScopeManager::new();
154        scope.enter_block(BlockType::Object);
155        scope.enter_block(BlockType::Object);
156        assert_eq!(scope.nesting_depth(), 2);
157
158        scope.exit_block().unwrap();
159        assert_eq!(scope.current_block(), BlockType::Object);
160
161        scope.exit_block().unwrap();
162        assert_eq!(scope.current_block(), BlockType::None);
163    }
164
165    #[test]
166    fn test_exit_without_enter() {
167        let mut scope = ScopeManager::new();
168        assert!(scope.exit_block().is_err());
169    }
170
171    #[test]
172    fn test_accumulation() {
173        let mut scope = ScopeManager::new();
174        assert_eq!(scope.accumulated_content(), "");
175
176        scope.accumulate("first");
177        assert_eq!(scope.accumulated_content(), "first");
178
179        scope.accumulate("second");
180        assert_eq!(scope.accumulated_content(), "first second");
181
182        scope.clear_accumulated();
183        assert_eq!(scope.accumulated_content(), "");
184    }
185
186    #[test]
187    fn test_block_complete() {
188        let mut scope = ScopeManager::new();
189        assert!(!scope.block_is_complete());
190
191        scope.accumulate("content");
192        assert!(scope.block_is_complete());
193
194        scope.clear_accumulated();
195        assert!(!scope.block_is_complete());
196    }
197}