use super::literal_search;
pub fn parse_rule_header(text: &str) -> Option<(String, usize)> {
let trimmed = text.trim_start();
if !trimmed.starts_with("rule") {
return None;
}
let skip = text.len() - trimmed.len(); let after_rule = trimmed[4..].trim_start();
if after_rule.starts_with('"') {
if let Some(end_quote) = memchr::memchr(b'"', &after_rule.as_bytes()[1..]) {
let name = after_rule[1..end_quote + 1].to_string();
let consumed = skip + 4 + (trimmed[4..].len() - after_rule.len()) + end_quote + 2;
return Some((name, consumed));
}
}
if let Some(ident) = literal_search::extract_identifier(after_rule) {
let consumed = skip + 4 + (trimmed[4..].len() - after_rule.len()) + ident.len();
return Some((ident, consumed));
}
None
}
pub fn split_into_rules(grl_text: &str) -> Vec<String> {
let mut rules = Vec::new();
let bytes = grl_text.as_bytes();
let mut i = 0;
while i < bytes.len() {
if let Some(rule_pos) = memchr::memmem::find(&bytes[i..], b"rule ") {
let abs_pos = i + rule_pos;
if let Some(brace_pos) = memchr::memchr(b'{', &bytes[abs_pos..]) {
let brace_abs = abs_pos + brace_pos;
if let Some(close_pos) = literal_search::find_matching_brace(grl_text, brace_abs) {
let rule_text = &grl_text[abs_pos..=close_pos];
rules.push(rule_text.to_string());
i = close_pos + 1;
continue;
}
}
}
i += 1;
}
rules
}
pub fn parse_when_then(body: &str) -> Option<(String, String)> {
let trimmed = body.trim();
let when_pos = literal_search::find_literal(trimmed, "when")?;
let after_when = trimmed[when_pos + 4..].trim_start();
let then_pos = find_then_keyword(after_when)?;
let condition = after_when[..then_pos].trim().to_string();
let action = after_when[then_pos + 4..].trim().to_string();
Some((condition, action))
}
fn find_then_keyword(text: &str) -> Option<usize> {
let bytes = text.as_bytes();
let mut in_string = false;
let mut escape_next = false;
let mut paren_depth = 0;
let mut brace_depth = 0;
let mut i = 0;
while i < bytes.len() {
if escape_next {
escape_next = false;
i += 1;
continue;
}
match bytes[i] {
b'\\' if in_string => escape_next = true,
b'"' => in_string = !in_string,
b'(' if !in_string => paren_depth += 1,
b')' if !in_string => paren_depth -= 1,
b'{' if !in_string => brace_depth += 1,
b'}' if !in_string => brace_depth -= 1,
b't' if !in_string && paren_depth == 0 && brace_depth == 0 => {
if i + 4 <= bytes.len() && &bytes[i..i + 4] == b"then" {
let before_ok = i == 0 || !bytes[i - 1].is_ascii_alphanumeric();
let after_ok = i + 4 >= bytes.len() || !bytes[i + 4].is_ascii_alphanumeric();
if before_ok && after_ok {
return Some(i);
}
}
}
_ => {}
}
i += 1;
}
None
}
pub fn extract_salience(attributes: &str) -> Option<i32> {
let salience_pos = literal_search::find_literal(attributes, "salience")?;
let after_salience = attributes[salience_pos + 8..].trim_start();
let digits: String = after_salience
.chars()
.take_while(|c| c.is_ascii_digit())
.collect();
digits.parse().ok()
}
pub fn parse_defmodule(text: &str) -> Option<(String, String, usize)> {
let trimmed = text.trim_start();
if !trimmed.starts_with("defmodule") {
return None;
}
let after_defmodule = trimmed[9..].trim_start();
let name_end = after_defmodule
.chars()
.position(|c| !c.is_alphanumeric() && c != '_')?;
let name = after_defmodule[..name_end].to_string();
if !name.chars().next()?.is_uppercase() {
return None;
}
let rest = after_defmodule[name_end..].trim_start();
if !rest.starts_with('{') {
return None;
}
let brace_pos = trimmed.len() - rest.len();
let close_pos = literal_search::find_matching_brace(trimmed, brace_pos)?;
let body = trimmed[brace_pos + 1..close_pos].to_string();
let consumed = close_pos + 1;
Some((name, body, consumed))
}
pub fn split_modules_and_rules(grl_text: &str) -> (Vec<String>, String) {
let mut modules = Vec::new();
let mut rules_text = String::new();
let bytes = grl_text.as_bytes();
let mut i = 0;
let mut last_copy = 0;
while i < bytes.len() {
if let Some(defmodule_pos) = memchr::memmem::find(&bytes[i..], b"defmodule ") {
let abs_pos = i + defmodule_pos;
if abs_pos > last_copy {
rules_text.push_str(&grl_text[last_copy..abs_pos]);
}
if let Some(brace_pos) = memchr::memchr(b'{', &bytes[abs_pos..]) {
let brace_abs = abs_pos + brace_pos;
if let Some(close_pos) = literal_search::find_matching_brace(grl_text, brace_abs) {
let module_text = &grl_text[abs_pos..=close_pos];
modules.push(module_text.to_string());
i = close_pos + 1;
last_copy = i;
continue;
}
}
}
i += 1;
}
if last_copy < grl_text.len() {
rules_text.push_str(&grl_text[last_copy..]);
}
(modules, rules_text)
}
pub fn parse_operator(text: &str) -> Option<(&str, usize)> {
let trimmed = text.trim_start();
if trimmed.len() >= 2 {
match &trimmed[..2] {
">=" => return Some((">=", 2)),
"<=" => return Some(("<=", 2)),
"==" => return Some(("==", 2)),
"!=" => return Some(("!=", 2)),
_ => {}
}
}
if let Some(first) = trimmed.chars().next() {
match first {
'>' => return Some((">", 1)),
'<' => return Some(("<", 1)),
_ => {}
}
}
if trimmed.starts_with("contains") {
return Some(("contains", 8));
}
if trimmed.starts_with("matches") {
return Some(("matches", 7));
}
None
}
pub fn has_attribute(text: &str, attr: &str) -> bool {
let bytes = text.as_bytes();
let attr_bytes = attr.as_bytes();
if let Some(pos) = memchr::memmem::find(bytes, attr_bytes) {
let before_ok = pos == 0 || !bytes[pos - 1].is_ascii_alphanumeric();
let after_pos = pos + attr_bytes.len();
let after_ok = after_pos >= bytes.len() || !bytes[after_pos].is_ascii_alphanumeric();
return before_ok && after_ok;
}
false
}
pub fn extract_date_attribute(text: &str, attr_name: &str) -> Option<String> {
let attr_pos = literal_search::find_literal(text, attr_name)?;
let after_attr = text[attr_pos + attr_name.len()..].trim_start();
if after_attr.starts_with('"') {
if let Some(end_quote) = memchr::memchr(b'"', &after_attr.as_bytes()[1..]) {
return Some(after_attr[1..end_quote + 1].to_string());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_rule_header() {
let (name, consumed) = parse_rule_header(r#"rule "MyRule" {"#).unwrap();
assert_eq!(name, "MyRule");
assert!(consumed > 0);
let (name2, consumed2) = parse_rule_header("rule SimpleRule {").unwrap();
assert_eq!(name2, "SimpleRule");
assert!(consumed2 > 0);
}
#[test]
fn test_parse_when_then() {
let body = "when X > 5 then Y = 10";
if let Some((cond, action)) = parse_when_then(body) {
assert_eq!(cond, "X > 5");
assert_eq!(action, "Y = 10");
} else {
panic!("Failed to parse when-then");
}
}
#[test]
fn test_extract_salience() {
assert_eq!(extract_salience("salience 10"), Some(10));
assert_eq!(extract_salience("salience 42 "), Some(42));
assert_eq!(extract_salience("no salience here"), None);
}
#[test]
fn test_parse_operator() {
assert_eq!(parse_operator(">="), Some((">=", 2)));
assert_eq!(parse_operator(" <= "), Some(("<=", 2)));
assert_eq!(parse_operator("contains"), Some(("contains", 8)));
assert_eq!(parse_operator("> 5"), Some((">", 1)));
}
#[test]
fn test_has_attribute() {
assert!(has_attribute("no-loop lock-on-active", "no-loop"));
assert!(has_attribute("salience 10 no-loop", "no-loop"));
assert!(!has_attribute("no-loops", "no-loop")); }
#[test]
fn test_split_into_rules() {
let grl = r#"
rule "Rule1" { when X > 5 then Y = 10 }
rule "Rule2" { when A < 3 then B = 7 }
"#;
let rules = split_into_rules(grl);
assert_eq!(rules.len(), 2);
assert!(rules[0].contains("Rule1"));
assert!(rules[1].contains("Rule2"));
}
#[test]
fn test_parse_defmodule() {
let text = "defmodule MYMODULE { export: all }";
if let Some((name, body, _)) = parse_defmodule(text) {
assert_eq!(name, "MYMODULE");
assert!(body.contains("export"));
} else {
panic!("Failed to parse defmodule");
}
}
}