1use 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
14pub struct GRLParserNoRegex;
19
20#[derive(Debug, Clone)]
22pub struct ParsedGRL {
23 pub rules: Vec<Rule>,
25 pub module_manager: ModuleManager,
27 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#[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 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 pub fn parse_rule(grl_text: &str) -> Result<Rule> {
75 Self::parse_single_rule(grl_text)
76 }
77
78 pub fn parse_with_modules(grl_text: &str) -> Result<ParsedGRL> {
80 let mut result = ParsedGRL::new();
81
82 let (module_texts, rules_text) = split_modules_and_rules(grl_text);
84
85 for module_text in module_texts {
87 Self::parse_and_register_module(&module_text, &mut result.module_manager)?;
88 }
89
90 let rules = Self::parse_rules(&rules_text)?;
92
93 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 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 let (rule_name, after_name) = extract_rule_name(after_rule)?;
123
124 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 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 let attributes = parse_rule_attributes(attributes_section)?;
147
148 let (when_clause, then_clause) = parse_when_then(rule_body)?;
150
151 let conditions = parse_when_clause(&when_clause)?;
153 let actions = parse_then_clause(&then_clause)?;
154
155 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 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 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
245fn 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 if abs_pos > 0 && bytes[abs_pos - 1].is_ascii_alphanumeric() {
261 i = abs_pos + 1;
262 continue;
263 }
264
265 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
288fn is_inside_comment(text: &str, pos: usize) -> bool {
290 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 let line_prefix = &text[line_start..pos];
299 line_prefix.contains("//")
300}
301
302fn 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
340fn clean_text(text: &str) -> String {
342 text.lines()
343 .map(|line| {
344 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
356fn 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 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
380fn extract_rule_name(text: &str) -> Result<(String, &str)> {
382 let trimmed = text.trim_start();
383
384 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 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
413fn parse_rule_attributes(attrs: &str) -> Result<RuleAttributes> {
415 let mut result = RuleAttributes::default();
416
417 let cleaned = remove_quoted_strings(attrs);
419
420 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 result.no_loop = has_keyword(&cleaned, "no-loop");
434 result.lock_on_active = has_keyword(&cleaned, "lock-on-active");
435
436 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
451fn 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
474fn has_keyword(text: &str, keyword: &str) -> bool {
476 find_keyword(text, keyword).is_some()
477}
478
479fn 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
492fn 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
514fn 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 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
533fn 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
573fn 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
628fn 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
641fn 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
666fn parse_when_clause(when_clause: &str) -> Result<ConditionGroup> {
672 let trimmed = when_clause.trim();
673
674 let clause = strip_outer_parens(trimmed);
676
677 if let Some(parts) = split_logical_operator(clause, "||") {
679 return parse_or_parts(parts);
680 }
681
682 if let Some(parts) = split_logical_operator(clause, "&&") {
684 return parse_and_parts(parts);
685 }
686
687 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 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 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 if clause.trim_start().starts_with("accumulate(") && clause.trim_end().ends_with(')') {
710 return parse_accumulate_condition(clause);
711 }
712
713 if clause.trim_start().starts_with("test(") && clause.trim_end().ends_with(')') {
715 return parse_test_condition(clause);
716 }
717
718 parse_single_condition(clause)
720}
721
722fn 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
734fn 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
752fn 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 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
803fn 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
825fn 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
847fn parse_single_condition(clause: &str) -> Result<ConditionGroup> {
849 let trimmed = strip_outer_parens(clause.trim());
850
851 if let Some(cond) = try_parse_multifield(trimmed)? {
853 return Ok(ConditionGroup::single(cond));
854 }
855
856 if let Some(cond) = try_parse_function_call(trimmed)? {
858 return Ok(ConditionGroup::single(cond));
859 }
860
861 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 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
881fn try_parse_multifield(clause: &str) -> Result<Option<Condition>> {
883 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 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 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 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 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 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
952fn try_parse_function_call(clause: &str) -> Result<Option<Condition>> {
954 if let Some(paren_start) = clause.find('(') {
956 if paren_start > 0 {
957 let func_name = clause[..paren_start].trim();
958
959 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 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 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
1003fn 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
1028fn 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
1045fn 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 split_condition(text)
1058}
1059
1060fn 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 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
1094fn contains_arithmetic(s: &str) -> bool {
1096 s.contains('+') || s.contains('-') || s.contains('*') || s.contains('/') || s.contains('%')
1097}
1098
1099fn parse_test_condition(clause: &str) -> Result<ConditionGroup> {
1101 let trimmed = clause.trim();
1102 let inner = &trimmed[5..trimmed.len() - 1]; 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 let condition = Condition::with_test(inner.trim().to_string(), vec![]);
1123 Ok(ConditionGroup::single(condition))
1124}
1125
1126fn parse_accumulate_condition(clause: &str) -> Result<ConditionGroup> {
1128 let trimmed = clause.trim();
1129 let inner = &trimmed[11..trimmed.len() - 1]; 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
1153fn 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
1191fn 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
1235fn 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
1260fn parse_value(value_str: &str) -> Result<Value> {
1266 let trimmed = value_str.trim();
1267
1268 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 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 if trimmed.eq_ignore_ascii_case("null") {
1286 return Ok(Value::Null);
1287 }
1288
1289 if let Ok(int_val) = trimmed.parse::<i64>() {
1291 return Ok(Value::Integer(int_val));
1292 }
1293
1294 if let Ok(float_val) = trimmed.parse::<f64>() {
1296 return Ok(Value::Number(float_val));
1297 }
1298
1299 if is_expression(trimmed) {
1301 return Ok(Value::Expression(trimmed.to_string()));
1302 }
1303
1304 if trimmed.contains('.') {
1306 return Ok(Value::String(trimmed.to_string()));
1307 }
1308
1309 if is_identifier(trimmed) {
1311 return Ok(Value::Expression(trimmed.to_string()));
1312 }
1313
1314 Ok(Value::String(trimmed.to_string()))
1316}
1317
1318fn 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
1332fn 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
1342fn 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
1364fn parse_action_statement(statement: &str) -> Result<ActionType> {
1366 let trimmed = statement.trim();
1367
1368 if trimmed.starts_with('$') && trimmed.contains('.') {
1370 if let Some(action) = try_parse_method_call(trimmed)? {
1371 return Ok(action);
1372 }
1373 }
1374
1375 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 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 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 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
1412fn 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 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
1447fn try_parse_method_call(text: &str) -> Result<Option<ActionType>> {
1449 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(); 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
1477fn 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 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
1500fn 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 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#[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}