Skip to main content

rust_rule_engine/parser/
grl.rs

1use crate::engine::module::{ExportItem, ExportList, ImportType, ItemType, ModuleManager};
2use crate::engine::rule::{Condition, ConditionGroup, Rule};
3use crate::errors::{Result, RuleEngineError};
4use crate::types::{ActionType, Operator, Value};
5use chrono::{DateTime, Utc};
6use rexile::Pattern;
7use std::collections::HashMap;
8use std::sync::OnceLock;
9
10// Stream syntax parser module
11#[cfg(feature = "streaming")]
12pub mod stream_syntax;
13
14// Cached main regexes - compiled once at startup
15static RULE_REGEX: OnceLock<Pattern> = OnceLock::new();
16static RULE_SPLIT_REGEX: OnceLock<Pattern> = OnceLock::new();
17static DEFMODULE_REGEX: OnceLock<Pattern> = OnceLock::new();
18static DEFMODULE_SPLIT_REGEX: OnceLock<Pattern> = OnceLock::new();
19static WHEN_THEN_REGEX: OnceLock<Pattern> = OnceLock::new();
20static SALIENCE_REGEX: OnceLock<Pattern> = OnceLock::new();
21static TEST_CONDITION_REGEX: OnceLock<Pattern> = OnceLock::new();
22static TYPED_TEST_CONDITION_REGEX: OnceLock<Pattern> = OnceLock::new();
23static FUNCTION_CALL_REGEX: OnceLock<Pattern> = OnceLock::new();
24static CONDITION_REGEX: OnceLock<Pattern> = OnceLock::new();
25static METHOD_CALL_REGEX: OnceLock<Pattern> = OnceLock::new();
26static FUNCTION_BINDING_REGEX: OnceLock<Pattern> = OnceLock::new();
27static MULTIFIELD_COLLECT_REGEX: OnceLock<Pattern> = OnceLock::new();
28static MULTIFIELD_COUNT_REGEX: OnceLock<Pattern> = OnceLock::new();
29static MULTIFIELD_FIRST_REGEX: OnceLock<Pattern> = OnceLock::new();
30static MULTIFIELD_LAST_REGEX: OnceLock<Pattern> = OnceLock::new();
31static MULTIFIELD_EMPTY_REGEX: OnceLock<Pattern> = OnceLock::new();
32static MULTIFIELD_NOT_EMPTY_REGEX: OnceLock<Pattern> = OnceLock::new();
33static SIMPLE_CONDITION_REGEX: OnceLock<Pattern> = OnceLock::new();
34
35// Helper functions to get or initialize regexes
36fn rule_regex() -> &'static Pattern {
37    RULE_REGEX.get_or_init(|| {
38        Pattern::new(r#"rule\s+(?:"([^"]+)"|([a-zA-Z_]\w*))\s*([^{]*)\{(.+)\}"#)
39            .expect("Invalid rule regex pattern")
40    })
41}
42
43fn rule_split_regex() -> &'static Pattern {
44    RULE_SPLIT_REGEX.get_or_init(|| {
45        Pattern::new(r#"(?s)rule\s+(?:"[^"]+"|[a-zA-Z_]\w*).*?\}"#)
46            .expect("Invalid rule split regex pattern")
47    })
48}
49
50fn defmodule_regex() -> &'static Pattern {
51    DEFMODULE_REGEX.get_or_init(|| {
52        Pattern::new(r#"defmodule\s+([A-Z_]\w*)\s*\{([^}]*)\}"#)
53            .expect("Invalid defmodule regex pattern")
54    })
55}
56
57fn defmodule_split_regex() -> &'static Pattern {
58    DEFMODULE_SPLIT_REGEX.get_or_init(|| {
59        Pattern::new(r#"(?s)defmodule\s+[A-Z_]\w*\s*\{[^}]*\}"#)
60            .expect("Invalid defmodule split regex pattern")
61    })
62}
63
64fn when_then_regex() -> &'static Pattern {
65    WHEN_THEN_REGEX.get_or_init(|| {
66        Pattern::new(r"when\s+(.+?)\s+then\s+(.+)").expect("Invalid when-then regex pattern")
67    })
68}
69
70fn salience_regex() -> &'static Pattern {
71    SALIENCE_REGEX
72        .get_or_init(|| Pattern::new(r"salience\s+(\d+)").expect("Invalid salience regex pattern"))
73}
74
75fn test_condition_regex() -> &'static Pattern {
76    TEST_CONDITION_REGEX.get_or_init(|| {
77        Pattern::new(r#"^test\s*\(\s*([a-zA-Z_]\w*)\s*\(([^)]*)\)\s*\)$"#)
78            .expect("Invalid test condition regex")
79    })
80}
81
82fn typed_test_condition_regex() -> &'static Pattern {
83    TYPED_TEST_CONDITION_REGEX.get_or_init(|| {
84        Pattern::new(r#"\$(\w+)\s*:\s*(\w+)\s*\(\s*(.+?)\s*\)"#)
85            .expect("Invalid typed test condition regex")
86    })
87}
88
89fn function_call_regex() -> &'static Pattern {
90    FUNCTION_CALL_REGEX.get_or_init(|| {
91        Pattern::new(r#"([a-zA-Z_]\w*)\s*\(([^)]*)\)\s*(>=|<=|==|!=|>|<|contains|startsWith|endsWith|matches|in)\s*(.+)"#)
92            .expect("Invalid function call regex")
93    })
94}
95
96fn condition_regex() -> &'static Pattern {
97    CONDITION_REGEX.get_or_init(|| {
98        Pattern::new(r#"([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*(?:\s*[+\-*/%]\s*[a-zA-Z0-9_\.]+)*)\s*(>=|<=|==|!=|>|<|contains|startsWith|endsWith|matches|in)\s*(.+)"#)
99            .expect("Invalid condition regex")
100    })
101}
102
103fn method_call_regex() -> &'static Pattern {
104    METHOD_CALL_REGEX.get_or_init(|| {
105        Pattern::new(r#"\$(\w+)\.(\w+)\s*\(([^)]*)\)"#).expect("Invalid method call regex")
106    })
107}
108
109fn function_binding_regex() -> &'static Pattern {
110    FUNCTION_BINDING_REGEX.get_or_init(|| {
111        Pattern::new(r#"(\w+)\s*\(\s*(.+?)?\s*\)"#).expect("Invalid function binding regex")
112    })
113}
114
115fn multifield_collect_regex() -> &'static Pattern {
116    MULTIFIELD_COLLECT_REGEX.get_or_init(|| {
117        Pattern::new(r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+(\$\?[a-zA-Z_]\w*)$"#)
118            .expect("Invalid multifield collect regex")
119    })
120}
121
122fn multifield_count_regex() -> &'static Pattern {
123    MULTIFIELD_COUNT_REGEX.get_or_init(|| {
124        Pattern::new(r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+count\s*(>=|<=|==|!=|>|<)\s*(.+)$"#)
125            .expect("Invalid multifield count regex")
126    })
127}
128
129fn multifield_first_regex() -> &'static Pattern {
130    MULTIFIELD_FIRST_REGEX.get_or_init(|| {
131        Pattern::new(r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+first(?:\s+(\$[a-zA-Z_]\w*))?$"#)
132            .expect("Invalid multifield first regex")
133    })
134}
135
136fn multifield_last_regex() -> &'static Pattern {
137    MULTIFIELD_LAST_REGEX.get_or_init(|| {
138        Pattern::new(r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+last(?:\s+(\$[a-zA-Z_]\w*))?$"#)
139            .expect("Invalid multifield last regex")
140    })
141}
142
143fn multifield_empty_regex() -> &'static Pattern {
144    MULTIFIELD_EMPTY_REGEX.get_or_init(|| {
145        Pattern::new(r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+empty$"#)
146            .expect("Invalid multifield empty regex")
147    })
148}
149
150fn multifield_not_empty_regex() -> &'static Pattern {
151    MULTIFIELD_NOT_EMPTY_REGEX.get_or_init(|| {
152        Pattern::new(r#"^([a-zA-Z_]\w*\.[a-zA-Z_]\w*)\s+not_empty$"#)
153            .expect("Invalid multifield not_empty regex")
154    })
155}
156
157fn simple_condition_regex() -> &'static Pattern {
158    SIMPLE_CONDITION_REGEX.get_or_init(|| {
159        Pattern::new(r#"(\w+)\s*(>=|<=|==|!=|>|<)\s*(.+)"#).expect("Invalid simple condition regex")
160    })
161}
162
163/// GRL (Grule Rule Language) Parser
164/// Parses Grule-like syntax into Rule objects
165pub struct GRLParser;
166
167/// Parsed rule attributes from GRL header
168#[derive(Debug, Default)]
169struct RuleAttributes {
170    pub no_loop: bool,
171    pub lock_on_active: bool,
172    pub agenda_group: Option<String>,
173    pub activation_group: Option<String>,
174    pub date_effective: Option<DateTime<Utc>>,
175    pub date_expires: Option<DateTime<Utc>>,
176}
177
178/// Result from parsing GRL with modules
179#[derive(Debug, Clone)]
180pub struct ParsedGRL {
181    /// Parsed rules
182    pub rules: Vec<Rule>,
183    /// Module manager with configured modules
184    pub module_manager: ModuleManager,
185    /// Map of rule name to module name
186    pub rule_modules: HashMap<String, String>,
187}
188
189impl Default for ParsedGRL {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195impl ParsedGRL {
196    pub fn new() -> Self {
197        Self {
198            rules: Vec::new(),
199            module_manager: ModuleManager::new(),
200            rule_modules: HashMap::new(),
201        }
202    }
203}
204
205impl GRLParser {
206    /// Parse a single rule from GRL syntax
207    ///
208    /// Example GRL syntax:
209    /// ```grl
210    /// rule CheckAge "Age verification rule" salience 10 {
211    ///     when
212    ///         User.Age >= 18 && User.Country == "US"
213    ///     then
214    ///         User.IsAdult = true;
215    ///         Retract("User");
216    /// }
217    /// ```
218    pub fn parse_rule(grl_text: &str) -> Result<Rule> {
219        let mut parser = GRLParser;
220        parser.parse_single_rule(grl_text)
221    }
222
223    /// Parse multiple rules from GRL text
224    pub fn parse_rules(grl_text: &str) -> Result<Vec<Rule>> {
225        let mut parser = GRLParser;
226        parser.parse_multiple_rules(grl_text)
227    }
228
229    /// Parse GRL text with module support
230    ///
231    /// Example:
232    /// ```grl
233    /// defmodule SENSORS {
234    ///   export: all
235    /// }
236    ///
237    /// defmodule CONTROL {
238    ///   import: SENSORS (rules * (templates temperature))
239    /// }
240    ///
241    /// rule "CheckTemp" {
242    ///   when temperature.value > 28
243    ///   then println("Hot");
244    /// }
245    /// ```
246    pub fn parse_with_modules(grl_text: &str) -> Result<ParsedGRL> {
247        let mut parser = GRLParser;
248        parser.parse_grl_with_modules(grl_text)
249    }
250
251    fn parse_grl_with_modules(&mut self, grl_text: &str) -> Result<ParsedGRL> {
252        let mut result = ParsedGRL::new();
253
254        // First, parse and register all modules
255        for module_match in defmodule_split_regex().find_iter(grl_text) {
256            let module_def = module_match.as_str();
257            self.parse_and_register_module(module_def, &mut result.module_manager)?;
258        }
259
260        // Remove all defmodule blocks from text before parsing rules
261        let rules_text = defmodule_split_regex().replace_all(grl_text, "");
262
263        // Then parse all rules from cleaned text
264        let rules = self.parse_multiple_rules(&rules_text)?;
265
266        // Try to assign rules to modules based on comments
267        for rule in rules {
268            let module_name = self.extract_module_from_context(grl_text, &rule.name);
269            result
270                .rule_modules
271                .insert(rule.name.clone(), module_name.clone());
272
273            // Add rule to module in manager
274            if let Ok(module) = result.module_manager.get_module_mut(&module_name) {
275                module.add_rule(&rule.name);
276            }
277
278            result.rules.push(rule);
279        }
280
281        Ok(result)
282    }
283
284    fn parse_and_register_module(
285        &self,
286        module_def: &str,
287        manager: &mut ModuleManager,
288    ) -> Result<()> {
289        // Parse: defmodule MODULE_NAME { export: all/none, import: ... }
290        if let Some(captures) = defmodule_regex().captures(module_def) {
291            let module_name = captures.get(1).unwrap().to_string();
292            let module_body = captures.get(2).unwrap();
293
294            // Create module (ignore if already exists)
295            let _ = manager.create_module(&module_name);
296            let module = manager.get_module_mut(&module_name)?;
297
298            // Parse export directive
299            if let Some(export_type) = self.extract_directive(module_body, "export:") {
300                let exports = if export_type.trim() == "all" {
301                    ExportList::All
302                } else if export_type.trim() == "none" {
303                    ExportList::None
304                } else {
305                    // Parse pattern-based exports
306                    ExportList::Specific(vec![ExportItem {
307                        item_type: ItemType::All,
308                        pattern: export_type.trim().to_string(),
309                    }])
310                };
311                module.set_exports(exports);
312            }
313
314            // Parse import directives
315            let import_lines: Vec<&str> = module_body
316                .lines()
317                .filter(|line| line.trim().starts_with("import:"))
318                .collect();
319
320            for import_line in import_lines {
321                if let Some(import_spec) = self.extract_directive(import_line, "import:") {
322                    // Parse: "MODULE_A (rules * (templates foo))"
323                    self.parse_import_spec(&module_name, &import_spec, manager)?;
324                }
325            }
326        }
327
328        Ok(())
329    }
330
331    fn extract_directive(&self, text: &str, directive: &str) -> Option<String> {
332        if let Some(pos) = text.find(directive) {
333            let after_directive = &text[pos + directive.len()..];
334
335            // Find the end of the directive (next directive, or end of block)
336            let end = after_directive
337                .find("import:")
338                .or_else(|| after_directive.find("export:"))
339                .unwrap_or(after_directive.len());
340
341            Some(after_directive[..end].trim().to_string())
342        } else {
343            None
344        }
345    }
346
347    fn parse_import_spec(
348        &self,
349        importing_module: &str,
350        spec: &str,
351        manager: &mut ModuleManager,
352    ) -> Result<()> {
353        // Parse: "SENSORS (rules * (templates temperature))"
354        let parts: Vec<&str> = spec.splitn(2, '(').collect();
355        if parts.is_empty() {
356            return Ok(());
357        }
358
359        let source_module = parts[0].trim().to_string();
360        let rest = if parts.len() > 1 { parts[1] } else { "" };
361
362        // Check if we're importing rules or templates
363        if rest.contains("rules") {
364            manager.import_from(importing_module, &source_module, ImportType::AllRules, "*")?;
365        }
366
367        if rest.contains("templates") {
368            manager.import_from(
369                importing_module,
370                &source_module,
371                ImportType::AllTemplates,
372                "*",
373            )?;
374        }
375
376        Ok(())
377    }
378
379    fn extract_module_from_context(&self, grl_text: &str, rule_name: &str) -> String {
380        // Look backward from rule to find the module comment
381        if let Some(rule_pos) = grl_text
382            .find(&format!("rule \"{}\"", rule_name))
383            .or_else(|| grl_text.find(&format!("rule {}", rule_name)))
384        {
385            // Look backward for ;; MODULE: comment
386            let before = &grl_text[..rule_pos];
387            if let Some(module_pos) = before.rfind(";; MODULE:") {
388                let after_module_marker = &before[module_pos + 10..];
389                if let Some(end_of_line) = after_module_marker.find('\n') {
390                    let module_line = &after_module_marker[..end_of_line].trim();
391                    // Extract module name from "SENSORS - Temperature Monitoring"
392                    if let Some(first_word) = module_line.split_whitespace().next() {
393                        return first_word.to_string();
394                    }
395                }
396            }
397        }
398
399        // Default to MAIN
400        "MAIN".to_string()
401    }
402
403    fn parse_single_rule(&mut self, grl_text: &str) -> Result<Rule> {
404        let cleaned = self.clean_text(grl_text);
405
406        // Extract rule components using cached regex
407        let captures =
408            rule_regex()
409                .captures(&cleaned)
410                .ok_or_else(|| RuleEngineError::ParseError {
411                    message: format!("Invalid GRL rule format. Input: {}", cleaned),
412                })?;
413
414        // Rule name can be either quoted (group 1) or unquoted (group 2)
415        let rule_name = if let Some(quoted_name) = captures.get(1) {
416            quoted_name.to_string()
417        } else if let Some(unquoted_name) = captures.get(2) {
418            unquoted_name.to_string()
419        } else {
420            return Err(RuleEngineError::ParseError {
421                message: "Could not extract rule name".to_string(),
422            });
423        };
424
425        // Attributes section (group 3)
426        let attributes_section = captures.get(3).unwrap_or("");
427
428        // Rule body (group 4)
429        let rule_body = captures.get(4).unwrap();
430
431        // Parse salience from attributes section
432        let salience = self.extract_salience(attributes_section)?;
433
434        // Parse when and then sections using cached regex
435        let when_then_captures =
436            when_then_regex()
437                .captures(rule_body)
438                .ok_or_else(|| RuleEngineError::ParseError {
439                    message: "Missing when or then clause".to_string(),
440                })?;
441
442        let when_clause = when_then_captures.get(1).unwrap().trim();
443        let then_clause = when_then_captures.get(2).unwrap().trim();
444
445        // Parse conditions and actions
446        let conditions = self.parse_when_clause(when_clause)?;
447        let actions = self.parse_then_clause(then_clause)?;
448
449        // Parse all attributes from rule header
450        let attributes = self.parse_rule_attributes(attributes_section)?;
451
452        // Build rule
453        let mut rule = Rule::new(rule_name, conditions, actions);
454        rule = rule.with_priority(salience);
455
456        // Apply parsed attributes
457        if attributes.no_loop {
458            rule = rule.with_no_loop(true);
459        }
460        if attributes.lock_on_active {
461            rule = rule.with_lock_on_active(true);
462        }
463        if let Some(agenda_group) = attributes.agenda_group {
464            rule = rule.with_agenda_group(agenda_group);
465        }
466        if let Some(activation_group) = attributes.activation_group {
467            rule = rule.with_activation_group(activation_group);
468        }
469        if let Some(date_effective) = attributes.date_effective {
470            rule = rule.with_date_effective(date_effective);
471        }
472        if let Some(date_expires) = attributes.date_expires {
473            rule = rule.with_date_expires(date_expires);
474        }
475
476        Ok(rule)
477    }
478
479    fn parse_multiple_rules(&mut self, grl_text: &str) -> Result<Vec<Rule>> {
480        // Split by rule boundaries - support both quoted and unquoted rule names
481        // Use DOTALL flag to match newlines in rule body
482        let mut rules = Vec::new();
483
484        for rule_match in rule_split_regex().find_iter(grl_text) {
485            let rule_text = rule_match.as_str();
486            let rule = self.parse_single_rule(rule_text)?;
487            rules.push(rule);
488        }
489
490        Ok(rules)
491    }
492
493    /// Parse rule attributes from the rule header
494    fn parse_rule_attributes(&self, rule_header: &str) -> Result<RuleAttributes> {
495        let mut attributes = RuleAttributes::default();
496
497        // Extract the attributes section (after rule name/description, before opening brace)
498        // This ensures we don't match keywords inside description strings
499        // Strategy: Find all quoted strings and remove them, then check for attributes
500        let mut attrs_section = rule_header.to_string();
501
502        // Remove all quoted strings (descriptions) to avoid false matches
503        let quoted_regex = Pattern::new(r#""[^"]*""#).map_err(|e| RuleEngineError::ParseError {
504            message: format!("Invalid quoted string regex: {}", e),
505        })?;
506        attrs_section = quoted_regex.replace_all(&attrs_section, "").to_string();
507
508        // Also remove the "rule" keyword and rule name (if unquoted)
509        if let Some(rule_pos) = attrs_section.find("rule") {
510            // Find the next space or attribute keyword after "rule"
511            let after_rule = &attrs_section[rule_pos + 4..];
512            if let Some(first_keyword) = after_rule
513                .find("salience")
514                .or_else(|| after_rule.find("no-loop"))
515                .or_else(|| after_rule.find("lock-on-active"))
516                .or_else(|| after_rule.find("agenda-group"))
517                .or_else(|| after_rule.find("activation-group"))
518                .or_else(|| after_rule.find("date-effective"))
519                .or_else(|| after_rule.find("date-expires"))
520            {
521                attrs_section = after_rule[first_keyword..].to_string();
522            }
523        }
524
525        // Now check for boolean attributes using word boundaries
526        let no_loop_regex =
527            Pattern::new(r"\bno-loop\b").map_err(|e| RuleEngineError::ParseError {
528                message: format!("Invalid no-loop regex: {}", e),
529            })?;
530        let lock_on_active_regex =
531            Pattern::new(r"\block-on-active\b").map_err(|e| RuleEngineError::ParseError {
532                message: format!("Invalid lock-on-active regex: {}", e),
533            })?;
534
535        if no_loop_regex.is_match(&attrs_section) {
536            attributes.no_loop = true;
537        }
538        if lock_on_active_regex.is_match(&attrs_section) {
539            attributes.lock_on_active = true;
540        }
541
542        // Parse agenda-group attribute
543        if let Some(agenda_group) = self.extract_quoted_attribute(rule_header, "agenda-group")? {
544            attributes.agenda_group = Some(agenda_group);
545        }
546
547        // Parse activation-group attribute
548        if let Some(activation_group) =
549            self.extract_quoted_attribute(rule_header, "activation-group")?
550        {
551            attributes.activation_group = Some(activation_group);
552        }
553
554        // Parse date-effective attribute
555        if let Some(date_str) = self.extract_quoted_attribute(rule_header, "date-effective")? {
556            attributes.date_effective = Some(self.parse_date_string(&date_str)?);
557        }
558
559        // Parse date-expires attribute
560        if let Some(date_str) = self.extract_quoted_attribute(rule_header, "date-expires")? {
561            attributes.date_expires = Some(self.parse_date_string(&date_str)?);
562        }
563
564        Ok(attributes)
565    }
566
567    /// Extract quoted attribute value from rule header
568    fn extract_quoted_attribute(&self, header: &str, attribute: &str) -> Result<Option<String>> {
569        let pattern = format!(r#"{}\s+"([^"]+)""#, attribute);
570        let regex = Pattern::new(&pattern).map_err(|e| RuleEngineError::ParseError {
571            message: format!("Invalid attribute regex for {}: {}", attribute, e),
572        })?;
573
574        if let Some(captures) = regex.captures(header) {
575            if let Some(value) = captures.get(1) {
576                return Ok(Some(value.to_string()));
577            }
578        }
579
580        Ok(None)
581    }
582
583    /// Parse date string in various formats
584    fn parse_date_string(&self, date_str: &str) -> Result<DateTime<Utc>> {
585        // Try ISO 8601 format first
586        if let Ok(date) = DateTime::parse_from_rfc3339(date_str) {
587            return Ok(date.with_timezone(&Utc));
588        }
589
590        // Try simple date formats
591        let formats = ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%d-%b-%Y", "%d-%m-%Y"];
592
593        for format in &formats {
594            if let Ok(naive_date) = chrono::NaiveDateTime::parse_from_str(date_str, format) {
595                return Ok(naive_date.and_utc());
596            }
597            if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(date_str, format) {
598                let datetime =
599                    naive_date
600                        .and_hms_opt(0, 0, 0)
601                        .ok_or_else(|| RuleEngineError::ParseError {
602                            message: format!("Invalid time for date: {}", naive_date),
603                        })?;
604                return Ok(datetime.and_utc());
605            }
606        }
607
608        Err(RuleEngineError::ParseError {
609            message: format!("Unable to parse date: {}", date_str),
610        })
611    }
612
613    /// Extract salience value from attributes section
614    fn extract_salience(&self, attributes_section: &str) -> Result<i32> {
615        if let Some(captures) = salience_regex().captures(attributes_section) {
616            if let Some(salience_match) = captures.get(1) {
617                return salience_match
618                    .parse::<i32>()
619                    .map_err(|e| RuleEngineError::ParseError {
620                        message: format!("Invalid salience value: {}", e),
621                    });
622            }
623        }
624
625        Ok(0) // Default salience
626    }
627
628    fn clean_text(&self, text: &str) -> String {
629        text.lines()
630            .map(|line| line.trim())
631            .filter(|line| !line.is_empty() && !line.starts_with("//"))
632            .collect::<Vec<_>>()
633            .join(" ")
634    }
635
636    fn parse_when_clause(&self, when_clause: &str) -> Result<ConditionGroup> {
637        // Handle logical operators with proper parentheses support
638        let trimmed = when_clause.trim();
639
640        // Strip outer parentheses if they exist
641        let clause = if trimmed.starts_with('(') && trimmed.ends_with(')') {
642            // Check if these are the outermost parentheses
643            let inner = &trimmed[1..trimmed.len() - 1];
644            if self.is_balanced_parentheses(inner) {
645                inner
646            } else {
647                trimmed
648            }
649        } else {
650            trimmed
651        };
652
653        // Parse OR at the top level (lowest precedence)
654        if let Some(parts) = self.split_logical_operator(clause, "||") {
655            return self.parse_or_parts(parts);
656        }
657
658        // Parse AND (higher precedence)
659        if let Some(parts) = self.split_logical_operator(clause, "&&") {
660            return self.parse_and_parts(parts);
661        }
662
663        // Handle NOT condition
664        if clause.trim_start().starts_with("!") {
665            return self.parse_not_condition(clause);
666        }
667
668        // Handle EXISTS condition
669        if clause.trim_start().starts_with("exists(") {
670            return self.parse_exists_condition(clause);
671        }
672
673        // Handle FORALL condition
674        if clause.trim_start().starts_with("forall(") {
675            return self.parse_forall_condition(clause);
676        }
677
678        // Handle ACCUMULATE condition
679        if clause.trim_start().starts_with("accumulate(") {
680            return self.parse_accumulate_condition(clause);
681        }
682
683        // Single condition
684        self.parse_single_condition(clause)
685    }
686
687    fn is_balanced_parentheses(&self, text: &str) -> bool {
688        let mut count = 0;
689        for ch in text.chars() {
690            match ch {
691                '(' => count += 1,
692                ')' => {
693                    count -= 1;
694                    if count < 0 {
695                        return false;
696                    }
697                }
698                _ => {}
699            }
700        }
701        count == 0
702    }
703
704    fn split_logical_operator(&self, clause: &str, operator: &str) -> Option<Vec<String>> {
705        let mut parts = Vec::new();
706        let mut current_part = String::new();
707        let mut paren_count = 0;
708        let mut chars = clause.chars().peekable();
709
710        while let Some(ch) = chars.next() {
711            match ch {
712                '(' => {
713                    paren_count += 1;
714                    current_part.push(ch);
715                }
716                ')' => {
717                    paren_count -= 1;
718                    current_part.push(ch);
719                }
720                '&' if operator == "&&" && paren_count == 0 && chars.peek() == Some(&'&') => {
721                    chars.next(); // consume second &
722                    parts.push(current_part.trim().to_string());
723                    current_part.clear();
724                }
725                '|' if operator == "||" && paren_count == 0 && chars.peek() == Some(&'|') => {
726                    chars.next(); // consume second |
727                    parts.push(current_part.trim().to_string());
728                    current_part.clear();
729                }
730                _ => {
731                    current_part.push(ch);
732                }
733            }
734        }
735
736        if !current_part.trim().is_empty() {
737            parts.push(current_part.trim().to_string());
738        }
739
740        if parts.len() > 1 {
741            Some(parts)
742        } else {
743            None
744        }
745    }
746
747    fn parse_or_parts(&self, parts: Vec<String>) -> Result<ConditionGroup> {
748        let mut conditions = Vec::new();
749        for part in parts {
750            let condition = self.parse_when_clause(&part)?;
751            conditions.push(condition);
752        }
753
754        if conditions.is_empty() {
755            return Err(RuleEngineError::ParseError {
756                message: "No conditions found in OR".to_string(),
757            });
758        }
759
760        let mut iter = conditions.into_iter();
761        let mut result = iter
762            .next()
763            .expect("Iterator cannot be empty after empty check");
764        for condition in iter {
765            result = ConditionGroup::or(result, condition);
766        }
767
768        Ok(result)
769    }
770
771    fn parse_and_parts(&self, parts: Vec<String>) -> Result<ConditionGroup> {
772        let mut conditions = Vec::new();
773        for part in parts {
774            let condition = self.parse_when_clause(&part)?;
775            conditions.push(condition);
776        }
777
778        if conditions.is_empty() {
779            return Err(RuleEngineError::ParseError {
780                message: "No conditions found in AND".to_string(),
781            });
782        }
783
784        let mut iter = conditions.into_iter();
785        let mut result = iter
786            .next()
787            .expect("Iterator cannot be empty after empty check");
788        for condition in iter {
789            result = ConditionGroup::and(result, condition);
790        }
791
792        Ok(result)
793    }
794
795    fn parse_not_condition(&self, clause: &str) -> Result<ConditionGroup> {
796        let inner_clause = clause
797            .strip_prefix('!')
798            .ok_or_else(|| RuleEngineError::ParseError {
799                message: format!("Expected '!' prefix in NOT condition: {}", clause),
800            })?
801            .trim();
802        let inner_condition = self.parse_when_clause(inner_clause)?;
803        Ok(ConditionGroup::not(inner_condition))
804    }
805
806    fn parse_exists_condition(&self, clause: &str) -> Result<ConditionGroup> {
807        let clause = clause.trim_start();
808        if !clause.starts_with("exists(") || !clause.ends_with(")") {
809            return Err(RuleEngineError::ParseError {
810                message: "Invalid exists syntax. Expected: exists(condition)".to_string(),
811            });
812        }
813
814        // Extract content between parentheses
815        let inner_clause = &clause[7..clause.len() - 1]; // Remove "exists(" and ")"
816        let inner_condition = self.parse_when_clause(inner_clause)?;
817        Ok(ConditionGroup::exists(inner_condition))
818    }
819
820    fn parse_forall_condition(&self, clause: &str) -> Result<ConditionGroup> {
821        let clause = clause.trim_start();
822        if !clause.starts_with("forall(") || !clause.ends_with(")") {
823            return Err(RuleEngineError::ParseError {
824                message: "Invalid forall syntax. Expected: forall(condition)".to_string(),
825            });
826        }
827
828        // Extract content between parentheses
829        let inner_clause = &clause[7..clause.len() - 1]; // Remove "forall(" and ")"
830        let inner_condition = self.parse_when_clause(inner_clause)?;
831        Ok(ConditionGroup::forall(inner_condition))
832    }
833
834    fn parse_accumulate_condition(&self, clause: &str) -> Result<ConditionGroup> {
835        let clause = clause.trim_start();
836        if !clause.starts_with("accumulate(") || !clause.ends_with(")") {
837            return Err(RuleEngineError::ParseError {
838                message: "Invalid accumulate syntax. Expected: accumulate(pattern, function)"
839                    .to_string(),
840            });
841        }
842
843        // Extract content between parentheses
844        let inner = &clause[11..clause.len() - 1]; // Remove "accumulate(" and ")"
845
846        // Split by comma at the top level (not inside parentheses)
847        let parts = self.split_accumulate_parts(inner)?;
848
849        if parts.len() != 2 {
850            return Err(RuleEngineError::ParseError {
851                message: format!(
852                    "Invalid accumulate syntax. Expected 2 parts (pattern, function), got {}",
853                    parts.len()
854                ),
855            });
856        }
857
858        let pattern_part = parts[0].trim();
859        let function_part = parts[1].trim();
860
861        // Parse the pattern: Order($amount: amount, status == "completed")
862        let (source_pattern, extract_field, source_conditions) =
863            self.parse_accumulate_pattern(pattern_part)?;
864
865        // Parse the function: sum($amount)
866        let (function, function_arg) = self.parse_accumulate_function(function_part)?;
867
868        // For now, we'll create a placeholder result variable
869        // In a full implementation, this would be extracted from the parent context
870        // e.g., from "$total: accumulate(...)"
871        let result_var = "$result".to_string();
872
873        Ok(ConditionGroup::accumulate(
874            result_var,
875            source_pattern,
876            extract_field,
877            source_conditions,
878            function,
879            function_arg,
880        ))
881    }
882
883    fn split_accumulate_parts(&self, content: &str) -> Result<Vec<String>> {
884        let mut parts = Vec::new();
885        let mut current = String::new();
886        let mut paren_depth = 0;
887
888        for ch in content.chars() {
889            match ch {
890                '(' => {
891                    paren_depth += 1;
892                    current.push(ch);
893                }
894                ')' => {
895                    paren_depth -= 1;
896                    current.push(ch);
897                }
898                ',' if paren_depth == 0 => {
899                    parts.push(current.trim().to_string());
900                    current.clear();
901                }
902                _ => {
903                    current.push(ch);
904                }
905            }
906        }
907
908        if !current.trim().is_empty() {
909            parts.push(current.trim().to_string());
910        }
911
912        Ok(parts)
913    }
914
915    fn parse_accumulate_pattern(&self, pattern: &str) -> Result<(String, String, Vec<String>)> {
916        // Pattern format: Order($amount: amount, status == "completed", category == "electronics")
917        // We need to extract:
918        // - source_pattern: "Order"
919        // - extract_field: "amount" (from $amount: amount)
920        // - source_conditions: ["status == \"completed\"", "category == \"electronics\""]
921
922        let pattern = pattern.trim();
923
924        // Find the opening parenthesis to get the pattern type
925        let paren_pos = pattern
926            .find('(')
927            .ok_or_else(|| RuleEngineError::ParseError {
928                message: format!("Invalid accumulate pattern: missing '(' in '{}'", pattern),
929            })?;
930
931        let source_pattern = pattern[..paren_pos].trim().to_string();
932
933        // Extract content between parentheses
934        if !pattern.ends_with(')') {
935            return Err(RuleEngineError::ParseError {
936                message: format!("Invalid accumulate pattern: missing ')' in '{}'", pattern),
937            });
938        }
939
940        let inner = &pattern[paren_pos + 1..pattern.len() - 1];
941
942        // Split by comma (respecting nested parentheses and quotes)
943        let parts = self.split_pattern_parts(inner)?;
944
945        let mut extract_field = String::new();
946        let mut source_conditions = Vec::new();
947
948        for part in parts {
949            let part = part.trim();
950
951            // Check if this is a variable binding: $var: field
952            if part.contains(':') && part.starts_with('$') {
953                if let Some(colon_pos) = part.find(':') {
954                    extract_field = part[colon_pos + 1..].trim().to_string();
955                }
956            } else if part.contains("==")
957                || part.contains("!=")
958                || part.contains(">=")
959                || part.contains("<=")
960                || part.contains('>')
961                || part.contains('<')
962            {
963                // This is a condition
964                source_conditions.push(part.to_string());
965            }
966        }
967
968        Ok((source_pattern, extract_field, source_conditions))
969    }
970
971    fn split_pattern_parts(&self, content: &str) -> Result<Vec<String>> {
972        let mut parts = Vec::new();
973        let mut current = String::new();
974        let mut paren_depth = 0;
975        let mut in_quotes = false;
976        let mut quote_char = ' ';
977
978        for ch in content.chars() {
979            match ch {
980                '"' | '\'' if !in_quotes => {
981                    in_quotes = true;
982                    quote_char = ch;
983                    current.push(ch);
984                }
985                '"' | '\'' if in_quotes && ch == quote_char => {
986                    in_quotes = false;
987                    current.push(ch);
988                }
989                '(' if !in_quotes => {
990                    paren_depth += 1;
991                    current.push(ch);
992                }
993                ')' if !in_quotes => {
994                    paren_depth -= 1;
995                    current.push(ch);
996                }
997                ',' if !in_quotes && paren_depth == 0 => {
998                    parts.push(current.trim().to_string());
999                    current.clear();
1000                }
1001                _ => {
1002                    current.push(ch);
1003                }
1004            }
1005        }
1006
1007        if !current.trim().is_empty() {
1008            parts.push(current.trim().to_string());
1009        }
1010
1011        Ok(parts)
1012    }
1013
1014    fn parse_accumulate_function(&self, function_str: &str) -> Result<(String, String)> {
1015        // Function format: sum($amount) or count() or average($price)
1016
1017        let function_str = function_str.trim();
1018
1019        let paren_pos = function_str
1020            .find('(')
1021            .ok_or_else(|| RuleEngineError::ParseError {
1022                message: format!(
1023                    "Invalid accumulate function: missing '(' in '{}'",
1024                    function_str
1025                ),
1026            })?;
1027
1028        let function_name = function_str[..paren_pos].trim().to_string();
1029
1030        if !function_str.ends_with(')') {
1031            return Err(RuleEngineError::ParseError {
1032                message: format!(
1033                    "Invalid accumulate function: missing ')' in '{}'",
1034                    function_str
1035                ),
1036            });
1037        }
1038
1039        let args = &function_str[paren_pos + 1..function_str.len() - 1];
1040        let function_arg = args.trim().to_string();
1041
1042        Ok((function_name, function_arg))
1043    }
1044
1045    fn parse_single_condition(&self, clause: &str) -> Result<ConditionGroup> {
1046        // Remove outer parentheses if they exist (handle new syntax like "(user.age >= 18)")
1047        let trimmed_clause = clause.trim();
1048        let clause_to_parse = if trimmed_clause.starts_with('(') && trimmed_clause.ends_with(')') {
1049            trimmed_clause[1..trimmed_clause.len() - 1].trim()
1050        } else {
1051            trimmed_clause
1052        };
1053
1054        // === STREAM PATTERNS ===
1055        // Check for stream pattern syntax: "var: Type from stream(...)"
1056        #[cfg(feature = "streaming")]
1057        if clause_to_parse.contains("from stream(") {
1058            return self.parse_stream_pattern_condition(clause_to_parse);
1059        }
1060
1061        // === MULTI-FIELD PATTERNS ===
1062        // Handle multi-field patterns before other patterns
1063        // These must be checked first to avoid conflict with standard patterns
1064
1065        // Pattern 1: Field.array $?var (Collect operation with variable binding)
1066        // Example: Order.items $?all_items
1067        if let Some(captures) = multifield_collect_regex().captures(clause_to_parse) {
1068            let field = captures.get(1).unwrap().to_string();
1069            let variable = captures.get(2).unwrap().to_string();
1070
1071            // Create a multifield Collect condition
1072            // Note: This will need to be handled by the engine
1073            let condition = Condition::with_multifield_collect(field, variable);
1074            return Ok(ConditionGroup::single(condition));
1075        }
1076
1077        // Pattern 2: Field.array contains "value"
1078        // Example: Product.tags contains "electronics"
1079        // This is already handled by the standard regex, but we need to distinguish array contains
1080
1081        // Pattern 3: Field.array count operator value
1082        // Example: Order.items count > 0, Order.items count >= 5
1083        if let Some(captures) = multifield_count_regex().captures(clause_to_parse) {
1084            let field = captures.get(1).unwrap().to_string();
1085            let operator_str = captures.get(2).unwrap();
1086            let value_str = captures.get(3).unwrap().trim();
1087
1088            let operator = Operator::from_str(operator_str).ok_or_else(|| {
1089                RuleEngineError::InvalidOperator {
1090                    operator: operator_str.to_string(),
1091                }
1092            })?;
1093
1094            let value = self.parse_value(value_str)?;
1095
1096            let condition = Condition::with_multifield_count(field, operator, value);
1097            return Ok(ConditionGroup::single(condition));
1098        }
1099
1100        // Pattern 4: Field.array first [optional: $var or operator value]
1101        // Example: Queue.tasks first, Queue.tasks first $first_task
1102        if let Some(captures) = multifield_first_regex().captures(clause_to_parse) {
1103            let field = captures.get(1).unwrap().to_string();
1104            let variable = captures.get(2).map(|m| m.to_string());
1105
1106            let condition = Condition::with_multifield_first(field, variable);
1107            return Ok(ConditionGroup::single(condition));
1108        }
1109
1110        // Pattern 5: Field.array last [optional: $var]
1111        // Example: Queue.tasks last, Queue.tasks last $last_task
1112        if let Some(captures) = multifield_last_regex().captures(clause_to_parse) {
1113            let field = captures.get(1).unwrap().to_string();
1114            let variable = captures.get(2).map(|m| m.to_string());
1115
1116            let condition = Condition::with_multifield_last(field, variable);
1117            return Ok(ConditionGroup::single(condition));
1118        }
1119
1120        // Pattern 6: Field.array empty
1121        // Example: ShoppingCart.items empty
1122        if let Some(captures) = multifield_empty_regex().captures(clause_to_parse) {
1123            let field = captures.get(1).unwrap().to_string();
1124
1125            let condition = Condition::with_multifield_empty(field);
1126            return Ok(ConditionGroup::single(condition));
1127        }
1128
1129        // Pattern 7: Field.array not_empty
1130        // Example: ShoppingCart.items not_empty
1131        if let Some(captures) = multifield_not_empty_regex().captures(clause_to_parse) {
1132            let field = captures.get(1).unwrap().to_string();
1133
1134            let condition = Condition::with_multifield_not_empty(field);
1135            return Ok(ConditionGroup::single(condition));
1136        }
1137
1138        // === END MULTI-FIELD PATTERNS ===
1139
1140        // Handle Test CE: test(functionName(args...))
1141        // This is a CLIPS-inspired feature for arbitrary boolean expressions
1142        if let Some(captures) = test_condition_regex().captures(clause_to_parse) {
1143            let function_name = captures.get(1).unwrap().to_string();
1144            let args_str = captures.get(2).unwrap();
1145
1146            // Parse arguments
1147            let args: Vec<String> = if args_str.trim().is_empty() {
1148                Vec::new()
1149            } else {
1150                args_str
1151                    .split(',')
1152                    .map(|arg| arg.trim().to_string())
1153                    .collect()
1154            };
1155
1156            let condition = Condition::with_test(function_name, args);
1157            return Ok(ConditionGroup::single(condition));
1158        }
1159
1160        // Handle typed object conditions like: $TestCar : TestCarClass( speedUp == true && speed < maxSpeed )
1161        if let Some(captures) = typed_test_condition_regex().captures(clause_to_parse) {
1162            let _object_name = captures.get(1).unwrap();
1163            let _object_type = captures.get(2).unwrap();
1164            let conditions_str = captures.get(3).unwrap();
1165
1166            // Parse conditions inside parentheses
1167            return self.parse_conditions_within_object(conditions_str);
1168        }
1169
1170        // Try to parse function call pattern: functionName(arg1, arg2, ...) operator value
1171        if let Some(captures) = function_call_regex().captures(clause_to_parse) {
1172            let function_name = captures.get(1).unwrap().to_string();
1173            let args_str = captures.get(2).unwrap();
1174            let operator_str = captures.get(3).unwrap();
1175            let value_str = captures.get(4).unwrap().trim();
1176
1177            // Parse arguments
1178            let args: Vec<String> = if args_str.trim().is_empty() {
1179                Vec::new()
1180            } else {
1181                args_str
1182                    .split(',')
1183                    .map(|arg| arg.trim().to_string())
1184                    .collect()
1185            };
1186
1187            let operator = Operator::from_str(operator_str).ok_or_else(|| {
1188                RuleEngineError::InvalidOperator {
1189                    operator: operator_str.to_string(),
1190                }
1191            })?;
1192
1193            let value = self.parse_value(value_str)?;
1194
1195            let condition = Condition::with_function(function_name, args, operator, value);
1196            return Ok(ConditionGroup::single(condition));
1197        }
1198
1199        // Parse expressions like: User.Age >= 18, Product.Price < 100.0, user.age >= 18, etc.
1200        // Support both PascalCase (User.Age) and lowercase (user.age) field naming
1201        // Also support arithmetic expressions like: User.Age % 3 == 0, User.Price * 2 > 100
1202        let captures = condition_regex().captures(clause_to_parse).ok_or_else(|| {
1203            RuleEngineError::ParseError {
1204                message: format!("Invalid condition format: {}", clause_to_parse),
1205            }
1206        })?;
1207
1208        let left_side = captures.get(1).unwrap().trim().to_string();
1209        let operator_str = captures.get(2).unwrap();
1210        let value_str = captures.get(3).unwrap().trim();
1211
1212        let operator =
1213            Operator::from_str(operator_str).ok_or_else(|| RuleEngineError::InvalidOperator {
1214                operator: operator_str.to_string(),
1215            })?;
1216
1217        let value = self.parse_value(value_str)?;
1218
1219        // Check if left_side contains arithmetic operators - if yes, it's an expression
1220        if left_side.contains('+')
1221            || left_side.contains('-')
1222            || left_side.contains('*')
1223            || left_side.contains('/')
1224            || left_side.contains('%')
1225        {
1226            // This is an arithmetic expression - use Test CE
1227            // Format: test(left_side operator value)
1228            let test_expr = format!("{} {} {}", left_side, operator_str, value_str);
1229            let condition = Condition::with_test(test_expr, vec![]);
1230            Ok(ConditionGroup::single(condition))
1231        } else {
1232            // Simple field reference
1233            let condition = Condition::new(left_side, operator, value);
1234            Ok(ConditionGroup::single(condition))
1235        }
1236    }
1237
1238    fn parse_conditions_within_object(&self, conditions_str: &str) -> Result<ConditionGroup> {
1239        // Parse conditions like: speedUp == true && speed < maxSpeed
1240        let parts: Vec<&str> = conditions_str.split("&&").collect();
1241
1242        let mut conditions = Vec::new();
1243        for part in parts {
1244            let trimmed = part.trim();
1245            let condition = self.parse_simple_condition(trimmed)?;
1246            conditions.push(condition);
1247        }
1248
1249        // Combine with AND
1250        if conditions.is_empty() {
1251            return Err(RuleEngineError::ParseError {
1252                message: "No conditions found".to_string(),
1253            });
1254        }
1255
1256        let mut iter = conditions.into_iter();
1257        let mut result = iter
1258            .next()
1259            .expect("Iterator cannot be empty after empty check");
1260        for condition in iter {
1261            result = ConditionGroup::and(result, condition);
1262        }
1263
1264        Ok(result)
1265    }
1266
1267    fn parse_simple_condition(&self, clause: &str) -> Result<ConditionGroup> {
1268        // Parse simple condition like: speedUp == true or speed < maxSpeed
1269        let captures = simple_condition_regex().captures(clause).ok_or_else(|| {
1270            RuleEngineError::ParseError {
1271                message: format!("Invalid simple condition format: {}", clause),
1272            }
1273        })?;
1274
1275        let field = captures.get(1).unwrap().to_string();
1276        let operator_str = captures.get(2).unwrap();
1277        let value_str = captures.get(3).unwrap().trim();
1278
1279        let operator =
1280            Operator::from_str(operator_str).ok_or_else(|| RuleEngineError::InvalidOperator {
1281                operator: operator_str.to_string(),
1282            })?;
1283
1284        let value = self.parse_value(value_str)?;
1285
1286        let condition = Condition::new(field, operator, value);
1287        Ok(ConditionGroup::single(condition))
1288    }
1289
1290    fn parse_value(&self, value_str: &str) -> Result<Value> {
1291        let trimmed = value_str.trim();
1292
1293        // Array literal: ["value1", "value2", 123]
1294        if trimmed.starts_with('[') && trimmed.ends_with(']') {
1295            return self.parse_array_literal(trimmed);
1296        }
1297
1298        // String literal
1299        if (trimmed.starts_with('"') && trimmed.ends_with('"'))
1300            || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
1301        {
1302            let unquoted = &trimmed[1..trimmed.len() - 1];
1303            return Ok(Value::String(unquoted.to_string()));
1304        }
1305
1306        // Boolean
1307        if trimmed.eq_ignore_ascii_case("true") {
1308            return Ok(Value::Boolean(true));
1309        }
1310        if trimmed.eq_ignore_ascii_case("false") {
1311            return Ok(Value::Boolean(false));
1312        }
1313
1314        // Null
1315        if trimmed.eq_ignore_ascii_case("null") {
1316            return Ok(Value::Null);
1317        }
1318
1319        // Number (try integer first, then float)
1320        if let Ok(int_val) = trimmed.parse::<i64>() {
1321            return Ok(Value::Integer(int_val));
1322        }
1323
1324        if let Ok(float_val) = trimmed.parse::<f64>() {
1325            return Ok(Value::Number(float_val));
1326        }
1327
1328        // Expression with arithmetic operators (e.g., "Order.quantity * Order.price")
1329        // Detect: contains operators AND (contains field reference OR multiple tokens)
1330        if self.is_expression(trimmed) {
1331            return Ok(Value::Expression(trimmed.to_string()));
1332        }
1333
1334        // Field reference (like User.Name)
1335        if trimmed.contains('.') {
1336            return Ok(Value::String(trimmed.to_string()));
1337        }
1338
1339        // Variable reference (identifier without quotes or dots)
1340        // This handles cases like: order_qty = moq
1341        // where 'moq' should be evaluated as a variable reference at runtime
1342        if self.is_identifier(trimmed) {
1343            return Ok(Value::Expression(trimmed.to_string()));
1344        }
1345
1346        // Default to string
1347        Ok(Value::String(trimmed.to_string()))
1348    }
1349
1350    /// Check if a string is a valid identifier (variable name)
1351    /// Valid identifiers: alphanumeric + underscore, starts with letter or underscore
1352    fn is_identifier(&self, s: &str) -> bool {
1353        if s.is_empty() {
1354            return false;
1355        }
1356        let first_char = s.chars().next().expect("Cannot be empty after empty check");
1357        if !first_char.is_alphabetic() && first_char != '_' {
1358            return false;
1359        }
1360
1361        // First character must be letter or underscore
1362        let first_char = s.chars().next().unwrap();
1363        if !first_char.is_alphabetic() && first_char != '_' {
1364            return false;
1365        }
1366
1367        // Rest must be alphanumeric or underscore
1368        s.chars().all(|c| c.is_alphanumeric() || c == '_')
1369    }
1370
1371    /// Check if a string is an arithmetic expression
1372    fn is_expression(&self, s: &str) -> bool {
1373        // Check for arithmetic operators
1374        let has_operator = s.contains('+')
1375            || s.contains('-')
1376            || s.contains('*')
1377            || s.contains('/')
1378            || s.contains('%');
1379
1380        // Check for field references (contains .)
1381        let has_field_ref = s.contains('.');
1382
1383        // Check for multiple tokens (spaces between operands/operators)
1384        let has_spaces = s.contains(' ');
1385
1386        // Expression if: has operator AND (has field reference OR has spaces)
1387        has_operator && (has_field_ref || has_spaces)
1388    }
1389
1390    /// Parse array literal like ["value1", "value2", 123]
1391    fn parse_array_literal(&self, array_str: &str) -> Result<Value> {
1392        let content = array_str.trim();
1393        if !content.starts_with('[') || !content.ends_with(']') {
1394            return Err(RuleEngineError::ParseError {
1395                message: format!("Invalid array literal: {}", array_str),
1396            });
1397        }
1398
1399        let inner = content[1..content.len() - 1].trim();
1400        if inner.is_empty() {
1401            return Ok(Value::Array(vec![]));
1402        }
1403
1404        // Split by comma, handling quoted strings
1405        let mut elements = Vec::new();
1406        let mut current_element = String::new();
1407        let mut in_quotes = false;
1408        let mut quote_char = ' ';
1409
1410        for ch in inner.chars() {
1411            match ch {
1412                '"' | '\'' if !in_quotes => {
1413                    in_quotes = true;
1414                    quote_char = ch;
1415                    current_element.push(ch);
1416                }
1417                c if in_quotes && c == quote_char => {
1418                    in_quotes = false;
1419                    current_element.push(ch);
1420                }
1421                ',' if !in_quotes => {
1422                    if !current_element.trim().is_empty() {
1423                        elements.push(current_element.trim().to_string());
1424                    }
1425                    current_element.clear();
1426                }
1427                _ => {
1428                    current_element.push(ch);
1429                }
1430            }
1431        }
1432
1433        // Don't forget the last element
1434        if !current_element.trim().is_empty() {
1435            elements.push(current_element.trim().to_string());
1436        }
1437
1438        // Parse each element
1439        let mut array_values = Vec::new();
1440        for elem in elements {
1441            let value = self.parse_value(&elem)?;
1442            array_values.push(value);
1443        }
1444
1445        Ok(Value::Array(array_values))
1446    }
1447
1448    fn parse_then_clause(&self, then_clause: &str) -> Result<Vec<ActionType>> {
1449        let statements: Vec<&str> = then_clause
1450            .split(';')
1451            .map(|s| s.trim())
1452            .filter(|s| !s.is_empty())
1453            .collect();
1454
1455        let mut actions = Vec::new();
1456
1457        for statement in statements {
1458            let action = self.parse_action_statement(statement)?;
1459            actions.push(action);
1460        }
1461
1462        Ok(actions)
1463    }
1464
1465    fn parse_action_statement(&self, statement: &str) -> Result<ActionType> {
1466        let trimmed = statement.trim();
1467
1468        // Method call: $Object.method(args)
1469        if let Some(captures) = method_call_regex().captures(trimmed) {
1470            let object = captures.get(1).unwrap().to_string();
1471            let method = captures.get(2).unwrap().to_string();
1472            let args_str = captures.get(3).unwrap();
1473
1474            let args = if args_str.trim().is_empty() {
1475                Vec::new()
1476            } else {
1477                self.parse_method_args(args_str)?
1478            };
1479
1480            return Ok(ActionType::MethodCall {
1481                object,
1482                method,
1483                args,
1484            });
1485        }
1486
1487        // Check for compound assignment operators first (+=, -=, etc.)
1488        if let Some(plus_eq_pos) = trimmed.find("+=") {
1489            // Append operator: Field += Value
1490            let field = trimmed[..plus_eq_pos].trim().to_string();
1491            let value_str = trimmed[plus_eq_pos + 2..].trim();
1492            let value = self.parse_value(value_str)?;
1493
1494            return Ok(ActionType::Append { field, value });
1495        }
1496
1497        // Assignment: Field = Value
1498        if let Some(eq_pos) = trimmed.find('=') {
1499            let field = trimmed[..eq_pos].trim().to_string();
1500            let value_str = trimmed[eq_pos + 1..].trim();
1501            let value = self.parse_value(value_str)?;
1502
1503            return Ok(ActionType::Set { field, value });
1504        }
1505
1506        // Function calls: update($Object), retract($Object), etc.
1507        if let Some(captures) = function_binding_regex().captures(trimmed) {
1508            let function_name = captures.get(1).unwrap();
1509            let args_str = captures.get(2).unwrap_or("");
1510
1511            match function_name.to_lowercase().as_str() {
1512                "retract" => {
1513                    // Extract object name from $Object
1514                    let object_name = if let Some(stripped) = args_str.strip_prefix('$') {
1515                        stripped.to_string()
1516                    } else {
1517                        args_str.to_string()
1518                    };
1519                    Ok(ActionType::Retract {
1520                        object: object_name,
1521                    })
1522                }
1523                "log" => {
1524                    let message = if args_str.is_empty() {
1525                        "Log message".to_string()
1526                    } else {
1527                        let value = self.parse_value(args_str.trim())?;
1528                        value.to_string()
1529                    };
1530                    Ok(ActionType::Log { message })
1531                }
1532                "activateagendagroup" | "activate_agenda_group" => {
1533                    let agenda_group = if args_str.is_empty() {
1534                        return Err(RuleEngineError::ParseError {
1535                            message: "ActivateAgendaGroup requires agenda group name".to_string(),
1536                        });
1537                    } else {
1538                        let value = self.parse_value(args_str.trim())?;
1539                        match value {
1540                            Value::String(s) => s,
1541                            _ => value.to_string(),
1542                        }
1543                    };
1544                    Ok(ActionType::ActivateAgendaGroup {
1545                        group: agenda_group,
1546                    })
1547                }
1548                "schedulerule" | "schedule_rule" => {
1549                    // Parse delay and target rule: ScheduleRule(5000, "next-rule")
1550                    let parts: Vec<&str> = args_str.split(',').collect();
1551                    if parts.len() != 2 {
1552                        return Err(RuleEngineError::ParseError {
1553                            message: "ScheduleRule requires delay_ms and rule_name".to_string(),
1554                        });
1555                    }
1556
1557                    let delay_ms = self.parse_value(parts[0].trim())?;
1558                    let rule_name = self.parse_value(parts[1].trim())?;
1559
1560                    let delay_ms = match delay_ms {
1561                        Value::Integer(i) => i as u64,
1562                        Value::Number(f) => f as u64,
1563                        _ => {
1564                            return Err(RuleEngineError::ParseError {
1565                                message: "ScheduleRule delay_ms must be a number".to_string(),
1566                            })
1567                        }
1568                    };
1569
1570                    let rule_name = match rule_name {
1571                        Value::String(s) => s,
1572                        _ => rule_name.to_string(),
1573                    };
1574
1575                    Ok(ActionType::ScheduleRule {
1576                        delay_ms,
1577                        rule_name,
1578                    })
1579                }
1580                "completeworkflow" | "complete_workflow" => {
1581                    let workflow_id = if args_str.is_empty() {
1582                        return Err(RuleEngineError::ParseError {
1583                            message: "CompleteWorkflow requires workflow_id".to_string(),
1584                        });
1585                    } else {
1586                        let value = self.parse_value(args_str.trim())?;
1587                        match value {
1588                            Value::String(s) => s,
1589                            _ => value.to_string(),
1590                        }
1591                    };
1592                    Ok(ActionType::CompleteWorkflow {
1593                        workflow_name: workflow_id,
1594                    })
1595                }
1596                "setworkflowdata" | "set_workflow_data" => {
1597                    // Parse key=value: SetWorkflowData("key=value")
1598                    let data_str = args_str.trim();
1599
1600                    // Simple key=value parsing
1601                    let (key, value) = if let Some(eq_pos) = data_str.find('=') {
1602                        let key = data_str[..eq_pos].trim().trim_matches('"');
1603                        let value_str = data_str[eq_pos + 1..].trim();
1604                        let value = self.parse_value(value_str)?;
1605                        (key.to_string(), value)
1606                    } else {
1607                        return Err(RuleEngineError::ParseError {
1608                            message: "SetWorkflowData data must be in key=value format".to_string(),
1609                        });
1610                    };
1611
1612                    Ok(ActionType::SetWorkflowData { key, value })
1613                }
1614                _ => {
1615                    // All other functions become custom actions
1616                    let params = if args_str.is_empty() {
1617                        HashMap::new()
1618                    } else {
1619                        self.parse_function_args_as_params(args_str)?
1620                    };
1621
1622                    Ok(ActionType::Custom {
1623                        action_type: function_name.to_string(),
1624                        params,
1625                    })
1626                }
1627            }
1628        } else {
1629            // Custom statement
1630            Ok(ActionType::Custom {
1631                action_type: "statement".to_string(),
1632                params: {
1633                    let mut params = HashMap::new();
1634                    params.insert("statement".to_string(), Value::String(trimmed.to_string()));
1635                    params
1636                },
1637            })
1638        }
1639    }
1640
1641    fn parse_method_args(&self, args_str: &str) -> Result<Vec<Value>> {
1642        if args_str.trim().is_empty() {
1643            return Ok(Vec::new());
1644        }
1645
1646        // Handle expressions like: $TestCar.Speed + $TestCar.SpeedIncrement
1647        let mut args = Vec::new();
1648        let parts: Vec<&str> = args_str.split(',').collect();
1649
1650        for part in parts {
1651            let trimmed = part.trim();
1652
1653            // Handle arithmetic expressions
1654            if trimmed.contains('+')
1655                || trimmed.contains('-')
1656                || trimmed.contains('*')
1657                || trimmed.contains('/')
1658            {
1659                // For now, store as string - the engine will evaluate
1660                args.push(Value::String(trimmed.to_string()));
1661            } else {
1662                args.push(self.parse_value(trimmed)?);
1663            }
1664        }
1665
1666        Ok(args)
1667    }
1668
1669    /// Parse function arguments as parameters for custom actions
1670    fn parse_function_args_as_params(&self, args_str: &str) -> Result<HashMap<String, Value>> {
1671        let mut params = HashMap::new();
1672
1673        if args_str.trim().is_empty() {
1674            return Ok(params);
1675        }
1676
1677        // Parse positional parameters as numbered args
1678        let parts: Vec<&str> = args_str.split(',').collect();
1679        for (i, part) in parts.iter().enumerate() {
1680            let trimmed = part.trim();
1681            let value = self.parse_value(trimmed)?;
1682
1683            // Use simple numeric indexing - engine will resolve references dynamically
1684            params.insert(i.to_string(), value);
1685        }
1686
1687        Ok(params)
1688    }
1689
1690    /// Parse stream pattern condition
1691    /// Example: "login: LoginEvent from stream(\"logins\") over window(10 min, sliding)"
1692    #[cfg(feature = "streaming")]
1693    fn parse_stream_pattern_condition(&self, clause: &str) -> Result<ConditionGroup> {
1694        use crate::engine::rule::{StreamWindow, StreamWindowType};
1695        use crate::parser::grl::stream_syntax::parse_stream_pattern;
1696
1697        // Parse using nom parser
1698        let parse_result =
1699            parse_stream_pattern(clause).map_err(|e| RuleEngineError::ParseError {
1700                message: format!("Failed to parse stream pattern: {:?}", e),
1701            })?;
1702
1703        let (_, pattern) = parse_result;
1704
1705        // Convert WindowType from parser to StreamWindowType
1706        let window = pattern.source.window.map(|w| StreamWindow {
1707            duration: w.duration,
1708            window_type: match w.window_type {
1709                crate::parser::grl::stream_syntax::WindowType::Sliding => StreamWindowType::Sliding,
1710                crate::parser::grl::stream_syntax::WindowType::Tumbling => {
1711                    StreamWindowType::Tumbling
1712                }
1713                crate::parser::grl::stream_syntax::WindowType::Session { timeout } => {
1714                    StreamWindowType::Session { timeout }
1715                }
1716            },
1717        });
1718
1719        Ok(ConditionGroup::stream_pattern(
1720            pattern.var_name,
1721            pattern.event_type,
1722            pattern.source.stream_name,
1723            window,
1724        ))
1725    }
1726}
1727
1728#[cfg(test)]
1729mod tests {
1730    use super::GRLParser;
1731
1732    #[test]
1733    fn test_parse_simple_rule() {
1734        let grl = r#"
1735        rule "CheckAge" salience 10 {
1736            when
1737                User.Age >= 18
1738            then
1739                log("User is adult");
1740        }
1741        "#;
1742
1743        let rules = GRLParser::parse_rules(grl).unwrap();
1744        assert_eq!(rules.len(), 1);
1745        let rule = &rules[0];
1746        assert_eq!(rule.name, "CheckAge");
1747        assert_eq!(rule.salience, 10);
1748        assert_eq!(rule.actions.len(), 1);
1749    }
1750
1751    #[test]
1752    fn test_parse_complex_condition() {
1753        let grl = r#"
1754        rule "ComplexRule" {
1755            when
1756                User.Age >= 18 && User.Country == "US"
1757            then
1758                User.Qualified = true;
1759        }
1760        "#;
1761
1762        let rules = GRLParser::parse_rules(grl).unwrap();
1763        assert_eq!(rules.len(), 1);
1764        let rule = &rules[0];
1765        assert_eq!(rule.name, "ComplexRule");
1766    }
1767
1768    #[test]
1769    fn test_parse_new_syntax_with_parentheses() {
1770        let grl = r#"
1771        rule "Default Rule" salience 10 {
1772            when
1773                (user.age >= 18)
1774            then
1775                set(user.status, "approved");
1776        }
1777        "#;
1778
1779        let rules = GRLParser::parse_rules(grl).unwrap();
1780        assert_eq!(rules.len(), 1);
1781        let rule = &rules[0];
1782        assert_eq!(rule.name, "Default Rule");
1783        assert_eq!(rule.salience, 10);
1784        assert_eq!(rule.actions.len(), 1);
1785
1786        // Check that the action is parsed as a Custom action (set is now custom)
1787        match &rule.actions[0] {
1788            crate::types::ActionType::Custom {
1789                action_type,
1790                params,
1791            } => {
1792                assert_eq!(action_type, "set");
1793                assert_eq!(
1794                    params.get("0"),
1795                    Some(&crate::types::Value::String("user.status".to_string()))
1796                );
1797                assert_eq!(
1798                    params.get("1"),
1799                    Some(&crate::types::Value::String("approved".to_string()))
1800                );
1801            }
1802            _ => panic!("Expected Custom action, got: {:?}", rule.actions[0]),
1803        }
1804    }
1805
1806    #[test]
1807    fn test_parse_complex_nested_conditions() {
1808        let grl = r#"
1809        rule "Complex Business Rule" salience 10 {
1810            when
1811                (((user.vipStatus == true) && (order.amount > 500)) || ((date.isHoliday == true) && (order.hasCoupon == true)))
1812            then
1813                apply_discount(20000);
1814        }
1815        "#;
1816
1817        let rules = GRLParser::parse_rules(grl).unwrap();
1818        assert_eq!(rules.len(), 1);
1819        let rule = &rules[0];
1820        assert_eq!(rule.name, "Complex Business Rule");
1821        assert_eq!(rule.salience, 10);
1822        assert_eq!(rule.actions.len(), 1);
1823
1824        // Check that the action is parsed as a Custom action (apply_discount is now custom)
1825        match &rule.actions[0] {
1826            crate::types::ActionType::Custom {
1827                action_type,
1828                params,
1829            } => {
1830                assert_eq!(action_type, "apply_discount");
1831                assert_eq!(params.get("0"), Some(&crate::types::Value::Integer(20000)));
1832            }
1833            _ => panic!("Expected Custom action, got: {:?}", rule.actions[0]),
1834        }
1835    }
1836
1837    #[test]
1838    fn test_parse_no_loop_attribute() {
1839        let grl = r#"
1840        rule "NoLoopRule" no-loop salience 15 {
1841            when
1842                User.Score < 100
1843            then
1844                set(User.Score, User.Score + 10);
1845        }
1846        "#;
1847
1848        let rules = GRLParser::parse_rules(grl).unwrap();
1849        assert_eq!(rules.len(), 1);
1850        let rule = &rules[0];
1851        assert_eq!(rule.name, "NoLoopRule");
1852        assert_eq!(rule.salience, 15);
1853        assert!(rule.no_loop, "Rule should have no-loop=true");
1854    }
1855
1856    #[test]
1857    fn test_parse_no_loop_different_positions() {
1858        // Test no-loop before salience
1859        let grl1 = r#"
1860        rule "Rule1" no-loop salience 10 {
1861            when User.Age >= 18
1862            then log("adult");
1863        }
1864        "#;
1865
1866        // Test no-loop after salience
1867        let grl2 = r#"
1868        rule "Rule2" salience 10 no-loop {
1869            when User.Age >= 18
1870            then log("adult");
1871        }
1872        "#;
1873
1874        let rules1 = GRLParser::parse_rules(grl1).unwrap();
1875        let rules2 = GRLParser::parse_rules(grl2).unwrap();
1876
1877        assert_eq!(rules1.len(), 1);
1878        assert_eq!(rules2.len(), 1);
1879
1880        assert!(rules1[0].no_loop, "Rule1 should have no-loop=true");
1881        assert!(rules2[0].no_loop, "Rule2 should have no-loop=true");
1882
1883        assert_eq!(rules1[0].salience, 10);
1884        assert_eq!(rules2[0].salience, 10);
1885    }
1886
1887    #[test]
1888    fn test_parse_without_no_loop() {
1889        let grl = r#"
1890        rule "RegularRule" salience 5 {
1891            when
1892                User.Active == true
1893            then
1894                log("active user");
1895        }
1896        "#;
1897
1898        let rules = GRLParser::parse_rules(grl).unwrap();
1899        assert_eq!(rules.len(), 1);
1900        let rule = &rules[0];
1901        assert_eq!(rule.name, "RegularRule");
1902        assert!(!rule.no_loop, "Rule should have no-loop=false by default");
1903    }
1904
1905    #[test]
1906    fn test_parse_exists_pattern() {
1907        let grl = r#"
1908        rule "ExistsRule" salience 20 {
1909            when
1910                exists(Customer.tier == "VIP")
1911            then
1912                System.premiumActive = true;
1913        }
1914        "#;
1915
1916        let rules = GRLParser::parse_rules(grl).unwrap();
1917        assert_eq!(rules.len(), 1);
1918        let rule = &rules[0];
1919        assert_eq!(rule.name, "ExistsRule");
1920        assert_eq!(rule.salience, 20);
1921
1922        // Check that condition is EXISTS pattern
1923        match &rule.conditions {
1924            crate::engine::rule::ConditionGroup::Exists(_) => {
1925                // Test passes
1926            }
1927            _ => panic!(
1928                "Expected EXISTS condition group, got: {:?}",
1929                rule.conditions
1930            ),
1931        }
1932    }
1933
1934    #[test]
1935    fn test_parse_forall_pattern() {
1936        let grl = r#"
1937        rule "ForallRule" salience 15 {
1938            when
1939                forall(Order.status == "processed")
1940            then
1941                Shipping.enabled = true;
1942        }
1943        "#;
1944
1945        let rules = GRLParser::parse_rules(grl).unwrap();
1946        assert_eq!(rules.len(), 1);
1947        let rule = &rules[0];
1948        assert_eq!(rule.name, "ForallRule");
1949
1950        // Check that condition is FORALL pattern
1951        match &rule.conditions {
1952            crate::engine::rule::ConditionGroup::Forall(_) => {
1953                // Test passes
1954            }
1955            _ => panic!(
1956                "Expected FORALL condition group, got: {:?}",
1957                rule.conditions
1958            ),
1959        }
1960    }
1961
1962    #[test]
1963    fn test_parse_combined_patterns() {
1964        let grl = r#"
1965        rule "CombinedRule" salience 25 {
1966            when
1967                exists(Customer.tier == "VIP") && !exists(Alert.priority == "high")
1968            then
1969                System.vipMode = true;
1970        }
1971        "#;
1972
1973        let rules = GRLParser::parse_rules(grl).unwrap();
1974        assert_eq!(rules.len(), 1);
1975        let rule = &rules[0];
1976        assert_eq!(rule.name, "CombinedRule");
1977
1978        // Check that condition is AND with EXISTS and NOT(EXISTS) patterns
1979        match &rule.conditions {
1980            crate::engine::rule::ConditionGroup::Compound {
1981                left,
1982                operator,
1983                right,
1984            } => {
1985                assert_eq!(*operator, crate::types::LogicalOperator::And);
1986
1987                // Left should be EXISTS
1988                match left.as_ref() {
1989                    crate::engine::rule::ConditionGroup::Exists(_) => {
1990                        // Expected
1991                    }
1992                    _ => panic!("Expected EXISTS in left side, got: {:?}", left),
1993                }
1994
1995                // Right should be NOT(EXISTS)
1996                match right.as_ref() {
1997                    crate::engine::rule::ConditionGroup::Not(inner) => {
1998                        match inner.as_ref() {
1999                            crate::engine::rule::ConditionGroup::Exists(_) => {
2000                                // Expected
2001                            }
2002                            _ => panic!("Expected EXISTS inside NOT, got: {:?}", inner),
2003                        }
2004                    }
2005                    _ => panic!("Expected NOT in right side, got: {:?}", right),
2006                }
2007            }
2008            _ => panic!("Expected compound condition, got: {:?}", rule.conditions),
2009        }
2010    }
2011
2012    #[test]
2013    fn test_parse_in_operator() {
2014        let grl = r#"
2015        rule "TestInOperator" salience 75 {
2016            when
2017                User.role in ["admin", "moderator", "vip"]
2018            then
2019                User.access = "granted";
2020        }
2021        "#;
2022
2023        let rules = GRLParser::parse_rules(grl).unwrap();
2024        assert_eq!(rules.len(), 1);
2025        let rule = &rules[0];
2026        assert_eq!(rule.name, "TestInOperator");
2027        assert_eq!(rule.salience, 75);
2028
2029        // Check the condition
2030        match &rule.conditions {
2031            crate::engine::rule::ConditionGroup::Single(cond) => {
2032                // The field might be in expression format
2033                println!("Condition: {:?}", cond);
2034                assert_eq!(cond.operator, crate::types::Operator::In);
2035
2036                // Value should be an array
2037                match &cond.value {
2038                    crate::types::Value::Array(arr) => {
2039                        assert_eq!(arr.len(), 3);
2040                        assert_eq!(arr[0], crate::types::Value::String("admin".to_string()));
2041                        assert_eq!(arr[1], crate::types::Value::String("moderator".to_string()));
2042                        assert_eq!(arr[2], crate::types::Value::String("vip".to_string()));
2043                    }
2044                    _ => panic!("Expected Array value, got {:?}", cond.value),
2045                }
2046            }
2047            _ => panic!("Expected Single condition, got: {:?}", rule.conditions),
2048        }
2049    }
2050
2051    #[test]
2052    fn test_parse_startswith_endswith_operators() {
2053        let grl = r#"
2054        rule "StringMethods" salience 50 {
2055            when
2056                User.email startsWith "admin@" &&
2057                User.filename endsWith ".txt"
2058            then
2059                User.validated = true;
2060        }
2061        "#;
2062
2063        let rules = GRLParser::parse_rules(grl).unwrap();
2064        assert_eq!(rules.len(), 1);
2065        let rule = &rules[0];
2066        assert_eq!(rule.name, "StringMethods");
2067        assert_eq!(rule.salience, 50);
2068
2069        // Check the compound condition (AND)
2070        match &rule.conditions {
2071            crate::engine::rule::ConditionGroup::Compound {
2072                left,
2073                operator,
2074                right,
2075            } => {
2076                assert_eq!(*operator, crate::types::LogicalOperator::And);
2077
2078                // Left should be startsWith
2079                match left.as_ref() {
2080                    crate::engine::rule::ConditionGroup::Single(cond) => {
2081                        assert_eq!(cond.operator, crate::types::Operator::StartsWith);
2082                    }
2083                    _ => panic!("Expected Single condition for startsWith, got: {:?}", left),
2084                }
2085
2086                // Right should be endsWith
2087                match right.as_ref() {
2088                    crate::engine::rule::ConditionGroup::Single(cond) => {
2089                        assert_eq!(cond.operator, crate::types::Operator::EndsWith);
2090                    }
2091                    _ => panic!("Expected Single condition for endsWith, got: {:?}", right),
2092                }
2093            }
2094            _ => panic!("Expected Compound condition, got: {:?}", rule.conditions),
2095        }
2096    }
2097}