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<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<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    /// Clear value (respects Filldown).
106    pub fn clear(&mut self) {
107        if self.def.has_option(ValueOption::Filldown) {
108            // Restore from cache
109            if let Some(ref cached) = self.filldown_cache {
110                self.current = cached.clone();
111                return;
112            }
113        }
114
115        // For List values without Filldown, clear the list
116        if self.def.has_option(ValueOption::List) && !self.def.has_option(ValueOption::Filldown) {
117            self.current = Value::List(Vec::new());
118        } else if !self.def.has_option(ValueOption::Filldown) {
119            self.current = Value::Empty;
120        }
121    }
122
123    /// Clear all (including Filldown cache).
124    pub fn clear_all(&mut self) {
125        self.filldown_cache = None;
126        self.current = if self.def.has_option(ValueOption::List) {
127            Value::List(Vec::new())
128        } else {
129            Value::Empty
130        };
131    }
132
133    /// Check if Required constraint is satisfied.
134    pub fn satisfies_required(&self) -> bool {
135        if !self.def.has_option(ValueOption::Required) {
136            return true;
137        }
138        !self.current.is_empty()
139    }
140
141    /// Get current value for recording.
142    pub fn take_for_record(&mut self) -> Value {
143        if self.def.has_option(ValueOption::List) {
144            // For List, return a copy and keep the original
145            // (OnSaveRecord in Python copies the list)
146            self.current.clone()
147        } else {
148            std::mem::take(&mut self.current)
149        }
150    }
151
152    /// Get current value (for inspection).
153    pub fn current(&self) -> &Value {
154        &self.current
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    fn make_def(name: &str, options: &[ValueOption]) -> ValueDef {
162        ValueDef {
163            name: name.to_string(),
164            pattern: "(\\S+)".to_string(),
165            options: options.iter().cloned().collect(),
166            template_pattern: format!("(?P<{}>\\S+)", name),
167            compiled_regex: None,
168        }
169    }
170
171    #[test]
172    fn test_simple_assign() {
173        let def = make_def("Test", &[]);
174        let mut state = ValueState::new(def, 0);
175        let mut results = Vec::new();
176
177        state.assign("hello".to_string(), &mut results);
178        assert_eq!(state.current(), &Value::Single("hello".into()));
179    }
180
181    #[test]
182    fn test_filldown() {
183        let def = make_def("Test", &[ValueOption::Filldown]);
184        let mut state = ValueState::new(def, 0);
185        let mut results = Vec::new();
186
187        state.assign("cached".to_string(), &mut results);
188        assert_eq!(state.current(), &Value::Single("cached".into()));
189
190        state.clear();
191        // Should restore from cache
192        assert_eq!(state.current(), &Value::Single("cached".into()));
193
194        state.clear_all();
195        // Now should be empty
196        assert_eq!(state.current(), &Value::Empty);
197    }
198
199    #[test]
200    fn test_list() {
201        let def = make_def("Items", &[ValueOption::List]);
202        let mut state = ValueState::new(def, 0);
203        let mut results = Vec::new();
204
205        state.assign("one".to_string(), &mut results);
206        state.assign("two".to_string(), &mut results);
207        state.assign("three".to_string(), &mut results);
208
209        match state.current() {
210            Value::List(items) => {
211                assert_eq!(items.len(), 3);
212            }
213            _ => panic!("Expected List value"),
214        }
215    }
216
217    #[test]
218    fn test_required() {
219        let def = make_def("Required", &[ValueOption::Required]);
220        let mut state = ValueState::new(def, 0);
221
222        assert!(!state.satisfies_required()); // Empty, should fail
223
224        let mut results = Vec::new();
225        state.assign("value".to_string(), &mut results);
226        assert!(state.satisfies_required()); // Has value, should pass
227    }
228}