Skip to main content

textfsm_core/parser/
state.rs

1//! Runtime state for values during parsing.
2
3use std::collections::HashMap;
4
5
6use crate::template::ValueDef;
7use crate::types::{ListItem, Value, ValueOption};
8
9/// Runtime state for a single value.
10#[derive(Debug, Clone)]
11pub struct ValueState {
12    /// Definition from template.
13    pub def: ValueDef,
14
15    /// Index of this value in the record.
16    pub index: usize,
17
18    /// Current value.
19    current: Value,
20
21    /// Cached value for Filldown option.
22    filldown_cache: Option<Value>,
23}
24
25impl ValueState {
26    /// Create a new value state.
27    pub fn new(def: ValueDef, index: usize) -> Self {
28        let initial = if def.has_option(ValueOption::List) {
29            Value::List(Vec::new())
30        } else {
31            Value::Empty
32        };
33
34        Self {
35            def,
36            index,
37            current: initial,
38            filldown_cache: None,
39        }
40    }
41
42    /// Assign a matched value.
43    pub fn assign(&mut self, value: String, all_results: &mut [Vec<Value>]) {
44        if self.def.has_option(ValueOption::List) {
45            self.assign_list(value.clone());
46        } else {
47            self.current = Value::Single(value.clone());
48        }
49
50        // Filldown: cache the value
51        if self.def.has_option(ValueOption::Filldown) {
52            self.filldown_cache = Some(self.current.clone());
53        }
54
55        // Fillup: backfill previous records
56        if self.def.has_option(ValueOption::Fillup) && !value.is_empty() {
57            self.backfill(&value, all_results);
58        }
59    }
60
61    fn assign_list(&mut self, value: String) {
62        let item = if let Some(ref regex) = self.def.compiled_regex {
63            // Check for nested capture groups
64            if let Ok(Some(caps)) = regex.captures(&value) {
65                let dict: HashMap<String, String> = regex
66                    .capture_names()
67                    .flatten()
68                    .filter_map(|name| {
69                        caps.name(name)
70                            .map(|m| (name.to_string(), m.as_str().to_string()))
71                    })
72                    .collect();
73
74                if !dict.is_empty() {
75                    ListItem::Dict(dict)
76                } else {
77                    ListItem::String(value)
78                }
79            } else {
80                ListItem::String(value)
81            }
82        } else {
83            ListItem::String(value)
84        };
85
86        if let Value::List(ref mut list) = self.current {
87            list.push(item);
88        }
89    }
90
91    fn backfill(&self, value: &str, results: &mut [Vec<Value>]) {
92        // Walk backwards through results, filling empty entries
93        for record in results.iter_mut().rev() {
94            if self.index < record.len() {
95                if record[self.index].is_empty() {
96                    record[self.index] = Value::Single(value.to_string());
97                } else {
98                    // Stop when we hit a non-empty value
99                    break;
100                }
101            }
102        }
103    }
104
105    /// Assign None (for unmatched optional capture groups).
106    ///
107    /// Matches Python's behavior: when a named group exists in a rule's regex
108    /// but the group didn't participate in the match (optional group), Python's
109    /// `groupdict()` yields `None` for that key, and `AssignVar(None)` is called,
110    /// which clears the current value and updates the Filldown cache.
111    ///
112    /// Without this, stale Filldown values persist across records when an optional
113    /// group stops matching.
114    pub fn assign_none(&mut self) {
115        if self.def.has_option(ValueOption::List) {
116            // Python doesn't append None to lists for unmatched optionals
117            return;
118        }
119        self.current = Value::Empty;
120        if self.def.has_option(ValueOption::Filldown) {
121            self.filldown_cache = Some(Value::Empty);
122        }
123    }
124
125    /// Clear value (respects Filldown).
126    pub fn clear(&mut self) {
127        if self.def.has_option(ValueOption::Filldown) {
128            // Restore from cache
129            if let Some(ref cached) = self.filldown_cache {
130                self.current = cached.clone();
131                return;
132            }
133        }
134
135        // For List values without Filldown, clear the list
136        if self.def.has_option(ValueOption::List) && !self.def.has_option(ValueOption::Filldown) {
137            self.current = Value::List(Vec::new());
138        } else if !self.def.has_option(ValueOption::Filldown) {
139            self.current = Value::Empty;
140        }
141    }
142
143    /// Clear all (including Filldown cache).
144    pub fn clear_all(&mut self) {
145        self.filldown_cache = None;
146        self.current = if self.def.has_option(ValueOption::List) {
147            Value::List(Vec::new())
148        } else {
149            Value::Empty
150        };
151    }
152
153    /// Check if Required constraint is satisfied.
154    pub fn satisfies_required(&self) -> bool {
155        if !self.def.has_option(ValueOption::Required) {
156            return true;
157        }
158        !self.current.is_empty()
159    }
160
161    /// Get current value for recording.
162    pub fn take_for_record(&mut self) -> Value {
163        if self.def.has_option(ValueOption::List) {
164            // For List, return a copy and keep the original
165            // (OnSaveRecord in Python copies the list)
166            self.current.clone()
167        } else {
168            std::mem::take(&mut self.current)
169        }
170    }
171
172    /// Get current value (for inspection).
173    pub fn current(&self) -> &Value {
174        &self.current
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    fn make_def(name: &str, options: &[ValueOption]) -> ValueDef {
182        ValueDef {
183            name: name.to_string(),
184            pattern: "(\\S+)".to_string(),
185            options: options.iter().cloned().collect(),
186            template_pattern: format!("(?P<{}>\\S+)", name),
187            compiled_regex: None,
188        }
189    }
190
191    #[test]
192    fn test_simple_assign() {
193        let def = make_def("Test", &[]);
194        let mut state = ValueState::new(def, 0);
195        let mut results = Vec::new();
196
197        state.assign("hello".to_string(), &mut results);
198        assert_eq!(state.current(), &Value::Single("hello".into()));
199    }
200
201    #[test]
202    fn test_filldown() {
203        let def = make_def("Test", &[ValueOption::Filldown]);
204        let mut state = ValueState::new(def, 0);
205        let mut results = Vec::new();
206
207        state.assign("cached".to_string(), &mut results);
208        assert_eq!(state.current(), &Value::Single("cached".into()));
209
210        state.clear();
211        // Should restore from cache
212        assert_eq!(state.current(), &Value::Single("cached".into()));
213
214        state.clear_all();
215        // Now should be empty
216        assert_eq!(state.current(), &Value::Empty);
217    }
218
219    #[test]
220    fn test_list() {
221        let def = make_def("Items", &[ValueOption::List]);
222        let mut state = ValueState::new(def, 0);
223        let mut results = Vec::new();
224
225        state.assign("one".to_string(), &mut results);
226        state.assign("two".to_string(), &mut results);
227        state.assign("three".to_string(), &mut results);
228
229        match state.current() {
230            Value::List(items) => {
231                assert_eq!(items.len(), 3);
232            }
233            _ => panic!("Expected List value"),
234        }
235    }
236
237    #[test]
238    fn test_required() {
239        let def = make_def("Required", &[ValueOption::Required]);
240        let mut state = ValueState::new(def, 0);
241
242        assert!(!state.satisfies_required()); // Empty, should fail
243
244        let mut results = Vec::new();
245        state.assign("value".to_string(), &mut results);
246        assert!(state.satisfies_required()); // Has value, should pass
247    }
248
249    #[test]
250    fn test_assign_none_clears_value() {
251        let def = make_def("Test", &[]);
252        let mut state = ValueState::new(def, 0);
253        let mut results = Vec::new();
254
255        state.assign("hello".to_string(), &mut results);
256        assert_eq!(state.current(), &Value::Single("hello".into()));
257
258        state.assign_none();
259        assert_eq!(state.current(), &Value::Empty);
260    }
261
262    #[test]
263    fn test_assign_none_clears_filldown_cache() {
264        let def = make_def("Test", &[ValueOption::Filldown]);
265        let mut state = ValueState::new(def, 0);
266        let mut results = Vec::new();
267
268        // Assign a value — fills the Filldown cache
269        state.assign("cached".to_string(), &mut results);
270        assert_eq!(state.current(), &Value::Single("cached".into()));
271
272        // Clear should restore from cache
273        state.clear();
274        assert_eq!(state.current(), &Value::Single("cached".into()));
275
276        // assign_none should clear the cache too
277        state.assign_none();
278        assert_eq!(state.current(), &Value::Empty);
279
280        // Now clear should NOT restore (cache was cleared)
281        state.clear();
282        assert_eq!(state.current(), &Value::Empty);
283    }
284
285    #[test]
286    fn test_assign_none_skips_list() {
287        let def = make_def("Items", &[ValueOption::List]);
288        let mut state = ValueState::new(def, 0);
289        let mut results = Vec::new();
290
291        state.assign("one".to_string(), &mut results);
292        // assign_none should not affect List values
293        state.assign_none();
294        match state.current() {
295            Value::List(items) => assert_eq!(items.len(), 1),
296            _ => panic!("Expected List value"),
297        }
298    }
299}