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());
#[derive(Debug, Clone, Copy, PartialEq)]
enum ParsePhase {
Values,
States,
}
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();
if COMMENT_RE.is_match(end_trimmed).unwrap_or(false) {
continue;
}
match phase {
ParsePhase::Values => {
if end_trimmed.is_empty() {
phase = ParsePhase::States;
continue;
}
if end_trimmed.starts_with("Value ") {
let value = ValueDef::parse(end_trimmed, line_num)?;
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 => {
let fully_trimmed = end_trimmed.trim();
if fully_trimmed.is_empty() {
current_state = None;
continue;
}
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() {
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());
}
}
}
}
if values.is_empty() {
return Err(TemplateError::NoValues);
}
if !states.contains_key("Start") {
return Err(TemplateError::MissingStartState);
}
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);
}
states.remove("End");
state_order.retain(|s| s != "End");
for state in states.values() {
for rule in &state.rules {
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()));
}
}
}
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(_))));
}
}