use crate::engine::module::{ExportItem, ExportList, ImportType, ItemType, ModuleManager};
use crate::engine::rule::{Condition, ConditionGroup, Rule};
use crate::errors::{Result, RuleEngineError};
use crate::types::{ActionType, Operator, Value};
use chrono::{DateTime, Utc};
use rexile::Pattern;
use std::collections::HashMap;
use std::sync::OnceLock;
#[cfg(feature = "streaming")]
pub mod stream_syntax;
static RULE_REGEX: OnceLock<Pattern> = OnceLock::new();
static RULE_SPLIT_REGEX: OnceLock<Pattern> = OnceLock::new();
static DEFMODULE_REGEX: OnceLock<Pattern> = OnceLock::new();
static DEFMODULE_SPLIT_REGEX: OnceLock<Pattern> = OnceLock::new();
static WHEN_THEN_REGEX: OnceLock<Pattern> = OnceLock::new();
static SALIENCE_REGEX: OnceLock<Pattern> = OnceLock::new();
static TEST_CONDITION_REGEX: OnceLock<Pattern> = OnceLock::new();
static TYPED_TEST_CONDITION_REGEX: OnceLock<Pattern> = OnceLock::new();
static FUNCTION_CALL_REGEX: OnceLock<Pattern> = OnceLock::new();
static CONDITION_REGEX: OnceLock<Pattern> = OnceLock::new();
static METHOD_CALL_REGEX: OnceLock<Pattern> = OnceLock::new();
static FUNCTION_BINDING_REGEX: OnceLock<Pattern> = OnceLock::new();
static MULTIFIELD_COLLECT_REGEX: OnceLock<Pattern> = OnceLock::new();
static MULTIFIELD_COUNT_REGEX: OnceLock<Pattern> = OnceLock::new();
static MULTIFIELD_FIRST_REGEX: OnceLock<Pattern> = OnceLock::new();
static MULTIFIELD_LAST_REGEX: OnceLock<Pattern> = OnceLock::new();
static MULTIFIELD_EMPTY_REGEX: OnceLock<Pattern> = OnceLock::new();
static MULTIFIELD_NOT_EMPTY_REGEX: OnceLock<Pattern> = OnceLock::new();
static SIMPLE_CONDITION_REGEX: OnceLock<Pattern> = OnceLock::new();
fn rule_regex() -> &'static Pattern {
RULE_REGEX.get_or_init(|| {
Pattern::new(r#"rule\s+(?:"([^"]+)"|([a-zA-Z_]\w*))\s*([^{]*)\{(.+)\}"#)
.expect("Invalid rule regex pattern")
})
}
fn rule_split_regex() -> &'static Pattern {
RULE_SPLIT_REGEX.get_or_init(|| {
Pattern::new(r#"(?s)rule\s+(?:"[^"]+"|[a-zA-Z_]\w*).*?\}"#)
.expect("Invalid rule split regex pattern")
})
}
fn defmodule_regex() -> &'static Pattern {
DEFMODULE_REGEX.get_or_init(|| {
Pattern::new(r#"defmodule\s+([A-Z_]\w*)\s*\{([^}]*)\}"#)
.expect("Invalid defmodule regex pattern")
})
}
fn defmodule_split_regex() -> &'static Pattern {
DEFMODULE_SPLIT_REGEX.get_or_init(|| {
Pattern::new(r#"(?s)defmodule\s+[A-Z_]\w*\s*\{[^}]*\}"#)
.expect("Invalid defmodule split regex pattern")
})
}
fn when_then_regex() -> &'static Pattern {
WHEN_THEN_REGEX.get_or_init(|| {
Pattern::new(r"when\s+(.+?)\s+then\s+(.+)").expect("Invalid when-then regex pattern")
})
}
fn salience_regex() -> &'static Pattern {
SALIENCE_REGEX
.get_or_init(|| Pattern::new(r"salience\s+(\d+)").expect("Invalid salience regex pattern"))
}
fn test_condition_regex() -> &'static Pattern {
TEST_CONDITION_REGEX.get_or_init(|| {
Pattern::new(r#"^test\s*\(\s*([a-zA-Z_]\w*)\s*\(([^)]*)\)\s*\)$"#)
.expect("Invalid test condition regex")
})
}
fn typed_test_condition_regex() -> &'static Pattern {
TYPED_TEST_CONDITION_REGEX.get_or_init(|| {
Pattern::new(r#"\$(\w+)\s*:\s*(\w+)\s*\(\s*(.+?)\s*\)"#)
.expect("Invalid typed test condition regex")
})
}
fn function_call_regex() -> &'static Pattern {
FUNCTION_CALL_REGEX.get_or_init(|| {
Pattern::new(r#"([a-zA-Z_]\w*)\s*\(([^)]*)\)\s*(>=|<=|==|!=|>|<|contains|startsWith|endsWith|matches|in)\s*(.+)"#)
.expect("Invalid function call regex")
})
}
fn condition_regex() -> &'static Pattern {
CONDITION_REGEX.get_or_init(|| {
Pattern::new(r#"([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*(?:\s*[+\-*/%]\s*[a-zA-Z0-9_\.]+)*)\s*(>=|<=|==|!=|>|<|contains|startsWith|endsWith|matches|in)\s*(.+)"#)
.expect("Invalid condition regex")
})
}
fn method_call_regex() -> &'static Pattern {
METHOD_CALL_REGEX.get_or_init(|| {
Pattern::new(r#"\$(\w+)\.(\w+)\s*\(([^)]*)\)"#).expect("Invalid method call regex")
})
}
fn function_binding_regex() -> &'static Pattern {
FUNCTION_BINDING_REGEX.get_or_init(|| {
Pattern::new(r#"(\w+)\s*\(\s*(.+?)?\s*\)"#).expect("Invalid function binding regex")
})
}
fn multifield_collect_regex() -> &'static Pattern {
MULTIFIELD_COLLECT_REGEX.get_or_init(|| {
Pattern::new(r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+(\$\?[a-zA-Z_]\w*)$"#)
.expect("Invalid multifield collect regex")
})
}
fn multifield_count_regex() -> &'static Pattern {
MULTIFIELD_COUNT_REGEX.get_or_init(|| {
Pattern::new(r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+count\s*(>=|<=|==|!=|>|<)\s*(.+)$"#)
.expect("Invalid multifield count regex")
})
}
fn multifield_first_regex() -> &'static Pattern {
MULTIFIELD_FIRST_REGEX.get_or_init(|| {
Pattern::new(r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+first(?:\s+(\$[a-zA-Z_]\w*))?$"#)
.expect("Invalid multifield first regex")
})
}
fn multifield_last_regex() -> &'static Pattern {
MULTIFIELD_LAST_REGEX.get_or_init(|| {
Pattern::new(r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+last(?:\s+(\$[a-zA-Z_]\w*))?$"#)
.expect("Invalid multifield last regex")
})
}
fn multifield_empty_regex() -> &'static Pattern {
MULTIFIELD_EMPTY_REGEX.get_or_init(|| {
Pattern::new(r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+empty$"#)
.expect("Invalid multifield empty regex")
})
}
fn multifield_not_empty_regex() -> &'static Pattern {
MULTIFIELD_NOT_EMPTY_REGEX.get_or_init(|| {
Pattern::new(r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+not_empty$"#)
.expect("Invalid multifield not_empty regex")
})
}
fn simple_condition_regex() -> &'static Pattern {
SIMPLE_CONDITION_REGEX.get_or_init(|| {
Pattern::new(r#"(\w+)\s*(>=|<=|==|!=|>|<)\s*(.+)"#).expect("Invalid simple condition regex")
})
}
pub struct GRLParser;
#[derive(Debug, Default)]
struct RuleAttributes {
pub no_loop: bool,
pub lock_on_active: bool,
pub agenda_group: Option<String>,
pub activation_group: Option<String>,
pub date_effective: Option<DateTime<Utc>>,
pub date_expires: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone)]
pub struct ParsedGRL {
pub rules: Vec<Rule>,
pub module_manager: ModuleManager,
pub rule_modules: HashMap<String, String>,
}
impl Default for ParsedGRL {
fn default() -> Self {
Self::new()
}
}
impl ParsedGRL {
pub fn new() -> Self {
Self {
rules: Vec::new(),
module_manager: ModuleManager::new(),
rule_modules: HashMap::new(),
}
}
}
impl GRLParser {
pub fn parse_rule(grl_text: &str) -> Result<Rule> {
let mut parser = GRLParser;
parser.parse_single_rule(grl_text)
}
pub fn parse_rules(grl_text: &str) -> Result<Vec<Rule>> {
let mut parser = GRLParser;
parser.parse_multiple_rules(grl_text)
}
pub fn parse_with_modules(grl_text: &str) -> Result<ParsedGRL> {
let mut parser = GRLParser;
parser.parse_grl_with_modules(grl_text)
}
fn parse_grl_with_modules(&mut self, grl_text: &str) -> Result<ParsedGRL> {
let mut result = ParsedGRL::new();
for module_match in defmodule_split_regex().find_iter(grl_text) {
let module_def = module_match.as_str();
self.parse_and_register_module(module_def, &mut result.module_manager)?;
}
let rules_text = defmodule_split_regex().replace_all(grl_text, "");
let rules = self.parse_multiple_rules(&rules_text)?;
for rule in rules {
let module_name = self.extract_module_from_context(grl_text, &rule.name);
result
.rule_modules
.insert(rule.name.clone(), module_name.clone());
if let Ok(module) = result.module_manager.get_module_mut(&module_name) {
module.add_rule(&rule.name);
}
result.rules.push(rule);
}
Ok(result)
}
fn parse_and_register_module(
&self,
module_def: &str,
manager: &mut ModuleManager,
) -> Result<()> {
if let Some(captures) = defmodule_regex().captures(module_def) {
let module_name = captures.get(1).unwrap().to_string();
let module_body = captures.get(2).unwrap();
let _ = manager.create_module(&module_name);
let module = manager.get_module_mut(&module_name)?;
if let Some(export_type) = self.extract_directive(module_body, "export:") {
let exports = if export_type.trim() == "all" {
ExportList::All
} else if export_type.trim() == "none" {
ExportList::None
} else {
ExportList::Specific(vec![ExportItem {
item_type: ItemType::All,
pattern: export_type.trim().to_string(),
}])
};
module.set_exports(exports);
}
let import_lines: Vec<&str> = module_body
.lines()
.filter(|line| line.trim().starts_with("import:"))
.collect();
for import_line in import_lines {
if let Some(import_spec) = self.extract_directive(import_line, "import:") {
self.parse_import_spec(&module_name, &import_spec, manager)?;
}
}
}
Ok(())
}
fn extract_directive(&self, text: &str, directive: &str) -> Option<String> {
if let Some(pos) = text.find(directive) {
let after_directive = &text[pos + directive.len()..];
let end = after_directive
.find("import:")
.or_else(|| after_directive.find("export:"))
.unwrap_or(after_directive.len());
Some(after_directive[..end].trim().to_string())
} else {
None
}
}
fn parse_import_spec(
&self,
importing_module: &str,
spec: &str,
manager: &mut ModuleManager,
) -> Result<()> {
let parts: Vec<&str> = spec.splitn(2, '(').collect();
if parts.is_empty() {
return Ok(());
}
let source_module = parts[0].trim().to_string();
let rest = if parts.len() > 1 { parts[1] } else { "" };
if rest.contains("rules") {
manager.import_from(importing_module, &source_module, ImportType::AllRules, "*")?;
}
if rest.contains("templates") {
manager.import_from(
importing_module,
&source_module,
ImportType::AllTemplates,
"*",
)?;
}
Ok(())
}
fn extract_module_from_context(&self, grl_text: &str, rule_name: &str) -> String {
if let Some(rule_pos) = grl_text
.find(&format!("rule \"{}\"", rule_name))
.or_else(|| grl_text.find(&format!("rule {}", rule_name)))
{
let before = &grl_text[..rule_pos];
if let Some(module_pos) = before.rfind(";; MODULE:") {
let after_module_marker = &before[module_pos + 10..];
if let Some(end_of_line) = after_module_marker.find('\n') {
let module_line = &after_module_marker[..end_of_line].trim();
if let Some(first_word) = module_line.split_whitespace().next() {
return first_word.to_string();
}
}
}
}
"MAIN".to_string()
}
fn parse_single_rule(&mut self, grl_text: &str) -> Result<Rule> {
let cleaned = self.clean_text(grl_text);
let captures =
rule_regex()
.captures(&cleaned)
.ok_or_else(|| RuleEngineError::ParseError {
message: format!("Invalid GRL rule format. Input: {}", cleaned),
})?;
let rule_name = if let Some(quoted_name) = captures.get(1) {
quoted_name.to_string()
} else if let Some(unquoted_name) = captures.get(2) {
unquoted_name.to_string()
} else {
return Err(RuleEngineError::ParseError {
message: "Could not extract rule name".to_string(),
});
};
let attributes_section = captures.get(3).unwrap_or("");
let rule_body = captures.get(4).unwrap();
let salience = self.extract_salience(attributes_section)?;
let when_then_captures =
when_then_regex()
.captures(rule_body)
.ok_or_else(|| RuleEngineError::ParseError {
message: "Missing when or then clause".to_string(),
})?;
let when_clause = when_then_captures.get(1).unwrap().trim();
let then_clause = when_then_captures.get(2).unwrap().trim();
let conditions = self.parse_when_clause(when_clause)?;
let actions = self.parse_then_clause(then_clause)?;
let attributes = self.parse_rule_attributes(attributes_section)?;
let mut rule = Rule::new(rule_name, conditions, actions);
rule = rule.with_priority(salience);
if attributes.no_loop {
rule = rule.with_no_loop(true);
}
if attributes.lock_on_active {
rule = rule.with_lock_on_active(true);
}
if let Some(agenda_group) = attributes.agenda_group {
rule = rule.with_agenda_group(agenda_group);
}
if let Some(activation_group) = attributes.activation_group {
rule = rule.with_activation_group(activation_group);
}
if let Some(date_effective) = attributes.date_effective {
rule = rule.with_date_effective(date_effective);
}
if let Some(date_expires) = attributes.date_expires {
rule = rule.with_date_expires(date_expires);
}
Ok(rule)
}
fn parse_multiple_rules(&mut self, grl_text: &str) -> Result<Vec<Rule>> {
let mut rules = Vec::new();
for rule_match in rule_split_regex().find_iter(grl_text) {
let rule_text = rule_match.as_str();
let rule = self.parse_single_rule(rule_text)?;
rules.push(rule);
}
Ok(rules)
}
fn parse_rule_attributes(&self, rule_header: &str) -> Result<RuleAttributes> {
let mut attributes = RuleAttributes::default();
let mut attrs_section = rule_header.to_string();
let quoted_regex = Pattern::new(r#""[^"]*""#).map_err(|e| RuleEngineError::ParseError {
message: format!("Invalid quoted string regex: {}", e),
})?;
attrs_section = quoted_regex.replace_all(&attrs_section, "").to_string();
if let Some(rule_pos) = attrs_section.find("rule") {
let after_rule = &attrs_section[rule_pos + 4..];
if let Some(first_keyword) = after_rule
.find("salience")
.or_else(|| after_rule.find("no-loop"))
.or_else(|| after_rule.find("lock-on-active"))
.or_else(|| after_rule.find("agenda-group"))
.or_else(|| after_rule.find("activation-group"))
.or_else(|| after_rule.find("date-effective"))
.or_else(|| after_rule.find("date-expires"))
{
attrs_section = after_rule[first_keyword..].to_string();
}
}
let no_loop_regex =
Pattern::new(r"\bno-loop\b").map_err(|e| RuleEngineError::ParseError {
message: format!("Invalid no-loop regex: {}", e),
})?;
let lock_on_active_regex =
Pattern::new(r"\block-on-active\b").map_err(|e| RuleEngineError::ParseError {
message: format!("Invalid lock-on-active regex: {}", e),
})?;
if no_loop_regex.is_match(&attrs_section) {
attributes.no_loop = true;
}
if lock_on_active_regex.is_match(&attrs_section) {
attributes.lock_on_active = true;
}
if let Some(agenda_group) = self.extract_quoted_attribute(rule_header, "agenda-group")? {
attributes.agenda_group = Some(agenda_group);
}
if let Some(activation_group) =
self.extract_quoted_attribute(rule_header, "activation-group")?
{
attributes.activation_group = Some(activation_group);
}
if let Some(date_str) = self.extract_quoted_attribute(rule_header, "date-effective")? {
attributes.date_effective = Some(self.parse_date_string(&date_str)?);
}
if let Some(date_str) = self.extract_quoted_attribute(rule_header, "date-expires")? {
attributes.date_expires = Some(self.parse_date_string(&date_str)?);
}
Ok(attributes)
}
fn extract_quoted_attribute(&self, header: &str, attribute: &str) -> Result<Option<String>> {
let pattern = format!(r#"{}\s+"([^"]+)""#, attribute);
let regex = Pattern::new(&pattern).map_err(|e| RuleEngineError::ParseError {
message: format!("Invalid attribute regex for {}: {}", attribute, e),
})?;
if let Some(captures) = regex.captures(header) {
if let Some(value) = captures.get(1) {
return Ok(Some(value.to_string()));
}
}
Ok(None)
}
fn parse_date_string(&self, date_str: &str) -> Result<DateTime<Utc>> {
if let Ok(date) = DateTime::parse_from_rfc3339(date_str) {
return Ok(date.with_timezone(&Utc));
}
let formats = ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%d-%b-%Y", "%d-%m-%Y"];
for format in &formats {
if let Ok(naive_date) = chrono::NaiveDateTime::parse_from_str(date_str, format) {
return Ok(naive_date.and_utc());
}
if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(date_str, format) {
let datetime =
naive_date
.and_hms_opt(0, 0, 0)
.ok_or_else(|| RuleEngineError::ParseError {
message: format!("Invalid time for date: {}", naive_date),
})?;
return Ok(datetime.and_utc());
}
}
Err(RuleEngineError::ParseError {
message: format!("Unable to parse date: {}", date_str),
})
}
fn extract_salience(&self, attributes_section: &str) -> Result<i32> {
if let Some(captures) = salience_regex().captures(attributes_section) {
if let Some(salience_match) = captures.get(1) {
return salience_match
.parse::<i32>()
.map_err(|e| RuleEngineError::ParseError {
message: format!("Invalid salience value: {}", e),
});
}
}
Ok(0) }
fn clean_text(&self, text: &str) -> String {
text.lines()
.map(|line| line.trim())
.filter(|line| !line.is_empty() && !line.starts_with("//"))
.collect::<Vec<_>>()
.join(" ")
}
fn parse_when_clause(&self, when_clause: &str) -> Result<ConditionGroup> {
let trimmed = when_clause.trim();
let clause = if trimmed.starts_with('(') && trimmed.ends_with(')') {
let inner = &trimmed[1..trimmed.len() - 1];
if self.is_balanced_parentheses(inner) {
inner
} else {
trimmed
}
} else {
trimmed
};
if let Some(parts) = self.split_logical_operator(clause, "||") {
return self.parse_or_parts(parts);
}
if let Some(parts) = self.split_logical_operator(clause, "&&") {
return self.parse_and_parts(parts);
}
if clause.trim_start().starts_with("!") {
return self.parse_not_condition(clause);
}
if clause.trim_start().starts_with("exists(") {
return self.parse_exists_condition(clause);
}
if clause.trim_start().starts_with("forall(") {
return self.parse_forall_condition(clause);
}
if clause.trim_start().starts_with("accumulate(") {
return self.parse_accumulate_condition(clause);
}
self.parse_single_condition(clause)
}
fn is_balanced_parentheses(&self, text: &str) -> bool {
let mut count = 0;
for ch in text.chars() {
match ch {
'(' => count += 1,
')' => {
count -= 1;
if count < 0 {
return false;
}
}
_ => {}
}
}
count == 0
}
fn split_logical_operator(&self, clause: &str, operator: &str) -> Option<Vec<String>> {
let mut parts = Vec::new();
let mut current_part = String::new();
let mut paren_count = 0;
let mut chars = clause.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'(' => {
paren_count += 1;
current_part.push(ch);
}
')' => {
paren_count -= 1;
current_part.push(ch);
}
'&' if operator == "&&" && paren_count == 0 => {
if chars.peek() == Some(&'&') {
chars.next(); parts.push(current_part.trim().to_string());
current_part.clear();
} else {
current_part.push(ch);
}
}
'|' if operator == "||" && paren_count == 0 => {
if chars.peek() == Some(&'|') {
chars.next(); parts.push(current_part.trim().to_string());
current_part.clear();
} else {
current_part.push(ch);
}
}
_ => {
current_part.push(ch);
}
}
}
if !current_part.trim().is_empty() {
parts.push(current_part.trim().to_string());
}
if parts.len() > 1 {
Some(parts)
} else {
None
}
}
fn parse_or_parts(&self, parts: Vec<String>) -> Result<ConditionGroup> {
let mut conditions = Vec::new();
for part in parts {
let condition = self.parse_when_clause(&part)?;
conditions.push(condition);
}
if conditions.is_empty() {
return Err(RuleEngineError::ParseError {
message: "No conditions found in OR".to_string(),
});
}
let mut iter = conditions.into_iter();
let mut result = iter
.next()
.expect("Iterator cannot be empty after empty check");
for condition in iter {
result = ConditionGroup::or(result, condition);
}
Ok(result)
}
fn parse_and_parts(&self, parts: Vec<String>) -> Result<ConditionGroup> {
let mut conditions = Vec::new();
for part in parts {
let condition = self.parse_when_clause(&part)?;
conditions.push(condition);
}
if conditions.is_empty() {
return Err(RuleEngineError::ParseError {
message: "No conditions found in AND".to_string(),
});
}
let mut iter = conditions.into_iter();
let mut result = iter
.next()
.expect("Iterator cannot be empty after empty check");
for condition in iter {
result = ConditionGroup::and(result, condition);
}
Ok(result)
}
fn parse_not_condition(&self, clause: &str) -> Result<ConditionGroup> {
let inner_clause = clause
.strip_prefix('!')
.ok_or_else(|| RuleEngineError::ParseError {
message: format!("Expected '!' prefix in NOT condition: {}", clause),
})?
.trim();
let inner_condition = self.parse_when_clause(inner_clause)?;
Ok(ConditionGroup::not(inner_condition))
}
fn parse_exists_condition(&self, clause: &str) -> Result<ConditionGroup> {
let clause = clause.trim_start();
if !clause.starts_with("exists(") || !clause.ends_with(")") {
return Err(RuleEngineError::ParseError {
message: "Invalid exists syntax. Expected: exists(condition)".to_string(),
});
}
let inner_clause = &clause[7..clause.len() - 1]; let inner_condition = self.parse_when_clause(inner_clause)?;
Ok(ConditionGroup::exists(inner_condition))
}
fn parse_forall_condition(&self, clause: &str) -> Result<ConditionGroup> {
let clause = clause.trim_start();
if !clause.starts_with("forall(") || !clause.ends_with(")") {
return Err(RuleEngineError::ParseError {
message: "Invalid forall syntax. Expected: forall(condition)".to_string(),
});
}
let inner_clause = &clause[7..clause.len() - 1]; let inner_condition = self.parse_when_clause(inner_clause)?;
Ok(ConditionGroup::forall(inner_condition))
}
fn parse_accumulate_condition(&self, clause: &str) -> Result<ConditionGroup> {
let clause = clause.trim_start();
if !clause.starts_with("accumulate(") || !clause.ends_with(")") {
return Err(RuleEngineError::ParseError {
message: "Invalid accumulate syntax. Expected: accumulate(pattern, function)"
.to_string(),
});
}
let inner = &clause[11..clause.len() - 1];
let parts = self.split_accumulate_parts(inner)?;
if parts.len() != 2 {
return Err(RuleEngineError::ParseError {
message: format!(
"Invalid accumulate syntax. Expected 2 parts (pattern, function), got {}",
parts.len()
),
});
}
let pattern_part = parts[0].trim();
let function_part = parts[1].trim();
let (source_pattern, extract_field, source_conditions) =
self.parse_accumulate_pattern(pattern_part)?;
let (function, function_arg) = self.parse_accumulate_function(function_part)?;
let result_var = "$result".to_string();
Ok(ConditionGroup::accumulate(
result_var,
source_pattern,
extract_field,
source_conditions,
function,
function_arg,
))
}
fn split_accumulate_parts(&self, content: &str) -> Result<Vec<String>> {
let mut parts = Vec::new();
let mut current = String::new();
let mut paren_depth = 0;
for ch in content.chars() {
match ch {
'(' => {
paren_depth += 1;
current.push(ch);
}
')' => {
paren_depth -= 1;
current.push(ch);
}
',' if paren_depth == 0 => {
parts.push(current.trim().to_string());
current.clear();
}
_ => {
current.push(ch);
}
}
}
if !current.trim().is_empty() {
parts.push(current.trim().to_string());
}
Ok(parts)
}
fn parse_accumulate_pattern(&self, pattern: &str) -> Result<(String, String, Vec<String>)> {
let pattern = pattern.trim();
let paren_pos = pattern
.find('(')
.ok_or_else(|| RuleEngineError::ParseError {
message: format!("Invalid accumulate pattern: missing '(' in '{}'", pattern),
})?;
let source_pattern = pattern[..paren_pos].trim().to_string();
if !pattern.ends_with(')') {
return Err(RuleEngineError::ParseError {
message: format!("Invalid accumulate pattern: missing ')' in '{}'", pattern),
});
}
let inner = &pattern[paren_pos + 1..pattern.len() - 1];
let parts = self.split_pattern_parts(inner)?;
let mut extract_field = String::new();
let mut source_conditions = Vec::new();
for part in parts {
let part = part.trim();
if part.contains(':') && part.starts_with('$') {
if let Some(colon_pos) = part.find(':') {
extract_field = part[colon_pos + 1..].trim().to_string();
}
} else if part.contains("==")
|| part.contains("!=")
|| part.contains(">=")
|| part.contains("<=")
|| part.contains('>')
|| part.contains('<')
{
source_conditions.push(part.to_string());
}
}
Ok((source_pattern, extract_field, source_conditions))
}
fn split_pattern_parts(&self, content: &str) -> Result<Vec<String>> {
let mut parts = Vec::new();
let mut current = String::new();
let mut paren_depth = 0;
let mut in_quotes = false;
let mut quote_char = ' ';
for ch in content.chars() {
match ch {
'"' | '\'' if !in_quotes => {
in_quotes = true;
quote_char = ch;
current.push(ch);
}
'"' | '\'' if in_quotes && ch == quote_char => {
in_quotes = false;
current.push(ch);
}
'(' if !in_quotes => {
paren_depth += 1;
current.push(ch);
}
')' if !in_quotes => {
paren_depth -= 1;
current.push(ch);
}
',' if !in_quotes && paren_depth == 0 => {
parts.push(current.trim().to_string());
current.clear();
}
_ => {
current.push(ch);
}
}
}
if !current.trim().is_empty() {
parts.push(current.trim().to_string());
}
Ok(parts)
}
fn parse_accumulate_function(&self, function_str: &str) -> Result<(String, String)> {
let function_str = function_str.trim();
let paren_pos = function_str
.find('(')
.ok_or_else(|| RuleEngineError::ParseError {
message: format!(
"Invalid accumulate function: missing '(' in '{}'",
function_str
),
})?;
let function_name = function_str[..paren_pos].trim().to_string();
if !function_str.ends_with(')') {
return Err(RuleEngineError::ParseError {
message: format!(
"Invalid accumulate function: missing ')' in '{}'",
function_str
),
});
}
let args = &function_str[paren_pos + 1..function_str.len() - 1];
let function_arg = args.trim().to_string();
Ok((function_name, function_arg))
}
fn parse_single_condition(&self, clause: &str) -> Result<ConditionGroup> {
let trimmed_clause = clause.trim();
let clause_to_parse = if trimmed_clause.starts_with('(') && trimmed_clause.ends_with(')') {
trimmed_clause[1..trimmed_clause.len() - 1].trim()
} else {
trimmed_clause
};
#[cfg(feature = "streaming")]
if clause_to_parse.contains("from stream(") {
return self.parse_stream_pattern_condition(clause_to_parse);
}
if let Some(captures) = multifield_collect_regex().captures(clause_to_parse) {
let field = captures.get(1).unwrap().to_string();
let variable = captures.get(2).unwrap().to_string();
let condition = Condition::with_multifield_collect(field, variable);
return Ok(ConditionGroup::single(condition));
}
if let Some(captures) = multifield_count_regex().captures(clause_to_parse) {
let field = captures.get(1).unwrap().to_string();
let operator_str = captures.get(2).unwrap();
let value_str = captures.get(3).unwrap().trim();
let operator = Operator::from_str(operator_str).ok_or_else(|| {
RuleEngineError::InvalidOperator {
operator: operator_str.to_string(),
}
})?;
let value = self.parse_value(value_str)?;
let condition = Condition::with_multifield_count(field, operator, value);
return Ok(ConditionGroup::single(condition));
}
if let Some(captures) = multifield_first_regex().captures(clause_to_parse) {
let field = captures.get(1).unwrap().to_string();
let variable = captures.get(2).map(|m| m.to_string());
let condition = Condition::with_multifield_first(field, variable);
return Ok(ConditionGroup::single(condition));
}
if let Some(captures) = multifield_last_regex().captures(clause_to_parse) {
let field = captures.get(1).unwrap().to_string();
let variable = captures.get(2).map(|m| m.to_string());
let condition = Condition::with_multifield_last(field, variable);
return Ok(ConditionGroup::single(condition));
}
if let Some(captures) = multifield_empty_regex().captures(clause_to_parse) {
let field = captures.get(1).unwrap().to_string();
let condition = Condition::with_multifield_empty(field);
return Ok(ConditionGroup::single(condition));
}
if let Some(captures) = multifield_not_empty_regex().captures(clause_to_parse) {
let field = captures.get(1).unwrap().to_string();
let condition = Condition::with_multifield_not_empty(field);
return Ok(ConditionGroup::single(condition));
}
if let Some(captures) = test_condition_regex().captures(clause_to_parse) {
let function_name = captures.get(1).unwrap().to_string();
let args_str = captures.get(2).unwrap();
let args: Vec<String> = if args_str.trim().is_empty() {
Vec::new()
} else {
args_str
.split(',')
.map(|arg| arg.trim().to_string())
.collect()
};
let condition = Condition::with_test(function_name, args);
return Ok(ConditionGroup::single(condition));
}
if let Some(captures) = typed_test_condition_regex().captures(clause_to_parse) {
let _object_name = captures.get(1).unwrap();
let _object_type = captures.get(2).unwrap();
let conditions_str = captures.get(3).unwrap();
return self.parse_conditions_within_object(conditions_str);
}
if let Some(captures) = function_call_regex().captures(clause_to_parse) {
let function_name = captures.get(1).unwrap().to_string();
let args_str = captures.get(2).unwrap();
let operator_str = captures.get(3).unwrap();
let value_str = captures.get(4).unwrap().trim();
let args: Vec<String> = if args_str.trim().is_empty() {
Vec::new()
} else {
args_str
.split(',')
.map(|arg| arg.trim().to_string())
.collect()
};
let operator = Operator::from_str(operator_str).ok_or_else(|| {
RuleEngineError::InvalidOperator {
operator: operator_str.to_string(),
}
})?;
let value = self.parse_value(value_str)?;
let condition = Condition::with_function(function_name, args, operator, value);
return Ok(ConditionGroup::single(condition));
}
let captures = condition_regex().captures(clause_to_parse).ok_or_else(|| {
RuleEngineError::ParseError {
message: format!("Invalid condition format: {}", clause_to_parse),
}
})?;
let left_side = captures.get(1).unwrap().trim().to_string();
let operator_str = captures.get(2).unwrap();
let value_str = captures.get(3).unwrap().trim();
let operator =
Operator::from_str(operator_str).ok_or_else(|| RuleEngineError::InvalidOperator {
operator: operator_str.to_string(),
})?;
let value = self.parse_value(value_str)?;
if left_side.contains('+')
|| left_side.contains('-')
|| left_side.contains('*')
|| left_side.contains('/')
|| left_side.contains('%')
{
let test_expr = format!("{} {} {}", left_side, operator_str, value_str);
let condition = Condition::with_test(test_expr, vec![]);
Ok(ConditionGroup::single(condition))
} else {
let condition = Condition::new(left_side, operator, value);
Ok(ConditionGroup::single(condition))
}
}
fn parse_conditions_within_object(&self, conditions_str: &str) -> Result<ConditionGroup> {
let parts: Vec<&str> = conditions_str.split("&&").collect();
let mut conditions = Vec::new();
for part in parts {
let trimmed = part.trim();
let condition = self.parse_simple_condition(trimmed)?;
conditions.push(condition);
}
if conditions.is_empty() {
return Err(RuleEngineError::ParseError {
message: "No conditions found".to_string(),
});
}
let mut iter = conditions.into_iter();
let mut result = iter
.next()
.expect("Iterator cannot be empty after empty check");
for condition in iter {
result = ConditionGroup::and(result, condition);
}
Ok(result)
}
fn parse_simple_condition(&self, clause: &str) -> Result<ConditionGroup> {
let captures = simple_condition_regex().captures(clause).ok_or_else(|| {
RuleEngineError::ParseError {
message: format!("Invalid simple condition format: {}", clause),
}
})?;
let field = captures.get(1).unwrap().to_string();
let operator_str = captures.get(2).unwrap();
let value_str = captures.get(3).unwrap().trim();
let operator =
Operator::from_str(operator_str).ok_or_else(|| RuleEngineError::InvalidOperator {
operator: operator_str.to_string(),
})?;
let value = self.parse_value(value_str)?;
let condition = Condition::new(field, operator, value);
Ok(ConditionGroup::single(condition))
}
fn parse_value(&self, value_str: &str) -> Result<Value> {
let trimmed = value_str.trim();
if trimmed.starts_with('[') && trimmed.ends_with(']') {
return self.parse_array_literal(trimmed);
}
if (trimmed.starts_with('"') && trimmed.ends_with('"'))
|| (trimmed.starts_with('\'') && trimmed.ends_with('\''))
{
let unquoted = &trimmed[1..trimmed.len() - 1];
return Ok(Value::String(unquoted.to_string()));
}
if trimmed.eq_ignore_ascii_case("true") {
return Ok(Value::Boolean(true));
}
if trimmed.eq_ignore_ascii_case("false") {
return Ok(Value::Boolean(false));
}
if trimmed.eq_ignore_ascii_case("null") {
return Ok(Value::Null);
}
if let Ok(int_val) = trimmed.parse::<i64>() {
return Ok(Value::Integer(int_val));
}
if let Ok(float_val) = trimmed.parse::<f64>() {
return Ok(Value::Number(float_val));
}
if self.is_expression(trimmed) {
return Ok(Value::Expression(trimmed.to_string()));
}
if trimmed.contains('.') {
return Ok(Value::String(trimmed.to_string()));
}
if self.is_identifier(trimmed) {
return Ok(Value::Expression(trimmed.to_string()));
}
Ok(Value::String(trimmed.to_string()))
}
fn is_identifier(&self, s: &str) -> bool {
if s.is_empty() {
return false;
}
let first_char = s.chars().next().expect("Cannot be empty after empty check");
if !first_char.is_alphabetic() && first_char != '_' {
return false;
}
let first_char = s.chars().next().unwrap();
if !first_char.is_alphabetic() && first_char != '_' {
return false;
}
s.chars().all(|c| c.is_alphanumeric() || c == '_')
}
fn is_expression(&self, s: &str) -> bool {
let has_operator = s.contains('+')
|| s.contains('-')
|| s.contains('*')
|| s.contains('/')
|| s.contains('%');
let has_field_ref = s.contains('.');
let has_spaces = s.contains(' ');
has_operator && (has_field_ref || has_spaces)
}
fn parse_array_literal(&self, array_str: &str) -> Result<Value> {
let content = array_str.trim();
if !content.starts_with('[') || !content.ends_with(']') {
return Err(RuleEngineError::ParseError {
message: format!("Invalid array literal: {}", array_str),
});
}
let inner = content[1..content.len() - 1].trim();
if inner.is_empty() {
return Ok(Value::Array(vec![]));
}
let mut elements = Vec::new();
let mut current_element = String::new();
let mut in_quotes = false;
let mut quote_char = ' ';
for ch in inner.chars() {
match ch {
'"' | '\'' if !in_quotes => {
in_quotes = true;
quote_char = ch;
current_element.push(ch);
}
c if in_quotes && c == quote_char => {
in_quotes = false;
current_element.push(ch);
}
',' if !in_quotes => {
if !current_element.trim().is_empty() {
elements.push(current_element.trim().to_string());
}
current_element.clear();
}
_ => {
current_element.push(ch);
}
}
}
if !current_element.trim().is_empty() {
elements.push(current_element.trim().to_string());
}
let mut array_values = Vec::new();
for elem in elements {
let value = self.parse_value(&elem)?;
array_values.push(value);
}
Ok(Value::Array(array_values))
}
fn parse_then_clause(&self, then_clause: &str) -> Result<Vec<ActionType>> {
let statements: Vec<&str> = then_clause
.split(';')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
let mut actions = Vec::new();
for statement in statements {
let action = self.parse_action_statement(statement)?;
actions.push(action);
}
Ok(actions)
}
fn parse_action_statement(&self, statement: &str) -> Result<ActionType> {
let trimmed = statement.trim();
if let Some(captures) = method_call_regex().captures(trimmed) {
let object = captures.get(1).unwrap().to_string();
let method = captures.get(2).unwrap().to_string();
let args_str = captures.get(3).unwrap();
let args = if args_str.trim().is_empty() {
Vec::new()
} else {
self.parse_method_args(args_str)?
};
return Ok(ActionType::MethodCall {
object,
method,
args,
});
}
if let Some(plus_eq_pos) = trimmed.find("+=") {
let field = trimmed[..plus_eq_pos].trim().to_string();
let value_str = trimmed[plus_eq_pos + 2..].trim();
let value = self.parse_value(value_str)?;
return Ok(ActionType::Append { field, value });
}
if let Some(eq_pos) = trimmed.find('=') {
let field = trimmed[..eq_pos].trim().to_string();
let value_str = trimmed[eq_pos + 1..].trim();
let value = self.parse_value(value_str)?;
return Ok(ActionType::Set { field, value });
}
if let Some(captures) = function_binding_regex().captures(trimmed) {
let function_name = captures.get(1).unwrap();
let args_str = captures.get(2).unwrap_or("");
match function_name.to_lowercase().as_str() {
"retract" => {
let object_name = if let Some(stripped) = args_str.strip_prefix('$') {
stripped.to_string()
} else {
args_str.to_string()
};
Ok(ActionType::Retract {
object: object_name,
})
}
"log" => {
let message = if args_str.is_empty() {
"Log message".to_string()
} else {
let value = self.parse_value(args_str.trim())?;
value.to_string()
};
Ok(ActionType::Log { message })
}
"activateagendagroup" | "activate_agenda_group" => {
let agenda_group = if args_str.is_empty() {
return Err(RuleEngineError::ParseError {
message: "ActivateAgendaGroup requires agenda group name".to_string(),
});
} else {
let value = self.parse_value(args_str.trim())?;
match value {
Value::String(s) => s,
_ => value.to_string(),
}
};
Ok(ActionType::ActivateAgendaGroup {
group: agenda_group,
})
}
"schedulerule" | "schedule_rule" => {
let parts: Vec<&str> = args_str.split(',').collect();
if parts.len() != 2 {
return Err(RuleEngineError::ParseError {
message: "ScheduleRule requires delay_ms and rule_name".to_string(),
});
}
let delay_ms = self.parse_value(parts[0].trim())?;
let rule_name = self.parse_value(parts[1].trim())?;
let delay_ms = match delay_ms {
Value::Integer(i) => i as u64,
Value::Number(f) => f as u64,
_ => {
return Err(RuleEngineError::ParseError {
message: "ScheduleRule delay_ms must be a number".to_string(),
})
}
};
let rule_name = match rule_name {
Value::String(s) => s,
_ => rule_name.to_string(),
};
Ok(ActionType::ScheduleRule {
delay_ms,
rule_name,
})
}
"completeworkflow" | "complete_workflow" => {
let workflow_id = if args_str.is_empty() {
return Err(RuleEngineError::ParseError {
message: "CompleteWorkflow requires workflow_id".to_string(),
});
} else {
let value = self.parse_value(args_str.trim())?;
match value {
Value::String(s) => s,
_ => value.to_string(),
}
};
Ok(ActionType::CompleteWorkflow {
workflow_name: workflow_id,
})
}
"setworkflowdata" | "set_workflow_data" => {
let data_str = args_str.trim();
let (key, value) = if let Some(eq_pos) = data_str.find('=') {
let key = data_str[..eq_pos].trim().trim_matches('"');
let value_str = data_str[eq_pos + 1..].trim();
let value = self.parse_value(value_str)?;
(key.to_string(), value)
} else {
return Err(RuleEngineError::ParseError {
message: "SetWorkflowData data must be in key=value format".to_string(),
});
};
Ok(ActionType::SetWorkflowData { key, value })
}
_ => {
let params = if args_str.is_empty() {
HashMap::new()
} else {
self.parse_function_args_as_params(args_str)?
};
Ok(ActionType::Custom {
action_type: function_name.to_string(),
params,
})
}
}
} else {
Ok(ActionType::Custom {
action_type: "statement".to_string(),
params: {
let mut params = HashMap::new();
params.insert("statement".to_string(), Value::String(trimmed.to_string()));
params
},
})
}
}
fn parse_method_args(&self, args_str: &str) -> Result<Vec<Value>> {
if args_str.trim().is_empty() {
return Ok(Vec::new());
}
let mut args = Vec::new();
let parts: Vec<&str> = args_str.split(',').collect();
for part in parts {
let trimmed = part.trim();
if trimmed.contains('+')
|| trimmed.contains('-')
|| trimmed.contains('*')
|| trimmed.contains('/')
{
args.push(Value::String(trimmed.to_string()));
} else {
args.push(self.parse_value(trimmed)?);
}
}
Ok(args)
}
fn parse_function_args_as_params(&self, args_str: &str) -> Result<HashMap<String, Value>> {
let mut params = HashMap::new();
if args_str.trim().is_empty() {
return Ok(params);
}
let parts: Vec<&str> = args_str.split(',').collect();
for (i, part) in parts.iter().enumerate() {
let trimmed = part.trim();
let value = self.parse_value(trimmed)?;
params.insert(i.to_string(), value);
}
Ok(params)
}
#[cfg(feature = "streaming")]
fn parse_stream_pattern_condition(&self, clause: &str) -> Result<ConditionGroup> {
use crate::engine::rule::{StreamWindow, StreamWindowType};
use crate::parser::grl::stream_syntax::parse_stream_pattern;
let parse_result =
parse_stream_pattern(clause).map_err(|e| RuleEngineError::ParseError {
message: format!("Failed to parse stream pattern: {:?}", e),
})?;
let (_, pattern) = parse_result;
let window = pattern.source.window.map(|w| StreamWindow {
duration: w.duration,
window_type: match w.window_type {
crate::parser::grl::stream_syntax::WindowType::Sliding => StreamWindowType::Sliding,
crate::parser::grl::stream_syntax::WindowType::Tumbling => {
StreamWindowType::Tumbling
}
crate::parser::grl::stream_syntax::WindowType::Session { timeout } => {
StreamWindowType::Session { timeout }
}
},
});
Ok(ConditionGroup::stream_pattern(
pattern.var_name,
pattern.event_type,
pattern.source.stream_name,
window,
))
}
}
#[cfg(test)]
mod tests {
use super::GRLParser;
#[test]
fn test_parse_simple_rule() {
let grl = r#"
rule "CheckAge" salience 10 {
when
User.Age >= 18
then
log("User is adult");
}
"#;
let rules = GRLParser::parse_rules(grl).unwrap();
assert_eq!(rules.len(), 1);
let rule = &rules[0];
assert_eq!(rule.name, "CheckAge");
assert_eq!(rule.salience, 10);
assert_eq!(rule.actions.len(), 1);
}
#[test]
fn test_parse_complex_condition() {
let grl = r#"
rule "ComplexRule" {
when
User.Age >= 18 && User.Country == "US"
then
User.Qualified = true;
}
"#;
let rules = GRLParser::parse_rules(grl).unwrap();
assert_eq!(rules.len(), 1);
let rule = &rules[0];
assert_eq!(rule.name, "ComplexRule");
}
#[test]
fn test_parse_new_syntax_with_parentheses() {
let grl = r#"
rule "Default Rule" salience 10 {
when
(user.age >= 18)
then
set(user.status, "approved");
}
"#;
let rules = GRLParser::parse_rules(grl).unwrap();
assert_eq!(rules.len(), 1);
let rule = &rules[0];
assert_eq!(rule.name, "Default Rule");
assert_eq!(rule.salience, 10);
assert_eq!(rule.actions.len(), 1);
match &rule.actions[0] {
crate::types::ActionType::Custom {
action_type,
params,
} => {
assert_eq!(action_type, "set");
assert_eq!(
params.get("0"),
Some(&crate::types::Value::String("user.status".to_string()))
);
assert_eq!(
params.get("1"),
Some(&crate::types::Value::String("approved".to_string()))
);
}
_ => panic!("Expected Custom action, got: {:?}", rule.actions[0]),
}
}
#[test]
fn test_parse_complex_nested_conditions() {
let grl = r#"
rule "Complex Business Rule" salience 10 {
when
(((user.vipStatus == true) && (order.amount > 500)) || ((date.isHoliday == true) && (order.hasCoupon == true)))
then
apply_discount(20000);
}
"#;
let rules = GRLParser::parse_rules(grl).unwrap();
assert_eq!(rules.len(), 1);
let rule = &rules[0];
assert_eq!(rule.name, "Complex Business Rule");
assert_eq!(rule.salience, 10);
assert_eq!(rule.actions.len(), 1);
match &rule.actions[0] {
crate::types::ActionType::Custom {
action_type,
params,
} => {
assert_eq!(action_type, "apply_discount");
assert_eq!(params.get("0"), Some(&crate::types::Value::Integer(20000)));
}
_ => panic!("Expected Custom action, got: {:?}", rule.actions[0]),
}
}
#[test]
fn test_parse_no_loop_attribute() {
let grl = r#"
rule "NoLoopRule" no-loop salience 15 {
when
User.Score < 100
then
set(User.Score, User.Score + 10);
}
"#;
let rules = GRLParser::parse_rules(grl).unwrap();
assert_eq!(rules.len(), 1);
let rule = &rules[0];
assert_eq!(rule.name, "NoLoopRule");
assert_eq!(rule.salience, 15);
assert!(rule.no_loop, "Rule should have no-loop=true");
}
#[test]
fn test_parse_no_loop_different_positions() {
let grl1 = r#"
rule "Rule1" no-loop salience 10 {
when User.Age >= 18
then log("adult");
}
"#;
let grl2 = r#"
rule "Rule2" salience 10 no-loop {
when User.Age >= 18
then log("adult");
}
"#;
let rules1 = GRLParser::parse_rules(grl1).unwrap();
let rules2 = GRLParser::parse_rules(grl2).unwrap();
assert_eq!(rules1.len(), 1);
assert_eq!(rules2.len(), 1);
assert!(rules1[0].no_loop, "Rule1 should have no-loop=true");
assert!(rules2[0].no_loop, "Rule2 should have no-loop=true");
assert_eq!(rules1[0].salience, 10);
assert_eq!(rules2[0].salience, 10);
}
#[test]
fn test_parse_without_no_loop() {
let grl = r#"
rule "RegularRule" salience 5 {
when
User.Active == true
then
log("active user");
}
"#;
let rules = GRLParser::parse_rules(grl).unwrap();
assert_eq!(rules.len(), 1);
let rule = &rules[0];
assert_eq!(rule.name, "RegularRule");
assert!(!rule.no_loop, "Rule should have no-loop=false by default");
}
#[test]
fn test_parse_exists_pattern() {
let grl = r#"
rule "ExistsRule" salience 20 {
when
exists(Customer.tier == "VIP")
then
System.premiumActive = true;
}
"#;
let rules = GRLParser::parse_rules(grl).unwrap();
assert_eq!(rules.len(), 1);
let rule = &rules[0];
assert_eq!(rule.name, "ExistsRule");
assert_eq!(rule.salience, 20);
match &rule.conditions {
crate::engine::rule::ConditionGroup::Exists(_) => {
}
_ => panic!(
"Expected EXISTS condition group, got: {:?}",
rule.conditions
),
}
}
#[test]
fn test_parse_forall_pattern() {
let grl = r#"
rule "ForallRule" salience 15 {
when
forall(Order.status == "processed")
then
Shipping.enabled = true;
}
"#;
let rules = GRLParser::parse_rules(grl).unwrap();
assert_eq!(rules.len(), 1);
let rule = &rules[0];
assert_eq!(rule.name, "ForallRule");
match &rule.conditions {
crate::engine::rule::ConditionGroup::Forall(_) => {
}
_ => panic!(
"Expected FORALL condition group, got: {:?}",
rule.conditions
),
}
}
#[test]
fn test_parse_combined_patterns() {
let grl = r#"
rule "CombinedRule" salience 25 {
when
exists(Customer.tier == "VIP") && !exists(Alert.priority == "high")
then
System.vipMode = true;
}
"#;
let rules = GRLParser::parse_rules(grl).unwrap();
assert_eq!(rules.len(), 1);
let rule = &rules[0];
assert_eq!(rule.name, "CombinedRule");
match &rule.conditions {
crate::engine::rule::ConditionGroup::Compound {
left,
operator,
right,
} => {
assert_eq!(*operator, crate::types::LogicalOperator::And);
match left.as_ref() {
crate::engine::rule::ConditionGroup::Exists(_) => {
}
_ => panic!("Expected EXISTS in left side, got: {:?}", left),
}
match right.as_ref() {
crate::engine::rule::ConditionGroup::Not(inner) => {
match inner.as_ref() {
crate::engine::rule::ConditionGroup::Exists(_) => {
}
_ => panic!("Expected EXISTS inside NOT, got: {:?}", inner),
}
}
_ => panic!("Expected NOT in right side, got: {:?}", right),
}
}
_ => panic!("Expected compound condition, got: {:?}", rule.conditions),
}
}
#[test]
fn test_parse_in_operator() {
let grl = r#"
rule "TestInOperator" salience 75 {
when
User.role in ["admin", "moderator", "vip"]
then
User.access = "granted";
}
"#;
let rules = GRLParser::parse_rules(grl).unwrap();
assert_eq!(rules.len(), 1);
let rule = &rules[0];
assert_eq!(rule.name, "TestInOperator");
assert_eq!(rule.salience, 75);
match &rule.conditions {
crate::engine::rule::ConditionGroup::Single(cond) => {
println!("Condition: {:?}", cond);
assert_eq!(cond.operator, crate::types::Operator::In);
match &cond.value {
crate::types::Value::Array(arr) => {
assert_eq!(arr.len(), 3);
assert_eq!(arr[0], crate::types::Value::String("admin".to_string()));
assert_eq!(arr[1], crate::types::Value::String("moderator".to_string()));
assert_eq!(arr[2], crate::types::Value::String("vip".to_string()));
}
_ => panic!("Expected Array value, got {:?}", cond.value),
}
}
_ => panic!("Expected Single condition, got: {:?}", rule.conditions),
}
}
#[test]
fn test_parse_startswith_endswith_operators() {
let grl = r#"
rule "StringMethods" salience 50 {
when
User.email startsWith "admin@" &&
User.filename endsWith ".txt"
then
User.validated = true;
}
"#;
let rules = GRLParser::parse_rules(grl).unwrap();
assert_eq!(rules.len(), 1);
let rule = &rules[0];
assert_eq!(rule.name, "StringMethods");
assert_eq!(rule.salience, 50);
match &rule.conditions {
crate::engine::rule::ConditionGroup::Compound {
left,
operator,
right,
} => {
assert_eq!(*operator, crate::types::LogicalOperator::And);
match left.as_ref() {
crate::engine::rule::ConditionGroup::Single(cond) => {
assert_eq!(cond.operator, crate::types::Operator::StartsWith);
}
_ => panic!("Expected Single condition for startsWith, got: {:?}", left),
}
match right.as_ref() {
crate::engine::rule::ConditionGroup::Single(cond) => {
assert_eq!(cond.operator, crate::types::Operator::EndsWith);
}
_ => panic!("Expected Single condition for endsWith, got: {:?}", right),
}
}
_ => panic!("Expected Compound condition, got: {:?}", rule.conditions),
}
}
}