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 => {
721                    if chars.peek() == Some(&'&') {
722                        chars.next(); // consume second &
723                        parts.push(current_part.trim().to_string());
724                        current_part.clear();
725                    } else {
726                        current_part.push(ch);
727                    }
728                }
729                '|' if operator == "||" && paren_count == 0 => {
730                    if chars.peek() == Some(&'|') {
731                        chars.next(); // consume second |
732                        parts.push(current_part.trim().to_string());
733                        current_part.clear();
734                    } else {
735                        current_part.push(ch);
736                    }
737                }
738                _ => {
739                    current_part.push(ch);
740                }
741            }
742        }
743
744        if !current_part.trim().is_empty() {
745            parts.push(current_part.trim().to_string());
746        }
747
748        if parts.len() > 1 {
749            Some(parts)
750        } else {
751            None
752        }
753    }
754
755    fn parse_or_parts(&self, parts: Vec<String>) -> Result<ConditionGroup> {
756        let mut conditions = Vec::new();
757        for part in parts {
758            let condition = self.parse_when_clause(&part)?;
759            conditions.push(condition);
760        }
761
762        if conditions.is_empty() {
763            return Err(RuleEngineError::ParseError {
764                message: "No conditions found in OR".to_string(),
765            });
766        }
767
768        let mut iter = conditions.into_iter();
769        let mut result = iter
770            .next()
771            .expect("Iterator cannot be empty after empty check");
772        for condition in iter {
773            result = ConditionGroup::or(result, condition);
774        }
775
776        Ok(result)
777    }
778
779    fn parse_and_parts(&self, parts: Vec<String>) -> Result<ConditionGroup> {
780        let mut conditions = Vec::new();
781        for part in parts {
782            let condition = self.parse_when_clause(&part)?;
783            conditions.push(condition);
784        }
785
786        if conditions.is_empty() {
787            return Err(RuleEngineError::ParseError {
788                message: "No conditions found in AND".to_string(),
789            });
790        }
791
792        let mut iter = conditions.into_iter();
793        let mut result = iter
794            .next()
795            .expect("Iterator cannot be empty after empty check");
796        for condition in iter {
797            result = ConditionGroup::and(result, condition);
798        }
799
800        Ok(result)
801    }
802
803    fn parse_not_condition(&self, clause: &str) -> Result<ConditionGroup> {
804        let inner_clause = clause
805            .strip_prefix('!')
806            .ok_or_else(|| RuleEngineError::ParseError {
807                message: format!("Expected '!' prefix in NOT condition: {}", clause),
808            })?
809            .trim();
810        let inner_condition = self.parse_when_clause(inner_clause)?;
811        Ok(ConditionGroup::not(inner_condition))
812    }
813
814    fn parse_exists_condition(&self, clause: &str) -> Result<ConditionGroup> {
815        let clause = clause.trim_start();
816        if !clause.starts_with("exists(") || !clause.ends_with(")") {
817            return Err(RuleEngineError::ParseError {
818                message: "Invalid exists syntax. Expected: exists(condition)".to_string(),
819            });
820        }
821
822        // Extract content between parentheses
823        let inner_clause = &clause[7..clause.len() - 1]; // Remove "exists(" and ")"
824        let inner_condition = self.parse_when_clause(inner_clause)?;
825        Ok(ConditionGroup::exists(inner_condition))
826    }
827
828    fn parse_forall_condition(&self, clause: &str) -> Result<ConditionGroup> {
829        let clause = clause.trim_start();
830        if !clause.starts_with("forall(") || !clause.ends_with(")") {
831            return Err(RuleEngineError::ParseError {
832                message: "Invalid forall syntax. Expected: forall(condition)".to_string(),
833            });
834        }
835
836        // Extract content between parentheses
837        let inner_clause = &clause[7..clause.len() - 1]; // Remove "forall(" and ")"
838        let inner_condition = self.parse_when_clause(inner_clause)?;
839        Ok(ConditionGroup::forall(inner_condition))
840    }
841
842    fn parse_accumulate_condition(&self, clause: &str) -> Result<ConditionGroup> {
843        let clause = clause.trim_start();
844        if !clause.starts_with("accumulate(") || !clause.ends_with(")") {
845            return Err(RuleEngineError::ParseError {
846                message: "Invalid accumulate syntax. Expected: accumulate(pattern, function)"
847                    .to_string(),
848            });
849        }
850
851        // Extract content between parentheses
852        let inner = &clause[11..clause.len() - 1]; // Remove "accumulate(" and ")"
853
854        // Split by comma at the top level (not inside parentheses)
855        let parts = self.split_accumulate_parts(inner)?;
856
857        if parts.len() != 2 {
858            return Err(RuleEngineError::ParseError {
859                message: format!(
860                    "Invalid accumulate syntax. Expected 2 parts (pattern, function), got {}",
861                    parts.len()
862                ),
863            });
864        }
865
866        let pattern_part = parts[0].trim();
867        let function_part = parts[1].trim();
868
869        // Parse the pattern: Order($amount: amount, status == "completed")
870        let (source_pattern, extract_field, source_conditions) =
871            self.parse_accumulate_pattern(pattern_part)?;
872
873        // Parse the function: sum($amount)
874        let (function, function_arg) = self.parse_accumulate_function(function_part)?;
875
876        // For now, we'll create a placeholder result variable
877        // In a full implementation, this would be extracted from the parent context
878        // e.g., from "$total: accumulate(...)"
879        let result_var = "$result".to_string();
880
881        Ok(ConditionGroup::accumulate(
882            result_var,
883            source_pattern,
884            extract_field,
885            source_conditions,
886            function,
887            function_arg,
888        ))
889    }
890
891    fn split_accumulate_parts(&self, content: &str) -> Result<Vec<String>> {
892        let mut parts = Vec::new();
893        let mut current = String::new();
894        let mut paren_depth = 0;
895
896        for ch in content.chars() {
897            match ch {
898                '(' => {
899                    paren_depth += 1;
900                    current.push(ch);
901                }
902                ')' => {
903                    paren_depth -= 1;
904                    current.push(ch);
905                }
906                ',' if paren_depth == 0 => {
907                    parts.push(current.trim().to_string());
908                    current.clear();
909                }
910                _ => {
911                    current.push(ch);
912                }
913            }
914        }
915
916        if !current.trim().is_empty() {
917            parts.push(current.trim().to_string());
918        }
919
920        Ok(parts)
921    }
922
923    fn parse_accumulate_pattern(&self, pattern: &str) -> Result<(String, String, Vec<String>)> {
924        // Pattern format: Order($amount: amount, status == "completed", category == "electronics")
925        // We need to extract:
926        // - source_pattern: "Order"
927        // - extract_field: "amount" (from $amount: amount)
928        // - source_conditions: ["status == \"completed\"", "category == \"electronics\""]
929
930        let pattern = pattern.trim();
931
932        // Find the opening parenthesis to get the pattern type
933        let paren_pos = pattern
934            .find('(')
935            .ok_or_else(|| RuleEngineError::ParseError {
936                message: format!("Invalid accumulate pattern: missing '(' in '{}'", pattern),
937            })?;
938
939        let source_pattern = pattern[..paren_pos].trim().to_string();
940
941        // Extract content between parentheses
942        if !pattern.ends_with(')') {
943            return Err(RuleEngineError::ParseError {
944                message: format!("Invalid accumulate pattern: missing ')' in '{}'", pattern),
945            });
946        }
947
948        let inner = &pattern[paren_pos + 1..pattern.len() - 1];
949
950        // Split by comma (respecting nested parentheses and quotes)
951        let parts = self.split_pattern_parts(inner)?;
952
953        let mut extract_field = String::new();
954        let mut source_conditions = Vec::new();
955
956        for part in parts {
957            let part = part.trim();
958
959            // Check if this is a variable binding: $var: field
960            if part.contains(':') && part.starts_with('$') {
961                if let Some(colon_pos) = part.find(':') {
962                    extract_field = part[colon_pos + 1..].trim().to_string();
963                }
964            } else if part.contains("==")
965                || part.contains("!=")
966                || part.contains(">=")
967                || part.contains("<=")
968                || part.contains('>')
969                || part.contains('<')
970            {
971                // This is a condition
972                source_conditions.push(part.to_string());
973            }
974        }
975
976        Ok((source_pattern, extract_field, source_conditions))
977    }
978
979    fn split_pattern_parts(&self, content: &str) -> Result<Vec<String>> {
980        let mut parts = Vec::new();
981        let mut current = String::new();
982        let mut paren_depth = 0;
983        let mut in_quotes = false;
984        let mut quote_char = ' ';
985
986        for ch in content.chars() {
987            match ch {
988                '"' | '\'' if !in_quotes => {
989                    in_quotes = true;
990                    quote_char = ch;
991                    current.push(ch);
992                }
993                '"' | '\'' if in_quotes && ch == quote_char => {
994                    in_quotes = false;
995                    current.push(ch);
996                }
997                '(' if !in_quotes => {
998                    paren_depth += 1;
999                    current.push(ch);
1000                }
1001                ')' if !in_quotes => {
1002                    paren_depth -= 1;
1003                    current.push(ch);
1004                }
1005                ',' if !in_quotes && paren_depth == 0 => {
1006                    parts.push(current.trim().to_string());
1007                    current.clear();
1008                }
1009                _ => {
1010                    current.push(ch);
1011                }
1012            }
1013        }
1014
1015        if !current.trim().is_empty() {
1016            parts.push(current.trim().to_string());
1017        }
1018
1019        Ok(parts)
1020    }
1021
1022    fn parse_accumulate_function(&self, function_str: &str) -> Result<(String, String)> {
1023        // Function format: sum($amount) or count() or average($price)
1024
1025        let function_str = function_str.trim();
1026
1027        let paren_pos = function_str
1028            .find('(')
1029            .ok_or_else(|| RuleEngineError::ParseError {
1030                message: format!(
1031                    "Invalid accumulate function: missing '(' in '{}'",
1032                    function_str
1033                ),
1034            })?;
1035
1036        let function_name = function_str[..paren_pos].trim().to_string();
1037
1038        if !function_str.ends_with(')') {
1039            return Err(RuleEngineError::ParseError {
1040                message: format!(
1041                    "Invalid accumulate function: missing ')' in '{}'",
1042                    function_str
1043                ),
1044            });
1045        }
1046
1047        let args = &function_str[paren_pos + 1..function_str.len() - 1];
1048        let function_arg = args.trim().to_string();
1049
1050        Ok((function_name, function_arg))
1051    }
1052
1053    fn parse_single_condition(&self, clause: &str) -> Result<ConditionGroup> {
1054        // Remove outer parentheses if they exist (handle new syntax like "(user.age >= 18)")
1055        let trimmed_clause = clause.trim();
1056        let clause_to_parse = if trimmed_clause.starts_with('(') && trimmed_clause.ends_with(')') {
1057            trimmed_clause[1..trimmed_clause.len() - 1].trim()
1058        } else {
1059            trimmed_clause
1060        };
1061
1062        // === STREAM PATTERNS ===
1063        // Check for stream pattern syntax: "var: Type from stream(...)"
1064        #[cfg(feature = "streaming")]
1065        if clause_to_parse.contains("from stream(") {
1066            return self.parse_stream_pattern_condition(clause_to_parse);
1067        }
1068
1069        // === MULTI-FIELD PATTERNS ===
1070        // Handle multi-field patterns before other patterns
1071        // These must be checked first to avoid conflict with standard patterns
1072
1073        // Pattern 1: Field.array $?var (Collect operation with variable binding)
1074        // Example: Order.items $?all_items
1075        if let Some(captures) = multifield_collect_regex().captures(clause_to_parse) {
1076            let field = captures.get(1).unwrap().to_string();
1077            let variable = captures.get(2).unwrap().to_string();
1078
1079            // Create a multifield Collect condition
1080            // Note: This will need to be handled by the engine
1081            let condition = Condition::with_multifield_collect(field, variable);
1082            return Ok(ConditionGroup::single(condition));
1083        }
1084
1085        // Pattern 2: Field.array contains "value"
1086        // Example: Product.tags contains "electronics"
1087        // This is already handled by the standard regex, but we need to distinguish array contains
1088
1089        // Pattern 3: Field.array count operator value
1090        // Example: Order.items count > 0, Order.items count >= 5
1091        if let Some(captures) = multifield_count_regex().captures(clause_to_parse) {
1092            let field = captures.get(1).unwrap().to_string();
1093            let operator_str = captures.get(2).unwrap();
1094            let value_str = captures.get(3).unwrap().trim();
1095
1096            let operator = Operator::from_str(operator_str).ok_or_else(|| {
1097                RuleEngineError::InvalidOperator {
1098                    operator: operator_str.to_string(),
1099                }
1100            })?;
1101
1102            let value = self.parse_value(value_str)?;
1103
1104            let condition = Condition::with_multifield_count(field, operator, value);
1105            return Ok(ConditionGroup::single(condition));
1106        }
1107
1108        // Pattern 4: Field.array first [optional: $var or operator value]
1109        // Example: Queue.tasks first, Queue.tasks first $first_task
1110        if let Some(captures) = multifield_first_regex().captures(clause_to_parse) {
1111            let field = captures.get(1).unwrap().to_string();
1112            let variable = captures.get(2).map(|m| m.to_string());
1113
1114            let condition = Condition::with_multifield_first(field, variable);
1115            return Ok(ConditionGroup::single(condition));
1116        }
1117
1118        // Pattern 5: Field.array last [optional: $var]
1119        // Example: Queue.tasks last, Queue.tasks last $last_task
1120        if let Some(captures) = multifield_last_regex().captures(clause_to_parse) {
1121            let field = captures.get(1).unwrap().to_string();
1122            let variable = captures.get(2).map(|m| m.to_string());
1123
1124            let condition = Condition::with_multifield_last(field, variable);
1125            return Ok(ConditionGroup::single(condition));
1126        }
1127
1128        // Pattern 6: Field.array empty
1129        // Example: ShoppingCart.items empty
1130        if let Some(captures) = multifield_empty_regex().captures(clause_to_parse) {
1131            let field = captures.get(1).unwrap().to_string();
1132
1133            let condition = Condition::with_multifield_empty(field);
1134            return Ok(ConditionGroup::single(condition));
1135        }
1136
1137        // Pattern 7: Field.array not_empty
1138        // Example: ShoppingCart.items not_empty
1139        if let Some(captures) = multifield_not_empty_regex().captures(clause_to_parse) {
1140            let field = captures.get(1).unwrap().to_string();
1141
1142            let condition = Condition::with_multifield_not_empty(field);
1143            return Ok(ConditionGroup::single(condition));
1144        }
1145
1146        // === END MULTI-FIELD PATTERNS ===
1147
1148        // Handle Test CE: test(functionName(args...))
1149        // This is a CLIPS-inspired feature for arbitrary boolean expressions
1150        if let Some(captures) = test_condition_regex().captures(clause_to_parse) {
1151            let function_name = captures.get(1).unwrap().to_string();
1152            let args_str = captures.get(2).unwrap();
1153
1154            // Parse arguments
1155            let args: Vec<String> = if args_str.trim().is_empty() {
1156                Vec::new()
1157            } else {
1158                args_str
1159                    .split(',')
1160                    .map(|arg| arg.trim().to_string())
1161                    .collect()
1162            };
1163
1164            let condition = Condition::with_test(function_name, args);
1165            return Ok(ConditionGroup::single(condition));
1166        }
1167
1168        // Handle typed object conditions like: $TestCar : TestCarClass( speedUp == true && speed < maxSpeed )
1169        if let Some(captures) = typed_test_condition_regex().captures(clause_to_parse) {
1170            let _object_name = captures.get(1).unwrap();
1171            let _object_type = captures.get(2).unwrap();
1172            let conditions_str = captures.get(3).unwrap();
1173
1174            // Parse conditions inside parentheses
1175            return self.parse_conditions_within_object(conditions_str);
1176        }
1177
1178        // Try to parse function call pattern: functionName(arg1, arg2, ...) operator value
1179        if let Some(captures) = function_call_regex().captures(clause_to_parse) {
1180            let function_name = captures.get(1).unwrap().to_string();
1181            let args_str = captures.get(2).unwrap();
1182            let operator_str = captures.get(3).unwrap();
1183            let value_str = captures.get(4).unwrap().trim();
1184
1185            // Parse arguments
1186            let args: Vec<String> = if args_str.trim().is_empty() {
1187                Vec::new()
1188            } else {
1189                args_str
1190                    .split(',')
1191                    .map(|arg| arg.trim().to_string())
1192                    .collect()
1193            };
1194
1195            let operator = Operator::from_str(operator_str).ok_or_else(|| {
1196                RuleEngineError::InvalidOperator {
1197                    operator: operator_str.to_string(),
1198                }
1199            })?;
1200
1201            let value = self.parse_value(value_str)?;
1202
1203            let condition = Condition::with_function(function_name, args, operator, value);
1204            return Ok(ConditionGroup::single(condition));
1205        }
1206
1207        // Parse expressions like: User.Age >= 18, Product.Price < 100.0, user.age >= 18, etc.
1208        // Support both PascalCase (User.Age) and lowercase (user.age) field naming
1209        // Also support arithmetic expressions like: User.Age % 3 == 0, User.Price * 2 > 100
1210        let captures = condition_regex().captures(clause_to_parse).ok_or_else(|| {
1211            RuleEngineError::ParseError {
1212                message: format!("Invalid condition format: {}", clause_to_parse),
1213            }
1214        })?;
1215
1216        let left_side = captures.get(1).unwrap().trim().to_string();
1217        let operator_str = captures.get(2).unwrap();
1218        let value_str = captures.get(3).unwrap().trim();
1219
1220        let operator =
1221            Operator::from_str(operator_str).ok_or_else(|| RuleEngineError::InvalidOperator {
1222                operator: operator_str.to_string(),
1223            })?;
1224
1225        let value = self.parse_value(value_str)?;
1226
1227        // Check if left_side contains arithmetic operators - if yes, it's an expression
1228        if left_side.contains('+')
1229            || left_side.contains('-')
1230            || left_side.contains('*')
1231            || left_side.contains('/')
1232            || left_side.contains('%')
1233        {
1234            // This is an arithmetic expression - use Test CE
1235            // Format: test(left_side operator value)
1236            let test_expr = format!("{} {} {}", left_side, operator_str, value_str);
1237            let condition = Condition::with_test(test_expr, vec![]);
1238            Ok(ConditionGroup::single(condition))
1239        } else {
1240            // Simple field reference
1241            let condition = Condition::new(left_side, operator, value);
1242            Ok(ConditionGroup::single(condition))
1243        }
1244    }
1245
1246    fn parse_conditions_within_object(&self, conditions_str: &str) -> Result<ConditionGroup> {
1247        // Parse conditions like: speedUp == true && speed < maxSpeed
1248        let parts: Vec<&str> = conditions_str.split("&&").collect();
1249
1250        let mut conditions = Vec::new();
1251        for part in parts {
1252            let trimmed = part.trim();
1253            let condition = self.parse_simple_condition(trimmed)?;
1254            conditions.push(condition);
1255        }
1256
1257        // Combine with AND
1258        if conditions.is_empty() {
1259            return Err(RuleEngineError::ParseError {
1260                message: "No conditions found".to_string(),
1261            });
1262        }
1263
1264        let mut iter = conditions.into_iter();
1265        let mut result = iter
1266            .next()
1267            .expect("Iterator cannot be empty after empty check");
1268        for condition in iter {
1269            result = ConditionGroup::and(result, condition);
1270        }
1271
1272        Ok(result)
1273    }
1274
1275    fn parse_simple_condition(&self, clause: &str) -> Result<ConditionGroup> {
1276        // Parse simple condition like: speedUp == true or speed < maxSpeed
1277        let captures = simple_condition_regex().captures(clause).ok_or_else(|| {
1278            RuleEngineError::ParseError {
1279                message: format!("Invalid simple condition format: {}", clause),
1280            }
1281        })?;
1282
1283        let field = captures.get(1).unwrap().to_string();
1284        let operator_str = captures.get(2).unwrap();
1285        let value_str = captures.get(3).unwrap().trim();
1286
1287        let operator =
1288            Operator::from_str(operator_str).ok_or_else(|| RuleEngineError::InvalidOperator {
1289                operator: operator_str.to_string(),
1290            })?;
1291
1292        let value = self.parse_value(value_str)?;
1293
1294        let condition = Condition::new(field, operator, value);
1295        Ok(ConditionGroup::single(condition))
1296    }
1297
1298    fn parse_value(&self, value_str: &str) -> Result<Value> {
1299        let trimmed = value_str.trim();
1300
1301        // Array literal: ["value1", "value2", 123]
1302        if trimmed.starts_with('[') && trimmed.ends_with(']') {
1303            return self.parse_array_literal(trimmed);
1304        }
1305
1306        // String literal
1307        if (trimmed.starts_with('"') && trimmed.ends_with('"'))
1308            || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
1309        {
1310            let unquoted = &trimmed[1..trimmed.len() - 1];
1311            return Ok(Value::String(unquoted.to_string()));
1312        }
1313
1314        // Boolean
1315        if trimmed.eq_ignore_ascii_case("true") {
1316            return Ok(Value::Boolean(true));
1317        }
1318        if trimmed.eq_ignore_ascii_case("false") {
1319            return Ok(Value::Boolean(false));
1320        }
1321
1322        // Null
1323        if trimmed.eq_ignore_ascii_case("null") {
1324            return Ok(Value::Null);
1325        }
1326
1327        // Number (try integer first, then float)
1328        if let Ok(int_val) = trimmed.parse::<i64>() {
1329            return Ok(Value::Integer(int_val));
1330        }
1331
1332        if let Ok(float_val) = trimmed.parse::<f64>() {
1333            return Ok(Value::Number(float_val));
1334        }
1335
1336        // Expression with arithmetic operators (e.g., "Order.quantity * Order.price")
1337        // Detect: contains operators AND (contains field reference OR multiple tokens)
1338        if self.is_expression(trimmed) {
1339            return Ok(Value::Expression(trimmed.to_string()));
1340        }
1341
1342        // Field reference (like User.Name)
1343        if trimmed.contains('.') {
1344            return Ok(Value::String(trimmed.to_string()));
1345        }
1346
1347        // Variable reference (identifier without quotes or dots)
1348        // This handles cases like: order_qty = moq
1349        // where 'moq' should be evaluated as a variable reference at runtime
1350        if self.is_identifier(trimmed) {
1351            return Ok(Value::Expression(trimmed.to_string()));
1352        }
1353
1354        // Default to string
1355        Ok(Value::String(trimmed.to_string()))
1356    }
1357
1358    /// Check if a string is a valid identifier (variable name)
1359    /// Valid identifiers: alphanumeric + underscore, starts with letter or underscore
1360    fn is_identifier(&self, s: &str) -> bool {
1361        if s.is_empty() {
1362            return false;
1363        }
1364        let first_char = s.chars().next().expect("Cannot be empty after empty check");
1365        if !first_char.is_alphabetic() && first_char != '_' {
1366            return false;
1367        }
1368
1369        // First character must be letter or underscore
1370        let first_char = s.chars().next().unwrap();
1371        if !first_char.is_alphabetic() && first_char != '_' {
1372            return false;
1373        }
1374
1375        // Rest must be alphanumeric or underscore
1376        s.chars().all(|c| c.is_alphanumeric() || c == '_')
1377    }
1378
1379    /// Check if a string is an arithmetic expression
1380    fn is_expression(&self, s: &str) -> bool {
1381        // Check for arithmetic operators
1382        let has_operator = s.contains('+')
1383            || s.contains('-')
1384            || s.contains('*')
1385            || s.contains('/')
1386            || s.contains('%');
1387
1388        // Check for field references (contains .)
1389        let has_field_ref = s.contains('.');
1390
1391        // Check for multiple tokens (spaces between operands/operators)
1392        let has_spaces = s.contains(' ');
1393
1394        // Expression if: has operator AND (has field reference OR has spaces)
1395        has_operator && (has_field_ref || has_spaces)
1396    }
1397
1398    /// Parse array literal like ["value1", "value2", 123]
1399    fn parse_array_literal(&self, array_str: &str) -> Result<Value> {
1400        let content = array_str.trim();
1401        if !content.starts_with('[') || !content.ends_with(']') {
1402            return Err(RuleEngineError::ParseError {
1403                message: format!("Invalid array literal: {}", array_str),
1404            });
1405        }
1406
1407        let inner = content[1..content.len() - 1].trim();
1408        if inner.is_empty() {
1409            return Ok(Value::Array(vec![]));
1410        }
1411
1412        // Split by comma, handling quoted strings
1413        let mut elements = Vec::new();
1414        let mut current_element = String::new();
1415        let mut in_quotes = false;
1416        let mut quote_char = ' ';
1417
1418        for ch in inner.chars() {
1419            match ch {
1420                '"' | '\'' if !in_quotes => {
1421                    in_quotes = true;
1422                    quote_char = ch;
1423                    current_element.push(ch);
1424                }
1425                c if in_quotes && c == quote_char => {
1426                    in_quotes = false;
1427                    current_element.push(ch);
1428                }
1429                ',' if !in_quotes => {
1430                    if !current_element.trim().is_empty() {
1431                        elements.push(current_element.trim().to_string());
1432                    }
1433                    current_element.clear();
1434                }
1435                _ => {
1436                    current_element.push(ch);
1437                }
1438            }
1439        }
1440
1441        // Don't forget the last element
1442        if !current_element.trim().is_empty() {
1443            elements.push(current_element.trim().to_string());
1444        }
1445
1446        // Parse each element
1447        let mut array_values = Vec::new();
1448        for elem in elements {
1449            let value = self.parse_value(&elem)?;
1450            array_values.push(value);
1451        }
1452
1453        Ok(Value::Array(array_values))
1454    }
1455
1456    fn parse_then_clause(&self, then_clause: &str) -> Result<Vec<ActionType>> {
1457        let statements: Vec<&str> = then_clause
1458            .split(';')
1459            .map(|s| s.trim())
1460            .filter(|s| !s.is_empty())
1461            .collect();
1462
1463        let mut actions = Vec::new();
1464
1465        for statement in statements {
1466            let action = self.parse_action_statement(statement)?;
1467            actions.push(action);
1468        }
1469
1470        Ok(actions)
1471    }
1472
1473    fn parse_action_statement(&self, statement: &str) -> Result<ActionType> {
1474        let trimmed = statement.trim();
1475
1476        // Method call: $Object.method(args)
1477        if let Some(captures) = method_call_regex().captures(trimmed) {
1478            let object = captures.get(1).unwrap().to_string();
1479            let method = captures.get(2).unwrap().to_string();
1480            let args_str = captures.get(3).unwrap();
1481
1482            let args = if args_str.trim().is_empty() {
1483                Vec::new()
1484            } else {
1485                self.parse_method_args(args_str)?
1486            };
1487
1488            return Ok(ActionType::MethodCall {
1489                object,
1490                method,
1491                args,
1492            });
1493        }
1494
1495        // Check for compound assignment operators first (+=, -=, etc.)
1496        if let Some(plus_eq_pos) = trimmed.find("+=") {
1497            // Append operator: Field += Value
1498            let field = trimmed[..plus_eq_pos].trim().to_string();
1499            let value_str = trimmed[plus_eq_pos + 2..].trim();
1500            let value = self.parse_value(value_str)?;
1501
1502            return Ok(ActionType::Append { field, value });
1503        }
1504
1505        // Assignment: Field = Value
1506        if let Some(eq_pos) = trimmed.find('=') {
1507            let field = trimmed[..eq_pos].trim().to_string();
1508            let value_str = trimmed[eq_pos + 1..].trim();
1509            let value = self.parse_value(value_str)?;
1510
1511            return Ok(ActionType::Set { field, value });
1512        }
1513
1514        // Function calls: update($Object), retract($Object), etc.
1515        if let Some(captures) = function_binding_regex().captures(trimmed) {
1516            let function_name = captures.get(1).unwrap();
1517            let args_str = captures.get(2).unwrap_or("");
1518
1519            match function_name.to_lowercase().as_str() {
1520                "retract" => {
1521                    // Extract object name from $Object
1522                    let object_name = if let Some(stripped) = args_str.strip_prefix('$') {
1523                        stripped.to_string()
1524                    } else {
1525                        args_str.to_string()
1526                    };
1527                    Ok(ActionType::Retract {
1528                        object: object_name,
1529                    })
1530                }
1531                "log" => {
1532                    let message = if args_str.is_empty() {
1533                        "Log message".to_string()
1534                    } else {
1535                        let value = self.parse_value(args_str.trim())?;
1536                        value.to_string()
1537                    };
1538                    Ok(ActionType::Log { message })
1539                }
1540                "activateagendagroup" | "activate_agenda_group" => {
1541                    let agenda_group = if args_str.is_empty() {
1542                        return Err(RuleEngineError::ParseError {
1543                            message: "ActivateAgendaGroup requires agenda group name".to_string(),
1544                        });
1545                    } else {
1546                        let value = self.parse_value(args_str.trim())?;
1547                        match value {
1548                            Value::String(s) => s,
1549                            _ => value.to_string(),
1550                        }
1551                    };
1552                    Ok(ActionType::ActivateAgendaGroup {
1553                        group: agenda_group,
1554                    })
1555                }
1556                "schedulerule" | "schedule_rule" => {
1557                    // Parse delay and target rule: ScheduleRule(5000, "next-rule")
1558                    let parts: Vec<&str> = args_str.split(',').collect();
1559                    if parts.len() != 2 {
1560                        return Err(RuleEngineError::ParseError {
1561                            message: "ScheduleRule requires delay_ms and rule_name".to_string(),
1562                        });
1563                    }
1564
1565                    let delay_ms = self.parse_value(parts[0].trim())?;
1566                    let rule_name = self.parse_value(parts[1].trim())?;
1567
1568                    let delay_ms = match delay_ms {
1569                        Value::Integer(i) => i as u64,
1570                        Value::Number(f) => f as u64,
1571                        _ => {
1572                            return Err(RuleEngineError::ParseError {
1573                                message: "ScheduleRule delay_ms must be a number".to_string(),
1574                            })
1575                        }
1576                    };
1577
1578                    let rule_name = match rule_name {
1579                        Value::String(s) => s,
1580                        _ => rule_name.to_string(),
1581                    };
1582
1583                    Ok(ActionType::ScheduleRule {
1584                        delay_ms,
1585                        rule_name,
1586                    })
1587                }
1588                "completeworkflow" | "complete_workflow" => {
1589                    let workflow_id = if args_str.is_empty() {
1590                        return Err(RuleEngineError::ParseError {
1591                            message: "CompleteWorkflow requires workflow_id".to_string(),
1592                        });
1593                    } else {
1594                        let value = self.parse_value(args_str.trim())?;
1595                        match value {
1596                            Value::String(s) => s,
1597                            _ => value.to_string(),
1598                        }
1599                    };
1600                    Ok(ActionType::CompleteWorkflow {
1601                        workflow_name: workflow_id,
1602                    })
1603                }
1604                "setworkflowdata" | "set_workflow_data" => {
1605                    // Parse key=value: SetWorkflowData("key=value")
1606                    let data_str = args_str.trim();
1607
1608                    // Simple key=value parsing
1609                    let (key, value) = if let Some(eq_pos) = data_str.find('=') {
1610                        let key = data_str[..eq_pos].trim().trim_matches('"');
1611                        let value_str = data_str[eq_pos + 1..].trim();
1612                        let value = self.parse_value(value_str)?;
1613                        (key.to_string(), value)
1614                    } else {
1615                        return Err(RuleEngineError::ParseError {
1616                            message: "SetWorkflowData data must be in key=value format".to_string(),
1617                        });
1618                    };
1619
1620                    Ok(ActionType::SetWorkflowData { key, value })
1621                }
1622                _ => {
1623                    // All other functions become custom actions
1624                    let params = if args_str.is_empty() {
1625                        HashMap::new()
1626                    } else {
1627                        self.parse_function_args_as_params(args_str)?
1628                    };
1629
1630                    Ok(ActionType::Custom {
1631                        action_type: function_name.to_string(),
1632                        params,
1633                    })
1634                }
1635            }
1636        } else {
1637            // Custom statement
1638            Ok(ActionType::Custom {
1639                action_type: "statement".to_string(),
1640                params: {
1641                    let mut params = HashMap::new();
1642                    params.insert("statement".to_string(), Value::String(trimmed.to_string()));
1643                    params
1644                },
1645            })
1646        }
1647    }
1648
1649    fn parse_method_args(&self, args_str: &str) -> Result<Vec<Value>> {
1650        if args_str.trim().is_empty() {
1651            return Ok(Vec::new());
1652        }
1653
1654        // Handle expressions like: $TestCar.Speed + $TestCar.SpeedIncrement
1655        let mut args = Vec::new();
1656        let parts: Vec<&str> = args_str.split(',').collect();
1657
1658        for part in parts {
1659            let trimmed = part.trim();
1660
1661            // Handle arithmetic expressions
1662            if trimmed.contains('+')
1663                || trimmed.contains('-')
1664                || trimmed.contains('*')
1665                || trimmed.contains('/')
1666            {
1667                // For now, store as string - the engine will evaluate
1668                args.push(Value::String(trimmed.to_string()));
1669            } else {
1670                args.push(self.parse_value(trimmed)?);
1671            }
1672        }
1673
1674        Ok(args)
1675    }
1676
1677    /// Parse function arguments as parameters for custom actions
1678    fn parse_function_args_as_params(&self, args_str: &str) -> Result<HashMap<String, Value>> {
1679        let mut params = HashMap::new();
1680
1681        if args_str.trim().is_empty() {
1682            return Ok(params);
1683        }
1684
1685        // Parse positional parameters as numbered args
1686        let parts: Vec<&str> = args_str.split(',').collect();
1687        for (i, part) in parts.iter().enumerate() {
1688            let trimmed = part.trim();
1689            let value = self.parse_value(trimmed)?;
1690
1691            // Use simple numeric indexing - engine will resolve references dynamically
1692            params.insert(i.to_string(), value);
1693        }
1694
1695        Ok(params)
1696    }
1697
1698    /// Parse stream pattern condition
1699    /// Example: "login: LoginEvent from stream(\"logins\") over window(10 min, sliding)"
1700    #[cfg(feature = "streaming")]
1701    fn parse_stream_pattern_condition(&self, clause: &str) -> Result<ConditionGroup> {
1702        use crate::engine::rule::{StreamWindow, StreamWindowType};
1703        use crate::parser::grl::stream_syntax::parse_stream_pattern;
1704
1705        // Parse using nom parser
1706        let parse_result =
1707            parse_stream_pattern(clause).map_err(|e| RuleEngineError::ParseError {
1708                message: format!("Failed to parse stream pattern: {:?}", e),
1709            })?;
1710
1711        let (_, pattern) = parse_result;
1712
1713        // Convert WindowType from parser to StreamWindowType
1714        let window = pattern.source.window.map(|w| StreamWindow {
1715            duration: w.duration,
1716            window_type: match w.window_type {
1717                crate::parser::grl::stream_syntax::WindowType::Sliding => StreamWindowType::Sliding,
1718                crate::parser::grl::stream_syntax::WindowType::Tumbling => {
1719                    StreamWindowType::Tumbling
1720                }
1721                crate::parser::grl::stream_syntax::WindowType::Session { timeout } => {
1722                    StreamWindowType::Session { timeout }
1723                }
1724            },
1725        });
1726
1727        Ok(ConditionGroup::stream_pattern(
1728            pattern.var_name,
1729            pattern.event_type,
1730            pattern.source.stream_name,
1731            window,
1732        ))
1733    }
1734}
1735
1736#[cfg(test)]
1737mod tests {
1738    use super::GRLParser;
1739
1740    #[test]
1741    fn test_parse_simple_rule() {
1742        let grl = r#"
1743        rule "CheckAge" salience 10 {
1744            when
1745                User.Age >= 18
1746            then
1747                log("User is adult");
1748        }
1749        "#;
1750
1751        let rules = GRLParser::parse_rules(grl).unwrap();
1752        assert_eq!(rules.len(), 1);
1753        let rule = &rules[0];
1754        assert_eq!(rule.name, "CheckAge");
1755        assert_eq!(rule.salience, 10);
1756        assert_eq!(rule.actions.len(), 1);
1757    }
1758
1759    #[test]
1760    fn test_parse_complex_condition() {
1761        let grl = r#"
1762        rule "ComplexRule" {
1763            when
1764                User.Age >= 18 && User.Country == "US"
1765            then
1766                User.Qualified = true;
1767        }
1768        "#;
1769
1770        let rules = GRLParser::parse_rules(grl).unwrap();
1771        assert_eq!(rules.len(), 1);
1772        let rule = &rules[0];
1773        assert_eq!(rule.name, "ComplexRule");
1774    }
1775
1776    #[test]
1777    fn test_parse_new_syntax_with_parentheses() {
1778        let grl = r#"
1779        rule "Default Rule" salience 10 {
1780            when
1781                (user.age >= 18)
1782            then
1783                set(user.status, "approved");
1784        }
1785        "#;
1786
1787        let rules = GRLParser::parse_rules(grl).unwrap();
1788        assert_eq!(rules.len(), 1);
1789        let rule = &rules[0];
1790        assert_eq!(rule.name, "Default Rule");
1791        assert_eq!(rule.salience, 10);
1792        assert_eq!(rule.actions.len(), 1);
1793
1794        // Check that the action is parsed as a Custom action (set is now custom)
1795        match &rule.actions[0] {
1796            crate::types::ActionType::Custom {
1797                action_type,
1798                params,
1799            } => {
1800                assert_eq!(action_type, "set");
1801                assert_eq!(
1802                    params.get("0"),
1803                    Some(&crate::types::Value::String("user.status".to_string()))
1804                );
1805                assert_eq!(
1806                    params.get("1"),
1807                    Some(&crate::types::Value::String("approved".to_string()))
1808                );
1809            }
1810            _ => panic!("Expected Custom action, got: {:?}", rule.actions[0]),
1811        }
1812    }
1813
1814    #[test]
1815    fn test_parse_complex_nested_conditions() {
1816        let grl = r#"
1817        rule "Complex Business Rule" salience 10 {
1818            when
1819                (((user.vipStatus == true) && (order.amount > 500)) || ((date.isHoliday == true) && (order.hasCoupon == true)))
1820            then
1821                apply_discount(20000);
1822        }
1823        "#;
1824
1825        let rules = GRLParser::parse_rules(grl).unwrap();
1826        assert_eq!(rules.len(), 1);
1827        let rule = &rules[0];
1828        assert_eq!(rule.name, "Complex Business Rule");
1829        assert_eq!(rule.salience, 10);
1830        assert_eq!(rule.actions.len(), 1);
1831
1832        // Check that the action is parsed as a Custom action (apply_discount is now custom)
1833        match &rule.actions[0] {
1834            crate::types::ActionType::Custom {
1835                action_type,
1836                params,
1837            } => {
1838                assert_eq!(action_type, "apply_discount");
1839                assert_eq!(params.get("0"), Some(&crate::types::Value::Integer(20000)));
1840            }
1841            _ => panic!("Expected Custom action, got: {:?}", rule.actions[0]),
1842        }
1843    }
1844
1845    #[test]
1846    fn test_parse_no_loop_attribute() {
1847        let grl = r#"
1848        rule "NoLoopRule" no-loop salience 15 {
1849            when
1850                User.Score < 100
1851            then
1852                set(User.Score, User.Score + 10);
1853        }
1854        "#;
1855
1856        let rules = GRLParser::parse_rules(grl).unwrap();
1857        assert_eq!(rules.len(), 1);
1858        let rule = &rules[0];
1859        assert_eq!(rule.name, "NoLoopRule");
1860        assert_eq!(rule.salience, 15);
1861        assert!(rule.no_loop, "Rule should have no-loop=true");
1862    }
1863
1864    #[test]
1865    fn test_parse_no_loop_different_positions() {
1866        // Test no-loop before salience
1867        let grl1 = r#"
1868        rule "Rule1" no-loop salience 10 {
1869            when User.Age >= 18
1870            then log("adult");
1871        }
1872        "#;
1873
1874        // Test no-loop after salience
1875        let grl2 = r#"
1876        rule "Rule2" salience 10 no-loop {
1877            when User.Age >= 18
1878            then log("adult");
1879        }
1880        "#;
1881
1882        let rules1 = GRLParser::parse_rules(grl1).unwrap();
1883        let rules2 = GRLParser::parse_rules(grl2).unwrap();
1884
1885        assert_eq!(rules1.len(), 1);
1886        assert_eq!(rules2.len(), 1);
1887
1888        assert!(rules1[0].no_loop, "Rule1 should have no-loop=true");
1889        assert!(rules2[0].no_loop, "Rule2 should have no-loop=true");
1890
1891        assert_eq!(rules1[0].salience, 10);
1892        assert_eq!(rules2[0].salience, 10);
1893    }
1894
1895    #[test]
1896    fn test_parse_without_no_loop() {
1897        let grl = r#"
1898        rule "RegularRule" salience 5 {
1899            when
1900                User.Active == true
1901            then
1902                log("active user");
1903        }
1904        "#;
1905
1906        let rules = GRLParser::parse_rules(grl).unwrap();
1907        assert_eq!(rules.len(), 1);
1908        let rule = &rules[0];
1909        assert_eq!(rule.name, "RegularRule");
1910        assert!(!rule.no_loop, "Rule should have no-loop=false by default");
1911    }
1912
1913    #[test]
1914    fn test_parse_exists_pattern() {
1915        let grl = r#"
1916        rule "ExistsRule" salience 20 {
1917            when
1918                exists(Customer.tier == "VIP")
1919            then
1920                System.premiumActive = true;
1921        }
1922        "#;
1923
1924        let rules = GRLParser::parse_rules(grl).unwrap();
1925        assert_eq!(rules.len(), 1);
1926        let rule = &rules[0];
1927        assert_eq!(rule.name, "ExistsRule");
1928        assert_eq!(rule.salience, 20);
1929
1930        // Check that condition is EXISTS pattern
1931        match &rule.conditions {
1932            crate::engine::rule::ConditionGroup::Exists(_) => {
1933                // Test passes
1934            }
1935            _ => panic!(
1936                "Expected EXISTS condition group, got: {:?}",
1937                rule.conditions
1938            ),
1939        }
1940    }
1941
1942    #[test]
1943    fn test_parse_forall_pattern() {
1944        let grl = r#"
1945        rule "ForallRule" salience 15 {
1946            when
1947                forall(Order.status == "processed")
1948            then
1949                Shipping.enabled = true;
1950        }
1951        "#;
1952
1953        let rules = GRLParser::parse_rules(grl).unwrap();
1954        assert_eq!(rules.len(), 1);
1955        let rule = &rules[0];
1956        assert_eq!(rule.name, "ForallRule");
1957
1958        // Check that condition is FORALL pattern
1959        match &rule.conditions {
1960            crate::engine::rule::ConditionGroup::Forall(_) => {
1961                // Test passes
1962            }
1963            _ => panic!(
1964                "Expected FORALL condition group, got: {:?}",
1965                rule.conditions
1966            ),
1967        }
1968    }
1969
1970    #[test]
1971    fn test_parse_combined_patterns() {
1972        let grl = r#"
1973        rule "CombinedRule" salience 25 {
1974            when
1975                exists(Customer.tier == "VIP") && !exists(Alert.priority == "high")
1976            then
1977                System.vipMode = true;
1978        }
1979        "#;
1980
1981        let rules = GRLParser::parse_rules(grl).unwrap();
1982        assert_eq!(rules.len(), 1);
1983        let rule = &rules[0];
1984        assert_eq!(rule.name, "CombinedRule");
1985
1986        // Check that condition is AND with EXISTS and NOT(EXISTS) patterns
1987        match &rule.conditions {
1988            crate::engine::rule::ConditionGroup::Compound {
1989                left,
1990                operator,
1991                right,
1992            } => {
1993                assert_eq!(*operator, crate::types::LogicalOperator::And);
1994
1995                // Left should be EXISTS
1996                match left.as_ref() {
1997                    crate::engine::rule::ConditionGroup::Exists(_) => {
1998                        // Expected
1999                    }
2000                    _ => panic!("Expected EXISTS in left side, got: {:?}", left),
2001                }
2002
2003                // Right should be NOT(EXISTS)
2004                match right.as_ref() {
2005                    crate::engine::rule::ConditionGroup::Not(inner) => {
2006                        match inner.as_ref() {
2007                            crate::engine::rule::ConditionGroup::Exists(_) => {
2008                                // Expected
2009                            }
2010                            _ => panic!("Expected EXISTS inside NOT, got: {:?}", inner),
2011                        }
2012                    }
2013                    _ => panic!("Expected NOT in right side, got: {:?}", right),
2014                }
2015            }
2016            _ => panic!("Expected compound condition, got: {:?}", rule.conditions),
2017        }
2018    }
2019
2020    #[test]
2021    fn test_parse_in_operator() {
2022        let grl = r#"
2023        rule "TestInOperator" salience 75 {
2024            when
2025                User.role in ["admin", "moderator", "vip"]
2026            then
2027                User.access = "granted";
2028        }
2029        "#;
2030
2031        let rules = GRLParser::parse_rules(grl).unwrap();
2032        assert_eq!(rules.len(), 1);
2033        let rule = &rules[0];
2034        assert_eq!(rule.name, "TestInOperator");
2035        assert_eq!(rule.salience, 75);
2036
2037        // Check the condition
2038        match &rule.conditions {
2039            crate::engine::rule::ConditionGroup::Single(cond) => {
2040                // The field might be in expression format
2041                println!("Condition: {:?}", cond);
2042                assert_eq!(cond.operator, crate::types::Operator::In);
2043
2044                // Value should be an array
2045                match &cond.value {
2046                    crate::types::Value::Array(arr) => {
2047                        assert_eq!(arr.len(), 3);
2048                        assert_eq!(arr[0], crate::types::Value::String("admin".to_string()));
2049                        assert_eq!(arr[1], crate::types::Value::String("moderator".to_string()));
2050                        assert_eq!(arr[2], crate::types::Value::String("vip".to_string()));
2051                    }
2052                    _ => panic!("Expected Array value, got {:?}", cond.value),
2053                }
2054            }
2055            _ => panic!("Expected Single condition, got: {:?}", rule.conditions),
2056        }
2057    }
2058
2059    #[test]
2060    fn test_parse_startswith_endswith_operators() {
2061        let grl = r#"
2062        rule "StringMethods" salience 50 {
2063            when
2064                User.email startsWith "admin@" &&
2065                User.filename endsWith ".txt"
2066            then
2067                User.validated = true;
2068        }
2069        "#;
2070
2071        let rules = GRLParser::parse_rules(grl).unwrap();
2072        assert_eq!(rules.len(), 1);
2073        let rule = &rules[0];
2074        assert_eq!(rule.name, "StringMethods");
2075        assert_eq!(rule.salience, 50);
2076
2077        // Check the compound condition (AND)
2078        match &rule.conditions {
2079            crate::engine::rule::ConditionGroup::Compound {
2080                left,
2081                operator,
2082                right,
2083            } => {
2084                assert_eq!(*operator, crate::types::LogicalOperator::And);
2085
2086                // Left should be startsWith
2087                match left.as_ref() {
2088                    crate::engine::rule::ConditionGroup::Single(cond) => {
2089                        assert_eq!(cond.operator, crate::types::Operator::StartsWith);
2090                    }
2091                    _ => panic!("Expected Single condition for startsWith, got: {:?}", left),
2092                }
2093
2094                // Right should be endsWith
2095                match right.as_ref() {
2096                    crate::engine::rule::ConditionGroup::Single(cond) => {
2097                        assert_eq!(cond.operator, crate::types::Operator::EndsWith);
2098                    }
2099                    _ => panic!("Expected Single condition for endsWith, got: {:?}", right),
2100                }
2101            }
2102            _ => panic!("Expected Compound condition, got: {:?}", rule.conditions),
2103        }
2104    }
2105}