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.len() >= 2 {
1300            let unquoted = &trimmed[1..trimmed.len() - 1];
1301            if (trimmed.starts_with('"') && trimmed.ends_with('"') && !unquoted.contains('"'))
1302                || (trimmed.starts_with('\'')
1303                    && trimmed.ends_with('\'')
1304                    && !unquoted.contains('\''))
1305            {
1306                return Ok(Value::String(unquoted.to_string()));
1307            }
1308        }
1309
1310        // Boolean
1311        if trimmed.eq_ignore_ascii_case("true") {
1312            return Ok(Value::Boolean(true));
1313        }
1314        if trimmed.eq_ignore_ascii_case("false") {
1315            return Ok(Value::Boolean(false));
1316        }
1317
1318        // Null
1319        if trimmed.eq_ignore_ascii_case("null") {
1320            return Ok(Value::Null);
1321        }
1322
1323        // Number (try integer first, then float)
1324        if let Ok(int_val) = trimmed.parse::<i64>() {
1325            return Ok(Value::Integer(int_val));
1326        }
1327
1328        if let Ok(float_val) = trimmed.parse::<f64>() {
1329            return Ok(Value::Number(float_val));
1330        }
1331
1332        // Expression with arithmetic operators (e.g., "Order.quantity * Order.price")
1333        // Detect: contains operators AND (contains field reference OR multiple tokens)
1334        if self.is_expression(trimmed) {
1335            return Ok(Value::Expression(trimmed.to_string()));
1336        }
1337
1338        // Field reference (like User.Name) - needs runtime evaluation, not a literal string
1339        if trimmed.contains('.') {
1340            return Ok(Value::Expression(trimmed.to_string()));
1341        }
1342
1343        // Variable reference (identifier without quotes or dots)
1344        // This handles cases like: order_qty = moq
1345        // where 'moq' should be evaluated as a variable reference at runtime
1346        if self.is_identifier(trimmed) {
1347            return Ok(Value::Expression(trimmed.to_string()));
1348        }
1349
1350        // Default to string
1351        Ok(Value::String(trimmed.to_string()))
1352    }
1353
1354    /// Check if a string is a valid identifier (variable name)
1355    /// Valid identifiers: alphanumeric + underscore, starts with letter or underscore
1356    fn is_identifier(&self, s: &str) -> bool {
1357        if s.is_empty() {
1358            return false;
1359        }
1360        let first_char = s.chars().next().expect("Cannot be empty after empty check");
1361        if !first_char.is_alphabetic() && first_char != '_' {
1362            return false;
1363        }
1364
1365        // First character must be letter or underscore
1366        let first_char = s.chars().next().unwrap();
1367        if !first_char.is_alphabetic() && first_char != '_' {
1368            return false;
1369        }
1370
1371        // Rest must be alphanumeric or underscore
1372        s.chars().all(|c| c.is_alphanumeric() || c == '_')
1373    }
1374
1375    /// Check if a string is an arithmetic expression
1376    fn is_expression(&self, s: &str) -> bool {
1377        // Check for arithmetic operators
1378        let has_operator = s.contains('+')
1379            || s.contains('-')
1380            || s.contains('*')
1381            || s.contains('/')
1382            || s.contains('%');
1383
1384        // Check for field references (contains .)
1385        let has_field_ref = s.contains('.');
1386
1387        // Check for multiple tokens (spaces between operands/operators)
1388        let has_spaces = s.contains(' ');
1389
1390        // Expression if: has operator AND (has field reference OR has spaces)
1391        has_operator && (has_field_ref || has_spaces)
1392    }
1393
1394    /// Parse array literal like ["value1", "value2", 123]
1395    fn parse_array_literal(&self, array_str: &str) -> Result<Value> {
1396        let content = array_str.trim();
1397        if !content.starts_with('[') || !content.ends_with(']') {
1398            return Err(RuleEngineError::ParseError {
1399                message: format!("Invalid array literal: {}", array_str),
1400            });
1401        }
1402
1403        let inner = content[1..content.len() - 1].trim();
1404        if inner.is_empty() {
1405            return Ok(Value::Array(vec![]));
1406        }
1407
1408        // Split by comma, handling quoted strings
1409        let mut elements = Vec::new();
1410        let mut current_element = String::new();
1411        let mut in_quotes = false;
1412        let mut quote_char = ' ';
1413
1414        for ch in inner.chars() {
1415            match ch {
1416                '"' | '\'' if !in_quotes => {
1417                    in_quotes = true;
1418                    quote_char = ch;
1419                    current_element.push(ch);
1420                }
1421                c if in_quotes && c == quote_char => {
1422                    in_quotes = false;
1423                    current_element.push(ch);
1424                }
1425                ',' if !in_quotes => {
1426                    if !current_element.trim().is_empty() {
1427                        elements.push(current_element.trim().to_string());
1428                    }
1429                    current_element.clear();
1430                }
1431                _ => {
1432                    current_element.push(ch);
1433                }
1434            }
1435        }
1436
1437        // Don't forget the last element
1438        if !current_element.trim().is_empty() {
1439            elements.push(current_element.trim().to_string());
1440        }
1441
1442        // Parse each element
1443        let mut array_values = Vec::new();
1444        for elem in elements {
1445            let value = self.parse_value(&elem)?;
1446            array_values.push(value);
1447        }
1448
1449        Ok(Value::Array(array_values))
1450    }
1451
1452    fn parse_then_clause(&self, then_clause: &str) -> Result<Vec<ActionType>> {
1453        let statements: Vec<&str> = then_clause
1454            .split(';')
1455            .map(|s| s.trim())
1456            .filter(|s| !s.is_empty())
1457            .collect();
1458
1459        let mut actions = Vec::new();
1460
1461        for statement in statements {
1462            let action = self.parse_action_statement(statement)?;
1463            actions.push(action);
1464        }
1465
1466        Ok(actions)
1467    }
1468
1469    fn parse_action_statement(&self, statement: &str) -> Result<ActionType> {
1470        let trimmed = statement.trim();
1471
1472        // Method call: $Object.method(args)
1473        if let Some(captures) = method_call_regex().captures(trimmed) {
1474            let object = captures.get(1).unwrap().to_string();
1475            let method = captures.get(2).unwrap().to_string();
1476            let args_str = captures.get(3).unwrap();
1477
1478            let args = if args_str.trim().is_empty() {
1479                Vec::new()
1480            } else {
1481                self.parse_method_args(args_str)?
1482            };
1483
1484            return Ok(ActionType::MethodCall {
1485                object,
1486                method,
1487                args,
1488            });
1489        }
1490
1491        // Check for compound assignment operators first (+=, -=, etc.)
1492        if let Some(plus_eq_pos) = trimmed.find("+=") {
1493            // Append operator: Field += Value
1494            let field = trimmed[..plus_eq_pos].trim().to_string();
1495            let value_str = trimmed[plus_eq_pos + 2..].trim();
1496            let value = self.parse_value(value_str)?;
1497
1498            return Ok(ActionType::Append { field, value });
1499        }
1500
1501        // Assignment: Field = Value
1502        if let Some(eq_pos) = trimmed.find('=') {
1503            let field = trimmed[..eq_pos].trim().to_string();
1504            let value_str = trimmed[eq_pos + 1..].trim();
1505            let value = self.parse_value(value_str)?;
1506
1507            return Ok(ActionType::Set { field, value });
1508        }
1509
1510        // Function calls: update($Object), retract($Object), etc.
1511        if let Some(captures) = function_binding_regex().captures(trimmed) {
1512            let function_name = captures.get(1).unwrap();
1513            let args_str = captures.get(2).unwrap_or("");
1514
1515            match function_name.to_lowercase().as_str() {
1516                "retract" => {
1517                    // Extract object name from $Object
1518                    let object_name = if let Some(stripped) = args_str.strip_prefix('$') {
1519                        stripped.to_string()
1520                    } else {
1521                        args_str.to_string()
1522                    };
1523                    Ok(ActionType::Retract {
1524                        object: object_name,
1525                    })
1526                }
1527                "log" => {
1528                    let message = if args_str.is_empty() {
1529                        "Log message".to_string()
1530                    } else {
1531                        let value = self.parse_value(args_str.trim())?;
1532                        value.to_string()
1533                    };
1534                    Ok(ActionType::Log { message })
1535                }
1536                "activateagendagroup" | "activate_agenda_group" => {
1537                    let agenda_group = if args_str.is_empty() {
1538                        return Err(RuleEngineError::ParseError {
1539                            message: "ActivateAgendaGroup requires agenda group name".to_string(),
1540                        });
1541                    } else {
1542                        let value = self.parse_value(args_str.trim())?;
1543                        match value {
1544                            Value::String(s) => s,
1545                            _ => value.to_string(),
1546                        }
1547                    };
1548                    Ok(ActionType::ActivateAgendaGroup {
1549                        group: agenda_group,
1550                    })
1551                }
1552                "schedulerule" | "schedule_rule" => {
1553                    // Parse delay and target rule: ScheduleRule(5000, "next-rule")
1554                    let parts: Vec<&str> = args_str.split(',').collect();
1555                    if parts.len() != 2 {
1556                        return Err(RuleEngineError::ParseError {
1557                            message: "ScheduleRule requires delay_ms and rule_name".to_string(),
1558                        });
1559                    }
1560
1561                    let delay_ms = self.parse_value(parts[0].trim())?;
1562                    let rule_name = self.parse_value(parts[1].trim())?;
1563
1564                    let delay_ms = match delay_ms {
1565                        Value::Integer(i) => i as u64,
1566                        Value::Number(f) => f as u64,
1567                        _ => {
1568                            return Err(RuleEngineError::ParseError {
1569                                message: "ScheduleRule delay_ms must be a number".to_string(),
1570                            })
1571                        }
1572                    };
1573
1574                    let rule_name = match rule_name {
1575                        Value::String(s) => s,
1576                        _ => rule_name.to_string(),
1577                    };
1578
1579                    Ok(ActionType::ScheduleRule {
1580                        delay_ms,
1581                        rule_name,
1582                    })
1583                }
1584                "completeworkflow" | "complete_workflow" => {
1585                    let workflow_id = if args_str.is_empty() {
1586                        return Err(RuleEngineError::ParseError {
1587                            message: "CompleteWorkflow requires workflow_id".to_string(),
1588                        });
1589                    } else {
1590                        let value = self.parse_value(args_str.trim())?;
1591                        match value {
1592                            Value::String(s) => s,
1593                            _ => value.to_string(),
1594                        }
1595                    };
1596                    Ok(ActionType::CompleteWorkflow {
1597                        workflow_name: workflow_id,
1598                    })
1599                }
1600                "setworkflowdata" | "set_workflow_data" => {
1601                    // Parse key=value: SetWorkflowData("key=value")
1602                    let data_str = args_str.trim();
1603
1604                    // Simple key=value parsing
1605                    let (key, value) = if let Some(eq_pos) = data_str.find('=') {
1606                        let key = data_str[..eq_pos].trim().trim_matches('"');
1607                        let value_str = data_str[eq_pos + 1..].trim();
1608                        let value = self.parse_value(value_str)?;
1609                        (key.to_string(), value)
1610                    } else {
1611                        return Err(RuleEngineError::ParseError {
1612                            message: "SetWorkflowData data must be in key=value format".to_string(),
1613                        });
1614                    };
1615
1616                    Ok(ActionType::SetWorkflowData { key, value })
1617                }
1618                _ => {
1619                    // All other functions become custom actions
1620                    let params = if args_str.is_empty() {
1621                        HashMap::new()
1622                    } else {
1623                        self.parse_function_args_as_params(args_str)?
1624                    };
1625
1626                    Ok(ActionType::Custom {
1627                        action_type: function_name.to_string(),
1628                        params,
1629                    })
1630                }
1631            }
1632        } else {
1633            // Custom statement
1634            Ok(ActionType::Custom {
1635                action_type: "statement".to_string(),
1636                params: {
1637                    let mut params = HashMap::new();
1638                    params.insert("statement".to_string(), Value::String(trimmed.to_string()));
1639                    params
1640                },
1641            })
1642        }
1643    }
1644
1645    fn parse_method_args(&self, args_str: &str) -> Result<Vec<Value>> {
1646        if args_str.trim().is_empty() {
1647            return Ok(Vec::new());
1648        }
1649
1650        // Handle expressions like: $TestCar.Speed + $TestCar.SpeedIncrement
1651        let mut args = Vec::new();
1652        let parts: Vec<&str> = args_str.split(',').collect();
1653
1654        for part in parts {
1655            let trimmed = part.trim();
1656
1657            // Handle arithmetic expressions
1658            if trimmed.contains('+')
1659                || trimmed.contains('-')
1660                || trimmed.contains('*')
1661                || trimmed.contains('/')
1662            {
1663                // For now, store as string - the engine will evaluate
1664                args.push(Value::String(trimmed.to_string()));
1665            } else {
1666                args.push(self.parse_value(trimmed)?);
1667            }
1668        }
1669
1670        Ok(args)
1671    }
1672
1673    /// Parse function arguments as parameters for custom actions
1674    fn parse_function_args_as_params(&self, args_str: &str) -> Result<HashMap<String, Value>> {
1675        let mut params = HashMap::new();
1676
1677        if args_str.trim().is_empty() {
1678            return Ok(params);
1679        }
1680
1681        // Parse positional parameters as numbered args
1682        let parts: Vec<&str> = args_str.split(',').collect();
1683        for (i, part) in parts.iter().enumerate() {
1684            let trimmed = part.trim();
1685            let value = self.parse_value(trimmed)?;
1686
1687            // Use simple numeric indexing - engine will resolve references dynamically
1688            params.insert(i.to_string(), value);
1689        }
1690
1691        Ok(params)
1692    }
1693
1694    /// Parse stream pattern condition
1695    /// Example: "login: LoginEvent from stream(\"logins\") over window(10 min, sliding)"
1696    #[cfg(feature = "streaming")]
1697    fn parse_stream_pattern_condition(&self, clause: &str) -> Result<ConditionGroup> {
1698        use crate::engine::rule::{StreamWindow, StreamWindowType};
1699        use crate::parser::grl::stream_syntax::parse_stream_pattern;
1700
1701        // Parse using nom parser
1702        let parse_result =
1703            parse_stream_pattern(clause).map_err(|e| RuleEngineError::ParseError {
1704                message: format!("Failed to parse stream pattern: {:?}", e),
1705            })?;
1706
1707        let (_, pattern) = parse_result;
1708
1709        // Convert WindowType from parser to StreamWindowType
1710        let window = pattern.source.window.map(|w| StreamWindow {
1711            duration: w.duration,
1712            window_type: match w.window_type {
1713                crate::parser::grl::stream_syntax::WindowType::Sliding => StreamWindowType::Sliding,
1714                crate::parser::grl::stream_syntax::WindowType::Tumbling => {
1715                    StreamWindowType::Tumbling
1716                }
1717                crate::parser::grl::stream_syntax::WindowType::Session { timeout } => {
1718                    StreamWindowType::Session { timeout }
1719                }
1720            },
1721        });
1722
1723        Ok(ConditionGroup::stream_pattern(
1724            pattern.var_name,
1725            pattern.event_type,
1726            pattern.source.stream_name,
1727            window,
1728        ))
1729    }
1730}
1731
1732#[cfg(test)]
1733mod tests {
1734    use super::GRLParser;
1735
1736    #[test]
1737    fn test_parse_simple_rule() {
1738        let grl = r#"
1739        rule "CheckAge" salience 10 {
1740            when
1741                User.Age >= 18
1742            then
1743                log("User is adult");
1744        }
1745        "#;
1746
1747        let rules = GRLParser::parse_rules(grl).unwrap();
1748        assert_eq!(rules.len(), 1);
1749        let rule = &rules[0];
1750        assert_eq!(rule.name, "CheckAge");
1751        assert_eq!(rule.salience, 10);
1752        assert_eq!(rule.actions.len(), 1);
1753    }
1754
1755    #[test]
1756    fn test_parse_complex_condition() {
1757        let grl = r#"
1758        rule "ComplexRule" {
1759            when
1760                User.Age >= 18 && User.Country == "US"
1761            then
1762                User.Qualified = true;
1763        }
1764        "#;
1765
1766        let rules = GRLParser::parse_rules(grl).unwrap();
1767        assert_eq!(rules.len(), 1);
1768        let rule = &rules[0];
1769        assert_eq!(rule.name, "ComplexRule");
1770    }
1771
1772    #[test]
1773    fn test_parse_new_syntax_with_parentheses() {
1774        let grl = r#"
1775        rule "Default Rule" salience 10 {
1776            when
1777                (user.age >= 18)
1778            then
1779                set(user.status, "approved");
1780        }
1781        "#;
1782
1783        let rules = GRLParser::parse_rules(grl).unwrap();
1784        assert_eq!(rules.len(), 1);
1785        let rule = &rules[0];
1786        assert_eq!(rule.name, "Default Rule");
1787        assert_eq!(rule.salience, 10);
1788        assert_eq!(rule.actions.len(), 1);
1789
1790        // Check that the action is parsed as a Custom action (set is now custom)
1791        match &rule.actions[0] {
1792            crate::types::ActionType::Custom {
1793                action_type,
1794                params,
1795            } => {
1796                assert_eq!(action_type, "set");
1797                assert_eq!(
1798                    params.get("0"),
1799                    Some(&crate::types::Value::Expression("user.status".to_string()))
1800                );
1801                assert_eq!(
1802                    params.get("1"),
1803                    Some(&crate::types::Value::String("approved".to_string()))
1804                );
1805            }
1806            _ => panic!("Expected Custom action, got: {:?}", rule.actions[0]),
1807        }
1808    }
1809
1810    #[test]
1811    fn test_parse_complex_nested_conditions() {
1812        let grl = r#"
1813        rule "Complex Business Rule" salience 10 {
1814            when
1815                (((user.vipStatus == true) && (order.amount > 500)) || ((date.isHoliday == true) && (order.hasCoupon == true)))
1816            then
1817                apply_discount(20000);
1818        }
1819        "#;
1820
1821        let rules = GRLParser::parse_rules(grl).unwrap();
1822        assert_eq!(rules.len(), 1);
1823        let rule = &rules[0];
1824        assert_eq!(rule.name, "Complex Business Rule");
1825        assert_eq!(rule.salience, 10);
1826        assert_eq!(rule.actions.len(), 1);
1827
1828        // Check that the action is parsed as a Custom action (apply_discount is now custom)
1829        match &rule.actions[0] {
1830            crate::types::ActionType::Custom {
1831                action_type,
1832                params,
1833            } => {
1834                assert_eq!(action_type, "apply_discount");
1835                assert_eq!(params.get("0"), Some(&crate::types::Value::Integer(20000)));
1836            }
1837            _ => panic!("Expected Custom action, got: {:?}", rule.actions[0]),
1838        }
1839    }
1840
1841    #[test]
1842    fn test_parse_no_loop_attribute() {
1843        let grl = r#"
1844        rule "NoLoopRule" no-loop salience 15 {
1845            when
1846                User.Score < 100
1847            then
1848                set(User.Score, User.Score + 10);
1849        }
1850        "#;
1851
1852        let rules = GRLParser::parse_rules(grl).unwrap();
1853        assert_eq!(rules.len(), 1);
1854        let rule = &rules[0];
1855        assert_eq!(rule.name, "NoLoopRule");
1856        assert_eq!(rule.salience, 15);
1857        assert!(rule.no_loop, "Rule should have no-loop=true");
1858    }
1859
1860    #[test]
1861    fn test_parse_no_loop_different_positions() {
1862        // Test no-loop before salience
1863        let grl1 = r#"
1864        rule "Rule1" no-loop salience 10 {
1865            when User.Age >= 18
1866            then log("adult");
1867        }
1868        "#;
1869
1870        // Test no-loop after salience
1871        let grl2 = r#"
1872        rule "Rule2" salience 10 no-loop {
1873            when User.Age >= 18
1874            then log("adult");
1875        }
1876        "#;
1877
1878        let rules1 = GRLParser::parse_rules(grl1).unwrap();
1879        let rules2 = GRLParser::parse_rules(grl2).unwrap();
1880
1881        assert_eq!(rules1.len(), 1);
1882        assert_eq!(rules2.len(), 1);
1883
1884        assert!(rules1[0].no_loop, "Rule1 should have no-loop=true");
1885        assert!(rules2[0].no_loop, "Rule2 should have no-loop=true");
1886
1887        assert_eq!(rules1[0].salience, 10);
1888        assert_eq!(rules2[0].salience, 10);
1889    }
1890
1891    #[test]
1892    fn test_parse_without_no_loop() {
1893        let grl = r#"
1894        rule "RegularRule" salience 5 {
1895            when
1896                User.Active == true
1897            then
1898                log("active user");
1899        }
1900        "#;
1901
1902        let rules = GRLParser::parse_rules(grl).unwrap();
1903        assert_eq!(rules.len(), 1);
1904        let rule = &rules[0];
1905        assert_eq!(rule.name, "RegularRule");
1906        assert!(!rule.no_loop, "Rule should have no-loop=false by default");
1907    }
1908
1909    #[test]
1910    fn test_parse_exists_pattern() {
1911        let grl = r#"
1912        rule "ExistsRule" salience 20 {
1913            when
1914                exists(Customer.tier == "VIP")
1915            then
1916                System.premiumActive = true;
1917        }
1918        "#;
1919
1920        let rules = GRLParser::parse_rules(grl).unwrap();
1921        assert_eq!(rules.len(), 1);
1922        let rule = &rules[0];
1923        assert_eq!(rule.name, "ExistsRule");
1924        assert_eq!(rule.salience, 20);
1925
1926        // Check that condition is EXISTS pattern
1927        match &rule.conditions {
1928            crate::engine::rule::ConditionGroup::Exists(_) => {
1929                // Test passes
1930            }
1931            _ => panic!(
1932                "Expected EXISTS condition group, got: {:?}",
1933                rule.conditions
1934            ),
1935        }
1936    }
1937
1938    #[test]
1939    fn test_parse_forall_pattern() {
1940        let grl = r#"
1941        rule "ForallRule" salience 15 {
1942            when
1943                forall(Order.status == "processed")
1944            then
1945                Shipping.enabled = true;
1946        }
1947        "#;
1948
1949        let rules = GRLParser::parse_rules(grl).unwrap();
1950        assert_eq!(rules.len(), 1);
1951        let rule = &rules[0];
1952        assert_eq!(rule.name, "ForallRule");
1953
1954        // Check that condition is FORALL pattern
1955        match &rule.conditions {
1956            crate::engine::rule::ConditionGroup::Forall(_) => {
1957                // Test passes
1958            }
1959            _ => panic!(
1960                "Expected FORALL condition group, got: {:?}",
1961                rule.conditions
1962            ),
1963        }
1964    }
1965
1966    #[test]
1967    fn test_parse_combined_patterns() {
1968        let grl = r#"
1969        rule "CombinedRule" salience 25 {
1970            when
1971                exists(Customer.tier == "VIP") && !exists(Alert.priority == "high")
1972            then
1973                System.vipMode = true;
1974        }
1975        "#;
1976
1977        let rules = GRLParser::parse_rules(grl).unwrap();
1978        assert_eq!(rules.len(), 1);
1979        let rule = &rules[0];
1980        assert_eq!(rule.name, "CombinedRule");
1981
1982        // Check that condition is AND with EXISTS and NOT(EXISTS) patterns
1983        match &rule.conditions {
1984            crate::engine::rule::ConditionGroup::Compound {
1985                left,
1986                operator,
1987                right,
1988            } => {
1989                assert_eq!(*operator, crate::types::LogicalOperator::And);
1990
1991                // Left should be EXISTS
1992                match left.as_ref() {
1993                    crate::engine::rule::ConditionGroup::Exists(_) => {
1994                        // Expected
1995                    }
1996                    _ => panic!("Expected EXISTS in left side, got: {:?}", left),
1997                }
1998
1999                // Right should be NOT(EXISTS)
2000                match right.as_ref() {
2001                    crate::engine::rule::ConditionGroup::Not(inner) => {
2002                        match inner.as_ref() {
2003                            crate::engine::rule::ConditionGroup::Exists(_) => {
2004                                // Expected
2005                            }
2006                            _ => panic!("Expected EXISTS inside NOT, got: {:?}", inner),
2007                        }
2008                    }
2009                    _ => panic!("Expected NOT in right side, got: {:?}", right),
2010                }
2011            }
2012            _ => panic!("Expected compound condition, got: {:?}", rule.conditions),
2013        }
2014    }
2015
2016    #[test]
2017    fn test_parse_in_operator() {
2018        let grl = r#"
2019        rule "TestInOperator" salience 75 {
2020            when
2021                User.role in ["admin", "moderator", "vip"]
2022            then
2023                User.access = "granted";
2024        }
2025        "#;
2026
2027        let rules = GRLParser::parse_rules(grl).unwrap();
2028        assert_eq!(rules.len(), 1);
2029        let rule = &rules[0];
2030        assert_eq!(rule.name, "TestInOperator");
2031        assert_eq!(rule.salience, 75);
2032
2033        // Check the condition
2034        match &rule.conditions {
2035            crate::engine::rule::ConditionGroup::Single(cond) => {
2036                // The field might be in expression format
2037                println!("Condition: {:?}", cond);
2038                assert_eq!(cond.operator, crate::types::Operator::In);
2039
2040                // Value should be an array
2041                match &cond.value {
2042                    crate::types::Value::Array(arr) => {
2043                        assert_eq!(arr.len(), 3);
2044                        assert_eq!(arr[0], crate::types::Value::String("admin".to_string()));
2045                        assert_eq!(arr[1], crate::types::Value::String("moderator".to_string()));
2046                        assert_eq!(arr[2], crate::types::Value::String("vip".to_string()));
2047                    }
2048                    _ => panic!("Expected Array value, got {:?}", cond.value),
2049                }
2050            }
2051            _ => panic!("Expected Single condition, got: {:?}", rule.conditions),
2052        }
2053    }
2054
2055    #[test]
2056    fn test_parse_startswith_endswith_operators() {
2057        let grl = r#"
2058        rule "StringMethods" salience 50 {
2059            when
2060                User.email startsWith "admin@" &&
2061                User.filename endsWith ".txt"
2062            then
2063                User.validated = true;
2064        }
2065        "#;
2066
2067        let rules = GRLParser::parse_rules(grl).unwrap();
2068        assert_eq!(rules.len(), 1);
2069        let rule = &rules[0];
2070        assert_eq!(rule.name, "StringMethods");
2071        assert_eq!(rule.salience, 50);
2072
2073        // Check the compound condition (AND)
2074        match &rule.conditions {
2075            crate::engine::rule::ConditionGroup::Compound {
2076                left,
2077                operator,
2078                right,
2079            } => {
2080                assert_eq!(*operator, crate::types::LogicalOperator::And);
2081
2082                // Left should be startsWith
2083                match left.as_ref() {
2084                    crate::engine::rule::ConditionGroup::Single(cond) => {
2085                        assert_eq!(cond.operator, crate::types::Operator::StartsWith);
2086                    }
2087                    _ => panic!("Expected Single condition for startsWith, got: {:?}", left),
2088                }
2089
2090                // Right should be endsWith
2091                match right.as_ref() {
2092                    crate::engine::rule::ConditionGroup::Single(cond) => {
2093                        assert_eq!(cond.operator, crate::types::Operator::EndsWith);
2094                    }
2095                    _ => panic!("Expected Single condition for endsWith, got: {:?}", right),
2096                }
2097            }
2098            _ => panic!("Expected Compound condition, got: {:?}", rule.conditions),
2099        }
2100    }
2101}