rust_rule_engine/parser/
grl_no_regex.rs

1/// GRL Parser without regex dependency
2///
3/// This module provides full GRL parsing using only memchr and manual string parsing.
4/// It's 4-60x faster than the regex-based GRLParser and has no regex dependency.
5use crate::engine::module::{ExportItem, ExportList, ImportType, ItemType, ModuleManager};
6use crate::engine::rule::{Condition, ConditionGroup, Rule};
7use crate::errors::{Result, RuleEngineError};
8use crate::types::{ActionType, Operator, Value};
9use chrono::{DateTime, Utc};
10use std::collections::HashMap;
11
12use super::literal_search;
13
14/// GRL Parser - No Regex Version
15///
16/// Parses Grule-like syntax into Rule objects without using regex.
17/// This is the recommended parser for new code - it's faster and has fewer dependencies.
18pub struct GRLParserNoRegex;
19
20/// Result from parsing GRL with modules
21#[derive(Debug, Clone)]
22pub struct ParsedGRL {
23    /// Parsed rules
24    pub rules: Vec<Rule>,
25    /// Module manager with configured modules
26    pub module_manager: ModuleManager,
27    /// Map of rule name to module name
28    pub rule_modules: HashMap<String, String>,
29}
30
31impl Default for ParsedGRL {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37impl ParsedGRL {
38    pub fn new() -> Self {
39        Self {
40            rules: Vec::new(),
41            module_manager: ModuleManager::new(),
42            rule_modules: HashMap::new(),
43        }
44    }
45}
46
47/// Parsed rule attributes
48#[derive(Debug, Default)]
49struct RuleAttributes {
50    pub salience: i32,
51    pub no_loop: bool,
52    pub lock_on_active: bool,
53    pub agenda_group: Option<String>,
54    pub activation_group: Option<String>,
55    pub date_effective: Option<DateTime<Utc>>,
56    pub date_expires: Option<DateTime<Utc>>,
57}
58
59impl GRLParserNoRegex {
60    /// Parse multiple rules from GRL text
61    pub fn parse_rules(grl_text: &str) -> Result<Vec<Rule>> {
62        let rule_texts = split_into_rules(grl_text);
63        let mut rules = Vec::with_capacity(rule_texts.len());
64
65        for rule_text in rule_texts {
66            let rule = Self::parse_single_rule(&rule_text)?;
67            rules.push(rule);
68        }
69
70        Ok(rules)
71    }
72
73    /// Parse a single rule from GRL syntax
74    pub fn parse_rule(grl_text: &str) -> Result<Rule> {
75        Self::parse_single_rule(grl_text)
76    }
77
78    /// Parse GRL text with module support
79    pub fn parse_with_modules(grl_text: &str) -> Result<ParsedGRL> {
80        let mut result = ParsedGRL::new();
81
82        // Split modules and rules
83        let (module_texts, rules_text) = split_modules_and_rules(grl_text);
84
85        // Parse modules
86        for module_text in module_texts {
87            Self::parse_and_register_module(&module_text, &mut result.module_manager)?;
88        }
89
90        // Parse rules
91        let rules = Self::parse_rules(&rules_text)?;
92
93        // Assign rules to modules
94        for rule in rules {
95            let module_name = extract_module_from_context(grl_text, &rule.name);
96            result
97                .rule_modules
98                .insert(rule.name.clone(), module_name.clone());
99
100            if let Ok(module) = result.module_manager.get_module_mut(&module_name) {
101                module.add_rule(&rule.name);
102            }
103
104            result.rules.push(rule);
105        }
106
107        Ok(result)
108    }
109
110    fn parse_single_rule(grl_text: &str) -> Result<Rule> {
111        let cleaned = clean_text(grl_text);
112
113        // Find "rule" keyword
114        let rule_pos =
115            find_keyword(&cleaned, "rule").ok_or_else(|| RuleEngineError::ParseError {
116                message: "Missing 'rule' keyword".to_string(),
117            })?;
118
119        let after_rule = cleaned[rule_pos + 4..].trim_start();
120
121        // Extract rule name (quoted or unquoted)
122        let (rule_name, after_name) = extract_rule_name(after_rule)?;
123
124        // Find opening brace
125        let brace_pos = after_name
126            .find('{')
127            .ok_or_else(|| RuleEngineError::ParseError {
128                message: "Missing opening brace".to_string(),
129            })?;
130
131        let attributes_section = &after_name[..brace_pos];
132        let body_start = brace_pos + 1;
133
134        // Find matching closing brace
135        let body_with_brace = &after_name[brace_pos..];
136        let close_pos =
137            literal_search::find_matching_brace(body_with_brace, 0).ok_or_else(|| {
138                RuleEngineError::ParseError {
139                    message: "Missing closing brace".to_string(),
140                }
141            })?;
142
143        let rule_body = &after_name[body_start..brace_pos + close_pos];
144
145        // Parse attributes
146        let attributes = parse_rule_attributes(attributes_section)?;
147
148        // Parse when-then
149        let (when_clause, then_clause) = parse_when_then(rule_body)?;
150
151        // Parse conditions and actions
152        let conditions = parse_when_clause(&when_clause)?;
153        let actions = parse_then_clause(&then_clause)?;
154
155        // Build rule
156        let mut rule = Rule::new(rule_name, conditions, actions);
157        rule = rule.with_priority(attributes.salience);
158
159        if attributes.no_loop {
160            rule = rule.with_no_loop(true);
161        }
162        if attributes.lock_on_active {
163            rule = rule.with_lock_on_active(true);
164        }
165        if let Some(agenda_group) = attributes.agenda_group {
166            rule = rule.with_agenda_group(agenda_group);
167        }
168        if let Some(activation_group) = attributes.activation_group {
169            rule = rule.with_activation_group(activation_group);
170        }
171        if let Some(date_effective) = attributes.date_effective {
172            rule = rule.with_date_effective(date_effective);
173        }
174        if let Some(date_expires) = attributes.date_expires {
175            rule = rule.with_date_expires(date_expires);
176        }
177
178        Ok(rule)
179    }
180
181    fn parse_and_register_module(module_def: &str, manager: &mut ModuleManager) -> Result<()> {
182        let (name, body, _) = parse_defmodule(module_def)?;
183
184        let _ = manager.create_module(&name);
185        let module = manager.get_module_mut(&name)?;
186
187        // Parse export directive
188        if let Some(export_type) = extract_directive(&body, "export:") {
189            let exports = if export_type.trim() == "all" {
190                ExportList::All
191            } else if export_type.trim() == "none" {
192                ExportList::None
193            } else {
194                ExportList::Specific(vec![ExportItem {
195                    item_type: ItemType::All,
196                    pattern: export_type.trim().to_string(),
197                }])
198            };
199            module.set_exports(exports);
200        }
201
202        // Parse import directives
203        for line in body.lines() {
204            let trimmed = line.trim();
205            if trimmed.starts_with("import:") {
206                if let Some(import_spec) = extract_directive(trimmed, "import:") {
207                    Self::parse_import_spec(&name, &import_spec, manager)?;
208                }
209            }
210        }
211
212        Ok(())
213    }
214
215    fn parse_import_spec(
216        importing_module: &str,
217        spec: &str,
218        manager: &mut ModuleManager,
219    ) -> Result<()> {
220        let parts: Vec<&str> = spec.splitn(2, '(').collect();
221        if parts.is_empty() {
222            return Ok(());
223        }
224
225        let source_module = parts[0].trim().to_string();
226        let rest = if parts.len() > 1 { parts[1] } else { "" };
227
228        if rest.contains("rules") {
229            manager.import_from(importing_module, &source_module, ImportType::AllRules, "*")?;
230        }
231
232        if rest.contains("templates") {
233            manager.import_from(
234                importing_module,
235                &source_module,
236                ImportType::AllTemplates,
237                "*",
238            )?;
239        }
240
241        Ok(())
242    }
243}
244
245// ============================================================================
246// Helper Functions
247// ============================================================================
248
249/// Split GRL text into individual rules
250fn split_into_rules(grl_text: &str) -> Vec<String> {
251    let mut rules = Vec::new();
252    let bytes = grl_text.as_bytes();
253    let mut i = 0;
254
255    while i < bytes.len() {
256        if let Some(rule_pos) = memchr::memmem::find(&bytes[i..], b"rule ") {
257            let abs_pos = i + rule_pos;
258
259            // Check word boundary before "rule"
260            if abs_pos > 0 && bytes[abs_pos - 1].is_ascii_alphanumeric() {
261                i = abs_pos + 1;
262                continue;
263            }
264
265            // Check if "rule " is inside a comment (look back for // on same line)
266            if is_inside_comment(grl_text, abs_pos) {
267                i = abs_pos + 5;
268                continue;
269            }
270
271            if let Some(brace_pos) = memchr::memchr(b'{', &bytes[abs_pos..]) {
272                let brace_abs = abs_pos + brace_pos;
273
274                if let Some(close_pos) = literal_search::find_matching_brace(grl_text, brace_abs) {
275                    let rule_text = &grl_text[abs_pos..=close_pos];
276                    rules.push(rule_text.to_string());
277                    i = close_pos + 1;
278                    continue;
279                }
280            }
281        }
282        break;
283    }
284
285    rules
286}
287
288/// Check if a position is inside a single-line comment
289fn is_inside_comment(text: &str, pos: usize) -> bool {
290    // Find the start of the current line
291    let bytes = text.as_bytes();
292    let mut line_start = pos;
293    while line_start > 0 && bytes[line_start - 1] != b'\n' {
294        line_start -= 1;
295    }
296
297    // Check if there's a // between line_start and pos
298    let line_prefix = &text[line_start..pos];
299    line_prefix.contains("//")
300}
301
302/// Split modules and rules from GRL text
303fn split_modules_and_rules(grl_text: &str) -> (Vec<String>, String) {
304    let mut modules = Vec::new();
305    let mut rules_text = String::new();
306    let bytes = grl_text.as_bytes();
307    let mut i = 0;
308    let mut last_copy = 0;
309
310    while i < bytes.len() {
311        if let Some(offset) = memchr::memmem::find(&bytes[i..], b"defmodule ") {
312            let abs_pos = i + offset;
313
314            if abs_pos > last_copy {
315                rules_text.push_str(&grl_text[last_copy..abs_pos]);
316            }
317
318            if let Some(brace_offset) = memchr::memchr(b'{', &bytes[abs_pos..]) {
319                let brace_abs = abs_pos + brace_offset;
320
321                if let Some(close_pos) = literal_search::find_matching_brace(grl_text, brace_abs) {
322                    let module_text = &grl_text[abs_pos..=close_pos];
323                    modules.push(module_text.to_string());
324                    i = close_pos + 1;
325                    last_copy = i;
326                    continue;
327                }
328            }
329        }
330        i += 1;
331    }
332
333    if last_copy < grl_text.len() {
334        rules_text.push_str(&grl_text[last_copy..]);
335    }
336
337    (modules, rules_text)
338}
339
340/// Clean text by removing comments and joining lines
341fn clean_text(text: &str) -> String {
342    text.lines()
343        .map(|line| {
344            // Remove single-line comments
345            if let Some(comment_pos) = line.find("//") {
346                line[..comment_pos].trim()
347            } else {
348                line.trim()
349            }
350        })
351        .filter(|line| !line.is_empty())
352        .collect::<Vec<_>>()
353        .join(" ")
354}
355
356/// Find keyword at word boundary
357fn find_keyword(text: &str, keyword: &str) -> Option<usize> {
358    let bytes = text.as_bytes();
359    let keyword_bytes = keyword.as_bytes();
360    let mut pos = 0;
361
362    while let Some(offset) = memchr::memmem::find(&bytes[pos..], keyword_bytes) {
363        let abs_pos = pos + offset;
364
365        // Check word boundaries
366        let before_ok = abs_pos == 0 || !bytes[abs_pos - 1].is_ascii_alphanumeric();
367        let after_pos = abs_pos + keyword_bytes.len();
368        let after_ok = after_pos >= bytes.len() || !bytes[after_pos].is_ascii_alphanumeric();
369
370        if before_ok && after_ok {
371            return Some(abs_pos);
372        }
373
374        pos = abs_pos + 1;
375    }
376
377    None
378}
379
380/// Extract rule name (quoted or unquoted)
381fn extract_rule_name(text: &str) -> Result<(String, &str)> {
382    let trimmed = text.trim_start();
383
384    // Try quoted name first
385    if trimmed.starts_with('"') {
386        if let Some(end_quote) = memchr::memchr(b'"', &trimmed.as_bytes()[1..]) {
387            let name = trimmed[1..end_quote + 1].to_string();
388            let remaining = &trimmed[end_quote + 2..];
389            return Ok((name, remaining));
390        }
391        return Err(RuleEngineError::ParseError {
392            message: "Unclosed quote in rule name".to_string(),
393        });
394    }
395
396    // Try identifier
397    let name_end = trimmed
398        .find(|c: char| !c.is_alphanumeric() && c != '_')
399        .unwrap_or(trimmed.len());
400
401    if name_end == 0 {
402        return Err(RuleEngineError::ParseError {
403            message: "Missing rule name".to_string(),
404        });
405    }
406
407    let name = trimmed[..name_end].to_string();
408    let remaining = &trimmed[name_end..];
409
410    Ok((name, remaining))
411}
412
413/// Parse rule attributes from the attributes section
414fn parse_rule_attributes(attrs: &str) -> Result<RuleAttributes> {
415    let mut result = RuleAttributes::default();
416
417    // Remove quoted strings to avoid false matches
418    let cleaned = remove_quoted_strings(attrs);
419
420    // Parse salience
421    if let Some(salience_pos) = find_keyword(&cleaned, "salience") {
422        let after_salience = cleaned[salience_pos + 8..].trim_start();
423        let digits: String = after_salience
424            .chars()
425            .take_while(|c| c.is_ascii_digit() || *c == '-')
426            .collect();
427        if let Ok(val) = digits.parse::<i32>() {
428            result.salience = val;
429        }
430    }
431
432    // Parse boolean flags
433    result.no_loop = has_keyword(&cleaned, "no-loop");
434    result.lock_on_active = has_keyword(&cleaned, "lock-on-active");
435
436    // Parse quoted attributes from original (not cleaned)
437    result.agenda_group = extract_quoted_attribute(attrs, "agenda-group");
438    result.activation_group = extract_quoted_attribute(attrs, "activation-group");
439
440    if let Some(date_str) = extract_quoted_attribute(attrs, "date-effective") {
441        result.date_effective = parse_date_string(&date_str).ok();
442    }
443
444    if let Some(date_str) = extract_quoted_attribute(attrs, "date-expires") {
445        result.date_expires = parse_date_string(&date_str).ok();
446    }
447
448    Ok(result)
449}
450
451/// Remove quoted strings from text
452fn remove_quoted_strings(text: &str) -> String {
453    let mut result = String::with_capacity(text.len());
454    let mut in_string = false;
455    let mut escape_next = false;
456
457    for ch in text.chars() {
458        if escape_next {
459            escape_next = false;
460            continue;
461        }
462
463        match ch {
464            '\\' if in_string => escape_next = true,
465            '"' => in_string = !in_string,
466            _ if !in_string => result.push(ch),
467            _ => {}
468        }
469    }
470
471    result
472}
473
474/// Check if keyword exists at word boundary
475fn has_keyword(text: &str, keyword: &str) -> bool {
476    find_keyword(text, keyword).is_some()
477}
478
479/// Extract quoted attribute value
480fn extract_quoted_attribute(text: &str, attr_name: &str) -> Option<String> {
481    let attr_pos = find_keyword(text, attr_name)?;
482    let after_attr = text[attr_pos + attr_name.len()..].trim_start();
483
484    if after_attr.starts_with('"') {
485        let end_quote = memchr::memchr(b'"', &after_attr.as_bytes()[1..])?;
486        Some(after_attr[1..end_quote + 1].to_string())
487    } else {
488        None
489    }
490}
491
492/// Parse date string
493fn parse_date_string(date_str: &str) -> Result<DateTime<Utc>> {
494    if let Ok(date) = DateTime::parse_from_rfc3339(date_str) {
495        return Ok(date.with_timezone(&Utc));
496    }
497
498    let formats = ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%d-%b-%Y", "%d-%m-%Y"];
499
500    for format in &formats {
501        if let Ok(naive_date) = chrono::NaiveDateTime::parse_from_str(date_str, format) {
502            return Ok(naive_date.and_utc());
503        }
504        if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(date_str, format) {
505            return Ok(naive_date.and_hms_opt(0, 0, 0).unwrap().and_utc());
506        }
507    }
508
509    Err(RuleEngineError::ParseError {
510        message: format!("Unable to parse date: {}", date_str),
511    })
512}
513
514/// Parse when-then sections
515fn parse_when_then(body: &str) -> Result<(String, String)> {
516    let when_pos = find_keyword(body, "when").ok_or_else(|| RuleEngineError::ParseError {
517        message: "Missing 'when' clause".to_string(),
518    })?;
519
520    let after_when = &body[when_pos + 4..];
521
522    // Find "then" at the correct nesting level
523    let then_pos = find_then_keyword(after_when).ok_or_else(|| RuleEngineError::ParseError {
524        message: "Missing 'then' clause".to_string(),
525    })?;
526
527    let when_clause = after_when[..then_pos].trim().to_string();
528    let then_clause = after_when[then_pos + 4..].trim().to_string();
529
530    Ok((when_clause, then_clause))
531}
532
533/// Find "then" keyword at the correct nesting level
534fn find_then_keyword(text: &str) -> Option<usize> {
535    let bytes = text.as_bytes();
536    let mut in_string = false;
537    let mut escape_next = false;
538    let mut paren_depth: i32 = 0;
539    let mut brace_depth: i32 = 0;
540
541    let mut i = 0;
542    while i < bytes.len() {
543        if escape_next {
544            escape_next = false;
545            i += 1;
546            continue;
547        }
548
549        match bytes[i] {
550            b'\\' if in_string => escape_next = true,
551            b'"' => in_string = !in_string,
552            b'(' if !in_string => paren_depth += 1,
553            b')' if !in_string => paren_depth = paren_depth.saturating_sub(1),
554            b'{' if !in_string => brace_depth += 1,
555            b'}' if !in_string => brace_depth = brace_depth.saturating_sub(1),
556            b't' if !in_string && paren_depth == 0 && brace_depth == 0 => {
557                if i + 4 <= bytes.len() && &bytes[i..i + 4] == b"then" {
558                    let before_ok = i == 0 || !bytes[i - 1].is_ascii_alphanumeric();
559                    let after_ok = i + 4 >= bytes.len() || !bytes[i + 4].is_ascii_alphanumeric();
560                    if before_ok && after_ok {
561                        return Some(i);
562                    }
563                }
564            }
565            _ => {}
566        }
567        i += 1;
568    }
569
570    None
571}
572
573/// Parse defmodule declaration
574fn parse_defmodule(text: &str) -> Result<(String, String, usize)> {
575    let trimmed = text.trim_start();
576
577    if !trimmed.starts_with("defmodule") {
578        return Err(RuleEngineError::ParseError {
579            message: "Expected 'defmodule'".to_string(),
580        });
581    }
582
583    let after_defmodule = trimmed[9..].trim_start();
584
585    let name_end = after_defmodule
586        .chars()
587        .position(|c| !c.is_alphanumeric() && c != '_')
588        .unwrap_or(after_defmodule.len());
589
590    if name_end == 0 {
591        return Err(RuleEngineError::ParseError {
592            message: "Missing module name".to_string(),
593        });
594    }
595
596    let name = after_defmodule[..name_end].to_string();
597
598    if !name
599        .chars()
600        .next()
601        .map(|c| c.is_uppercase())
602        .unwrap_or(false)
603    {
604        return Err(RuleEngineError::ParseError {
605            message: "Module name must start with uppercase".to_string(),
606        });
607    }
608
609    let rest = after_defmodule[name_end..].trim_start();
610    if !rest.starts_with('{') {
611        return Err(RuleEngineError::ParseError {
612            message: "Expected '{' after module name".to_string(),
613        });
614    }
615
616    let brace_pos = trimmed.len() - rest.len();
617    let close_pos = literal_search::find_matching_brace(trimmed, brace_pos).ok_or_else(|| {
618        RuleEngineError::ParseError {
619            message: "Missing closing brace for module".to_string(),
620        }
621    })?;
622
623    let body = trimmed[brace_pos + 1..close_pos].to_string();
624
625    Ok((name, body, close_pos + 1))
626}
627
628/// Extract directive value
629fn extract_directive(text: &str, directive: &str) -> Option<String> {
630    let pos = text.find(directive)?;
631    let after_directive = &text[pos + directive.len()..];
632
633    let end = after_directive
634        .find("import:")
635        .or_else(|| after_directive.find("export:"))
636        .unwrap_or(after_directive.len());
637
638    Some(after_directive[..end].trim().to_string())
639}
640
641/// Extract module name from context
642fn extract_module_from_context(grl_text: &str, rule_name: &str) -> String {
643    let rule_patterns = [
644        format!("rule \"{}\"", rule_name),
645        format!("rule {}", rule_name),
646    ];
647
648    for pattern in &rule_patterns {
649        if let Some(rule_pos) = grl_text.find(pattern) {
650            let before = &grl_text[..rule_pos];
651            if let Some(module_pos) = before.rfind(";; MODULE:") {
652                let after_marker = &before[module_pos + 10..];
653                if let Some(end_line) = after_marker.find('\n') {
654                    let module_line = after_marker[..end_line].trim();
655                    if let Some(first_word) = module_line.split_whitespace().next() {
656                        return first_word.to_string();
657                    }
658                }
659            }
660        }
661    }
662
663    "MAIN".to_string()
664}
665
666// ============================================================================
667// Condition Parsing
668// ============================================================================
669
670/// Parse the when clause into a ConditionGroup
671fn parse_when_clause(when_clause: &str) -> Result<ConditionGroup> {
672    let trimmed = when_clause.trim();
673
674    // Strip outer parentheses if balanced
675    let clause = strip_outer_parens(trimmed);
676
677    // Parse OR (lowest precedence)
678    if let Some(parts) = split_logical_operator(clause, "||") {
679        return parse_or_parts(parts);
680    }
681
682    // Parse AND
683    if let Some(parts) = split_logical_operator(clause, "&&") {
684        return parse_and_parts(parts);
685    }
686
687    // Handle NOT
688    if clause.trim_start().starts_with('!') {
689        let inner = clause.trim_start()[1..].trim();
690        let inner_condition = parse_when_clause(inner)?;
691        return Ok(ConditionGroup::not(inner_condition));
692    }
693
694    // Handle EXISTS
695    if clause.trim_start().starts_with("exists(") && clause.trim_end().ends_with(')') {
696        let inner = &clause.trim()[7..clause.trim().len() - 1];
697        let inner_condition = parse_when_clause(inner)?;
698        return Ok(ConditionGroup::exists(inner_condition));
699    }
700
701    // Handle FORALL
702    if clause.trim_start().starts_with("forall(") && clause.trim_end().ends_with(')') {
703        let inner = &clause.trim()[7..clause.trim().len() - 1];
704        let inner_condition = parse_when_clause(inner)?;
705        return Ok(ConditionGroup::forall(inner_condition));
706    }
707
708    // Handle ACCUMULATE
709    if clause.trim_start().starts_with("accumulate(") && clause.trim_end().ends_with(')') {
710        return parse_accumulate_condition(clause);
711    }
712
713    // Handle TEST
714    if clause.trim_start().starts_with("test(") && clause.trim_end().ends_with(')') {
715        return parse_test_condition(clause);
716    }
717
718    // Single condition
719    parse_single_condition(clause)
720}
721
722/// Strip outer parentheses if they are balanced
723fn strip_outer_parens(text: &str) -> &str {
724    let trimmed = text.trim();
725    if trimmed.starts_with('(') && trimmed.ends_with(')') {
726        let inner = &trimmed[1..trimmed.len() - 1];
727        if is_balanced_parens(inner) {
728            return inner;
729        }
730    }
731    trimmed
732}
733
734/// Check if parentheses are balanced
735fn is_balanced_parens(text: &str) -> bool {
736    let mut count = 0;
737    for ch in text.chars() {
738        match ch {
739            '(' => count += 1,
740            ')' => {
741                count -= 1;
742                if count < 0 {
743                    return false;
744                }
745            }
746            _ => {}
747        }
748    }
749    count == 0
750}
751
752/// Split by logical operator at top level
753fn split_logical_operator(clause: &str, operator: &str) -> Option<Vec<String>> {
754    let mut parts = Vec::new();
755    let mut current = String::new();
756    let mut paren_count = 0;
757    let mut in_string = false;
758    let mut chars = clause.chars().peekable();
759
760    let op_chars: Vec<char> = operator.chars().collect();
761
762    while let Some(ch) = chars.next() {
763        match ch {
764            '"' => {
765                in_string = !in_string;
766                current.push(ch);
767            }
768            '(' if !in_string => {
769                paren_count += 1;
770                current.push(ch);
771            }
772            ')' if !in_string => {
773                paren_count -= 1;
774                current.push(ch);
775            }
776            _ if !in_string && paren_count == 0 => {
777                // Check for operator
778                if op_chars.len() == 2 && ch == op_chars[0] && chars.peek() == Some(&op_chars[1]) {
779                    chars.next();
780                    parts.push(current.trim().to_string());
781                    current.clear();
782                    continue;
783                }
784                current.push(ch);
785            }
786            _ => {
787                current.push(ch);
788            }
789        }
790    }
791
792    if !current.trim().is_empty() {
793        parts.push(current.trim().to_string());
794    }
795
796    if parts.len() > 1 {
797        Some(parts)
798    } else {
799        None
800    }
801}
802
803/// Parse OR parts
804fn parse_or_parts(parts: Vec<String>) -> Result<ConditionGroup> {
805    let mut conditions = Vec::new();
806    for part in parts {
807        conditions.push(parse_when_clause(&part)?);
808    }
809
810    if conditions.is_empty() {
811        return Err(RuleEngineError::ParseError {
812            message: "No conditions in OR".to_string(),
813        });
814    }
815
816    let mut iter = conditions.into_iter();
817    let mut result = iter.next().unwrap();
818    for condition in iter {
819        result = ConditionGroup::or(result, condition);
820    }
821
822    Ok(result)
823}
824
825/// Parse AND parts
826fn parse_and_parts(parts: Vec<String>) -> Result<ConditionGroup> {
827    let mut conditions = Vec::new();
828    for part in parts {
829        conditions.push(parse_when_clause(&part)?);
830    }
831
832    if conditions.is_empty() {
833        return Err(RuleEngineError::ParseError {
834            message: "No conditions in AND".to_string(),
835        });
836    }
837
838    let mut iter = conditions.into_iter();
839    let mut result = iter.next().unwrap();
840    for condition in iter {
841        result = ConditionGroup::and(result, condition);
842    }
843
844    Ok(result)
845}
846
847/// Parse single condition like "User.Age >= 18"
848fn parse_single_condition(clause: &str) -> Result<ConditionGroup> {
849    let trimmed = strip_outer_parens(clause.trim());
850
851    // Check for multifield patterns first
852    if let Some(cond) = try_parse_multifield(trimmed)? {
853        return Ok(ConditionGroup::single(cond));
854    }
855
856    // Check for function call pattern: func(args) op value
857    if let Some(cond) = try_parse_function_call(trimmed)? {
858        return Ok(ConditionGroup::single(cond));
859    }
860
861    // Parse standard condition: field op value
862    let (field, op_str, value_str) = split_condition(trimmed)?;
863
864    let operator = Operator::from_str(op_str).ok_or_else(|| RuleEngineError::InvalidOperator {
865        operator: op_str.to_string(),
866    })?;
867
868    let value = parse_value(value_str)?;
869
870    // Check if field contains arithmetic
871    if contains_arithmetic(field) {
872        let test_expr = format!("{} {} {}", field, op_str, value_str);
873        let condition = Condition::with_test(test_expr, vec![]);
874        return Ok(ConditionGroup::single(condition));
875    }
876
877    let condition = Condition::new(field.to_string(), operator, value);
878    Ok(ConditionGroup::single(condition))
879}
880
881/// Try to parse multifield patterns
882fn try_parse_multifield(clause: &str) -> Result<Option<Condition>> {
883    // Pattern: field.array $?var (collect)
884    if clause.contains(" $?") {
885        let parts: Vec<&str> = clause.splitn(2, " $?").collect();
886        if parts.len() == 2 {
887            let field = parts[0].trim().to_string();
888            let variable = format!("$?{}", parts[1].trim());
889            return Ok(Some(Condition::with_multifield_collect(field, variable)));
890        }
891    }
892
893    // Pattern: field.array count op value
894    if clause.contains(" count ") {
895        let count_pos = clause.find(" count ").unwrap();
896        let field = clause[..count_pos].trim().to_string();
897        let rest = clause[count_pos + 7..].trim();
898
899        let (_, op_str, value_str) = split_condition_from_start(rest)?;
900        let operator =
901            Operator::from_str(op_str).ok_or_else(|| RuleEngineError::InvalidOperator {
902                operator: op_str.to_string(),
903            })?;
904        let value = parse_value(value_str)?;
905
906        return Ok(Some(Condition::with_multifield_count(
907            field, operator, value,
908        )));
909    }
910
911    // Pattern: field.array first [$var]
912    if clause.contains(" first") {
913        let first_pos = clause.find(" first").unwrap();
914        let field = clause[..first_pos].trim().to_string();
915        let rest = clause[first_pos + 6..].trim();
916        let variable = if rest.starts_with('$') {
917            Some(rest.split_whitespace().next().unwrap_or(rest).to_string())
918        } else {
919            None
920        };
921        return Ok(Some(Condition::with_multifield_first(field, variable)));
922    }
923
924    // Pattern: field.array last [$var]
925    if clause.contains(" last") {
926        let last_pos = clause.find(" last").unwrap();
927        let field = clause[..last_pos].trim().to_string();
928        let rest = clause[last_pos + 5..].trim();
929        let variable = if rest.starts_with('$') {
930            Some(rest.split_whitespace().next().unwrap_or(rest).to_string())
931        } else {
932            None
933        };
934        return Ok(Some(Condition::with_multifield_last(field, variable)));
935    }
936
937    // Pattern: field.array empty
938    if let Some(stripped) = clause.strip_suffix(" empty") {
939        let field = stripped.trim().to_string();
940        return Ok(Some(Condition::with_multifield_empty(field)));
941    }
942
943    // Pattern: field.array not_empty
944    if let Some(stripped) = clause.strip_suffix(" not_empty") {
945        let field = stripped.trim().to_string();
946        return Ok(Some(Condition::with_multifield_not_empty(field)));
947    }
948
949    Ok(None)
950}
951
952/// Try to parse function call condition
953fn try_parse_function_call(clause: &str) -> Result<Option<Condition>> {
954    // Look for pattern: identifier(args) operator value
955    if let Some(paren_start) = clause.find('(') {
956        if paren_start > 0 {
957            let func_name = clause[..paren_start].trim();
958
959            // Check it's a valid identifier
960            if func_name.chars().all(|c| c.is_alphanumeric() || c == '_')
961                && func_name
962                    .chars()
963                    .next()
964                    .map(|c| c.is_alphabetic())
965                    .unwrap_or(false)
966            {
967                // Find matching close paren
968                if let Some(paren_end) = find_matching_paren(clause, paren_start) {
969                    let args_str = &clause[paren_start + 1..paren_end];
970                    let after_paren = clause[paren_end + 1..].trim();
971
972                    // Check if there's an operator after
973                    if let Ok((_, op_str, value_str)) = split_condition_from_start(after_paren) {
974                        let args: Vec<String> = if args_str.trim().is_empty() {
975                            Vec::new()
976                        } else {
977                            args_str.split(',').map(|s| s.trim().to_string()).collect()
978                        };
979
980                        let operator = Operator::from_str(op_str).ok_or_else(|| {
981                            RuleEngineError::InvalidOperator {
982                                operator: op_str.to_string(),
983                            }
984                        })?;
985
986                        let value = parse_value(value_str)?;
987
988                        return Ok(Some(Condition::with_function(
989                            func_name.to_string(),
990                            args,
991                            operator,
992                            value,
993                        )));
994                    }
995                }
996            }
997        }
998    }
999
1000    Ok(None)
1001}
1002
1003/// Find matching closing parenthesis
1004fn find_matching_paren(text: &str, open_pos: usize) -> Option<usize> {
1005    let bytes = text.as_bytes();
1006    let mut depth = 1;
1007    let mut i = open_pos + 1;
1008    let mut in_string = false;
1009
1010    while i < bytes.len() {
1011        match bytes[i] {
1012            b'"' => in_string = !in_string,
1013            b'(' if !in_string => depth += 1,
1014            b')' if !in_string => {
1015                depth -= 1;
1016                if depth == 0 {
1017                    return Some(i);
1018                }
1019            }
1020            _ => {}
1021        }
1022        i += 1;
1023    }
1024
1025    None
1026}
1027
1028/// Split condition into field, operator, value
1029fn split_condition(clause: &str) -> Result<(&str, &str, &str)> {
1030    let operators = [">=", "<=", "==", "!=", ">", "<", "contains", "matches"];
1031
1032    for op in &operators {
1033        if let Some(op_pos) = find_operator(clause, op) {
1034            let field = clause[..op_pos].trim();
1035            let value = clause[op_pos + op.len()..].trim();
1036            return Ok((field, op, value));
1037        }
1038    }
1039
1040    Err(RuleEngineError::ParseError {
1041        message: format!("Invalid condition format: {}", clause),
1042    })
1043}
1044
1045/// Split condition starting from the beginning (for partial parsing)
1046fn split_condition_from_start(text: &str) -> Result<(&str, &str, &str)> {
1047    let operators = [">=", "<=", "==", "!=", ">", "<", "contains", "matches"];
1048
1049    for op in &operators {
1050        if let Some(stripped) = text.strip_prefix(op) {
1051            let value = stripped.trim();
1052            return Ok(("", op, value));
1053        }
1054    }
1055
1056    // Try to find operator in text
1057    split_condition(text)
1058}
1059
1060/// Find operator position (not inside strings)
1061fn find_operator(text: &str, op: &str) -> Option<usize> {
1062    let bytes = text.as_bytes();
1063    let op_bytes = op.as_bytes();
1064    let mut in_string = false;
1065    let mut i = 0;
1066
1067    while i + op_bytes.len() <= bytes.len() {
1068        if bytes[i] == b'"' {
1069            in_string = !in_string;
1070            i += 1;
1071            continue;
1072        }
1073
1074        if !in_string && &bytes[i..i + op_bytes.len()] == op_bytes {
1075            // For keyword operators, check word boundaries
1076            if op.chars().next().unwrap().is_alphabetic() {
1077                let before_ok = i == 0 || !bytes[i - 1].is_ascii_alphanumeric();
1078                let after_ok = i + op_bytes.len() >= bytes.len()
1079                    || !bytes[i + op_bytes.len()].is_ascii_alphanumeric();
1080                if before_ok && after_ok {
1081                    return Some(i);
1082                }
1083            } else {
1084                return Some(i);
1085            }
1086        }
1087
1088        i += 1;
1089    }
1090
1091    None
1092}
1093
1094/// Check if string contains arithmetic operators
1095fn contains_arithmetic(s: &str) -> bool {
1096    s.contains('+') || s.contains('-') || s.contains('*') || s.contains('/') || s.contains('%')
1097}
1098
1099/// Parse test condition
1100fn parse_test_condition(clause: &str) -> Result<ConditionGroup> {
1101    let trimmed = clause.trim();
1102    let inner = &trimmed[5..trimmed.len() - 1]; // Remove "test(" and ")"
1103
1104    // Check if it's a function call: test(funcName(args))
1105    if let Some(paren_pos) = inner.find('(') {
1106        if let Some(close_paren) = find_matching_paren(inner, paren_pos) {
1107            let func_name = inner[..paren_pos].trim().to_string();
1108            let args_str = &inner[paren_pos + 1..close_paren];
1109
1110            let args: Vec<String> = if args_str.trim().is_empty() {
1111                Vec::new()
1112            } else {
1113                args_str.split(',').map(|s| s.trim().to_string()).collect()
1114            };
1115
1116            let condition = Condition::with_test(func_name, args);
1117            return Ok(ConditionGroup::single(condition));
1118        }
1119    }
1120
1121    // Otherwise treat the whole thing as an expression
1122    let condition = Condition::with_test(inner.trim().to_string(), vec![]);
1123    Ok(ConditionGroup::single(condition))
1124}
1125
1126/// Parse accumulate condition
1127fn parse_accumulate_condition(clause: &str) -> Result<ConditionGroup> {
1128    let trimmed = clause.trim();
1129    let inner = &trimmed[11..trimmed.len() - 1]; // Remove "accumulate(" and ")"
1130
1131    // Split by comma at top level
1132    let parts = split_top_level_comma(inner)?;
1133
1134    if parts.len() != 2 {
1135        return Err(RuleEngineError::ParseError {
1136            message: format!("Expected 2 parts in accumulate, got {}", parts.len()),
1137        });
1138    }
1139
1140    let (source_pattern, extract_field, source_conditions) = parse_accumulate_pattern(&parts[0])?;
1141    let (function, function_arg) = parse_accumulate_function(&parts[1])?;
1142
1143    Ok(ConditionGroup::accumulate(
1144        "$result".to_string(),
1145        source_pattern,
1146        extract_field,
1147        source_conditions,
1148        function,
1149        function_arg,
1150    ))
1151}
1152
1153/// Split by comma at top level
1154fn split_top_level_comma(text: &str) -> Result<Vec<String>> {
1155    let mut parts = Vec::new();
1156    let mut current = String::new();
1157    let mut paren_depth = 0;
1158    let mut in_string = false;
1159
1160    for ch in text.chars() {
1161        match ch {
1162            '"' => {
1163                in_string = !in_string;
1164                current.push(ch);
1165            }
1166            '(' if !in_string => {
1167                paren_depth += 1;
1168                current.push(ch);
1169            }
1170            ')' if !in_string => {
1171                paren_depth -= 1;
1172                current.push(ch);
1173            }
1174            ',' if !in_string && paren_depth == 0 => {
1175                parts.push(current.trim().to_string());
1176                current.clear();
1177            }
1178            _ => {
1179                current.push(ch);
1180            }
1181        }
1182    }
1183
1184    if !current.trim().is_empty() {
1185        parts.push(current.trim().to_string());
1186    }
1187
1188    Ok(parts)
1189}
1190
1191/// Parse accumulate pattern
1192fn parse_accumulate_pattern(pattern: &str) -> Result<(String, String, Vec<String>)> {
1193    let pattern = pattern.trim();
1194
1195    let paren_pos = pattern
1196        .find('(')
1197        .ok_or_else(|| RuleEngineError::ParseError {
1198            message: format!("Missing '(' in accumulate pattern: {}", pattern),
1199        })?;
1200
1201    let source_pattern = pattern[..paren_pos].trim().to_string();
1202
1203    if !pattern.ends_with(')') {
1204        return Err(RuleEngineError::ParseError {
1205            message: format!("Missing ')' in accumulate pattern: {}", pattern),
1206        });
1207    }
1208
1209    let inner = &pattern[paren_pos + 1..pattern.len() - 1];
1210    let parts = split_top_level_comma(inner)?;
1211
1212    let mut extract_field = String::new();
1213    let mut source_conditions = Vec::new();
1214
1215    for part in parts {
1216        let part = part.trim();
1217
1218        if part.contains(':') && part.starts_with('$') {
1219            let colon_pos = part.find(':').unwrap();
1220            extract_field = part[colon_pos + 1..].trim().to_string();
1221        } else if part.contains("==")
1222            || part.contains("!=")
1223            || part.contains(">=")
1224            || part.contains("<=")
1225            || part.contains('>')
1226            || part.contains('<')
1227        {
1228            source_conditions.push(part.to_string());
1229        }
1230    }
1231
1232    Ok((source_pattern, extract_field, source_conditions))
1233}
1234
1235/// Parse accumulate function
1236fn parse_accumulate_function(func_str: &str) -> Result<(String, String)> {
1237    let func_str = func_str.trim();
1238
1239    let paren_pos = func_str
1240        .find('(')
1241        .ok_or_else(|| RuleEngineError::ParseError {
1242            message: format!("Missing '(' in accumulate function: {}", func_str),
1243        })?;
1244
1245    let function_name = func_str[..paren_pos].trim().to_string();
1246
1247    if !func_str.ends_with(')') {
1248        return Err(RuleEngineError::ParseError {
1249            message: format!("Missing ')' in accumulate function: {}", func_str),
1250        });
1251    }
1252
1253    let args = func_str[paren_pos + 1..func_str.len() - 1]
1254        .trim()
1255        .to_string();
1256
1257    Ok((function_name, args))
1258}
1259
1260// ============================================================================
1261// Value Parsing
1262// ============================================================================
1263
1264/// Parse a value string into a Value
1265fn parse_value(value_str: &str) -> Result<Value> {
1266    let trimmed = value_str.trim();
1267
1268    // String literal
1269    if (trimmed.starts_with('"') && trimmed.ends_with('"'))
1270        || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
1271    {
1272        let unquoted = &trimmed[1..trimmed.len() - 1];
1273        return Ok(Value::String(unquoted.to_string()));
1274    }
1275
1276    // Boolean
1277    if trimmed.eq_ignore_ascii_case("true") {
1278        return Ok(Value::Boolean(true));
1279    }
1280    if trimmed.eq_ignore_ascii_case("false") {
1281        return Ok(Value::Boolean(false));
1282    }
1283
1284    // Null
1285    if trimmed.eq_ignore_ascii_case("null") {
1286        return Ok(Value::Null);
1287    }
1288
1289    // Integer
1290    if let Ok(int_val) = trimmed.parse::<i64>() {
1291        return Ok(Value::Integer(int_val));
1292    }
1293
1294    // Float
1295    if let Ok(float_val) = trimmed.parse::<f64>() {
1296        return Ok(Value::Number(float_val));
1297    }
1298
1299    // Expression (contains arithmetic or field reference)
1300    if is_expression(trimmed) {
1301        return Ok(Value::Expression(trimmed.to_string()));
1302    }
1303
1304    // Field reference
1305    if trimmed.contains('.') {
1306        return Ok(Value::String(trimmed.to_string()));
1307    }
1308
1309    // Variable/identifier
1310    if is_identifier(trimmed) {
1311        return Ok(Value::Expression(trimmed.to_string()));
1312    }
1313
1314    // Default to string
1315    Ok(Value::String(trimmed.to_string()))
1316}
1317
1318/// Check if string is a valid identifier
1319fn is_identifier(s: &str) -> bool {
1320    if s.is_empty() {
1321        return false;
1322    }
1323
1324    let first = s.chars().next().unwrap();
1325    if !first.is_alphabetic() && first != '_' {
1326        return false;
1327    }
1328
1329    s.chars().all(|c| c.is_alphanumeric() || c == '_')
1330}
1331
1332/// Check if string is an expression
1333fn is_expression(s: &str) -> bool {
1334    let has_operator =
1335        s.contains('+') || s.contains('-') || s.contains('*') || s.contains('/') || s.contains('%');
1336    let has_field_ref = s.contains('.');
1337    let has_spaces = s.contains(' ');
1338
1339    has_operator && (has_field_ref || has_spaces)
1340}
1341
1342// ============================================================================
1343// Action Parsing
1344// ============================================================================
1345
1346/// Parse the then clause into actions
1347fn parse_then_clause(then_clause: &str) -> Result<Vec<ActionType>> {
1348    let statements: Vec<&str> = then_clause
1349        .split(';')
1350        .map(|s| s.trim())
1351        .filter(|s| !s.is_empty())
1352        .collect();
1353
1354    let mut actions = Vec::new();
1355
1356    for statement in statements {
1357        let action = parse_action_statement(statement)?;
1358        actions.push(action);
1359    }
1360
1361    Ok(actions)
1362}
1363
1364/// Parse a single action statement
1365fn parse_action_statement(statement: &str) -> Result<ActionType> {
1366    let trimmed = statement.trim();
1367
1368    // Method call: $Object.method(args)
1369    if trimmed.starts_with('$') && trimmed.contains('.') {
1370        if let Some(action) = try_parse_method_call(trimmed)? {
1371            return Ok(action);
1372        }
1373    }
1374
1375    // Compound assignment: field += value
1376    if let Some(pos) = trimmed.find("+=") {
1377        let field = trimmed[..pos].trim().to_string();
1378        let value_str = trimmed[pos + 2..].trim();
1379        let value = parse_value(value_str)?;
1380        return Ok(ActionType::Append { field, value });
1381    }
1382
1383    // Assignment: field = value
1384    if let Some(eq_pos) = find_assignment_operator(trimmed) {
1385        let field = trimmed[..eq_pos].trim().to_string();
1386        let value_str = trimmed[eq_pos + 1..].trim();
1387        let value = parse_value(value_str)?;
1388        return Ok(ActionType::Set { field, value });
1389    }
1390
1391    // Function call: funcName(args)
1392    if let Some(paren_pos) = trimmed.find('(') {
1393        if trimmed.ends_with(')') {
1394            let func_name = trimmed[..paren_pos].trim();
1395            let args_str = &trimmed[paren_pos + 1..trimmed.len() - 1];
1396
1397            return parse_function_action(func_name, args_str);
1398        }
1399    }
1400
1401    // Unknown statement
1402    Ok(ActionType::Custom {
1403        action_type: "statement".to_string(),
1404        params: {
1405            let mut params = HashMap::new();
1406            params.insert("statement".to_string(), Value::String(trimmed.to_string()));
1407            params
1408        },
1409    })
1410}
1411
1412/// Find assignment operator (=) but not == or !=
1413fn find_assignment_operator(text: &str) -> Option<usize> {
1414    let bytes = text.as_bytes();
1415    let mut in_string = false;
1416    let mut i = 0;
1417
1418    while i < bytes.len() {
1419        if bytes[i] == b'"' {
1420            in_string = !in_string;
1421            i += 1;
1422            continue;
1423        }
1424
1425        if !in_string && bytes[i] == b'=' {
1426            // Check it's not == or !=
1427            let is_double = i + 1 < bytes.len() && bytes[i + 1] == b'=';
1428            let is_not_eq = i > 0 && bytes[i - 1] == b'!';
1429            let is_compound = i > 0
1430                && (bytes[i - 1] == b'+'
1431                    || bytes[i - 1] == b'-'
1432                    || bytes[i - 1] == b'*'
1433                    || bytes[i - 1] == b'/'
1434                    || bytes[i - 1] == b'%');
1435
1436            if !is_double && !is_not_eq && !is_compound {
1437                return Some(i);
1438            }
1439        }
1440
1441        i += 1;
1442    }
1443
1444    None
1445}
1446
1447/// Try to parse method call
1448fn try_parse_method_call(text: &str) -> Result<Option<ActionType>> {
1449    // Pattern: $Object.method(args)
1450    let dot_pos = match text.find('.') {
1451        Some(pos) => pos,
1452        None => return Ok(None),
1453    };
1454    let object = text[1..dot_pos].to_string(); // Skip $
1455
1456    let rest = &text[dot_pos + 1..];
1457    let paren_pos = match rest.find('(') {
1458        Some(pos) => pos,
1459        None => return Ok(None),
1460    };
1461    let method = rest[..paren_pos].to_string();
1462
1463    if !rest.ends_with(')') {
1464        return Ok(None);
1465    }
1466
1467    let args_str = &rest[paren_pos + 1..rest.len() - 1];
1468    let args = parse_method_args(args_str)?;
1469
1470    Ok(Some(ActionType::MethodCall {
1471        object,
1472        method,
1473        args,
1474    }))
1475}
1476
1477/// Parse method arguments
1478fn parse_method_args(args_str: &str) -> Result<Vec<Value>> {
1479    if args_str.trim().is_empty() {
1480        return Ok(Vec::new());
1481    }
1482
1483    let parts = split_top_level_comma(args_str)?;
1484    let mut args = Vec::new();
1485
1486    for part in parts {
1487        let trimmed = part.trim();
1488
1489        // Handle arithmetic expressions
1490        if contains_arithmetic(trimmed) {
1491            args.push(Value::String(trimmed.to_string()));
1492        } else {
1493            args.push(parse_value(trimmed)?);
1494        }
1495    }
1496
1497    Ok(args)
1498}
1499
1500/// Parse function-style action
1501fn parse_function_action(func_name: &str, args_str: &str) -> Result<ActionType> {
1502    match func_name.to_lowercase().as_str() {
1503        "retract" => {
1504            let object = args_str.trim().trim_start_matches('$').to_string();
1505            Ok(ActionType::Retract { object })
1506        }
1507        "log" => {
1508            let message = if args_str.is_empty() {
1509                "Log message".to_string()
1510            } else {
1511                let value = parse_value(args_str.trim())?;
1512                value.to_string()
1513            };
1514            Ok(ActionType::Log { message })
1515        }
1516        "activateagendagroup" | "activate_agenda_group" => {
1517            if args_str.is_empty() {
1518                return Err(RuleEngineError::ParseError {
1519                    message: "ActivateAgendaGroup requires agenda group name".to_string(),
1520                });
1521            }
1522            let value = parse_value(args_str.trim())?;
1523            let group = match value {
1524                Value::String(s) => s,
1525                _ => value.to_string(),
1526            };
1527            Ok(ActionType::ActivateAgendaGroup { group })
1528        }
1529        "schedulerule" | "schedule_rule" => {
1530            let parts = split_top_level_comma(args_str)?;
1531            if parts.len() != 2 {
1532                return Err(RuleEngineError::ParseError {
1533                    message: "ScheduleRule requires delay_ms and rule_name".to_string(),
1534                });
1535            }
1536
1537            let delay_ms = parse_value(parts[0].trim())?;
1538            let rule_name = parse_value(parts[1].trim())?;
1539
1540            let delay_ms = match delay_ms {
1541                Value::Integer(i) => i as u64,
1542                Value::Number(f) => f as u64,
1543                _ => {
1544                    return Err(RuleEngineError::ParseError {
1545                        message: "ScheduleRule delay_ms must be a number".to_string(),
1546                    })
1547                }
1548            };
1549
1550            let rule_name = match rule_name {
1551                Value::String(s) => s,
1552                _ => rule_name.to_string(),
1553            };
1554
1555            Ok(ActionType::ScheduleRule {
1556                delay_ms,
1557                rule_name,
1558            })
1559        }
1560        "completeworkflow" | "complete_workflow" => {
1561            if args_str.is_empty() {
1562                return Err(RuleEngineError::ParseError {
1563                    message: "CompleteWorkflow requires workflow_id".to_string(),
1564                });
1565            }
1566            let value = parse_value(args_str.trim())?;
1567            let workflow_name = match value {
1568                Value::String(s) => s,
1569                _ => value.to_string(),
1570            };
1571            Ok(ActionType::CompleteWorkflow { workflow_name })
1572        }
1573        "setworkflowdata" | "set_workflow_data" => {
1574            let data_str = args_str.trim();
1575            if let Some(eq_pos) = data_str.find('=') {
1576                let key = data_str[..eq_pos].trim().trim_matches('"').to_string();
1577                let value_str = data_str[eq_pos + 1..].trim();
1578                let value = parse_value(value_str)?;
1579                Ok(ActionType::SetWorkflowData { key, value })
1580            } else {
1581                Err(RuleEngineError::ParseError {
1582                    message: "SetWorkflowData data must be in key=value format".to_string(),
1583                })
1584            }
1585        }
1586        _ => {
1587            // Custom function
1588            let params = if args_str.is_empty() {
1589                HashMap::new()
1590            } else {
1591                let parts = split_top_level_comma(args_str)?;
1592                let mut params = HashMap::new();
1593                for (i, part) in parts.iter().enumerate() {
1594                    let value = parse_value(part.trim())?;
1595                    params.insert(i.to_string(), value);
1596                }
1597                params
1598            };
1599
1600            Ok(ActionType::Custom {
1601                action_type: func_name.to_string(),
1602                params,
1603            })
1604        }
1605    }
1606}
1607
1608// ============================================================================
1609// Tests
1610// ============================================================================
1611
1612#[cfg(test)]
1613mod tests {
1614    use super::*;
1615
1616    #[test]
1617    fn test_parse_simple_rule() {
1618        let grl = r#"
1619        rule "CheckAge" salience 10 {
1620            when
1621                User.Age >= 18
1622            then
1623                log("User is adult");
1624        }
1625        "#;
1626
1627        let rules = GRLParserNoRegex::parse_rules(grl).unwrap();
1628        assert_eq!(rules.len(), 1);
1629        let rule = &rules[0];
1630        assert_eq!(rule.name, "CheckAge");
1631        assert_eq!(rule.salience, 10);
1632        assert_eq!(rule.actions.len(), 1);
1633    }
1634
1635    #[test]
1636    fn test_parse_complex_condition() {
1637        let grl = r#"
1638        rule "ComplexRule" {
1639            when
1640                User.Age >= 18 && User.Country == "US"
1641            then
1642                User.Qualified = true;
1643        }
1644        "#;
1645
1646        let rules = GRLParserNoRegex::parse_rules(grl).unwrap();
1647        assert_eq!(rules.len(), 1);
1648        assert_eq!(rules[0].name, "ComplexRule");
1649    }
1650
1651    #[test]
1652    fn test_parse_no_loop_attribute() {
1653        let grl = r#"
1654        rule "NoLoopRule" no-loop salience 15 {
1655            when
1656                User.Score < 100
1657            then
1658                User.Score = 50;
1659        }
1660        "#;
1661
1662        let rules = GRLParserNoRegex::parse_rules(grl).unwrap();
1663        assert!(rules[0].no_loop);
1664        assert_eq!(rules[0].salience, 15);
1665    }
1666
1667    #[test]
1668    fn test_parse_or_condition() {
1669        let grl = r#"
1670        rule "OrRule" {
1671            when
1672                User.Status == "active" || User.Status == "premium"
1673            then
1674                User.Valid = true;
1675        }
1676        "#;
1677
1678        let rules = GRLParserNoRegex::parse_rules(grl).unwrap();
1679        assert_eq!(rules.len(), 1);
1680    }
1681
1682    #[test]
1683    fn test_parse_exists_pattern() {
1684        let grl = r#"
1685        rule "ExistsRule" {
1686            when
1687                exists(Customer.tier == "VIP")
1688            then
1689                System.premium = true;
1690        }
1691        "#;
1692
1693        let rules = GRLParserNoRegex::parse_rules(grl).unwrap();
1694        assert_eq!(rules.len(), 1);
1695
1696        match &rules[0].conditions {
1697            ConditionGroup::Exists(_) => {}
1698            _ => panic!("Expected EXISTS condition"),
1699        }
1700    }
1701
1702    #[test]
1703    fn test_parse_multiple_rules() {
1704        let grl = r#"
1705        rule "Rule1" { when A > 1 then B = 2; }
1706        rule "Rule2" { when C < 3 then D = 4; }
1707        rule "Rule3" { when E == 5 then F = 6; }
1708        "#;
1709
1710        let rules = GRLParserNoRegex::parse_rules(grl).unwrap();
1711        assert_eq!(rules.len(), 3);
1712        assert_eq!(rules[0].name, "Rule1");
1713        assert_eq!(rules[1].name, "Rule2");
1714        assert_eq!(rules[2].name, "Rule3");
1715    }
1716
1717    #[test]
1718    fn test_parse_assignment_action() {
1719        let grl = r#"
1720        rule "SetRule" {
1721            when
1722                X > 0
1723            then
1724                Y = 100;
1725                Z = "hello";
1726        }
1727        "#;
1728
1729        let rules = GRLParserNoRegex::parse_rules(grl).unwrap();
1730        assert_eq!(rules[0].actions.len(), 2);
1731
1732        match &rules[0].actions[0] {
1733            ActionType::Set { field, value } => {
1734                assert_eq!(field, "Y");
1735                assert_eq!(*value, Value::Integer(100));
1736            }
1737            _ => panic!("Expected Set action"),
1738        }
1739    }
1740
1741    #[test]
1742    fn test_parse_append_action() {
1743        let grl = r#"
1744        rule "AppendRule" {
1745            when
1746                X > 0
1747            then
1748                Items += "new_item";
1749        }
1750        "#;
1751
1752        let rules = GRLParserNoRegex::parse_rules(grl).unwrap();
1753
1754        match &rules[0].actions[0] {
1755            ActionType::Append { field, value } => {
1756                assert_eq!(field, "Items");
1757                assert_eq!(*value, Value::String("new_item".to_string()));
1758            }
1759            _ => panic!("Expected Append action"),
1760        }
1761    }
1762}