textfsm-core 0.3.1

Core parsing library for TextFSM template-based state machine
Documentation
//! Template file parsing logic.

use std::collections::HashMap;
use std::io::BufRead;
use std::sync::LazyLock;

use super::{Rule, State, Template, ValueDef};
use crate::error::TemplateError;
use crate::types::{LineOp, Transition};

static COMMENT_RE: LazyLock<fancy_regex::Regex> =
    LazyLock::new(|| fancy_regex::Regex::new(r"^\s*#").unwrap());

/// Parse phase during template processing.
#[derive(Debug, Clone, Copy, PartialEq)]
enum ParsePhase {
    Values,
    States,
}

/// Parse a template from a reader.
pub fn parse_template<R: BufRead>(reader: R) -> Result<Template, TemplateError> {
    let mut values: Vec<ValueDef> = Vec::new();
    let mut value_templates: HashMap<String, String> = HashMap::new();
    let mut states: HashMap<String, State> = HashMap::new();
    let mut state_order: Vec<String> = Vec::new();

    let mut phase = ParsePhase::Values;
    let mut current_state: Option<String> = None;
    let mut line_num = 0;

    for line in reader.lines() {
        line_num += 1;
        let line = line.map_err(|e| TemplateError::InvalidValue {
            line: line_num,
            message: e.to_string(),
        })?;

        let end_trimmed = line.trim_end();

        // Skip comments
        if COMMENT_RE.is_match(end_trimmed).unwrap_or(false) {
            continue;
        }

        match phase {
            ParsePhase::Values => {
                if end_trimmed.is_empty() {
                    // First blank line ends the Value parsing section
                    phase = ParsePhase::States;
                    continue;
                }

                if end_trimmed.starts_with("Value ") {
                    let value = ValueDef::parse(end_trimmed, line_num)?;

                    // Check for duplicate names
                    if value_templates.contains_key(&value.name) {
                        return Err(TemplateError::DuplicateValue(value.name.clone()));
                    }

                    value_templates.insert(value.name.clone(), value.template_pattern.clone());
                    values.push(value);
                } else if values.is_empty() {
                    return Err(TemplateError::NoValues);
                } else {
                    return Err(TemplateError::ExpectedBlankLine(line_num));
                }
            }

            ParsePhase::States => {
                // For state parsing, we need both the original line (for indentation check)
                // and the fully trimmed version (for content check)
                let fully_trimmed = end_trimmed.trim();

                if fully_trimmed.is_empty() {
                    // Blank line ends current state
                    current_state = None;
                    continue;
                }

                // Check if this is a rule (starts with whitespace and ^)
                let is_rule = line.starts_with(' ') || line.starts_with('\t');

                if is_rule && fully_trimmed.starts_with('^') {
                    let state_name =
                        current_state
                            .as_ref()
                            .ok_or_else(|| TemplateError::InvalidRule {
                                line: line_num,
                                message: "rule found outside of state".into(),
                            })?;

                    let rule = Rule::parse(end_trimmed, line_num, &value_templates)?;

                    states
                        .get_mut(state_name)
                        .expect("current state must exist")
                        .rules
                        .push(rule);
                } else if !is_rule && !fully_trimmed.is_empty() {
                    // This is a state name
                    if !State::is_valid_name(fully_trimmed) {
                        return Err(TemplateError::InvalidStateName {
                            name: fully_trimmed.to_string(),
                            reason: "invalid characters or reserved word".into(),
                        });
                    }

                    if states.contains_key(fully_trimmed) {
                        return Err(TemplateError::DuplicateState(fully_trimmed.to_string()));
                    }

                    let state = State::new(fully_trimmed.to_string());
                    states.insert(fully_trimmed.to_string(), state);
                    state_order.push(fully_trimmed.to_string());
                    current_state = Some(fully_trimmed.to_string());
                }
            }
        }
    }

    // Validate we have values
    if values.is_empty() {
        return Err(TemplateError::NoValues);
    }

    // Validate we have a Start state
    if !states.contains_key("Start") {
        return Err(TemplateError::MissingStartState);
    }

    // Validate End/EOF states are empty
    if let Some(state) = states.get("End")
        && !state.rules.is_empty()
    {
        return Err(TemplateError::NonEmptyEndState);
    }

    if let Some(state) = states.get("EOF")
        && !state.rules.is_empty()
    {
        return Err(TemplateError::NonEmptyEofState);
    }

    // Remove End state if present (it's just a marker)
    states.remove("End");
    state_order.retain(|s| s != "End");

    // Validate all state transitions point to valid states
    // Skip validation for Error rules (the "state" is actually an error message)
    for state in states.values() {
        for rule in &state.rules {
            // Error actions store their message in transition, not a real state
            if rule.line_op == LineOp::Error {
                continue;
            }

            if let Transition::State(ref target) = rule.transition
                && target != "End"
                && target != "EOF"
                && !states.contains_key(target)
            {
                return Err(TemplateError::UndefinedState(target.clone()));
            }
        }
    }

    // Build value index
    let value_index: HashMap<String, usize> = values
        .iter()
        .enumerate()
        .map(|(i, v)| (v.name.clone(), i))
        .collect();

    Ok(Template {
        values,
        value_index,
        value_templates,
        states,
        state_order,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_simple_template() {
        let template_str = r#"Value Interface (\S+)
Value Status (up|down)

Start
  ^Interface: ${Interface}
  ^Status: ${Status} -> Record
"#;

        let template = parse_template(template_str.as_bytes()).unwrap();
        assert_eq!(template.values.len(), 2);
        assert_eq!(template.values[0].name, "Interface");
        assert_eq!(template.values[1].name, "Status");
        assert!(template.states.contains_key("Start"));
        assert_eq!(template.states["Start"].rules.len(), 2);
    }

    #[test]
    fn test_missing_start_state() {
        let template_str = r#"Value Interface (\S+)

NotStart
  ^Interface: ${Interface}
"#;

        let result = parse_template(template_str.as_bytes());
        assert!(matches!(result, Err(TemplateError::MissingStartState)));
    }

    #[test]
    fn test_no_values() {
        let template_str = r#"Start
  ^Something
"#;

        let result = parse_template(template_str.as_bytes());
        assert!(matches!(result, Err(TemplateError::NoValues)));
    }

    #[test]
    fn test_undefined_state() {
        let template_str = r#"Value Test (\S+)

Start
  ^Test -> NonExistent
"#;

        let result = parse_template(template_str.as_bytes());
        assert!(matches!(result, Err(TemplateError::UndefinedState(_))));
    }
}