1use fancy_regex::Regex;
4use std::collections::HashMap;
5use std::sync::LazyLock;
6
7use crate::error::TemplateError;
8use crate::types::{LineOp, RecordOp, Transition};
9
10use super::value::normalize_pattern;
11
12static MATCH_ACTION: LazyLock<fancy_regex::Regex> =
16 LazyLock::new(|| fancy_regex::Regex::new(r"(?P<match>.*)(\s->(?P<action>.*))").unwrap());
17
18static ACTION_RE: LazyLock<fancy_regex::Regex> = LazyLock::new(|| {
20 fancy_regex::Regex::new(
21 r#"\s+(?P<ln_op>Continue|Next|Error)(\.(?P<rec_op>Clear|Clearall|Record|NoRecord))?(\s+(?P<new_state>\w+|".*"))?$"#
22 ).unwrap()
23});
24
25static ACTION2_RE: LazyLock<fancy_regex::Regex> = LazyLock::new(|| {
27 fancy_regex::Regex::new(
28 r#"\s+(?P<rec_op>Clear|Clearall|Record|NoRecord)(\s+(?P<new_state>\w+|".*"))?$"#,
29 )
30 .unwrap()
31});
32
33static ACTION3_RE: LazyLock<fancy_regex::Regex> =
35 LazyLock::new(|| fancy_regex::Regex::new(r#"(\s+(?P<new_state>\w+|".*"))?$"#).unwrap());
36
37#[derive(Debug, Clone)]
39pub struct Rule {
40 pub match_pattern: String,
42
43 pub regex_pattern: String,
45
46 pub(crate) regex: Regex,
48
49 pub line_op: LineOp,
51
52 pub record_op: RecordOp,
54
55 pub transition: Transition,
57
58 pub line_num: usize,
60}
61
62const RESERVED_LINE_OPS: &[&str] = &["Continue", "Next", "Error"];
64const RESERVED_RECORD_OPS: &[&str] = &["Clear", "Clearall", "Record", "NoRecord"];
65
66impl Rule {
67 pub fn parse(
69 line: &str,
70 line_num: usize,
71 value_templates: &HashMap<String, String>,
72 ) -> Result<Self, TemplateError> {
73 if !line.starts_with(" ^") && !line.starts_with(" ^") && !line.starts_with("\t^") {
75 return Err(TemplateError::InvalidValue {
76 line: line_num,
77 message: "Rule must be indented with 1 or spaces or a tab and start with '^'"
78 .into(),
79 });
80 }
81 let trimmed = line.trim();
82
83 let (match_pattern, action_str) = match MATCH_ACTION.captures(trimmed) {
84 Ok(Some(caps)) => {
85 let pattern = caps.name("match").unwrap().as_str();
88 let action = caps.name("action").unwrap().as_str();
89 (pattern, Some(action))
90 }
91 _ => (trimmed, None),
92 };
93
94 let regex_pattern = Self::substitute_variables(match_pattern, value_templates, line_num)?;
96
97 let regex_pattern = normalize_pattern(®ex_pattern);
108
109 let regex = Regex::new(®ex_pattern).map_err(|e| TemplateError::InvalidRegex {
110 pattern: regex_pattern.clone(),
111 message: e.to_string(),
112 })?;
113
114 let (line_op, record_op, transition) = if let Some(action) = action_str {
116 Self::parse_action(action, line_num)?
117 } else {
118 (
119 LineOp::default(),
120 RecordOp::default(),
121 Transition::default(),
122 )
123 };
124
125 if line_op == LineOp::Continue && !matches!(transition, Transition::Stay) {
127 return Err(TemplateError::ContinueWithTransition(line_num));
128 }
129
130 Ok(Self {
131 match_pattern: match_pattern.to_string(),
132 regex_pattern,
133 regex,
134 line_op,
135 record_op,
136 transition,
137 line_num,
138 })
139 }
140
141 fn substitute_variables(
145 pattern: &str,
146 templates: &HashMap<String, String>,
147 line_num: usize,
148 ) -> Result<String, TemplateError> {
149 let mut result = String::with_capacity(pattern.len());
150 let mut rest = pattern;
151
152 while let Some(start) = rest.find("${") {
153 result.push_str(&rest[..start]);
154
155 let after_dollar = &rest[start + 2..];
156 let end = after_dollar
157 .find('}')
158 .ok_or_else(|| TemplateError::InvalidRule {
159 line: line_num,
160 message: "unclosed variable substitution".into(),
161 })?;
162 let var_name = &after_dollar[..end];
163 let template =
164 templates
165 .get(var_name)
166 .ok_or_else(|| TemplateError::InvalidSubstitution {
167 line: line_num,
168 message: format!("unknown variable '{}'", var_name),
169 })?;
170
171 result.push_str(template);
172 rest = &after_dollar[end + 1..];
173 }
174
175 result.push_str(rest);
176 Ok(result)
177 }
178
179 fn parse_action(
180 action: &str,
181 line_num: usize,
182 ) -> Result<(LineOp, RecordOp, Transition), TemplateError> {
183 if action.is_empty() {
184 return Ok((
185 LineOp::default(),
186 RecordOp::default(),
187 Transition::default(),
188 ));
189 }
190
191 let caps = ACTION_RE
195 .captures(action)
196 .ok()
197 .flatten()
198 .or_else(|| ACTION2_RE.captures(action).ok().flatten())
199 .or_else(|| ACTION3_RE.captures(action).ok().flatten())
200 .ok_or_else(|| TemplateError::InvalidRule {
201 line: line_num,
202 message: format!("badly formatted action '{}'", action),
203 })?;
204
205 let line_op = match caps.name("ln_op").map(|m| m.as_str()) {
206 Some(s) => Self::parse_line_op(s, line_num)?,
207 None => LineOp::default(),
208 };
209
210 let record_op = match caps.name("rec_op").map(|m| m.as_str()) {
211 Some(s) => Self::parse_record_op(s, line_num)?,
212 None => RecordOp::default(),
213 };
214
215 let transition = match caps.name("new_state").map(|m| m.as_str()) {
216 Some(s) => Self::parse_transition(s),
217 None => Transition::default(),
218 };
219
220 Ok((line_op, record_op, transition))
221 }
222
223 fn try_parse_line_op(s: &str) -> Option<LineOp> {
224 match s {
225 "Next" => Some(LineOp::Next),
226 "Continue" => Some(LineOp::Continue),
227 "Error" => Some(LineOp::Error),
228 _ => None,
229 }
230 }
231
232 fn parse_line_op(s: &str, line_num: usize) -> Result<LineOp, TemplateError> {
233 Self::try_parse_line_op(s).ok_or_else(|| TemplateError::InvalidRule {
234 line: line_num,
235 message: format!("invalid line operator '{}'", s),
236 })
237 }
238
239 fn try_parse_record_op(s: &str) -> Option<RecordOp> {
240 match s {
241 "NoRecord" => Some(RecordOp::NoRecord),
242 "Record" => Some(RecordOp::Record),
243 "Clear" => Some(RecordOp::Clear),
244 "Clearall" => Some(RecordOp::ClearAll),
245 _ => None,
246 }
247 }
248
249 fn parse_record_op(s: &str, line_num: usize) -> Result<RecordOp, TemplateError> {
250 Self::try_parse_record_op(s).ok_or_else(|| TemplateError::InvalidRule {
251 line: line_num,
252 message: format!("invalid record operator '{}'", s),
253 })
254 }
255
256 fn parse_transition(s: &str) -> Transition {
257 match s {
258 "End" => Transition::End,
259 "EOF" => Transition::Eof,
260 _ => {
261 if s.starts_with('"') && s.ends_with('"') {
263 Transition::State(s[1..s.len() - 1].to_string())
264 } else {
265 Transition::State(s.to_string())
266 }
267 }
268 }
269 }
270}
271
272#[derive(Debug, Clone)]
274pub struct State {
275 pub name: String,
277
278 pub rules: Vec<Rule>,
280}
281
282impl State {
283 pub fn new(name: String) -> Self {
285 Self {
286 name,
287 rules: Vec::new(),
288 }
289 }
290
291 pub fn is_valid_name(name: &str) -> bool {
293 if name.is_empty() || name.len() > 48 {
294 return false;
295 }
296
297 if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
299 return false;
300 }
301
302 if RESERVED_LINE_OPS.contains(&name) || RESERVED_RECORD_OPS.contains(&name) {
304 return false;
305 }
306
307 true
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 fn empty_templates() -> HashMap<String, String> {
316 HashMap::new()
317 }
318
319 fn sample_templates() -> HashMap<String, String> {
320 let mut m = HashMap::new();
321 m.insert("Interface".into(), "(?P<Interface>\\S+)".into());
322 m.insert("Status".into(), "(?P<Status>up|down)".into());
323 m
324 }
325
326 #[test]
327 fn test_parse_simple_rule() {
328 let r = Rule::parse(" ^Interface: (\\S+)", 1, &empty_templates()).unwrap();
329 assert_eq!(r.match_pattern, "^Interface: (\\S+)");
330 assert_eq!(r.line_op, LineOp::Next);
331 assert_eq!(r.record_op, RecordOp::NoRecord);
332 assert!(matches!(r.transition, Transition::Stay));
333 }
334
335 #[test]
336 fn test_parse_rule_with_record() {
337 let r = Rule::parse(" ^End -> Record", 1, &empty_templates()).unwrap();
338 assert_eq!(r.line_op, LineOp::Next);
339 assert_eq!(r.record_op, RecordOp::Record);
340 }
341
342 #[test]
343 fn test_parse_rule_with_compound_action() {
344 let r = Rule::parse(" ^Line -> Next.Record", 1, &empty_templates()).unwrap();
345 assert_eq!(r.line_op, LineOp::Next);
346 assert_eq!(r.record_op, RecordOp::Record);
347 }
348
349 #[test]
350 fn test_parse_rule_with_state_transition() {
351 let r = Rule::parse(" ^Start -> Continue.Record NextState", 1, &empty_templates());
352 assert!(matches!(r, Err(TemplateError::ContinueWithTransition(_))));
354 }
355
356 #[test]
357 fn test_parse_rule_with_variable_substitution() {
358 let templates = sample_templates();
359 let r = Rule::parse(" ^Interface: ${Interface} is ${Status}", 1, &templates).unwrap();
360 assert!(r.regex_pattern.contains("(?P<Interface>"));
361 assert!(r.regex_pattern.contains("(?P<Status>"));
362 }
363
364 #[test]
365 fn test_state_valid_names() {
366 assert!(State::is_valid_name("Start"));
367 assert!(State::is_valid_name("State1"));
368 assert!(State::is_valid_name("my_state"));
369 assert!(!State::is_valid_name("Continue")); assert!(!State::is_valid_name("Record")); assert!(!State::is_valid_name("")); }
373}