rust_rule_engine/parser/
grl.rs

1use crate::engine::rule::{Condition, ConditionGroup, Rule};
2use crate::errors::{Result, RuleEngineError};
3use crate::types::{ActionType, Operator, Value};
4use chrono::{DateTime, Utc};
5use regex::Regex;
6use std::collections::HashMap;
7
8/// GRL (Grule Rule Language) Parser
9/// Parses Grule-like syntax into Rule objects
10pub struct GRLParser;
11
12/// Parsed rule attributes from GRL header
13#[derive(Debug, Default)]
14struct RuleAttributes {
15    pub no_loop: bool,
16    pub lock_on_active: bool,
17    pub agenda_group: Option<String>,
18    pub activation_group: Option<String>,
19    pub date_effective: Option<DateTime<Utc>>,
20    pub date_expires: Option<DateTime<Utc>>,
21}
22
23impl GRLParser {
24    /// Parse a single rule from GRL syntax
25    ///
26    /// Example GRL syntax:
27    /// ```grl
28    /// rule CheckAge "Age verification rule" salience 10 {
29    ///     when
30    ///         User.Age >= 18 && User.Country == "US"
31    ///     then
32    ///         User.IsAdult = true;
33    ///         Retract("User");
34    /// }
35    /// ```
36    pub fn parse_rule(grl_text: &str) -> Result<Rule> {
37        let mut parser = GRLParser;
38        parser.parse_single_rule(grl_text)
39    }
40
41    /// Parse multiple rules from GRL text
42    pub fn parse_rules(grl_text: &str) -> Result<Vec<Rule>> {
43        let mut parser = GRLParser;
44        parser.parse_multiple_rules(grl_text)
45    }
46
47    fn parse_single_rule(&mut self, grl_text: &str) -> Result<Rule> {
48        let cleaned = self.clean_text(grl_text);
49
50        // Extract rule components using regex - support various attributes
51        let rule_regex = Regex::new(r#"rule\s+(?:"([^"]+)"|([a-zA-Z_]\w*))\s*([^{]*)\{(.+)\}"#)
52            .map_err(|e| RuleEngineError::ParseError {
53                message: format!("Invalid rule regex: {}", e),
54            })?;
55
56        let captures =
57            rule_regex
58                .captures(&cleaned)
59                .ok_or_else(|| RuleEngineError::ParseError {
60                    message: format!("Invalid GRL rule format. Input: {}", cleaned),
61                })?;
62
63        // Rule name can be either quoted (group 1) or unquoted (group 2)
64        let rule_name = if let Some(quoted_name) = captures.get(1) {
65            quoted_name.as_str().to_string()
66        } else if let Some(unquoted_name) = captures.get(2) {
67            unquoted_name.as_str().to_string()
68        } else {
69            return Err(RuleEngineError::ParseError {
70                message: "Could not extract rule name".to_string(),
71            });
72        };
73
74        // Attributes section (group 3)
75        let attributes_section = captures.get(3).map(|m| m.as_str()).unwrap_or("");
76
77        // Rule body (group 4)
78        let rule_body = captures.get(4).unwrap().as_str();
79
80        // Parse salience from attributes section
81        let salience = self.extract_salience(attributes_section)?;
82
83        // Parse when and then sections
84        let when_then_regex =
85            Regex::new(r"when\s+(.+?)\s+then\s+(.+)").map_err(|e| RuleEngineError::ParseError {
86                message: format!("Invalid when-then regex: {}", e),
87            })?;
88
89        let when_then_captures =
90            when_then_regex
91                .captures(rule_body)
92                .ok_or_else(|| RuleEngineError::ParseError {
93                    message: "Missing when or then clause".to_string(),
94                })?;
95
96        let when_clause = when_then_captures.get(1).unwrap().as_str().trim();
97        let then_clause = when_then_captures.get(2).unwrap().as_str().trim();
98
99        // Parse conditions and actions
100        let conditions = self.parse_when_clause(when_clause)?;
101        let actions = self.parse_then_clause(then_clause)?;
102
103        // Parse all attributes from rule header
104        let attributes = self.parse_rule_attributes(attributes_section)?;
105
106        // Build rule
107        let mut rule = Rule::new(rule_name, conditions, actions);
108        rule = rule.with_priority(salience);
109
110        // Apply parsed attributes
111        if attributes.no_loop {
112            rule = rule.with_no_loop(true);
113        }
114        if attributes.lock_on_active {
115            rule = rule.with_lock_on_active(true);
116        }
117        if let Some(agenda_group) = attributes.agenda_group {
118            rule = rule.with_agenda_group(agenda_group);
119        }
120        if let Some(activation_group) = attributes.activation_group {
121            rule = rule.with_activation_group(activation_group);
122        }
123        if let Some(date_effective) = attributes.date_effective {
124            rule = rule.with_date_effective(date_effective);
125        }
126        if let Some(date_expires) = attributes.date_expires {
127            rule = rule.with_date_expires(date_expires);
128        }
129
130        Ok(rule)
131    }
132
133    fn parse_multiple_rules(&mut self, grl_text: &str) -> Result<Vec<Rule>> {
134        // Split by rule boundaries - support both quoted and unquoted rule names
135        // Use DOTALL flag to match newlines in rule body
136        let rule_regex =
137            Regex::new(r#"(?s)rule\s+(?:"[^"]+"|[a-zA-Z_]\w*).*?\}"#).map_err(|e| {
138                RuleEngineError::ParseError {
139                    message: format!("Rule splitting regex error: {}", e),
140                }
141            })?;
142
143        let mut rules = Vec::new();
144
145        for rule_match in rule_regex.find_iter(grl_text) {
146            let rule_text = rule_match.as_str();
147            let rule = self.parse_single_rule(rule_text)?;
148            rules.push(rule);
149        }
150
151        Ok(rules)
152    }
153
154    /// Parse rule attributes from the rule header
155    fn parse_rule_attributes(&self, rule_header: &str) -> Result<RuleAttributes> {
156        let mut attributes = RuleAttributes::default();
157
158        // Check for simple boolean attributes
159        if rule_header.contains("no-loop") {
160            attributes.no_loop = true;
161        }
162        if rule_header.contains("lock-on-active") {
163            attributes.lock_on_active = true;
164        }
165
166        // Parse agenda-group attribute
167        if let Some(agenda_group) = self.extract_quoted_attribute(rule_header, "agenda-group")? {
168            attributes.agenda_group = Some(agenda_group);
169        }
170
171        // Parse activation-group attribute
172        if let Some(activation_group) =
173            self.extract_quoted_attribute(rule_header, "activation-group")?
174        {
175            attributes.activation_group = Some(activation_group);
176        }
177
178        // Parse date-effective attribute
179        if let Some(date_str) = self.extract_quoted_attribute(rule_header, "date-effective")? {
180            attributes.date_effective = Some(self.parse_date_string(&date_str)?);
181        }
182
183        // Parse date-expires attribute
184        if let Some(date_str) = self.extract_quoted_attribute(rule_header, "date-expires")? {
185            attributes.date_expires = Some(self.parse_date_string(&date_str)?);
186        }
187
188        Ok(attributes)
189    }
190
191    /// Extract quoted attribute value from rule header
192    fn extract_quoted_attribute(&self, header: &str, attribute: &str) -> Result<Option<String>> {
193        let pattern = format!(r#"{}\s+"([^"]+)""#, attribute);
194        let regex = Regex::new(&pattern).map_err(|e| RuleEngineError::ParseError {
195            message: format!("Invalid attribute regex for {}: {}", attribute, e),
196        })?;
197
198        if let Some(captures) = regex.captures(header) {
199            if let Some(value) = captures.get(1) {
200                return Ok(Some(value.as_str().to_string()));
201            }
202        }
203
204        Ok(None)
205    }
206
207    /// Parse date string in various formats
208    fn parse_date_string(&self, date_str: &str) -> Result<DateTime<Utc>> {
209        // Try ISO 8601 format first
210        if let Ok(date) = DateTime::parse_from_rfc3339(date_str) {
211            return Ok(date.with_timezone(&Utc));
212        }
213
214        // Try simple date formats
215        let formats = ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%d-%b-%Y", "%d-%m-%Y"];
216
217        for format in &formats {
218            if let Ok(naive_date) = chrono::NaiveDateTime::parse_from_str(date_str, format) {
219                return Ok(naive_date.and_utc());
220            }
221            if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(date_str, format) {
222                return Ok(naive_date.and_hms_opt(0, 0, 0).unwrap().and_utc());
223            }
224        }
225
226        Err(RuleEngineError::ParseError {
227            message: format!("Unable to parse date: {}", date_str),
228        })
229    }
230
231    /// Extract salience value from attributes section
232    fn extract_salience(&self, attributes_section: &str) -> Result<i32> {
233        let salience_regex =
234            Regex::new(r"salience\s+(\d+)").map_err(|e| RuleEngineError::ParseError {
235                message: format!("Invalid salience regex: {}", e),
236            })?;
237
238        if let Some(captures) = salience_regex.captures(attributes_section) {
239            if let Some(salience_match) = captures.get(1) {
240                return salience_match.as_str().parse::<i32>().map_err(|e| {
241                    RuleEngineError::ParseError {
242                        message: format!("Invalid salience value: {}", e),
243                    }
244                });
245            }
246        }
247
248        Ok(0) // Default salience
249    }
250
251    fn clean_text(&self, text: &str) -> String {
252        text.lines()
253            .map(|line| line.trim())
254            .filter(|line| !line.is_empty() && !line.starts_with("//"))
255            .collect::<Vec<_>>()
256            .join(" ")
257    }
258
259    fn parse_when_clause(&self, when_clause: &str) -> Result<ConditionGroup> {
260        // Handle logical operators with proper parentheses support
261        let trimmed = when_clause.trim();
262
263        // Strip outer parentheses if they exist
264        let clause = if trimmed.starts_with('(') && trimmed.ends_with(')') {
265            // Check if these are the outermost parentheses
266            let inner = &trimmed[1..trimmed.len() - 1];
267            if self.is_balanced_parentheses(inner) {
268                inner
269            } else {
270                trimmed
271            }
272        } else {
273            trimmed
274        };
275
276        // Parse OR at the top level (lowest precedence)
277        if let Some(parts) = self.split_logical_operator(clause, "||") {
278            return self.parse_or_parts(parts);
279        }
280
281        // Parse AND (higher precedence)
282        if let Some(parts) = self.split_logical_operator(clause, "&&") {
283            return self.parse_and_parts(parts);
284        }
285
286        // Handle NOT condition
287        if clause.trim_start().starts_with("!") {
288            return self.parse_not_condition(clause);
289        }
290
291        // Handle EXISTS condition
292        if clause.trim_start().starts_with("exists(") {
293            return self.parse_exists_condition(clause);
294        }
295
296        // Handle FORALL condition
297        if clause.trim_start().starts_with("forall(") {
298            return self.parse_forall_condition(clause);
299        }
300
301        // Handle ACCUMULATE condition
302        if clause.trim_start().starts_with("accumulate(") {
303            return self.parse_accumulate_condition(clause);
304        }
305
306        // Single condition
307        self.parse_single_condition(clause)
308    }
309
310    fn is_balanced_parentheses(&self, text: &str) -> bool {
311        let mut count = 0;
312        for ch in text.chars() {
313            match ch {
314                '(' => count += 1,
315                ')' => {
316                    count -= 1;
317                    if count < 0 {
318                        return false;
319                    }
320                }
321                _ => {}
322            }
323        }
324        count == 0
325    }
326
327    fn split_logical_operator(&self, clause: &str, operator: &str) -> Option<Vec<String>> {
328        let mut parts = Vec::new();
329        let mut current_part = String::new();
330        let mut paren_count = 0;
331        let mut chars = clause.chars().peekable();
332
333        while let Some(ch) = chars.next() {
334            match ch {
335                '(' => {
336                    paren_count += 1;
337                    current_part.push(ch);
338                }
339                ')' => {
340                    paren_count -= 1;
341                    current_part.push(ch);
342                }
343                '&' if operator == "&&" && paren_count == 0 => {
344                    if chars.peek() == Some(&'&') {
345                        chars.next(); // consume second &
346                        parts.push(current_part.trim().to_string());
347                        current_part.clear();
348                    } else {
349                        current_part.push(ch);
350                    }
351                }
352                '|' if operator == "||" && paren_count == 0 => {
353                    if chars.peek() == Some(&'|') {
354                        chars.next(); // consume second |
355                        parts.push(current_part.trim().to_string());
356                        current_part.clear();
357                    } else {
358                        current_part.push(ch);
359                    }
360                }
361                _ => {
362                    current_part.push(ch);
363                }
364            }
365        }
366
367        if !current_part.trim().is_empty() {
368            parts.push(current_part.trim().to_string());
369        }
370
371        if parts.len() > 1 {
372            Some(parts)
373        } else {
374            None
375        }
376    }
377
378    fn parse_or_parts(&self, parts: Vec<String>) -> Result<ConditionGroup> {
379        let mut conditions = Vec::new();
380        for part in parts {
381            let condition = self.parse_when_clause(&part)?;
382            conditions.push(condition);
383        }
384
385        if conditions.is_empty() {
386            return Err(RuleEngineError::ParseError {
387                message: "No conditions found in OR".to_string(),
388            });
389        }
390
391        let mut iter = conditions.into_iter();
392        let mut result = iter.next().unwrap();
393        for condition in iter {
394            result = ConditionGroup::or(result, condition);
395        }
396
397        Ok(result)
398    }
399
400    fn parse_and_parts(&self, parts: Vec<String>) -> Result<ConditionGroup> {
401        let mut conditions = Vec::new();
402        for part in parts {
403            let condition = self.parse_when_clause(&part)?;
404            conditions.push(condition);
405        }
406
407        if conditions.is_empty() {
408            return Err(RuleEngineError::ParseError {
409                message: "No conditions found in AND".to_string(),
410            });
411        }
412
413        let mut iter = conditions.into_iter();
414        let mut result = iter.next().unwrap();
415        for condition in iter {
416            result = ConditionGroup::and(result, condition);
417        }
418
419        Ok(result)
420    }
421
422    fn parse_not_condition(&self, clause: &str) -> Result<ConditionGroup> {
423        let inner_clause = clause.strip_prefix("!").unwrap().trim();
424        let inner_condition = self.parse_when_clause(inner_clause)?;
425        Ok(ConditionGroup::not(inner_condition))
426    }
427
428    fn parse_exists_condition(&self, clause: &str) -> Result<ConditionGroup> {
429        let clause = clause.trim_start();
430        if !clause.starts_with("exists(") || !clause.ends_with(")") {
431            return Err(RuleEngineError::ParseError {
432                message: "Invalid exists syntax. Expected: exists(condition)".to_string(),
433            });
434        }
435
436        // Extract content between parentheses
437        let inner_clause = &clause[7..clause.len() - 1]; // Remove "exists(" and ")"
438        let inner_condition = self.parse_when_clause(inner_clause)?;
439        Ok(ConditionGroup::exists(inner_condition))
440    }
441
442    fn parse_forall_condition(&self, clause: &str) -> Result<ConditionGroup> {
443        let clause = clause.trim_start();
444        if !clause.starts_with("forall(") || !clause.ends_with(")") {
445            return Err(RuleEngineError::ParseError {
446                message: "Invalid forall syntax. Expected: forall(condition)".to_string(),
447            });
448        }
449
450        // Extract content between parentheses
451        let inner_clause = &clause[7..clause.len() - 1]; // Remove "forall(" and ")"
452        let inner_condition = self.parse_when_clause(inner_clause)?;
453        Ok(ConditionGroup::forall(inner_condition))
454    }
455
456    fn parse_accumulate_condition(&self, clause: &str) -> Result<ConditionGroup> {
457        let clause = clause.trim_start();
458        if !clause.starts_with("accumulate(") || !clause.ends_with(")") {
459            return Err(RuleEngineError::ParseError {
460                message: "Invalid accumulate syntax. Expected: accumulate(pattern, function)".to_string(),
461            });
462        }
463
464        // Extract content between parentheses
465        let inner = &clause[11..clause.len() - 1]; // Remove "accumulate(" and ")"
466
467        // Split by comma at the top level (not inside parentheses)
468        let parts = self.split_accumulate_parts(inner)?;
469
470        if parts.len() != 2 {
471            return Err(RuleEngineError::ParseError {
472                message: format!(
473                    "Invalid accumulate syntax. Expected 2 parts (pattern, function), got {}",
474                    parts.len()
475                ),
476            });
477        }
478
479        let pattern_part = parts[0].trim();
480        let function_part = parts[1].trim();
481
482        // Parse the pattern: Order($amount: amount, status == "completed")
483        let (source_pattern, extract_field, source_conditions) =
484            self.parse_accumulate_pattern(pattern_part)?;
485
486        // Parse the function: sum($amount)
487        let (function, function_arg) = self.parse_accumulate_function(function_part)?;
488
489        // For now, we'll create a placeholder result variable
490        // In a full implementation, this would be extracted from the parent context
491        // e.g., from "$total: accumulate(...)"
492        let result_var = "$result".to_string();
493
494        Ok(ConditionGroup::accumulate(
495            result_var,
496            source_pattern,
497            extract_field,
498            source_conditions,
499            function,
500            function_arg,
501        ))
502    }
503
504    fn split_accumulate_parts(&self, content: &str) -> Result<Vec<String>> {
505        let mut parts = Vec::new();
506        let mut current = String::new();
507        let mut paren_depth = 0;
508
509        for ch in content.chars() {
510            match ch {
511                '(' => {
512                    paren_depth += 1;
513                    current.push(ch);
514                }
515                ')' => {
516                    paren_depth -= 1;
517                    current.push(ch);
518                }
519                ',' if paren_depth == 0 => {
520                    parts.push(current.trim().to_string());
521                    current.clear();
522                }
523                _ => {
524                    current.push(ch);
525                }
526            }
527        }
528
529        if !current.trim().is_empty() {
530            parts.push(current.trim().to_string());
531        }
532
533        Ok(parts)
534    }
535
536    fn parse_accumulate_pattern(&self, pattern: &str) -> Result<(String, String, Vec<String>)> {
537        // Pattern format: Order($amount: amount, status == "completed", category == "electronics")
538        // We need to extract:
539        // - source_pattern: "Order"
540        // - extract_field: "amount" (from $amount: amount)
541        // - source_conditions: ["status == \"completed\"", "category == \"electronics\""]
542
543        let pattern = pattern.trim();
544
545        // Find the opening parenthesis to get the pattern type
546        let paren_pos = pattern.find('(').ok_or_else(|| RuleEngineError::ParseError {
547            message: format!("Invalid accumulate pattern: missing '(' in '{}'", pattern),
548        })?;
549
550        let source_pattern = pattern[..paren_pos].trim().to_string();
551
552        // Extract content between parentheses
553        if !pattern.ends_with(')') {
554            return Err(RuleEngineError::ParseError {
555                message: format!("Invalid accumulate pattern: missing ')' in '{}'", pattern),
556            });
557        }
558
559        let inner = &pattern[paren_pos + 1..pattern.len() - 1];
560
561        // Split by comma (respecting nested parentheses and quotes)
562        let parts = self.split_pattern_parts(inner)?;
563
564        let mut extract_field = String::new();
565        let mut source_conditions = Vec::new();
566
567        for part in parts {
568            let part = part.trim();
569
570            // Check if this is a variable binding: $var: field
571            if part.contains(':') && part.starts_with('$') {
572                let colon_pos = part.find(':').unwrap();
573                let _var_name = part[..colon_pos].trim();
574                let field_name = part[colon_pos + 1..].trim();
575                extract_field = field_name.to_string();
576            } else if part.contains("==") || part.contains("!=") ||
577                      part.contains(">=") || part.contains("<=") ||
578                      part.contains('>') || part.contains('<') {
579                // This is a condition
580                source_conditions.push(part.to_string());
581            }
582        }
583
584        Ok((source_pattern, extract_field, source_conditions))
585    }
586
587    fn split_pattern_parts(&self, content: &str) -> Result<Vec<String>> {
588        let mut parts = Vec::new();
589        let mut current = String::new();
590        let mut paren_depth = 0;
591        let mut in_quotes = false;
592        let mut quote_char = ' ';
593
594        for ch in content.chars() {
595            match ch {
596                '"' | '\'' if !in_quotes => {
597                    in_quotes = true;
598                    quote_char = ch;
599                    current.push(ch);
600                }
601                '"' | '\'' if in_quotes && ch == quote_char => {
602                    in_quotes = false;
603                    current.push(ch);
604                }
605                '(' if !in_quotes => {
606                    paren_depth += 1;
607                    current.push(ch);
608                }
609                ')' if !in_quotes => {
610                    paren_depth -= 1;
611                    current.push(ch);
612                }
613                ',' if !in_quotes && paren_depth == 0 => {
614                    parts.push(current.trim().to_string());
615                    current.clear();
616                }
617                _ => {
618                    current.push(ch);
619                }
620            }
621        }
622
623        if !current.trim().is_empty() {
624            parts.push(current.trim().to_string());
625        }
626
627        Ok(parts)
628    }
629
630    fn parse_accumulate_function(&self, function_str: &str) -> Result<(String, String)> {
631        // Function format: sum($amount) or count() or average($price)
632
633        let function_str = function_str.trim();
634
635        let paren_pos = function_str.find('(').ok_or_else(|| RuleEngineError::ParseError {
636            message: format!("Invalid accumulate function: missing '(' in '{}'", function_str),
637        })?;
638
639        let function_name = function_str[..paren_pos].trim().to_string();
640
641        if !function_str.ends_with(')') {
642            return Err(RuleEngineError::ParseError {
643                message: format!("Invalid accumulate function: missing ')' in '{}'", function_str),
644            });
645        }
646
647        let args = &function_str[paren_pos + 1..function_str.len() - 1];
648        let function_arg = args.trim().to_string();
649
650        Ok((function_name, function_arg))
651    }
652
653    fn parse_single_condition(&self, clause: &str) -> Result<ConditionGroup> {
654        // Remove outer parentheses if they exist (handle new syntax like "(user.age >= 18)")
655        let trimmed_clause = clause.trim();
656        let clause_to_parse = if trimmed_clause.starts_with('(') && trimmed_clause.ends_with(')') {
657            trimmed_clause[1..trimmed_clause.len() - 1].trim()
658        } else {
659            trimmed_clause
660        };
661
662        // === MULTI-FIELD PATTERNS ===
663        // Handle multi-field patterns before other patterns
664        // These must be checked first to avoid conflict with standard patterns
665
666        // Pattern 1: Field.array $?var (Collect operation with variable binding)
667        // Example: Order.items $?all_items
668        let multifield_collect_regex = Regex::new(r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+(\$\?[a-zA-Z_]\w*)$"#)
669            .map_err(|e| RuleEngineError::ParseError {
670                message: format!("Multifield collect regex error: {}", e),
671            })?;
672
673        if let Some(captures) = multifield_collect_regex.captures(clause_to_parse) {
674            let field = captures.get(1).unwrap().as_str().to_string();
675            let variable = captures.get(2).unwrap().as_str().to_string();
676
677            // Create a multifield Collect condition
678            // Note: This will need to be handled by the engine
679            let condition = Condition::with_multifield_collect(field, variable);
680            return Ok(ConditionGroup::single(condition));
681        }
682
683        // Pattern 2: Field.array contains "value"
684        // Example: Product.tags contains "electronics"
685        // This is already handled by the standard regex, but we need to distinguish array contains
686
687        // Pattern 3: Field.array count operator value
688        // Example: Order.items count > 0, Order.items count >= 5
689        let multifield_count_regex = Regex::new(
690            r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+count\s*(>=|<=|==|!=|>|<)\s*(.+)$"#
691        ).map_err(|e| RuleEngineError::ParseError {
692            message: format!("Multifield count regex error: {}", e),
693        })?;
694
695        if let Some(captures) = multifield_count_regex.captures(clause_to_parse) {
696            let field = captures.get(1).unwrap().as_str().to_string();
697            let operator_str = captures.get(2).unwrap().as_str();
698            let value_str = captures.get(3).unwrap().as_str().trim();
699
700            let operator = Operator::from_str(operator_str)
701                .ok_or_else(|| RuleEngineError::InvalidOperator {
702                    operator: operator_str.to_string(),
703                })?;
704
705            let value = self.parse_value(value_str)?;
706
707            let condition = Condition::with_multifield_count(field, operator, value);
708            return Ok(ConditionGroup::single(condition));
709        }
710
711        // Pattern 4: Field.array first [optional: $var or operator value]
712        // Example: Queue.tasks first, Queue.tasks first $first_task
713        let multifield_first_regex = Regex::new(
714            r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+first(?:\s+(\$[a-zA-Z_]\w*))?$"#
715        ).map_err(|e| RuleEngineError::ParseError {
716            message: format!("Multifield first regex error: {}", e),
717        })?;
718
719        if let Some(captures) = multifield_first_regex.captures(clause_to_parse) {
720            let field = captures.get(1).unwrap().as_str().to_string();
721            let variable = captures.get(2).map(|m| m.as_str().to_string());
722
723            let condition = Condition::with_multifield_first(field, variable);
724            return Ok(ConditionGroup::single(condition));
725        }
726
727        // Pattern 5: Field.array last [optional: $var]
728        // Example: Queue.tasks last, Queue.tasks last $last_task
729        let multifield_last_regex = Regex::new(
730            r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+last(?:\s+(\$[a-zA-Z_]\w*))?$"#
731        ).map_err(|e| RuleEngineError::ParseError {
732            message: format!("Multifield last regex error: {}", e),
733        })?;
734
735        if let Some(captures) = multifield_last_regex.captures(clause_to_parse) {
736            let field = captures.get(1).unwrap().as_str().to_string();
737            let variable = captures.get(2).map(|m| m.as_str().to_string());
738
739            let condition = Condition::with_multifield_last(field, variable);
740            return Ok(ConditionGroup::single(condition));
741        }
742
743        // Pattern 6: Field.array empty
744        // Example: ShoppingCart.items empty
745        let multifield_empty_regex = Regex::new(
746            r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+empty$"#
747        ).map_err(|e| RuleEngineError::ParseError {
748            message: format!("Multifield empty regex error: {}", e),
749        })?;
750
751        if let Some(captures) = multifield_empty_regex.captures(clause_to_parse) {
752            let field = captures.get(1).unwrap().as_str().to_string();
753
754            let condition = Condition::with_multifield_empty(field);
755            return Ok(ConditionGroup::single(condition));
756        }
757
758        // Pattern 7: Field.array not_empty
759        // Example: ShoppingCart.items not_empty
760        let multifield_not_empty_regex = Regex::new(
761            r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+not_empty$"#
762        ).map_err(|e| RuleEngineError::ParseError {
763            message: format!("Multifield not_empty regex error: {}", e),
764        })?;
765
766        if let Some(captures) = multifield_not_empty_regex.captures(clause_to_parse) {
767            let field = captures.get(1).unwrap().as_str().to_string();
768
769            let condition = Condition::with_multifield_not_empty(field);
770            return Ok(ConditionGroup::single(condition));
771        }
772
773        // === END MULTI-FIELD PATTERNS ===
774
775        // Handle Test CE: test(functionName(args...))
776        // This is a CLIPS-inspired feature for arbitrary boolean expressions
777        let test_regex = Regex::new(r#"^test\s*\(\s*([a-zA-Z_]\w*)\s*\(([^)]*)\)\s*\)$"#)
778            .map_err(|e| RuleEngineError::ParseError {
779                message: format!("Test CE regex error: {}", e),
780            })?;
781
782        if let Some(captures) = test_regex.captures(clause_to_parse) {
783            let function_name = captures.get(1).unwrap().as_str().to_string();
784            let args_str = captures.get(2).unwrap().as_str();
785
786            // Parse arguments
787            let args: Vec<String> = if args_str.trim().is_empty() {
788                Vec::new()
789            } else {
790                args_str
791                    .split(',')
792                    .map(|arg| arg.trim().to_string())
793                    .collect()
794            };
795
796            let condition = Condition::with_test(function_name, args);
797            return Ok(ConditionGroup::single(condition));
798        }
799
800        // Handle typed object conditions like: $TestCar : TestCarClass( speedUp == true && speed < maxSpeed )
801        let typed_object_regex =
802            Regex::new(r#"\$(\w+)\s*:\s*(\w+)\s*\(\s*(.+?)\s*\)"#).map_err(|e| {
803                RuleEngineError::ParseError {
804                    message: format!("Typed object regex error: {}", e),
805                }
806            })?;
807
808        if let Some(captures) = typed_object_regex.captures(clause_to_parse) {
809            let _object_name = captures.get(1).unwrap().as_str();
810            let _object_type = captures.get(2).unwrap().as_str();
811            let conditions_str = captures.get(3).unwrap().as_str();
812
813            // Parse conditions inside parentheses
814            return self.parse_conditions_within_object(conditions_str);
815        }
816
817        // Try to parse function call pattern: functionName(arg1, arg2, ...) operator value
818        let function_regex = Regex::new(
819            r#"([a-zA-Z_]\w*)\s*\(([^)]*)\)\s*(>=|<=|==|!=|>|<|contains|matches)\s*(.+)"#,
820        )
821        .map_err(|e| RuleEngineError::ParseError {
822            message: format!("Function regex error: {}", e),
823        })?;
824
825        if let Some(captures) = function_regex.captures(clause_to_parse) {
826            let function_name = captures.get(1).unwrap().as_str().to_string();
827            let args_str = captures.get(2).unwrap().as_str();
828            let operator_str = captures.get(3).unwrap().as_str();
829            let value_str = captures.get(4).unwrap().as_str().trim();
830
831            // Parse arguments
832            let args: Vec<String> = if args_str.trim().is_empty() {
833                Vec::new()
834            } else {
835                args_str
836                    .split(',')
837                    .map(|arg| arg.trim().to_string())
838                    .collect()
839            };
840
841            let operator =
842                Operator::from_str(operator_str).ok_or_else(|| RuleEngineError::InvalidOperator {
843                    operator: operator_str.to_string(),
844                })?;
845
846            let value = self.parse_value(value_str)?;
847
848            let condition = Condition::with_function(function_name, args, operator, value);
849            return Ok(ConditionGroup::single(condition));
850        }
851
852        // Parse expressions like: User.Age >= 18, Product.Price < 100.0, user.age >= 18, etc.
853        // Support both PascalCase (User.Age) and lowercase (user.age) field naming
854        // Also support arithmetic expressions like: User.Age % 3 == 0, User.Price * 2 > 100
855        let condition_regex = Regex::new(
856            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*(.+)"#,
857        )
858        .map_err(|e| RuleEngineError::ParseError {
859            message: format!("Condition regex error: {}", e),
860        })?;
861
862        let captures = condition_regex.captures(clause_to_parse).ok_or_else(|| {
863            RuleEngineError::ParseError {
864                message: format!("Invalid condition format: {}", clause_to_parse),
865            }
866        })?;
867
868        let left_side = captures.get(1).unwrap().as_str().trim().to_string();
869        let operator_str = captures.get(2).unwrap().as_str();
870        let value_str = captures.get(3).unwrap().as_str().trim();
871
872        let operator =
873            Operator::from_str(operator_str).ok_or_else(|| RuleEngineError::InvalidOperator {
874                operator: operator_str.to_string(),
875            })?;
876
877        let value = self.parse_value(value_str)?;
878
879        // Check if left_side contains arithmetic operators - if yes, it's an expression
880        if left_side.contains('+') || left_side.contains('-') || left_side.contains('*') 
881            || left_side.contains('/') || left_side.contains('%') {
882            // This is an arithmetic expression - use Test CE
883            // Format: test(left_side operator value)
884            let test_expr = format!("{} {} {}", left_side, operator_str, value_str);
885            let condition = Condition::with_test(test_expr, vec![]);
886            Ok(ConditionGroup::single(condition))
887        } else {
888            // Simple field reference
889            let condition = Condition::new(left_side, operator, value);
890            Ok(ConditionGroup::single(condition))
891        }
892    }
893
894    fn parse_conditions_within_object(&self, conditions_str: &str) -> Result<ConditionGroup> {
895        // Parse conditions like: speedUp == true && speed < maxSpeed
896        let parts: Vec<&str> = conditions_str.split("&&").collect();
897
898        let mut conditions = Vec::new();
899        for part in parts {
900            let trimmed = part.trim();
901            let condition = self.parse_simple_condition(trimmed)?;
902            conditions.push(condition);
903        }
904
905        // Combine with AND
906        if conditions.is_empty() {
907            return Err(RuleEngineError::ParseError {
908                message: "No conditions found".to_string(),
909            });
910        }
911
912        let mut iter = conditions.into_iter();
913        let mut result = iter.next().unwrap();
914        for condition in iter {
915            result = ConditionGroup::and(result, condition);
916        }
917
918        Ok(result)
919    }
920
921    fn parse_simple_condition(&self, clause: &str) -> Result<ConditionGroup> {
922        // Parse simple condition like: speedUp == true or speed < maxSpeed
923        let condition_regex = Regex::new(r#"(\w+)\s*(>=|<=|==|!=|>|<)\s*(.+)"#).map_err(|e| {
924            RuleEngineError::ParseError {
925                message: format!("Simple condition regex error: {}", e),
926            }
927        })?;
928
929        let captures =
930            condition_regex
931                .captures(clause)
932                .ok_or_else(|| RuleEngineError::ParseError {
933                    message: format!("Invalid simple condition format: {}", clause),
934                })?;
935
936        let field = captures.get(1).unwrap().as_str().to_string();
937        let operator_str = captures.get(2).unwrap().as_str();
938        let value_str = captures.get(3).unwrap().as_str().trim();
939
940        let operator =
941            Operator::from_str(operator_str).ok_or_else(|| RuleEngineError::InvalidOperator {
942                operator: operator_str.to_string(),
943            })?;
944
945        let value = self.parse_value(value_str)?;
946
947        let condition = Condition::new(field, operator, value);
948        Ok(ConditionGroup::single(condition))
949    }
950
951    fn parse_value(&self, value_str: &str) -> Result<Value> {
952        let trimmed = value_str.trim();
953
954        // String literal
955        if (trimmed.starts_with('"') && trimmed.ends_with('"'))
956            || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
957        {
958            let unquoted = &trimmed[1..trimmed.len() - 1];
959            return Ok(Value::String(unquoted.to_string()));
960        }
961
962        // Boolean
963        if trimmed.eq_ignore_ascii_case("true") {
964            return Ok(Value::Boolean(true));
965        }
966        if trimmed.eq_ignore_ascii_case("false") {
967            return Ok(Value::Boolean(false));
968        }
969
970        // Null
971        if trimmed.eq_ignore_ascii_case("null") {
972            return Ok(Value::Null);
973        }
974
975        // Number (try integer first, then float)
976        if let Ok(int_val) = trimmed.parse::<i64>() {
977            return Ok(Value::Integer(int_val));
978        }
979
980        if let Ok(float_val) = trimmed.parse::<f64>() {
981            return Ok(Value::Number(float_val));
982        }
983
984        // Expression with arithmetic operators (e.g., "Order.quantity * Order.price")
985        // Detect: contains operators AND (contains field reference OR multiple tokens)
986        if self.is_expression(trimmed) {
987            return Ok(Value::Expression(trimmed.to_string()));
988        }
989
990        // Field reference (like User.Name)
991        if trimmed.contains('.') {
992            return Ok(Value::String(trimmed.to_string()));
993        }
994
995        // Variable reference (identifier without quotes or dots)
996        // This handles cases like: order_qty = moq
997        // where 'moq' should be evaluated as a variable reference at runtime
998        if self.is_identifier(trimmed) {
999            return Ok(Value::Expression(trimmed.to_string()));
1000        }
1001
1002        // Default to string
1003        Ok(Value::String(trimmed.to_string()))
1004    }
1005
1006    /// Check if a string is a valid identifier (variable name)
1007    /// Valid identifiers: alphanumeric + underscore, starts with letter or underscore
1008    fn is_identifier(&self, s: &str) -> bool {
1009        if s.is_empty() {
1010            return false;
1011        }
1012
1013        // First character must be letter or underscore
1014        let first_char = s.chars().next().unwrap();
1015        if !first_char.is_alphabetic() && first_char != '_' {
1016            return false;
1017        }
1018
1019        // Rest must be alphanumeric or underscore
1020        s.chars().all(|c| c.is_alphanumeric() || c == '_')
1021    }
1022
1023    /// Check if a string is an arithmetic expression
1024    fn is_expression(&self, s: &str) -> bool {
1025        // Check for arithmetic operators
1026        let has_operator = s.contains('+') || s.contains('-') || s.contains('*') || s.contains('/') || s.contains('%');
1027
1028        // Check for field references (contains .)
1029        let has_field_ref = s.contains('.');
1030
1031        // Check for multiple tokens (spaces between operands/operators)
1032        let has_spaces = s.contains(' ');
1033
1034        // Expression if: has operator AND (has field reference OR has spaces)
1035        has_operator && (has_field_ref || has_spaces)
1036    }
1037
1038    fn parse_then_clause(&self, then_clause: &str) -> Result<Vec<ActionType>> {
1039        let statements: Vec<&str> = then_clause
1040            .split(';')
1041            .map(|s| s.trim())
1042            .filter(|s| !s.is_empty())
1043            .collect();
1044
1045        let mut actions = Vec::new();
1046
1047        for statement in statements {
1048            let action = self.parse_action_statement(statement)?;
1049            actions.push(action);
1050        }
1051
1052        Ok(actions)
1053    }
1054
1055    fn parse_action_statement(&self, statement: &str) -> Result<ActionType> {
1056        let trimmed = statement.trim();
1057
1058        // Method call: $Object.method(args)
1059        let method_regex = Regex::new(r#"\$(\w+)\.(\w+)\s*\(([^)]*)\)"#).map_err(|e| {
1060            RuleEngineError::ParseError {
1061                message: format!("Method regex error: {}", e),
1062            }
1063        })?;
1064
1065        if let Some(captures) = method_regex.captures(trimmed) {
1066            let object = captures.get(1).unwrap().as_str().to_string();
1067            let method = captures.get(2).unwrap().as_str().to_string();
1068            let args_str = captures.get(3).unwrap().as_str();
1069
1070            let args = if args_str.trim().is_empty() {
1071                Vec::new()
1072            } else {
1073                self.parse_method_args(args_str)?
1074            };
1075
1076            return Ok(ActionType::MethodCall {
1077                object,
1078                method,
1079                args,
1080            });
1081        }
1082
1083        // Assignment: Field = Value
1084        if let Some(eq_pos) = trimmed.find('=') {
1085            let field = trimmed[..eq_pos].trim().to_string();
1086            let value_str = trimmed[eq_pos + 1..].trim();
1087            let value = self.parse_value(value_str)?;
1088
1089            return Ok(ActionType::Set { field, value });
1090        }
1091
1092        // Function calls: update($Object), retract($Object), etc.
1093        let func_regex =
1094            Regex::new(r#"(\w+)\s*\(\s*(.+?)?\s*\)"#).map_err(|e| RuleEngineError::ParseError {
1095                message: format!("Function regex error: {}", e),
1096            })?;
1097
1098        if let Some(captures) = func_regex.captures(trimmed) {
1099            let function_name = captures.get(1).unwrap().as_str();
1100            let args_str = captures.get(2).map(|m| m.as_str()).unwrap_or("");
1101
1102            match function_name.to_lowercase().as_str() {
1103                "update" => {
1104                    // Extract object name from $Object
1105                    let object_name = if let Some(stripped) = args_str.strip_prefix('$') {
1106                        stripped.to_string()
1107                    } else {
1108                        args_str.to_string()
1109                    };
1110                    Ok(ActionType::Update {
1111                        object: object_name,
1112                    })
1113                }
1114                "retract" => {
1115                    // Extract object name from $Object
1116                    let object_name = if let Some(stripped) = args_str.strip_prefix('$') {
1117                        stripped.to_string()
1118                    } else {
1119                        args_str.to_string()
1120                    };
1121                    Ok(ActionType::Retract {
1122                        object: object_name,
1123                    })
1124                }
1125                "log" => {
1126                    let message = if args_str.is_empty() {
1127                        "Log message".to_string()
1128                    } else {
1129                        let value = self.parse_value(args_str.trim())?;
1130                        value.to_string()
1131                    };
1132                    Ok(ActionType::Log { message })
1133                }
1134                "activateagendagroup" | "activate_agenda_group" => {
1135                    let agenda_group = if args_str.is_empty() {
1136                        return Err(RuleEngineError::ParseError {
1137                            message: "ActivateAgendaGroup requires agenda group name".to_string(),
1138                        });
1139                    } else {
1140                        let value = self.parse_value(args_str.trim())?;
1141                        match value {
1142                            Value::String(s) => s,
1143                            _ => value.to_string(),
1144                        }
1145                    };
1146                    Ok(ActionType::ActivateAgendaGroup {
1147                        group: agenda_group,
1148                    })
1149                }
1150                "schedulerule" | "schedule_rule" => {
1151                    // Parse delay and target rule: ScheduleRule(5000, "next-rule")
1152                    let parts: Vec<&str> = args_str.split(',').collect();
1153                    if parts.len() != 2 {
1154                        return Err(RuleEngineError::ParseError {
1155                            message: "ScheduleRule requires delay_ms and rule_name".to_string(),
1156                        });
1157                    }
1158
1159                    let delay_ms = self.parse_value(parts[0].trim())?;
1160                    let rule_name = self.parse_value(parts[1].trim())?;
1161
1162                    let delay_ms = match delay_ms {
1163                        Value::Integer(i) => i as u64,
1164                        Value::Number(f) => f as u64,
1165                        _ => {
1166                            return Err(RuleEngineError::ParseError {
1167                                message: "ScheduleRule delay_ms must be a number".to_string(),
1168                            })
1169                        }
1170                    };
1171
1172                    let rule_name = match rule_name {
1173                        Value::String(s) => s,
1174                        _ => rule_name.to_string(),
1175                    };
1176
1177                    Ok(ActionType::ScheduleRule {
1178                        delay_ms,
1179                        rule_name,
1180                    })
1181                }
1182                "completeworkflow" | "complete_workflow" => {
1183                    let workflow_id = if args_str.is_empty() {
1184                        return Err(RuleEngineError::ParseError {
1185                            message: "CompleteWorkflow requires workflow_id".to_string(),
1186                        });
1187                    } else {
1188                        let value = self.parse_value(args_str.trim())?;
1189                        match value {
1190                            Value::String(s) => s,
1191                            _ => value.to_string(),
1192                        }
1193                    };
1194                    Ok(ActionType::CompleteWorkflow {
1195                        workflow_name: workflow_id,
1196                    })
1197                }
1198                "setworkflowdata" | "set_workflow_data" => {
1199                    // Parse key=value: SetWorkflowData("key=value")
1200                    let data_str = args_str.trim();
1201
1202                    // Simple key=value parsing
1203                    let (key, value) = if let Some(eq_pos) = data_str.find('=') {
1204                        let key = data_str[..eq_pos].trim().trim_matches('"');
1205                        let value_str = data_str[eq_pos + 1..].trim();
1206                        let value = self.parse_value(value_str)?;
1207                        (key.to_string(), value)
1208                    } else {
1209                        return Err(RuleEngineError::ParseError {
1210                            message: "SetWorkflowData data must be in key=value format".to_string(),
1211                        });
1212                    };
1213
1214                    Ok(ActionType::SetWorkflowData { key, value })
1215                }
1216                _ => {
1217                    // All other functions become custom actions
1218                    let params = if args_str.is_empty() {
1219                        HashMap::new()
1220                    } else {
1221                        self.parse_function_args_as_params(args_str)?
1222                    };
1223
1224                    Ok(ActionType::Custom {
1225                        action_type: function_name.to_string(),
1226                        params,
1227                    })
1228                }
1229            }
1230        } else {
1231            // Custom statement
1232            Ok(ActionType::Custom {
1233                action_type: "statement".to_string(),
1234                params: {
1235                    let mut params = HashMap::new();
1236                    params.insert("statement".to_string(), Value::String(trimmed.to_string()));
1237                    params
1238                },
1239            })
1240        }
1241    }
1242
1243    fn parse_method_args(&self, args_str: &str) -> Result<Vec<Value>> {
1244        if args_str.trim().is_empty() {
1245            return Ok(Vec::new());
1246        }
1247
1248        // Handle expressions like: $TestCar.Speed + $TestCar.SpeedIncrement
1249        let mut args = Vec::new();
1250        let parts: Vec<&str> = args_str.split(',').collect();
1251
1252        for part in parts {
1253            let trimmed = part.trim();
1254
1255            // Handle arithmetic expressions
1256            if trimmed.contains('+')
1257                || trimmed.contains('-')
1258                || trimmed.contains('*')
1259                || trimmed.contains('/')
1260            {
1261                // For now, store as string - the engine will evaluate
1262                args.push(Value::String(trimmed.to_string()));
1263            } else {
1264                args.push(self.parse_value(trimmed)?);
1265            }
1266        }
1267
1268        Ok(args)
1269    }
1270
1271    /// Parse function arguments as parameters for custom actions
1272    fn parse_function_args_as_params(&self, args_str: &str) -> Result<HashMap<String, Value>> {
1273        let mut params = HashMap::new();
1274
1275        if args_str.trim().is_empty() {
1276            return Ok(params);
1277        }
1278
1279        // Parse positional parameters as numbered args
1280        let parts: Vec<&str> = args_str.split(',').collect();
1281        for (i, part) in parts.iter().enumerate() {
1282            let trimmed = part.trim();
1283            let value = self.parse_value(trimmed)?;
1284
1285            // Use simple numeric indexing - engine will resolve references dynamically
1286            params.insert(i.to_string(), value);
1287        }
1288
1289        Ok(params)
1290    }
1291}
1292
1293#[cfg(test)]
1294mod tests {
1295    use super::GRLParser;
1296
1297    #[test]
1298    fn test_parse_simple_rule() {
1299        let grl = r#"
1300        rule "CheckAge" salience 10 {
1301            when
1302                User.Age >= 18
1303            then
1304                log("User is adult");
1305        }
1306        "#;
1307
1308        let rules = GRLParser::parse_rules(grl).unwrap();
1309        assert_eq!(rules.len(), 1);
1310        let rule = &rules[0];
1311        assert_eq!(rule.name, "CheckAge");
1312        assert_eq!(rule.salience, 10);
1313        assert_eq!(rule.actions.len(), 1);
1314    }
1315
1316    #[test]
1317    fn test_parse_complex_condition() {
1318        let grl = r#"
1319        rule "ComplexRule" {
1320            when
1321                User.Age >= 18 && User.Country == "US"
1322            then
1323                User.Qualified = true;
1324        }
1325        "#;
1326
1327        let rules = GRLParser::parse_rules(grl).unwrap();
1328        assert_eq!(rules.len(), 1);
1329        let rule = &rules[0];
1330        assert_eq!(rule.name, "ComplexRule");
1331    }
1332
1333    #[test]
1334    fn test_parse_new_syntax_with_parentheses() {
1335        let grl = r#"
1336        rule "Default Rule" salience 10 {
1337            when
1338                (user.age >= 18)
1339            then
1340                set(user.status, "approved");
1341        }
1342        "#;
1343
1344        let rules = GRLParser::parse_rules(grl).unwrap();
1345        assert_eq!(rules.len(), 1);
1346        let rule = &rules[0];
1347        assert_eq!(rule.name, "Default Rule");
1348        assert_eq!(rule.salience, 10);
1349        assert_eq!(rule.actions.len(), 1);
1350
1351        // Check that the action is parsed as a Custom action (set is now custom)
1352        match &rule.actions[0] {
1353            crate::types::ActionType::Custom {
1354                action_type,
1355                params,
1356            } => {
1357                assert_eq!(action_type, "set");
1358                assert_eq!(
1359                    params.get("0"),
1360                    Some(&crate::types::Value::String("user.status".to_string()))
1361                );
1362                assert_eq!(
1363                    params.get("1"),
1364                    Some(&crate::types::Value::String("approved".to_string()))
1365                );
1366            }
1367            _ => panic!("Expected Custom action, got: {:?}", rule.actions[0]),
1368        }
1369    }
1370
1371    #[test]
1372    fn test_parse_complex_nested_conditions() {
1373        let grl = r#"
1374        rule "Complex Business Rule" salience 10 {
1375            when
1376                (((user.vipStatus == true) && (order.amount > 500)) || ((date.isHoliday == true) && (order.hasCoupon == true)))
1377            then
1378                apply_discount(20000);
1379        }
1380        "#;
1381
1382        let rules = GRLParser::parse_rules(grl).unwrap();
1383        assert_eq!(rules.len(), 1);
1384        let rule = &rules[0];
1385        assert_eq!(rule.name, "Complex Business Rule");
1386        assert_eq!(rule.salience, 10);
1387        assert_eq!(rule.actions.len(), 1);
1388
1389        // Check that the action is parsed as a Custom action (apply_discount is now custom)
1390        match &rule.actions[0] {
1391            crate::types::ActionType::Custom {
1392                action_type,
1393                params,
1394            } => {
1395                assert_eq!(action_type, "apply_discount");
1396                assert_eq!(params.get("0"), Some(&crate::types::Value::Integer(20000)));
1397            }
1398            _ => panic!("Expected Custom action, got: {:?}", rule.actions[0]),
1399        }
1400    }
1401
1402    #[test]
1403    fn test_parse_no_loop_attribute() {
1404        let grl = r#"
1405        rule "NoLoopRule" no-loop salience 15 {
1406            when
1407                User.Score < 100
1408            then
1409                set(User.Score, User.Score + 10);
1410        }
1411        "#;
1412
1413        let rules = GRLParser::parse_rules(grl).unwrap();
1414        assert_eq!(rules.len(), 1);
1415        let rule = &rules[0];
1416        assert_eq!(rule.name, "NoLoopRule");
1417        assert_eq!(rule.salience, 15);
1418        assert!(rule.no_loop, "Rule should have no-loop=true");
1419    }
1420
1421    #[test]
1422    fn test_parse_no_loop_different_positions() {
1423        // Test no-loop before salience
1424        let grl1 = r#"
1425        rule "Rule1" no-loop salience 10 {
1426            when User.Age >= 18
1427            then log("adult");
1428        }
1429        "#;
1430
1431        // Test no-loop after salience
1432        let grl2 = r#"
1433        rule "Rule2" salience 10 no-loop {
1434            when User.Age >= 18
1435            then log("adult");
1436        }
1437        "#;
1438
1439        let rules1 = GRLParser::parse_rules(grl1).unwrap();
1440        let rules2 = GRLParser::parse_rules(grl2).unwrap();
1441
1442        assert_eq!(rules1.len(), 1);
1443        assert_eq!(rules2.len(), 1);
1444
1445        assert!(rules1[0].no_loop, "Rule1 should have no-loop=true");
1446        assert!(rules2[0].no_loop, "Rule2 should have no-loop=true");
1447
1448        assert_eq!(rules1[0].salience, 10);
1449        assert_eq!(rules2[0].salience, 10);
1450    }
1451
1452    #[test]
1453    fn test_parse_without_no_loop() {
1454        let grl = r#"
1455        rule "RegularRule" salience 5 {
1456            when
1457                User.Active == true
1458            then
1459                log("active user");
1460        }
1461        "#;
1462
1463        let rules = GRLParser::parse_rules(grl).unwrap();
1464        assert_eq!(rules.len(), 1);
1465        let rule = &rules[0];
1466        assert_eq!(rule.name, "RegularRule");
1467        assert!(!rule.no_loop, "Rule should have no-loop=false by default");
1468    }
1469
1470    #[test]
1471    fn test_parse_exists_pattern() {
1472        let grl = r#"
1473        rule "ExistsRule" salience 20 {
1474            when
1475                exists(Customer.tier == "VIP")
1476            then
1477                System.premiumActive = true;
1478        }
1479        "#;
1480
1481        let rules = GRLParser::parse_rules(grl).unwrap();
1482        assert_eq!(rules.len(), 1);
1483        let rule = &rules[0];
1484        assert_eq!(rule.name, "ExistsRule");
1485        assert_eq!(rule.salience, 20);
1486
1487        // Check that condition is EXISTS pattern
1488        match &rule.conditions {
1489            crate::engine::rule::ConditionGroup::Exists(_) => {
1490                // Test passes
1491            }
1492            _ => panic!(
1493                "Expected EXISTS condition group, got: {:?}",
1494                rule.conditions
1495            ),
1496        }
1497    }
1498
1499    #[test]
1500    fn test_parse_forall_pattern() {
1501        let grl = r#"
1502        rule "ForallRule" salience 15 {
1503            when
1504                forall(Order.status == "processed")
1505            then
1506                Shipping.enabled = true;
1507        }
1508        "#;
1509
1510        let rules = GRLParser::parse_rules(grl).unwrap();
1511        assert_eq!(rules.len(), 1);
1512        let rule = &rules[0];
1513        assert_eq!(rule.name, "ForallRule");
1514
1515        // Check that condition is FORALL pattern
1516        match &rule.conditions {
1517            crate::engine::rule::ConditionGroup::Forall(_) => {
1518                // Test passes
1519            }
1520            _ => panic!(
1521                "Expected FORALL condition group, got: {:?}",
1522                rule.conditions
1523            ),
1524        }
1525    }
1526
1527    #[test]
1528    fn test_parse_combined_patterns() {
1529        let grl = r#"
1530        rule "CombinedRule" salience 25 {
1531            when
1532                exists(Customer.tier == "VIP") && !exists(Alert.priority == "high")
1533            then
1534                System.vipMode = true;
1535        }
1536        "#;
1537
1538        let rules = GRLParser::parse_rules(grl).unwrap();
1539        assert_eq!(rules.len(), 1);
1540        let rule = &rules[0];
1541        assert_eq!(rule.name, "CombinedRule");
1542
1543        // Check that condition is AND with EXISTS and NOT(EXISTS) patterns
1544        match &rule.conditions {
1545            crate::engine::rule::ConditionGroup::Compound {
1546                left,
1547                operator,
1548                right,
1549            } => {
1550                assert_eq!(*operator, crate::types::LogicalOperator::And);
1551
1552                // Left should be EXISTS
1553                match left.as_ref() {
1554                    crate::engine::rule::ConditionGroup::Exists(_) => {
1555                        // Expected
1556                    }
1557                    _ => panic!("Expected EXISTS in left side, got: {:?}", left),
1558                }
1559
1560                // Right should be NOT(EXISTS)
1561                match right.as_ref() {
1562                    crate::engine::rule::ConditionGroup::Not(inner) => {
1563                        match inner.as_ref() {
1564                            crate::engine::rule::ConditionGroup::Exists(_) => {
1565                                // Expected
1566                            }
1567                            _ => panic!("Expected EXISTS inside NOT, got: {:?}", inner),
1568                        }
1569                    }
1570                    _ => panic!("Expected NOT in right side, got: {:?}", right),
1571                }
1572            }
1573            _ => panic!("Expected compound condition, got: {:?}", rule.conditions),
1574        }
1575    }
1576}