textfsm-core 0.3.1

Core parsing library for TextFSM template-based state machine
Documentation
//! Runtime state for values during parsing.

use std::collections::HashMap;


use crate::template::ValueDef;
use crate::types::{ListItem, Value, ValueOption};

/// Runtime state for a single value.
#[derive(Debug, Clone)]
pub struct ValueState {
    /// Definition from template.
    pub def: ValueDef,

    /// Index of this value in the record.
    pub index: usize,

    /// Current value.
    current: Value,

    /// Cached value for Filldown option.
    filldown_cache: Option<Value>,
}

impl ValueState {
    /// Create a new value state.
    pub fn new(def: ValueDef, index: usize) -> Self {
        let initial = if def.has_option(ValueOption::List) {
            Value::List(Vec::new())
        } else {
            Value::Empty
        };

        Self {
            def,
            index,
            current: initial,
            filldown_cache: None,
        }
    }

    /// Assign a matched value.
    pub fn assign(&mut self, value: String, all_results: &mut [Vec<Value>]) {
        if self.def.has_option(ValueOption::List) {
            self.assign_list(value.clone());
        } else {
            self.current = Value::Single(value.clone());
        }

        // Filldown: cache the value
        if self.def.has_option(ValueOption::Filldown) {
            self.filldown_cache = Some(self.current.clone());
        }

        // Fillup: backfill previous records
        if self.def.has_option(ValueOption::Fillup) && !value.is_empty() {
            self.backfill(&value, all_results);
        }
    }

    fn assign_list(&mut self, value: String) {
        let item = if let Some(ref regex) = self.def.compiled_regex {
            // Check for nested capture groups
            if let Ok(Some(caps)) = regex.captures(&value) {
                let dict: HashMap<String, String> = regex
                    .capture_names()
                    .flatten()
                    .filter_map(|name| {
                        caps.name(name)
                            .map(|m| (name.to_string(), m.as_str().to_string()))
                    })
                    .collect();

                if !dict.is_empty() {
                    ListItem::Dict(dict)
                } else {
                    ListItem::String(value)
                }
            } else {
                ListItem::String(value)
            }
        } else {
            ListItem::String(value)
        };

        if let Value::List(ref mut list) = self.current {
            list.push(item);
        }
    }

    fn backfill(&self, value: &str, results: &mut [Vec<Value>]) {
        // Walk backwards through results, filling empty entries
        for record in results.iter_mut().rev() {
            if self.index < record.len() {
                if record[self.index].is_empty() {
                    record[self.index] = Value::Single(value.to_string());
                } else {
                    // Stop when we hit a non-empty value
                    break;
                }
            }
        }
    }

    /// Assign None (for unmatched optional capture groups).
    ///
    /// Matches Python's behavior: when a named group exists in a rule's regex
    /// but the group didn't participate in the match (optional group), Python's
    /// `groupdict()` yields `None` for that key, and `AssignVar(None)` is called,
    /// which clears the current value and updates the Filldown cache.
    ///
    /// Without this, stale Filldown values persist across records when an optional
    /// group stops matching.
    pub fn assign_none(&mut self) {
        if self.def.has_option(ValueOption::List) {
            // Python doesn't append None to lists for unmatched optionals
            return;
        }
        self.current = Value::Empty;
        if self.def.has_option(ValueOption::Filldown) {
            self.filldown_cache = Some(Value::Empty);
        }
    }

    /// Clear value (respects Filldown).
    pub fn clear(&mut self) {
        if self.def.has_option(ValueOption::Filldown) {
            // Restore from cache
            if let Some(ref cached) = self.filldown_cache {
                self.current = cached.clone();
                return;
            }
        }

        // For List values without Filldown, clear the list
        if self.def.has_option(ValueOption::List) && !self.def.has_option(ValueOption::Filldown) {
            self.current = Value::List(Vec::new());
        } else if !self.def.has_option(ValueOption::Filldown) {
            self.current = Value::Empty;
        }
    }

    /// Clear all (including Filldown cache).
    pub fn clear_all(&mut self) {
        self.filldown_cache = None;
        self.current = if self.def.has_option(ValueOption::List) {
            Value::List(Vec::new())
        } else {
            Value::Empty
        };
    }

