1use crate::engine::rule::{Condition, ConditionGroup, Rule};
2use crate::engine::module::{Module, ModuleManager, ExportList, ExportItem, ItemType, ImportDecl, ImportType};
3use crate::errors::{Result, RuleEngineError};
4use crate::types::{ActionType, Operator, Value};
5use chrono::{DateTime, Utc};
6use regex::Regex;
7use once_cell::sync::Lazy;
8use std::collections::HashMap;
9
10static RULE_REGEX: Lazy<Regex> = Lazy::new(|| {
12 Regex::new(r#"rule\s+(?:"([^"]+)"|([a-zA-Z_]\w*))\s*([^{]*)\{(.+)\}"#)
13 .expect("Invalid rule regex pattern")
14});
15
16static RULE_SPLIT_REGEX: Lazy<Regex> = Lazy::new(|| {
17 Regex::new(r#"(?s)rule\s+(?:"[^"]+"|[a-zA-Z_]\w*).*?\}"#)
18 .expect("Invalid rule split regex pattern")
19});
20
21static DEFMODULE_REGEX: Lazy<Regex> = Lazy::new(|| {
23 Regex::new(r#"defmodule\s+([A-Z_]\w*)\s*\{([^}]*)\}"#)
24 .expect("Invalid defmodule regex pattern")
25});
26
27static DEFMODULE_SPLIT_REGEX: Lazy<Regex> = Lazy::new(|| {
28 Regex::new(r#"(?s)defmodule\s+[A-Z_]\w*\s*\{[^}]*\}"#)
29 .expect("Invalid defmodule split regex pattern")
30});
31
32static WHEN_THEN_REGEX: Lazy<Regex> = Lazy::new(|| {
33 Regex::new(r"when\s+(.+?)\s+then\s+(.+)")
34 .expect("Invalid when-then regex pattern")
35});
36
37static SALIENCE_REGEX: Lazy<Regex> = Lazy::new(|| {
38 Regex::new(r"salience\s+(\d+)")
39 .expect("Invalid salience regex pattern")
40});
41
42static TEST_CONDITION_REGEX: Lazy<Regex> = Lazy::new(|| {
43 Regex::new(r#"^test\s*\(\s*([a-zA-Z_]\w*)\s*\(([^)]*)\)\s*\)$"#)
44 .expect("Invalid test condition regex")
45});
46
47static TYPED_TEST_CONDITION_REGEX: Lazy<Regex> = Lazy::new(|| {
48 Regex::new(r#"\$(\w+)\s*:\s*(\w+)\s*\(\s*(.+?)\s*\)"#)
49 .expect("Invalid typed test condition regex")
50});
51
52static FUNCTION_CALL_REGEX: Lazy<Regex> = Lazy::new(|| {
53 Regex::new(r#"([a-zA-Z_]\w*)\s*\(([^)]*)\)\s*(>=|<=|==|!=|>|<|contains|matches)\s*(.+)"#)
54 .expect("Invalid function call regex")
55});
56
57static CONDITION_REGEX: Lazy<Regex> = Lazy::new(|| {
58 Regex::new(r#"([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*(?:\s*[+\-*/%]\s*[a-zA-Z0-9_\.]+)*)\s*(>=|<=|==|!=|>|<|contains|matches)\s*(.+)"#)
59 .expect("Invalid condition regex")
60});
61
62static METHOD_CALL_REGEX: Lazy<Regex> = Lazy::new(|| {
63 Regex::new(r#"\$(\w+)\.(\w+)\s*\(([^)]*)\)"#)
64 .expect("Invalid method call regex")
65});
66
67static FUNCTION_BINDING_REGEX: Lazy<Regex> = Lazy::new(|| {
68 Regex::new(r#"(\w+)\s*\(\s*(.+?)?\s*\)"#)
69 .expect("Invalid function binding regex")
70});
71
72static MULTIFIELD_COLLECT_REGEX: Lazy<Regex> = Lazy::new(|| {
74 Regex::new(r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+(\$\?[a-zA-Z_]\w*)$"#)
75 .expect("Invalid multifield collect regex")
76});
77
78static MULTIFIELD_COUNT_REGEX: Lazy<Regex> = Lazy::new(|| {
79 Regex::new(r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+count\s*(>=|<=|==|!=|>|<)\s*(.+)$"#)
80 .expect("Invalid multifield count regex")
81});
82
83static MULTIFIELD_FIRST_REGEX: Lazy<Regex> = Lazy::new(|| {
84 Regex::new(r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+first(?:\s+(\$[a-zA-Z_]\w*))?$"#)
85 .expect("Invalid multifield first regex")
86});
87
88static MULTIFIELD_LAST_REGEX: Lazy<Regex> = Lazy::new(|| {
89 Regex::new(r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+last(?:\s+(\$[a-zA-Z_]\w*))?$"#)
90 .expect("Invalid multifield last regex")
91});
92
93static MULTIFIELD_EMPTY_REGEX: Lazy<Regex> = Lazy::new(|| {
94 Regex::new(r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+empty$"#)
95 .expect("Invalid multifield empty regex")
96});
97
98static MULTIFIELD_NOT_EMPTY_REGEX: Lazy<Regex> = Lazy::new(|| {
99 Regex::new(r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+not_empty$"#)
100 .expect("Invalid multifield not_empty regex")
101});
102
103static SIMPLE_CONDITION_REGEX: Lazy<Regex> = Lazy::new(|| {
104 Regex::new(r#"(\w+)\s*(>=|<=|==|!=|>|<)\s*(.+)"#)
105 .expect("Invalid simple condition regex")
106});
107
108pub struct GRLParser;
111
112#[derive(Debug, Default)]
114struct RuleAttributes {
115 pub no_loop: bool,
116 pub lock_on_active: bool,
117 pub agenda_group: Option<String>,
118 pub activation_group: Option<String>,
119 pub date_effective: Option<DateTime<Utc>>,
120 pub date_expires: Option<DateTime<Utc>>,
121}
122
123#[derive(Debug, Clone)]
125pub struct ParsedGRL {
126 pub rules: Vec<Rule>,
128 pub module_manager: ModuleManager,
130 pub rule_modules: HashMap<String, String>,
132}
133
134impl ParsedGRL {
135 pub fn new() -> Self {
136 Self {
137 rules: Vec::new(),
138 module_manager: ModuleManager::new(),
139 rule_modules: HashMap::new(),
140 }
141 }
142}
143
144impl GRLParser {
145 pub fn parse_rule(grl_text: &str) -> Result<Rule> {
158 let mut parser = GRLParser;
159 parser.parse_single_rule(grl_text)
160 }
161
162 pub fn parse_rules(grl_text: &str) -> Result<Vec<Rule>> {
164 let mut parser = GRLParser;
165 parser.parse_multiple_rules(grl_text)
166 }
167
168 pub fn parse_with_modules(grl_text: &str) -> Result<ParsedGRL> {
186 let mut parser = GRLParser;
187 parser.parse_grl_with_modules(grl_text)
188 }
189
190 fn parse_grl_with_modules(&mut self, grl_text: &str) -> Result<ParsedGRL> {
191 let mut result = ParsedGRL::new();
192
193 for module_match in DEFMODULE_SPLIT_REGEX.find_iter(grl_text) {
195 let module_def = module_match.as_str();
196 self.parse_and_register_module(module_def, &mut result.module_manager)?;
197 }
198
199 let rules_text = DEFMODULE_SPLIT_REGEX.replace_all(grl_text, "");
201
202 let rules = self.parse_multiple_rules(&rules_text)?;
204
205 for rule in rules {
207 let module_name = self.extract_module_from_context(grl_text, &rule.name);
208 result.rule_modules.insert(rule.name.clone(), module_name.clone());
209
210 if let Ok(module) = result.module_manager.get_module_mut(&module_name) {
212 module.add_rule(&rule.name);
213 }
214
215 result.rules.push(rule);
216 }
217
218 Ok(result)
219 }
220
221 fn parse_and_register_module(&self, module_def: &str, manager: &mut ModuleManager) -> Result<()> {
222 if let Some(captures) = DEFMODULE_REGEX.captures(module_def) {
224 let module_name = captures.get(1).unwrap().as_str().to_string();
225 let module_body = captures.get(2).unwrap().as_str();
226
227 let _ = manager.create_module(&module_name);
229 let module = manager.get_module_mut(&module_name)?;
230
231 if let Some(export_type) = self.extract_directive(module_body, "export:") {
233 let exports = if export_type.trim() == "all" {
234 ExportList::All
235 } else if export_type.trim() == "none" {
236 ExportList::None
237 } else {
238 ExportList::Specific(vec![
240 ExportItem {
241 item_type: ItemType::All,
242 pattern: export_type.trim().to_string(),
243 }
244 ])
245 };
246 module.set_exports(exports);
247 }
248
249 let import_lines: Vec<&str> = module_body
251 .lines()
252 .filter(|line| line.trim().starts_with("import:"))
253 .collect();
254
255 for import_line in import_lines {
256 if let Some(import_spec) = self.extract_directive(import_line, "import:") {
257 self.parse_import_spec(&module_name, &import_spec, manager)?;
259 }
260 }
261 }
262
263 Ok(())
264 }
265
266 fn extract_directive(&self, text: &str, directive: &str) -> Option<String> {
267 if let Some(pos) = text.find(directive) {
268 let after_directive = &text[pos + directive.len()..];
269
270 let end = after_directive
272 .find("import:")
273 .or_else(|| after_directive.find("export:"))
274 .unwrap_or(after_directive.len());
275
276 Some(after_directive[..end].trim().to_string())
277 } else {
278 None
279 }
280 }
281
282 fn parse_import_spec(&self, importing_module: &str, spec: &str, manager: &mut ModuleManager) -> Result<()> {
283 let parts: Vec<&str> = spec.splitn(2, '(').collect();
285 if parts.is_empty() {
286 return Ok(());
287 }
288
289 let source_module = parts[0].trim().to_string();
290 let rest = if parts.len() > 1 { parts[1] } else { "" };
291
292 if rest.contains("rules") {
294 manager.import_from(
295 importing_module,
296 &source_module,
297 ImportType::AllRules,
298 "*",
299 )?;
300 }
301
302 if rest.contains("templates") {
303 manager.import_from(
304 importing_module,
305 &source_module,
306 ImportType::AllTemplates,
307 "*",
308 )?;
309 }
310
311 Ok(())
312 }
313
314 fn extract_module_from_context(&self, grl_text: &str, rule_name: &str) -> String {
315 if let Some(rule_pos) = grl_text.find(&format!("rule \"{}\"", rule_name))
317 .or_else(|| grl_text.find(&format!("rule {}", rule_name)))
318 {
319 let before = &grl_text[..rule_pos];
321 if let Some(module_pos) = before.rfind(";; MODULE:") {
322 let after_module_marker = &before[module_pos + 10..];
323 if let Some(end_of_line) = after_module_marker.find('\n') {
324 let module_line = &after_module_marker[..end_of_line].trim();
325 if let Some(first_word) = module_line.split_whitespace().next() {
327 return first_word.to_string();
328 }
329 }
330 }
331 }
332
333 "MAIN".to_string()
335 }
336
337
338 fn parse_single_rule(&mut self, grl_text: &str) -> Result<Rule> {
339 let cleaned = self.clean_text(grl_text);
340
341 let captures =
343 RULE_REGEX
344 .captures(&cleaned)
345 .ok_or_else(|| RuleEngineError::ParseError {
346 message: format!("Invalid GRL rule format. Input: {}", cleaned),
347 })?;
348
349 let rule_name = if let Some(quoted_name) = captures.get(1) {
351 quoted_name.as_str().to_string()
352 } else if let Some(unquoted_name) = captures.get(2) {
353 unquoted_name.as_str().to_string()
354 } else {
355 return Err(RuleEngineError::ParseError {
356 message: "Could not extract rule name".to_string(),
357 });
358 };
359
360 let attributes_section = captures.get(3).map(|m| m.as_str()).unwrap_or("");
362
363 let rule_body = captures.get(4).unwrap().as_str();
365
366 let salience = self.extract_salience(attributes_section)?;
368
369 let when_then_captures =
371 WHEN_THEN_REGEX
372 .captures(rule_body)
373 .ok_or_else(|| RuleEngineError::ParseError {
374 message: "Missing when or then clause".to_string(),
375 })?;
376
377 let when_clause = when_then_captures.get(1).unwrap().as_str().trim();
378 let then_clause = when_then_captures.get(2).unwrap().as_str().trim();
379
380 let conditions = self.parse_when_clause(when_clause)?;
382 let actions = self.parse_then_clause(then_clause)?;
383
384 let attributes = self.parse_rule_attributes(attributes_section)?;
386
387 let mut rule = Rule::new(rule_name, conditions, actions);
389 rule = rule.with_priority(salience);
390
391 if attributes.no_loop {
393 rule = rule.with_no_loop(true);
394 }
395 if attributes.lock_on_active {
396 rule = rule.with_lock_on_active(true);
397 }
398 if let Some(agenda_group) = attributes.agenda_group {
399 rule = rule.with_agenda_group(agenda_group);
400 }
401 if let Some(activation_group) = attributes.activation_group {
402 rule = rule.with_activation_group(activation_group);
403 }
404 if let Some(date_effective) = attributes.date_effective {
405 rule = rule.with_date_effective(date_effective);
406 }
407 if let Some(date_expires) = attributes.date_expires {
408 rule = rule.with_date_expires(date_expires);
409 }
410
411 Ok(rule)
412 }
413
414 fn parse_multiple_rules(&mut self, grl_text: &str) -> Result<Vec<Rule>> {
415 let mut rules = Vec::new();
418
419 for rule_match in RULE_SPLIT_REGEX.find_iter(grl_text) {
420 let rule_text = rule_match.as_str();
421 let rule = self.parse_single_rule(rule_text)?;
422 rules.push(rule);
423 }
424
425 Ok(rules)
426 }
427
428 fn parse_rule_attributes(&self, rule_header: &str) -> Result<RuleAttributes> {
430 let mut attributes = RuleAttributes::default();
431
432 let mut attrs_section = rule_header.to_string();
436
437 let quoted_regex = Regex::new(r#""[^"]*""#).map_err(|e| RuleEngineError::ParseError {
439 message: format!("Invalid quoted string regex: {}", e),
440 })?;
441 attrs_section = quoted_regex.replace_all(&attrs_section, "").to_string();
442
443 if let Some(rule_pos) = attrs_section.find("rule") {
445 let after_rule = &attrs_section[rule_pos + 4..];
447 if let Some(first_keyword) = after_rule.find("salience")
448 .or_else(|| after_rule.find("no-loop"))
449 .or_else(|| after_rule.find("lock-on-active"))
450 .or_else(|| after_rule.find("agenda-group"))
451 .or_else(|| after_rule.find("activation-group"))
452 .or_else(|| after_rule.find("date-effective"))
453 .or_else(|| after_rule.find("date-expires"))
454 {
455 attrs_section = after_rule[first_keyword..].to_string();
456 }
457 }
458
459 let no_loop_regex = Regex::new(r"\bno-loop\b").map_err(|e| RuleEngineError::ParseError {
461 message: format!("Invalid no-loop regex: {}", e),
462 })?;
463 let lock_on_active_regex = Regex::new(r"\block-on-active\b").map_err(|e| RuleEngineError::ParseError {
464 message: format!("Invalid lock-on-active regex: {}", e),
465 })?;
466
467 if no_loop_regex.is_match(&attrs_section) {
468 attributes.no_loop = true;
469 }
470 if lock_on_active_regex.is_match(&attrs_section) {
471 attributes.lock_on_active = true;
472 }
473
474 if let Some(agenda_group) = self.extract_quoted_attribute(rule_header, "agenda-group")? {
476 attributes.agenda_group = Some(agenda_group);
477 }
478
479 if let Some(activation_group) =
481 self.extract_quoted_attribute(rule_header, "activation-group")?
482 {
483 attributes.activation_group = Some(activation_group);
484 }
485
486 if let Some(date_str) = self.extract_quoted_attribute(rule_header, "date-effective")? {
488 attributes.date_effective = Some(self.parse_date_string(&date_str)?);
489 }
490
491 if let Some(date_str) = self.extract_quoted_attribute(rule_header, "date-expires")? {
493 attributes.date_expires = Some(self.parse_date_string(&date_str)?);
494 }
495
496 Ok(attributes)
497 }
498
499 fn extract_quoted_attribute(&self, header: &str, attribute: &str) -> Result<Option<String>> {
501 let pattern = format!(r#"{}\s+"([^"]+)""#, attribute);
502 let regex = Regex::new(&pattern).map_err(|e| RuleEngineError::ParseError {
503 message: format!("Invalid attribute regex for {}: {}", attribute, e),
504 })?;
505
506 if let Some(captures) = regex.captures(header) {
507 if let Some(value) = captures.get(1) {
508 return Ok(Some(value.as_str().to_string()));
509 }
510 }
511
512 Ok(None)
513 }
514
515 fn parse_date_string(&self, date_str: &str) -> Result<DateTime<Utc>> {
517 if let Ok(date) = DateTime::parse_from_rfc3339(date_str) {
519 return Ok(date.with_timezone(&Utc));
520 }
521
522 let formats = ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%d-%b-%Y", "%d-%m-%Y"];
524
525 for format in &formats {
526 if let Ok(naive_date) = chrono::NaiveDateTime::parse_from_str(date_str, format) {
527 return Ok(naive_date.and_utc());
528 }
529 if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(date_str, format) {
530 return Ok(naive_date.and_hms_opt(0, 0, 0).unwrap().and_utc());
531 }
532 }
533
534 Err(RuleEngineError::ParseError {
535 message: format!("Unable to parse date: {}", date_str),
536 })
537 }
538
539 fn extract_salience(&self, attributes_section: &str) -> Result<i32> {
541 if let Some(captures) = SALIENCE_REGEX.captures(attributes_section) {
542 if let Some(salience_match) = captures.get(1) {
543 return salience_match.as_str().parse::<i32>().map_err(|e| {
544 RuleEngineError::ParseError {
545 message: format!("Invalid salience value: {}", e),
546 }
547 });
548 }
549 }
550
551 Ok(0) }
553
554 fn clean_text(&self, text: &str) -> String {
555 text.lines()
556 .map(|line| line.trim())
557 .filter(|line| !line.is_empty() && !line.starts_with("//"))
558 .collect::<Vec<_>>()
559 .join(" ")
560 }
561
562 fn parse_when_clause(&self, when_clause: &str) -> Result<ConditionGroup> {
563 let trimmed = when_clause.trim();
565
566 let clause = if trimmed.starts_with('(') && trimmed.ends_with(')') {
568 let inner = &trimmed[1..trimmed.len() - 1];
570 if self.is_balanced_parentheses(inner) {
571 inner
572 } else {
573 trimmed
574 }
575 } else {
576 trimmed
577 };
578
579 if let Some(parts) = self.split_logical_operator(clause, "||") {
581 return self.parse_or_parts(parts);
582 }
583
584 if let Some(parts) = self.split_logical_operator(clause, "&&") {
586 return self.parse_and_parts(parts);
587 }
588
589 if clause.trim_start().starts_with("!") {
591 return self.parse_not_condition(clause);
592 }
593
594 if clause.trim_start().starts_with("exists(") {
596 return self.parse_exists_condition(clause);
597 }
598
599 if clause.trim_start().starts_with("forall(") {
601 return self.parse_forall_condition(clause);
602 }
603
604 if clause.trim_start().starts_with("accumulate(") {
606 return self.parse_accumulate_condition(clause);
607 }
608
609 self.parse_single_condition(clause)
611 }
612
613 fn is_balanced_parentheses(&self, text: &str) -> bool {
614 let mut count = 0;
615 for ch in text.chars() {
616 match ch {
617 '(' => count += 1,
618 ')' => {
619 count -= 1;
620 if count < 0 {
621 return false;
622 }
623 }
624 _ => {}
625 }
626 }
627 count == 0
628 }
629
630 fn split_logical_operator(&self, clause: &str, operator: &str) -> Option<Vec<String>> {
631 let mut parts = Vec::new();
632 let mut current_part = String::new();
633 let mut paren_count = 0;
634 let mut chars = clause.chars().peekable();
635
636 while let Some(ch) = chars.next() {
637 match ch {
638 '(' => {
639 paren_count += 1;
640 current_part.push(ch);
641 }
642 ')' => {
643 paren_count -= 1;
644 current_part.push(ch);
645 }
646 '&' if operator == "&&" && paren_count == 0 => {
647 if chars.peek() == Some(&'&') {
648 chars.next(); parts.push(current_part.trim().to_string());
650 current_part.clear();
651 } else {
652 current_part.push(ch);
653 }
654 }
655 '|' if operator == "||" && paren_count == 0 => {
656 if chars.peek() == Some(&'|') {
657 chars.next(); parts.push(current_part.trim().to_string());
659 current_part.clear();
660 } else {
661 current_part.push(ch);
662 }
663 }
664 _ => {
665 current_part.push(ch);
666 }
667 }
668 }
669
670 if !current_part.trim().is_empty() {
671 parts.push(current_part.trim().to_string());
672 }
673
674 if parts.len() > 1 {
675 Some(parts)
676 } else {
677 None
678 }
679 }
680
681 fn parse_or_parts(&self, parts: Vec<String>) -> Result<ConditionGroup> {
682 let mut conditions = Vec::new();
683 for part in parts {
684 let condition = self.parse_when_clause(&part)?;
685 conditions.push(condition);
686 }
687
688 if conditions.is_empty() {
689 return Err(RuleEngineError::ParseError {
690 message: "No conditions found in OR".to_string(),
691 });
692 }
693
694 let mut iter = conditions.into_iter();
695 let mut result = iter.next().unwrap();
696 for condition in iter {
697 result = ConditionGroup::or(result, condition);
698 }
699
700 Ok(result)
701 }
702
703 fn parse_and_parts(&self, parts: Vec<String>) -> Result<ConditionGroup> {
704 let mut conditions = Vec::new();
705 for part in parts {
706 let condition = self.parse_when_clause(&part)?;
707 conditions.push(condition);
708 }
709
710 if conditions.is_empty() {
711 return Err(RuleEngineError::ParseError {
712 message: "No conditions found in AND".to_string(),
713 });
714 }
715
716 let mut iter = conditions.into_iter();
717 let mut result = iter.next().unwrap();
718 for condition in iter {
719 result = ConditionGroup::and(result, condition);
720 }
721
722 Ok(result)
723 }
724
725 fn parse_not_condition(&self, clause: &str) -> Result<ConditionGroup> {
726 let inner_clause = clause.strip_prefix("!").unwrap().trim();
727 let inner_condition = self.parse_when_clause(inner_clause)?;
728 Ok(ConditionGroup::not(inner_condition))
729 }
730
731 fn parse_exists_condition(&self, clause: &str) -> Result<ConditionGroup> {
732 let clause = clause.trim_start();
733 if !clause.starts_with("exists(") || !clause.ends_with(")") {
734 return Err(RuleEngineError::ParseError {
735 message: "Invalid exists syntax. Expected: exists(condition)".to_string(),
736 });
737 }
738
739 let inner_clause = &clause[7..clause.len() - 1]; let inner_condition = self.parse_when_clause(inner_clause)?;
742 Ok(ConditionGroup::exists(inner_condition))
743 }
744
745 fn parse_forall_condition(&self, clause: &str) -> Result<ConditionGroup> {
746 let clause = clause.trim_start();
747 if !clause.starts_with("forall(") || !clause.ends_with(")") {
748 return Err(RuleEngineError::ParseError {
749 message: "Invalid forall syntax. Expected: forall(condition)".to_string(),
750 });
751 }
752
753 let inner_clause = &clause[7..clause.len() - 1]; let inner_condition = self.parse_when_clause(inner_clause)?;
756 Ok(ConditionGroup::forall(inner_condition))
757 }
758
759 fn parse_accumulate_condition(&self, clause: &str) -> Result<ConditionGroup> {
760 let clause = clause.trim_start();
761 if !clause.starts_with("accumulate(") || !clause.ends_with(")") {
762 return Err(RuleEngineError::ParseError {
763 message: "Invalid accumulate syntax. Expected: accumulate(pattern, function)".to_string(),
764 });
765 }
766
767 let inner = &clause[11..clause.len() - 1]; let parts = self.split_accumulate_parts(inner)?;
772
773 if parts.len() != 2 {
774 return Err(RuleEngineError::ParseError {
775 message: format!(
776 "Invalid accumulate syntax. Expected 2 parts (pattern, function), got {}",
777 parts.len()
778 ),
779 });
780 }
781
782 let pattern_part = parts[0].trim();
783 let function_part = parts[1].trim();
784
785 let (source_pattern, extract_field, source_conditions) =
787 self.parse_accumulate_pattern(pattern_part)?;
788
789 let (function, function_arg) = self.parse_accumulate_function(function_part)?;
791
792 let result_var = "$result".to_string();
796
797 Ok(ConditionGroup::accumulate(
798 result_var,
799 source_pattern,
800 extract_field,
801 source_conditions,
802 function,
803 function_arg,
804 ))
805 }
806
807 fn split_accumulate_parts(&self, content: &str) -> Result<Vec<String>> {
808 let mut parts = Vec::new();
809 let mut current = String::new();
810 let mut paren_depth = 0;
811
812 for ch in content.chars() {
813 match ch {
814 '(' => {
815 paren_depth += 1;
816 current.push(ch);
817 }
818 ')' => {
819 paren_depth -= 1;
820 current.push(ch);
821 }
822 ',' if paren_depth == 0 => {
823 parts.push(current.trim().to_string());
824 current.clear();
825 }
826 _ => {
827 current.push(ch);
828 }
829 }
830 }
831
832 if !current.trim().is_empty() {
833 parts.push(current.trim().to_string());
834 }
835
836 Ok(parts)
837 }
838
839 fn parse_accumulate_pattern(&self, pattern: &str) -> Result<(String, String, Vec<String>)> {
840 let pattern = pattern.trim();
847
848 let paren_pos = pattern.find('(').ok_or_else(|| RuleEngineError::ParseError {
850 message: format!("Invalid accumulate pattern: missing '(' in '{}'", pattern),
851 })?;
852
853 let source_pattern = pattern[..paren_pos].trim().to_string();
854
855 if !pattern.ends_with(')') {
857 return Err(RuleEngineError::ParseError {
858 message: format!("Invalid accumulate pattern: missing ')' in '{}'", pattern),
859 });
860 }
861
862 let inner = &pattern[paren_pos + 1..pattern.len() - 1];
863
864 let parts = self.split_pattern_parts(inner)?;
866
867 let mut extract_field = String::new();
868 let mut source_conditions = Vec::new();
869
870 for part in parts {
871 let part = part.trim();
872
873 if part.contains(':') && part.starts_with('$') {
875 let colon_pos = part.find(':').unwrap();
876 let _var_name = part[..colon_pos].trim();
877 let field_name = part[colon_pos + 1..].trim();
878 extract_field = field_name.to_string();
879 } else if part.contains("==") || part.contains("!=") ||
880 part.contains(">=") || part.contains("<=") ||
881 part.contains('>') || part.contains('<') {
882 source_conditions.push(part.to_string());
884 }
885 }
886
887 Ok((source_pattern, extract_field, source_conditions))
888 }
889
890 fn split_pattern_parts(&self, content: &str) -> Result<Vec<String>> {
891 let mut parts = Vec::new();
892 let mut current = String::new();
893 let mut paren_depth = 0;
894 let mut in_quotes = false;
895 let mut quote_char = ' ';
896
897 for ch in content.chars() {
898 match ch {
899 '"' | '\'' if !in_quotes => {
900 in_quotes = true;
901 quote_char = ch;
902 current.push(ch);
903 }
904 '"' | '\'' if in_quotes && ch == quote_char => {
905 in_quotes = false;
906 current.push(ch);
907 }
908 '(' if !in_quotes => {
909 paren_depth += 1;
910 current.push(ch);
911 }
912 ')' if !in_quotes => {
913 paren_depth -= 1;
914 current.push(ch);
915 }
916 ',' if !in_quotes && paren_depth == 0 => {
917 parts.push(current.trim().to_string());
918 current.clear();
919 }
920 _ => {
921 current.push(ch);
922 }
923 }
924 }
925
926 if !current.trim().is_empty() {
927 parts.push(current.trim().to_string());
928 }
929
930 Ok(parts)
931 }
932
933 fn parse_accumulate_function(&self, function_str: &str) -> Result<(String, String)> {
934 let function_str = function_str.trim();
937
938 let paren_pos = function_str.find('(').ok_or_else(|| RuleEngineError::ParseError {
939 message: format!("Invalid accumulate function: missing '(' in '{}'", function_str),
940 })?;
941
942 let function_name = function_str[..paren_pos].trim().to_string();
943
944 if !function_str.ends_with(')') {
945 return Err(RuleEngineError::ParseError {
946 message: format!("Invalid accumulate function: missing ')' in '{}'", function_str),
947 });
948 }
949
950 let args = &function_str[paren_pos + 1..function_str.len() - 1];
951 let function_arg = args.trim().to_string();
952
953 Ok((function_name, function_arg))
954 }
955
956 fn parse_single_condition(&self, clause: &str) -> Result<ConditionGroup> {
957 let trimmed_clause = clause.trim();
959 let clause_to_parse = if trimmed_clause.starts_with('(') && trimmed_clause.ends_with(')') {
960 trimmed_clause[1..trimmed_clause.len() - 1].trim()
961 } else {
962 trimmed_clause
963 };
964
965 if let Some(captures) = MULTIFIELD_COLLECT_REGEX.captures(clause_to_parse) {
972 let field = captures.get(1).unwrap().as_str().to_string();
973 let variable = captures.get(2).unwrap().as_str().to_string();
974
975 let condition = Condition::with_multifield_collect(field, variable);
978 return Ok(ConditionGroup::single(condition));
979 }
980
981 if let Some(captures) = MULTIFIELD_COUNT_REGEX.captures(clause_to_parse) {
988 let field = captures.get(1).unwrap().as_str().to_string();
989 let operator_str = captures.get(2).unwrap().as_str();
990 let value_str = captures.get(3).unwrap().as_str().trim();
991
992 let operator = Operator::from_str(operator_str)
993 .ok_or_else(|| RuleEngineError::InvalidOperator {
994 operator: operator_str.to_string(),
995 })?;
996
997 let value = self.parse_value(value_str)?;
998
999 let condition = Condition::with_multifield_count(field, operator, value);
1000 return Ok(ConditionGroup::single(condition));
1001 }
1002
1003 if let Some(captures) = MULTIFIELD_FIRST_REGEX.captures(clause_to_parse) {
1006 let field = captures.get(1).unwrap().as_str().to_string();
1007 let variable = captures.get(2).map(|m| m.as_str().to_string());
1008
1009 let condition = Condition::with_multifield_first(field, variable);
1010 return Ok(ConditionGroup::single(condition));
1011 }
1012
1013 if let Some(captures) = MULTIFIELD_LAST_REGEX.captures(clause_to_parse) {
1016 let field = captures.get(1).unwrap().as_str().to_string();
1017 let variable = captures.get(2).map(|m| m.as_str().to_string());
1018
1019 let condition = Condition::with_multifield_last(field, variable);
1020 return Ok(ConditionGroup::single(condition));
1021 }
1022
1023 if let Some(captures) = MULTIFIELD_EMPTY_REGEX.captures(clause_to_parse) {
1026 let field = captures.get(1).unwrap().as_str().to_string();
1027
1028 let condition = Condition::with_multifield_empty(field);
1029 return Ok(ConditionGroup::single(condition));
1030 }
1031
1032 if let Some(captures) = MULTIFIELD_NOT_EMPTY_REGEX.captures(clause_to_parse) {
1035 let field = captures.get(1).unwrap().as_str().to_string();
1036
1037 let condition = Condition::with_multifield_not_empty(field);
1038 return Ok(ConditionGroup::single(condition));
1039 }
1040
1041 if let Some(captures) = TEST_CONDITION_REGEX.captures(clause_to_parse) {
1046 let function_name = captures.get(1).unwrap().as_str().to_string();
1047 let args_str = captures.get(2).unwrap().as_str();
1048
1049 let args: Vec<String> = if args_str.trim().is_empty() {
1051 Vec::new()
1052 } else {
1053 args_str
1054 .split(',')
1055 .map(|arg| arg.trim().to_string())
1056 .collect()
1057 };
1058
1059 let condition = Condition::with_test(function_name, args);
1060 return Ok(ConditionGroup::single(condition));
1061 }
1062
1063 if let Some(captures) = TYPED_TEST_CONDITION_REGEX.captures(clause_to_parse) {
1065 let _object_name = captures.get(1).unwrap().as_str();
1066 let _object_type = captures.get(2).unwrap().as_str();
1067 let conditions_str = captures.get(3).unwrap().as_str();
1068
1069 return self.parse_conditions_within_object(conditions_str);
1071 }
1072
1073 if let Some(captures) = FUNCTION_CALL_REGEX.captures(clause_to_parse) {
1075 let function_name = captures.get(1).unwrap().as_str().to_string();
1076 let args_str = captures.get(2).unwrap().as_str();
1077 let operator_str = captures.get(3).unwrap().as_str();
1078 let value_str = captures.get(4).unwrap().as_str().trim();
1079
1080 let args: Vec<String> = if args_str.trim().is_empty() {
1082 Vec::new()
1083 } else {
1084 args_str
1085 .split(',')
1086 .map(|arg| arg.trim().to_string())
1087 .collect()
1088 };
1089
1090 let operator =
1091 Operator::from_str(operator_str).ok_or_else(|| RuleEngineError::InvalidOperator {
1092 operator: operator_str.to_string(),
1093 })?;
1094
1095 let value = self.parse_value(value_str)?;
1096
1097 let condition = Condition::with_function(function_name, args, operator, value);
1098 return Ok(ConditionGroup::single(condition));
1099 }
1100
1101 let captures = CONDITION_REGEX.captures(clause_to_parse).ok_or_else(|| {
1105 RuleEngineError::ParseError {
1106 message: format!("Invalid condition format: {}", clause_to_parse),
1107 }
1108 })?;
1109
1110 let left_side = captures.get(1).unwrap().as_str().trim().to_string();
1111 let operator_str = captures.get(2).unwrap().as_str();
1112 let value_str = captures.get(3).unwrap().as_str().trim();
1113
1114 let operator =
1115 Operator::from_str(operator_str).ok_or_else(|| RuleEngineError::InvalidOperator {
1116 operator: operator_str.to_string(),
1117 })?;
1118
1119 let value = self.parse_value(value_str)?;
1120
1121 if left_side.contains('+') || left_side.contains('-') || left_side.contains('*')
1123 || left_side.contains('/') || left_side.contains('%') {
1124 let test_expr = format!("{} {} {}", left_side, operator_str, value_str);
1127 let condition = Condition::with_test(test_expr, vec![]);
1128 Ok(ConditionGroup::single(condition))
1129 } else {
1130 let condition = Condition::new(left_side, operator, value);
1132 Ok(ConditionGroup::single(condition))
1133 }
1134 }
1135
1136 fn parse_conditions_within_object(&self, conditions_str: &str) -> Result<ConditionGroup> {
1137 let parts: Vec<&str> = conditions_str.split("&&").collect();
1139
1140 let mut conditions = Vec::new();
1141 for part in parts {
1142 let trimmed = part.trim();
1143 let condition = self.parse_simple_condition(trimmed)?;
1144 conditions.push(condition);
1145 }
1146
1147 if conditions.is_empty() {
1149 return Err(RuleEngineError::ParseError {
1150 message: "No conditions found".to_string(),
1151 });
1152 }
1153
1154 let mut iter = conditions.into_iter();
1155 let mut result = iter.next().unwrap();
1156 for condition in iter {
1157 result = ConditionGroup::and(result, condition);
1158 }
1159
1160 Ok(result)
1161 }
1162
1163 fn parse_simple_condition(&self, clause: &str) -> Result<ConditionGroup> {
1164 let captures =
1166 SIMPLE_CONDITION_REGEX
1167 .captures(clause)
1168 .ok_or_else(|| RuleEngineError::ParseError {
1169 message: format!("Invalid simple condition format: {}", clause),
1170 })?;
1171
1172 let field = captures.get(1).unwrap().as_str().to_string();
1173 let operator_str = captures.get(2).unwrap().as_str();
1174 let value_str = captures.get(3).unwrap().as_str().trim();
1175
1176 let operator =
1177 Operator::from_str(operator_str).ok_or_else(|| RuleEngineError::InvalidOperator {
1178 operator: operator_str.to_string(),
1179 })?;
1180
1181 let value = self.parse_value(value_str)?;
1182
1183 let condition = Condition::new(field, operator, value);
1184 Ok(ConditionGroup::single(condition))
1185 }
1186
1187 fn parse_value(&self, value_str: &str) -> Result<Value> {
1188 let trimmed = value_str.trim();
1189
1190 if (trimmed.starts_with('"') && trimmed.ends_with('"'))
1192 || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
1193 {
1194 let unquoted = &trimmed[1..trimmed.len() - 1];
1195 return Ok(Value::String(unquoted.to_string()));
1196 }
1197
1198 if trimmed.eq_ignore_ascii_case("true") {
1200 return Ok(Value::Boolean(true));
1201 }
1202 if trimmed.eq_ignore_ascii_case("false") {
1203 return Ok(Value::Boolean(false));
1204 }
1205
1206 if trimmed.eq_ignore_ascii_case("null") {
1208 return Ok(Value::Null);
1209 }
1210
1211 if let Ok(int_val) = trimmed.parse::<i64>() {
1213 return Ok(Value::Integer(int_val));
1214 }
1215
1216 if let Ok(float_val) = trimmed.parse::<f64>() {
1217 return Ok(Value::Number(float_val));
1218 }
1219
1220 if self.is_expression(trimmed) {
1223 return Ok(Value::Expression(trimmed.to_string()));
1224 }
1225
1226 if trimmed.contains('.') {
1228 return Ok(Value::String(trimmed.to_string()));
1229 }
1230
1231 if self.is_identifier(trimmed) {
1235 return Ok(Value::Expression(trimmed.to_string()));
1236 }
1237
1238 Ok(Value::String(trimmed.to_string()))
1240 }
1241
1242 fn is_identifier(&self, s: &str) -> bool {
1245 if s.is_empty() {
1246 return false;
1247 }
1248
1249 let first_char = s.chars().next().unwrap();
1251 if !first_char.is_alphabetic() && first_char != '_' {
1252 return false;
1253 }
1254
1255 s.chars().all(|c| c.is_alphanumeric() || c == '_')
1257 }
1258
1259 fn is_expression(&self, s: &str) -> bool {
1261 let has_operator = s.contains('+') || s.contains('-') || s.contains('*') || s.contains('/') || s.contains('%');
1263
1264 let has_field_ref = s.contains('.');
1266
1267 let has_spaces = s.contains(' ');
1269
1270 has_operator && (has_field_ref || has_spaces)
1272 }
1273
1274 fn parse_then_clause(&self, then_clause: &str) -> Result<Vec<ActionType>> {
1275 let statements: Vec<&str> = then_clause
1276 .split(';')
1277 .map(|s| s.trim())
1278 .filter(|s| !s.is_empty())
1279 .collect();
1280
1281 let mut actions = Vec::new();
1282
1283 for statement in statements {
1284 let action = self.parse_action_statement(statement)?;
1285 actions.push(action);
1286 }
1287
1288 Ok(actions)
1289 }
1290
1291 fn parse_action_statement(&self, statement: &str) -> Result<ActionType> {
1292 let trimmed = statement.trim();
1293
1294 if let Some(captures) = METHOD_CALL_REGEX.captures(trimmed) {
1296 let object = captures.get(1).unwrap().as_str().to_string();
1297 let method = captures.get(2).unwrap().as_str().to_string();
1298 let args_str = captures.get(3).unwrap().as_str();
1299
1300 let args = if args_str.trim().is_empty() {
1301 Vec::new()
1302 } else {
1303 self.parse_method_args(args_str)?
1304 };
1305
1306 return Ok(ActionType::MethodCall {
1307 object,
1308 method,
1309 args,
1310 });
1311 }
1312
1313 if let Some(eq_pos) = trimmed.find('=') {
1315 let field = trimmed[..eq_pos].trim().to_string();
1316 let value_str = trimmed[eq_pos + 1..].trim();
1317 let value = self.parse_value(value_str)?;
1318
1319 return Ok(ActionType::Set { field, value });
1320 }
1321
1322 if let Some(captures) = FUNCTION_BINDING_REGEX.captures(trimmed) {
1324 let function_name = captures.get(1).unwrap().as_str();
1325 let args_str = captures.get(2).map(|m| m.as_str()).unwrap_or("");
1326
1327 match function_name.to_lowercase().as_str() {
1328 "retract" => {
1329 let object_name = if let Some(stripped) = args_str.strip_prefix('$') {
1331 stripped.to_string()
1332 } else {
1333 args_str.to_string()
1334 };
1335 Ok(ActionType::Retract {
1336 object: object_name,
1337 })
1338 }
1339 "log" => {
1340 let message = if args_str.is_empty() {
1341 "Log message".to_string()
1342 } else {
1343 let value = self.parse_value(args_str.trim())?;
1344 value.to_string()
1345 };
1346 Ok(ActionType::Log { message })
1347 }
1348 "activateagendagroup" | "activate_agenda_group" => {
1349 let agenda_group = if args_str.is_empty() {
1350 return Err(RuleEngineError::ParseError {
1351 message: "ActivateAgendaGroup requires agenda group name".to_string(),
1352 });
1353 } else {
1354 let value = self.parse_value(args_str.trim())?;
1355 match value {
1356 Value::String(s) => s,
1357 _ => value.to_string(),
1358 }
1359 };
1360 Ok(ActionType::ActivateAgendaGroup {
1361 group: agenda_group,
1362 })
1363 }
1364 "schedulerule" | "schedule_rule" => {
1365 let parts: Vec<&str> = args_str.split(',').collect();
1367 if parts.len() != 2 {
1368 return Err(RuleEngineError::ParseError {
1369 message: "ScheduleRule requires delay_ms and rule_name".to_string(),
1370 });
1371 }
1372
1373 let delay_ms = self.parse_value(parts[0].trim())?;
1374 let rule_name = self.parse_value(parts[1].trim())?;
1375
1376 let delay_ms = match delay_ms {
1377 Value::Integer(i) => i as u64,
1378 Value::Number(f) => f as u64,
1379 _ => {
1380 return Err(RuleEngineError::ParseError {
1381 message: "ScheduleRule delay_ms must be a number".to_string(),
1382 })
1383 }
1384 };
1385
1386 let rule_name = match rule_name {
1387 Value::String(s) => s,
1388 _ => rule_name.to_string(),
1389 };
1390
1391 Ok(ActionType::ScheduleRule {
1392 delay_ms,
1393 rule_name,
1394 })
1395 }
1396 "completeworkflow" | "complete_workflow" => {
1397 let workflow_id = if args_str.is_empty() {
1398 return Err(RuleEngineError::ParseError {
1399 message: "CompleteWorkflow requires workflow_id".to_string(),
1400 });
1401 } else {
1402 let value = self.parse_value(args_str.trim())?;
1403 match value {
1404 Value::String(s) => s,
1405 _ => value.to_string(),
1406 }
1407 };
1408 Ok(ActionType::CompleteWorkflow {
1409 workflow_name: workflow_id,
1410 })
1411 }
1412 "setworkflowdata" | "set_workflow_data" => {
1413 let data_str = args_str.trim();
1415
1416 let (key, value) = if let Some(eq_pos) = data_str.find('=') {
1418 let key = data_str[..eq_pos].trim().trim_matches('"');
1419 let value_str = data_str[eq_pos + 1..].trim();
1420 let value = self.parse_value(value_str)?;
1421 (key.to_string(), value)
1422 } else {
1423 return Err(RuleEngineError::ParseError {
1424 message: "SetWorkflowData data must be in key=value format".to_string(),
1425 });
1426 };
1427
1428 Ok(ActionType::SetWorkflowData { key, value })
1429 }
1430 _ => {
1431 let params = if args_str.is_empty() {
1433 HashMap::new()
1434 } else {
1435 self.parse_function_args_as_params(args_str)?
1436 };
1437
1438 Ok(ActionType::Custom {
1439 action_type: function_name.to_string(),
1440 params,
1441 })
1442 }
1443 }
1444 } else {
1445 Ok(ActionType::Custom {
1447 action_type: "statement".to_string(),
1448 params: {
1449 let mut params = HashMap::new();
1450 params.insert("statement".to_string(), Value::String(trimmed.to_string()));
1451 params
1452 },
1453 })
1454 }
1455 }
1456
1457 fn parse_method_args(&self, args_str: &str) -> Result<Vec<Value>> {
1458 if args_str.trim().is_empty() {
1459 return Ok(Vec::new());
1460 }
1461
1462 let mut args = Vec::new();
1464 let parts: Vec<&str> = args_str.split(',').collect();
1465
1466 for part in parts {
1467 let trimmed = part.trim();
1468
1469 if trimmed.contains('+')
1471 || trimmed.contains('-')
1472 || trimmed.contains('*')
1473 || trimmed.contains('/')
1474 {
1475 args.push(Value::String(trimmed.to_string()));
1477 } else {
1478 args.push(self.parse_value(trimmed)?);
1479 }
1480 }
1481
1482 Ok(args)
1483 }
1484
1485 fn parse_function_args_as_params(&self, args_str: &str) -> Result<HashMap<String, Value>> {
1487 let mut params = HashMap::new();
1488
1489 if args_str.trim().is_empty() {
1490 return Ok(params);
1491 }
1492
1493 let parts: Vec<&str> = args_str.split(',').collect();
1495 for (i, part) in parts.iter().enumerate() {
1496 let trimmed = part.trim();
1497 let value = self.parse_value(trimmed)?;
1498
1499 params.insert(i.to_string(), value);
1501 }
1502
1503 Ok(params)
1504 }
1505}
1506
1507#[cfg(test)]
1508mod tests {
1509 use super::GRLParser;
1510
1511 #[test]
1512 fn test_parse_simple_rule() {
1513 let grl = r#"
1514 rule "CheckAge" salience 10 {
1515 when
1516 User.Age >= 18
1517 then
1518 log("User is adult");
1519 }
1520 "#;
1521
1522 let rules = GRLParser::parse_rules(grl).unwrap();
1523 assert_eq!(rules.len(), 1);
1524 let rule = &rules[0];
1525 assert_eq!(rule.name, "CheckAge");
1526 assert_eq!(rule.salience, 10);
1527 assert_eq!(rule.actions.len(), 1);
1528 }
1529
1530 #[test]
1531 fn test_parse_complex_condition() {
1532 let grl = r#"
1533 rule "ComplexRule" {
1534 when
1535 User.Age >= 18 && User.Country == "US"
1536 then
1537 User.Qualified = true;
1538 }
1539 "#;
1540
1541 let rules = GRLParser::parse_rules(grl).unwrap();
1542 assert_eq!(rules.len(), 1);
1543 let rule = &rules[0];
1544 assert_eq!(rule.name, "ComplexRule");
1545 }
1546
1547 #[test]
1548 fn test_parse_new_syntax_with_parentheses() {
1549 let grl = r#"
1550 rule "Default Rule" salience 10 {
1551 when
1552 (user.age >= 18)
1553 then
1554 set(user.status, "approved");
1555 }
1556 "#;
1557
1558 let rules = GRLParser::parse_rules(grl).unwrap();
1559 assert_eq!(rules.len(), 1);
1560 let rule = &rules[0];
1561 assert_eq!(rule.name, "Default Rule");
1562 assert_eq!(rule.salience, 10);
1563 assert_eq!(rule.actions.len(), 1);
1564
1565 match &rule.actions[0] {
1567 crate::types::ActionType::Custom {
1568 action_type,
1569 params,
1570 } => {
1571 assert_eq!(action_type, "set");
1572 assert_eq!(
1573 params.get("0"),
1574 Some(&crate::types::Value::String("user.status".to_string()))
1575 );
1576 assert_eq!(
1577 params.get("1"),
1578 Some(&crate::types::Value::String("approved".to_string()))
1579 );
1580 }
1581 _ => panic!("Expected Custom action, got: {:?}", rule.actions[0]),
1582 }
1583 }
1584
1585 #[test]
1586 fn test_parse_complex_nested_conditions() {
1587 let grl = r#"
1588 rule "Complex Business Rule" salience 10 {
1589 when
1590 (((user.vipStatus == true) && (order.amount > 500)) || ((date.isHoliday == true) && (order.hasCoupon == true)))
1591 then
1592 apply_discount(20000);
1593 }
1594 "#;
1595
1596 let rules = GRLParser::parse_rules(grl).unwrap();
1597 assert_eq!(rules.len(), 1);
1598 let rule = &rules[0];
1599 assert_eq!(rule.name, "Complex Business Rule");
1600 assert_eq!(rule.salience, 10);
1601 assert_eq!(rule.actions.len(), 1);
1602
1603 match &rule.actions[0] {
1605 crate::types::ActionType::Custom {
1606 action_type,
1607 params,
1608 } => {
1609 assert_eq!(action_type, "apply_discount");
1610 assert_eq!(params.get("0"), Some(&crate::types::Value::Integer(20000)));
1611 }
1612 _ => panic!("Expected Custom action, got: {:?}", rule.actions[0]),
1613 }
1614 }
1615
1616 #[test]
1617 fn test_parse_no_loop_attribute() {
1618 let grl = r#"
1619 rule "NoLoopRule" no-loop salience 15 {
1620 when
1621 User.Score < 100
1622 then
1623 set(User.Score, User.Score + 10);
1624 }
1625 "#;
1626
1627 let rules = GRLParser::parse_rules(grl).unwrap();
1628 assert_eq!(rules.len(), 1);
1629 let rule = &rules[0];
1630 assert_eq!(rule.name, "NoLoopRule");
1631 assert_eq!(rule.salience, 15);
1632 assert!(rule.no_loop, "Rule should have no-loop=true");
1633 }
1634
1635 #[test]
1636 fn test_parse_no_loop_different_positions() {
1637 let grl1 = r#"
1639 rule "Rule1" no-loop salience 10 {
1640 when User.Age >= 18
1641 then log("adult");
1642 }
1643 "#;
1644
1645 let grl2 = r#"
1647 rule "Rule2" salience 10 no-loop {
1648 when User.Age >= 18
1649 then log("adult");
1650 }
1651 "#;
1652
1653 let rules1 = GRLParser::parse_rules(grl1).unwrap();
1654 let rules2 = GRLParser::parse_rules(grl2).unwrap();
1655
1656 assert_eq!(rules1.len(), 1);
1657 assert_eq!(rules2.len(), 1);
1658
1659 assert!(rules1[0].no_loop, "Rule1 should have no-loop=true");
1660 assert!(rules2[0].no_loop, "Rule2 should have no-loop=true");
1661
1662 assert_eq!(rules1[0].salience, 10);
1663 assert_eq!(rules2[0].salience, 10);
1664 }
1665
1666 #[test]
1667 fn test_parse_without_no_loop() {
1668 let grl = r#"
1669 rule "RegularRule" salience 5 {
1670 when
1671 User.Active == true
1672 then
1673 log("active user");
1674 }
1675 "#;
1676
1677 let rules = GRLParser::parse_rules(grl).unwrap();
1678 assert_eq!(rules.len(), 1);
1679 let rule = &rules[0];
1680 assert_eq!(rule.name, "RegularRule");
1681 assert!(!rule.no_loop, "Rule should have no-loop=false by default");
1682 }
1683
1684 #[test]
1685 fn test_parse_exists_pattern() {
1686 let grl = r#"
1687 rule "ExistsRule" salience 20 {
1688 when
1689 exists(Customer.tier == "VIP")
1690 then
1691 System.premiumActive = true;
1692 }
1693 "#;
1694
1695 let rules = GRLParser::parse_rules(grl).unwrap();
1696 assert_eq!(rules.len(), 1);
1697 let rule = &rules[0];
1698 assert_eq!(rule.name, "ExistsRule");
1699 assert_eq!(rule.salience, 20);
1700
1701 match &rule.conditions {
1703 crate::engine::rule::ConditionGroup::Exists(_) => {
1704 }
1706 _ => panic!(
1707 "Expected EXISTS condition group, got: {:?}",
1708 rule.conditions
1709 ),
1710 }
1711 }
1712
1713 #[test]
1714 fn test_parse_forall_pattern() {
1715 let grl = r#"
1716 rule "ForallRule" salience 15 {
1717 when
1718 forall(Order.status == "processed")
1719 then
1720 Shipping.enabled = true;
1721 }
1722 "#;
1723
1724 let rules = GRLParser::parse_rules(grl).unwrap();
1725 assert_eq!(rules.len(), 1);
1726 let rule = &rules[0];
1727 assert_eq!(rule.name, "ForallRule");
1728
1729 match &rule.conditions {
1731 crate::engine::rule::ConditionGroup::Forall(_) => {
1732 }
1734 _ => panic!(
1735 "Expected FORALL condition group, got: {:?}",
1736 rule.conditions
1737 ),
1738 }
1739 }
1740
1741 #[test]
1742 fn test_parse_combined_patterns() {
1743 let grl = r#"
1744 rule "CombinedRule" salience 25 {
1745 when
1746 exists(Customer.tier == "VIP") && !exists(Alert.priority == "high")
1747 then
1748 System.vipMode = true;
1749 }
1750 "#;
1751
1752 let rules = GRLParser::parse_rules(grl).unwrap();
1753 assert_eq!(rules.len(), 1);
1754 let rule = &rules[0];
1755 assert_eq!(rule.name, "CombinedRule");
1756
1757 match &rule.conditions {
1759 crate::engine::rule::ConditionGroup::Compound {
1760 left,
1761 operator,
1762 right,
1763 } => {
1764 assert_eq!(*operator, crate::types::LogicalOperator::And);
1765
1766 match left.as_ref() {
1768 crate::engine::rule::ConditionGroup::Exists(_) => {
1769 }
1771 _ => panic!("Expected EXISTS in left side, got: {:?}", left),
1772 }
1773
1774 match right.as_ref() {
1776 crate::engine::rule::ConditionGroup::Not(inner) => {
1777 match inner.as_ref() {
1778 crate::engine::rule::ConditionGroup::Exists(_) => {
1779 }
1781 _ => panic!("Expected EXISTS inside NOT, got: {:?}", inner),
1782 }
1783 }
1784 _ => panic!("Expected NOT in right side, got: {:?}", right),
1785 }
1786 }
1787 _ => panic!("Expected compound condition, got: {:?}", rule.conditions),
1788 }
1789 }
1790}