Skip to main content

textfsm_core/parser/
mod.rs

1//! FSM parser for processing input text.
2
3mod state;
4
5pub use state::ValueState;
6
7use std::collections::HashMap;
8
9use crate::error::ParseError;
10use crate::template::Template;
11use crate::types::{LineOp, RecordOp, Transition, Value};
12
13/// Parser for processing text with a compiled template.
14pub struct Parser<'t> {
15    /// Reference to the compiled template.
16    template: &'t Template,
17
18    /// Current state name.
19    current_state: String,
20
21    /// Runtime state for each value.
22    value_states: Vec<ValueState>,
23
24    /// Accumulated results.
25    results: Vec<Vec<Value>>,
26}
27
28impl<'t> Parser<'t> {
29    /// Create a new parser for the given template.
30    pub fn new(template: &'t Template) -> Self {
31        let value_states = template
32            .values()
33            .iter()
34            .enumerate()
35            .map(|(idx, def)| ValueState::new(def.clone(), idx))
36            .collect();
37
38        Self {
39            template,
40            current_state: "Start".to_string(),
41            value_states,
42            results: Vec::new(),
43        }
44    }
45
46    /// Reset the parser state for reuse.
47    pub fn reset(&mut self) {
48        self.current_state = "Start".to_string();
49        self.results.clear();
50        for vs in &mut self.value_states {
51            vs.clear_all();
52        }
53    }
54
55    /// Parse text and return list of records.
56    pub fn parse_text(&mut self, text: &str) -> Result<Vec<Vec<Value>>, ParseError> {
57        self.parse_text_with_eof(text, true)
58    }
59
60    /// Parse text with explicit EOF control.
61    pub fn parse_text_with_eof(
62        &mut self,
63        text: &str,
64        eof: bool,
65    ) -> Result<Vec<Vec<Value>>, ParseError> {
66        for line in text.lines() {
67            self.process_line(line)?;
68
69            if self.current_state == "End" || self.current_state == "EOF" {
70                break;
71            }
72        }
73
74        // Implicit EOF behavior
75        if self.current_state != "End"
76            && self.template.get_state("EOF").is_none()
77            && eof
78        {
79            self.append_record();
80        }
81
82        Ok(std::mem::take(&mut self.results))
83    }
84
85    /// Parse text and return results as list of dicts.
86    pub fn parse_text_to_dicts(
87        &mut self,
88        text: &str,
89    ) -> Result<Vec<HashMap<String, String>>, ParseError> {
90        let results = self.parse_text(text)?;
91        let header = self.template.header();
92
93        Ok(results
94            .into_iter()
95            .map(|row| {
96                header
97                    .iter()
98                    .zip(row)
99                    .map(|(k, v)| (k.to_lowercase(), v.as_string()))
100                    .collect()
101            })
102            .collect())
103    }
104
105    /// Parse text and deserialize results into typed structs.
106    ///
107    /// This method parses the input text and deserializes each record directly
108    /// into the specified type `T` using serde.
109    ///
110    /// # Example
111    ///
112    /// ```rust,ignore
113    /// use serde::Deserialize;
114    ///
115    /// #[derive(Deserialize)]
116    /// struct Interface {
117    ///     interface: String,
118    ///     status: String,
119    /// }
120    ///
121    /// let interfaces: Vec<Interface> = parser.parse_text_into(input)?;
122    /// ```
123    #[cfg(feature = "serde")]
124    pub fn parse_text_into<T>(&mut self, text: &str) -> Result<Vec<T>, ParseError>
125    where
126        T: serde::de::DeserializeOwned,
127    {
128        let results = self.parse_text(text)?;
129
130        // Pre-compute lowercased headers once, not per-record
131        let header: Vec<String> = self
132            .template
133            .header()
134            .iter()
135            .map(|s| s.to_lowercase())
136            .collect();
137
138        results
139            .into_iter()
140            .map(|record| {
141                // Use optimized path that borrows pre-lowercased headers
142                crate::de::from_record_borrowed(&header, record)
143                    .map_err(|e| ParseError::DeserializeError(e.to_string()))
144            })
145            .collect()
146    }
147
148    /// Process a single input line.
149    fn process_line(&mut self, line: &str) -> Result<(), ParseError> {
150        let state = match self.template.get_state(&self.current_state) {
151            Some(s) => s,
152            None => return Ok(()), // End/EOF state
153        };
154
155        for rule in &state.rules {
156            if let Ok(Some(captures)) = rule.regex.captures(line) {
157                // Extract matched values
158                for vs in &mut self.value_states {
159                    if let Some(matched) = captures.name(&vs.def.name) {
160                        vs.assign(matched.as_str().to_string(), &mut self.results);
161                    }
162                }
163
164                // Apply record operation
165                match rule.record_op {
166                    RecordOp::Record => self.append_record(),
167                    RecordOp::Clear => self.clear_values(),
168                    RecordOp::ClearAll => self.clear_all_values(),
169                    RecordOp::NoRecord => {}
170                }
171
172                // Apply line operation
173                match rule.line_op {
174                    LineOp::Error => {
175                        let message = match &rule.transition {
176                            Transition::State(msg) => msg.clone(),
177                            _ => "state error".into(),
178                        };
179                        return Err(ParseError::RuleError {
180                            rule_line: rule.line_num,
181                            message,
182                        });
183                    }
184                    LineOp::Continue => {
185                        // Don't break, continue checking rules
186                        continue;
187                    }
188                    LineOp::Next => {
189                        // Apply state transition and break
190                        self.apply_transition(&rule.transition);
191                        break;
192                    }
193                }
194            }
195        }
196
197        Ok(())
198    }
199
200    /// Append current record to results.
201    fn append_record(&mut self) {
202        // Check Required constraints
203        for vs in &self.value_states {
204            if !vs.satisfies_required() {
205                // Skip record
206                self.clear_values();
207                return;
208            }
209        }
210
211        // Build record
212        let record: Vec<Value> = self
213            .value_states
214            .iter_mut()
215            .map(|vs| vs.take_for_record())
216            .collect();
217
218        // Don't record if all empty
219        if record.iter().all(|v| v.is_empty()) {
220            return;
221        }
222
223        self.results.push(record);
224        self.clear_values();
225    }
226
227    /// Clear non-Filldown values.
228    fn clear_values(&mut self) {
229        for vs in &mut self.value_states {
230            vs.clear();
231        }
232    }
233
234    /// Clear all values including Filldown.
235    fn clear_all_values(&mut self) {
236        for vs in &mut self.value_states {
237            vs.clear_all();
238        }
239    }
240
241    /// Apply a state transition.
242    fn apply_transition(&mut self, transition: &Transition) {
243        match transition {
244            Transition::Stay => {}
245            Transition::State(name) => {
246                self.current_state = name.clone();
247            }
248            Transition::End => {
249                self.current_state = "End".to_string();
250            }
251            Transition::Eof => {
252                self.current_state = "EOF".to_string();
253            }
254        }
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use crate::template::Template;
262
263    #[test]
264    fn test_simple_parse() {
265        let template_str = r#"
266Value Interface (\S+)
267Value Status (up|down)
268
269Start
270  ^Interface: ${Interface} is ${Status} -> Record
271"#;
272
273        let template = Template::parse_str(template_str).unwrap();
274        let mut parser = template.parser();
275
276        let input = "Interface: eth0 is up\nInterface: eth1 is down\n";
277        let results = parser.parse_text(input).unwrap();
278
279        assert_eq!(results.len(), 2);
280        assert_eq!(results[0][0], Value::Single("eth0".into()));
281        assert_eq!(results[0][1], Value::Single("up".into()));
282        assert_eq!(results[1][0], Value::Single("eth1".into()));
283        assert_eq!(results[1][1], Value::Single("down".into()));
284    }
285
286    #[test]
287    fn test_parse_to_dicts() {
288        let template_str = r#"
289Value Name (\S+)
290Value Age (\d+)
291
292Start
293  ^Name: ${Name}, Age: ${Age} -> Record
294"#;
295
296        let template = Template::parse_str(template_str).unwrap();
297        let mut parser = template.parser();
298
299        let input = "Name: Alice, Age: 30\nName: Bob, Age: 25\n";
300        let results = parser.parse_text_to_dicts(input).unwrap();
301
302        assert_eq!(results.len(), 2);
303        assert_eq!(results[0].get("name"), Some(&"Alice".to_string()));
304        assert_eq!(results[0].get("age"), Some(&"30".to_string()));
305    }
306
307    #[test]
308    fn test_required_skips_empty() {
309        let template_str = r#"
310Value Required Name (\S+)
311Value Optional (\S+)
312
313Start
314  ^Name: ${Name}
315  ^Optional: ${Optional}
316  ^--- -> Record
317"#;
318
319        let template = Template::parse_str(template_str).unwrap();
320        let mut parser = template.parser();
321
322        // Record with no Name should be skipped
323        let input = "Optional: foo\n---\nName: bar\n---\n";
324        let results = parser.parse_text(input).unwrap();
325
326        assert_eq!(results.len(), 1);
327        assert_eq!(results[0][0], Value::Single("bar".into()));
328    }
329}