    /// Check if Required constraint is satisfied.
    pub fn satisfies_required(&self) -> bool {
        if !self.def.has_option(ValueOption::Required) {
            return true;
        }
        !self.current.is_empty()
    }

    /// Get current value for recording.
    pub fn take_for_record(&mut self) -> Value {
        if self.def.has_option(ValueOption::List) {
            // For List, return a copy and keep the original
            // (OnSaveRecord in Python copies the list)
            self.current.clone()
        } else {
            std::mem::take(&mut self.current)
        }
    }

    /// Get current value (for inspection).
    pub fn current(&self) -> &Value {
        &self.current
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    fn make_def(name: &str, options: &[ValueOption]) -> ValueDef {
        ValueDef {
            name: name.to_string(),
            pattern: "(\\S+)".to_string(),
            options: options.iter().cloned().collect(),
            template_pattern: format!("(?P<{}>\\S+)", name),
            compiled_regex: None,
        }
    }

    #[test]
    fn test_simple_assign() {
        let def = make_def("Test", &[]);
        let mut state = ValueState::new(def, 0);
        let mut results = Vec::new();

        state.assign("hello".to_string(), &mut results);
        assert_eq!(state.current(), &Value::Single("hello".into()));
    }

    #[test]
    fn test_filldown() {
        let def = make_def("Test", &[ValueOption::Filldown]);
        let mut state = ValueState::new(def, 0);
        let mut results = Vec::new();

        state.assign("cached".to_string(), &mut results);
        assert_eq!(state.current(), &Value::Single("cached".into()));

        state.clear();
        // Should restore from cache
        assert_eq!(state.current(), &Value::Single("cached".into()));

        state.clear_all();
        // Now should be empty
        assert_eq!(state.current(), &Value::Empty);
    }

    #[test]
    fn test_list() {
        let def = make_def("Items", &[ValueOption::List]);
        let mut state = ValueState::new(def, 0);
        let mut results = Vec::new();

        state.assign("one".to_string(), &mut results);
        state.assign("two".to_string(), &mut results);
        state.assign("three".to_string(), &mut results);

        match state.current() {
            Value::List(items) => {
                assert_eq!(items.len(), 3);
            }
            _ => panic!("Expected List value"),
        }
    }

    #[test]
    fn test_required() {
        let def = make_def("Required", &[ValueOption::Required]);
        let mut state = ValueState::new(def, 0);

        assert!(!state.satisfies_required()); // Empty, should fail

        let mut results = Vec::new();
        state.assign("value".to_string(), &mut results);
        assert!(state.satisfies_required()); // Has value, should pass
    }

    #[test]
    fn test_assign_none_clears_value() {
        let def = make_def("Test", &[]);
        let mut state = ValueState::new(def, 0);
        let mut results = Vec::new();

        state.assign("hello".to_string(), &mut results);
        assert_eq!(state.current(), &Value::Single("hello".into()));

        state.assign_none();
        assert_eq!(state.current(), &Value::Empty);
    }

    #[test]
    fn test_assign_none_clears_filldown_cache() {
        let def = make_def("Test", &[ValueOption::Filldown]);
        let mut state = ValueState::new(def, 0);
        let mut results = Vec::new();

        // Assign a value — fills the Filldown cache
        state.assign("cached".to_string(), &mut results);
        assert_eq!(state.current(), &Value::Single("cached".into()));

        // Clear should restore from cache
        state.clear();
        assert_eq!(state.current(), &Value::Single("cached".into()));

        // assign_none should clear the cache too
        state.assign_none();
        assert_eq!(state.current(), &Value::Empty);

        // Now clear should NOT restore (cache was cleared)
        state.clear();
        assert_eq!(state.current(), &Value::Empty);
    }

    #[test]
    fn test_assign_none_skips_list() {
        let def = make_def("Items", &[ValueOption::List]);
        let mut state = ValueState::new(def, 0);
        let mut results = Vec::new();

        state.assign("one".to_string(), &mut results);
        // assign_none should not affect List values
        state.assign_none();
        match state.current() {
            Value::List(items) => assert_eq!(items.len(), 1),
            _ => panic!("Expected List value"),
        }
    }
}