1use fancy_regex::Regex;
4use std::collections::HashMap;
5
6use crate::error::TemplateError;
7use crate::types::{LineOp, RecordOp, Transition};
8
9#[derive(Debug, Clone)]
11pub struct Rule {
12 pub match_pattern: String,
14
15 pub regex_pattern: String,
17
18 pub(crate) regex: Regex,
20
21 pub line_op: LineOp,
23
24 pub record_op: RecordOp,
26
27 pub transition: Transition,
29
30 pub line_num: usize,
32}
33
34const RESERVED_LINE_OPS: &[&str] = &["Continue", "Next", "Error"];
36const RESERVED_RECORD_OPS: &[&str] = &["Clear", "Clearall", "Record", "NoRecord"];
37
38impl Rule {
39 pub fn parse(
41 line: &str,
42 line_num: usize,
43 value_templates: &HashMap<String, String>,
44 ) -> Result<Self, TemplateError> {
45 let trimmed = line.trim();
46
47 if !trimmed.starts_with('^') {
48 return Err(TemplateError::InvalidRule {
49 line: line_num,
50 message: "rule must start with '^'".into(),
51 });
52 }
53
54 let (match_pattern, action_str) = if let Some(idx) = trimmed.find(" -> ") {
56 (&trimmed[..idx], Some(&trimmed[idx + 4..]))
57 } else {
58 (trimmed, None)
59 };
60
61 let regex_pattern = Self::substitute_variables(match_pattern, value_templates, line_num)?;
63
64 let regex = Regex::new(®ex_pattern).map_err(|e| TemplateError::InvalidRegex {
66 pattern: regex_pattern.clone(),
67 message: e.to_string(),
68 })?;
69
70 let (line_op, record_op, transition) = if let Some(action) = action_str {
72 Self::parse_action(action.trim(), line_num)?
73 } else {
74 (LineOp::default(), RecordOp::default(), Transition::default())
75 };
76
77 if line_op == LineOp::Continue && !matches!(transition, Transition::Stay) {
79 return Err(TemplateError::ContinueWithTransition(line_num));
80 }
81
82 Ok(Self {
83 match_pattern: match_pattern.to_string(),
84 regex_pattern,
85 regex,
86 line_op,
87 record_op,
88 transition,
89 line_num,
90 })
91 }
92
93 fn substitute_variables(
94 pattern: &str,
95 templates: &HashMap<String, String>,
96 line_num: usize,
97 ) -> Result<String, TemplateError> {
98 let mut result = pattern.to_string();
99
100 loop {
103 let start = match result.find("${") {
104 Some(i) => i,
105 None => break,
106 };
107
108 let end = match result[start..].find('}') {
109 Some(i) => start + i,
110 None => {
111 return Err(TemplateError::InvalidRule {
112 line: line_num,
113 message: "unclosed variable substitution".into(),
114 })
115 }
116 };
117
118 let var_name = &result[start + 2..end];
119
120 let template = templates.get(var_name).ok_or_else(|| {
121 TemplateError::InvalidSubstitution {
122 line: line_num,
123 message: format!("unknown variable '{}'", var_name),
124 }
125 })?;
126
127 result = format!("{}{}{}", &result[..start], template, &result[end + 1..]);
128 }
129
130 Ok(result)
131 }
132
133 fn parse_action(
134 action: &str,
135 line_num: usize,
136 ) -> Result<(LineOp, RecordOp, Transition), TemplateError> {
137 if action.is_empty() {
138 return Ok((LineOp::default(), RecordOp::default(), Transition::default()));
139 }
140
141 let parts: Vec<&str> = action.split_whitespace().collect();
142
143 let mut line_op = LineOp::default();
144 let mut record_op = RecordOp::default();
145 let mut transition = Transition::default();
146 let mut idx = 0;
147
148 if idx < parts.len() {
149 let first = parts[idx];
150
151 if let Some((line_part, record_part)) = first.split_once('.') {
153 line_op = Self::parse_line_op(line_part, line_num)?;
154 record_op = Self::parse_record_op(record_part, line_num)?;
155 idx += 1;
156 } else if let Some(op) = Self::try_parse_line_op(first) {
157 line_op = op;
158 idx += 1;
159
160 if line_op == LineOp::Error && idx < parts.len() {
163 let rest = parts[idx..].join(" ");
164 if rest.starts_with('"') {
165 if let Some(end_quote) = rest[1..].find('"') {
167 let message = rest[1..end_quote + 1].to_string();
168 transition = Transition::State(message);
169 } else {
170 transition = Transition::State(rest.trim_matches('"').to_string());
172 }
173 return Ok((line_op, record_op, transition));
174 }
175 }
176 } else if let Some(op) = Self::try_parse_record_op(first) {
177 record_op = op;
178 idx += 1;
179 } else {
180 transition = Self::parse_transition(first);
182 idx += 1;
183 }
184 }
185
186 if idx < parts.len() && matches!(transition, Transition::Stay) {
188 transition = Self::parse_transition(parts[idx]);
189 }
190
191 Ok((line_op, record_op, transition))
192 }
193
194 fn try_parse_line_op(s: &str) -> Option<LineOp> {
195 match s {
196 "Next" => Some(LineOp::Next),
197 "Continue" => Some(LineOp::Continue),
198 "Error" => Some(LineOp::Error),
199 _ => None,
200 }
201 }
202
203 fn parse_line_op(s: &str, line_num: usize) -> Result<LineOp, TemplateError> {
204 Self::try_parse_line_op(s).ok_or_else(|| TemplateError::InvalidRule {
205 line: line_num,
206 message: format!("invalid line operator '{}'", s),
207 })
208 }
209
210 fn try_parse_record_op(s: &str) -> Option<RecordOp> {
211 match s {
212 "NoRecord" => Some(RecordOp::NoRecord),
213 "Record" => Some(RecordOp::Record),
214 "Clear" => Some(RecordOp::Clear),
215 "Clearall" => Some(RecordOp::ClearAll),
216 _ => None,
217 }
218 }
219
220 fn parse_record_op(s: &str, line_num: usize) -> Result<RecordOp, TemplateError> {
221 Self::try_parse_record_op(s).ok_or_else(|| TemplateError::InvalidRule {
222 line: line_num,
223 message: format!("invalid record operator '{}'", s),
224 })
225 }
226
227 fn parse_transition(s: &str) -> Transition {
228 match s {
229 "End" => Transition::End,
230 "EOF" => Transition::Eof,
231 _ => {
232 if s.starts_with('"') && s.ends_with('"') {
234 Transition::State(s[1..s.len() - 1].to_string())
235 } else {
236 Transition::State(s.to_string())
237 }
238 }
239 }
240 }
241}
242
243#[derive(Debug, Clone)]
245pub struct State {
246 pub name: String,
248
249 pub rules: Vec<Rule>,
251}
252
253impl State {
254 pub fn new(name: String) -> Self {
256 Self {
257 name,
258 rules: Vec::new(),
259 }
260 }
261
262 pub fn is_valid_name(name: &str) -> bool {
264 if name.is_empty() || name.len() > 48 {
265 return false;
266 }
267
268 if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
270 return false;
271 }
272
273 if RESERVED_LINE_OPS.contains(&name) || RESERVED_RECORD_OPS.contains(&name) {
275 return false;
276 }
277
278 true
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 fn empty_templates() -> HashMap<String, String> {
287 HashMap::new()
288 }
289
290 fn sample_templates() -> HashMap<String, String> {
291 let mut m = HashMap::new();
292 m.insert("Interface".into(), "(?P<Interface>\\S+)".into());
293 m.insert("Status".into(), "(?P<Status>up|down)".into());
294 m
295 }
296
297 #[test]
298 fn test_parse_simple_rule() {
299 let r = Rule::parse("^Interface: (\\S+)", 1, &empty_templates()).unwrap();
300 assert_eq!(r.match_pattern, "^Interface: (\\S+)");
301 assert_eq!(r.line_op, LineOp::Next);
302 assert_eq!(r.record_op, RecordOp::NoRecord);
303 assert!(matches!(r.transition, Transition::Stay));
304 }
305
306 #[test]
307 fn test_parse_rule_with_record() {
308 let r = Rule::parse("^End -> Record", 1, &empty_templates()).unwrap();
309 assert_eq!(r.line_op, LineOp::Next);
310 assert_eq!(r.record_op, RecordOp::Record);
311 }
312
313 #[test]
314 fn test_parse_rule_with_compound_action() {
315 let r = Rule::parse("^Line -> Next.Record", 1, &empty_templates()).unwrap();
316 assert_eq!(r.line_op, LineOp::Next);
317 assert_eq!(r.record_op, RecordOp::Record);
318 }
319
320 #[test]
321 fn test_parse_rule_with_state_transition() {
322 let r = Rule::parse("^Start -> Continue.Record NextState", 1, &empty_templates());
323 assert!(matches!(r, Err(TemplateError::ContinueWithTransition(_))));
325 }
326
327 #[test]
328 fn test_parse_rule_with_variable_substitution() {
329 let templates = sample_templates();
330 let r = Rule::parse("^Interface: ${Interface} is ${Status}", 1, &templates).unwrap();
331 assert!(r.regex_pattern.contains("(?P<Interface>"));
332 assert!(r.regex_pattern.contains("(?P<Status>"));
333 }
334
335 #[test]
336 fn test_state_valid_names() {
337 assert!(State::is_valid_name("Start"));
338 assert!(State::is_valid_name("State1"));
339 assert!(State::is_valid_name("my_state"));
340 assert!(!State::is_valid_name("Continue")); assert!(!State::is_valid_name("Record")); assert!(!State::is_valid_name("")); }
344}