1use crate::engine::rule::{Condition, ConditionGroup, Rule};
2use crate::errors::{Result, RuleEngineError};
3use crate::types::{ActionType, Operator, Value};
4use chrono::{DateTime, Utc};
5use regex::Regex;
6use std::collections::HashMap;
7
8pub struct GRLParser;
11
12#[derive(Debug, Default)]
14struct RuleAttributes {
15 pub no_loop: bool,
16 pub lock_on_active: bool,
17 pub agenda_group: Option<String>,
18 pub activation_group: Option<String>,
19 pub date_effective: Option<DateTime<Utc>>,
20 pub date_expires: Option<DateTime<Utc>>,
21}
22
23impl GRLParser {
24 pub fn parse_rule(grl_text: &str) -> Result<Rule> {
37 let mut parser = GRLParser;
38 parser.parse_single_rule(grl_text)
39 }
40
41 pub fn parse_rules(grl_text: &str) -> Result<Vec<Rule>> {
43 let mut parser = GRLParser;
44 parser.parse_multiple_rules(grl_text)
45 }
46
47 fn parse_single_rule(&mut self, grl_text: &str) -> Result<Rule> {
48 let cleaned = self.clean_text(grl_text);
49
50 let rule_regex = Regex::new(r#"rule\s+(?:"([^"]+)"|([a-zA-Z_]\w*))\s*([^{]*)\{(.+)\}"#)
52 .map_err(|e| RuleEngineError::ParseError {
53 message: format!("Invalid rule regex: {}", e),
54 })?;
55
56 let captures =
57 rule_regex
58 .captures(&cleaned)
59 .ok_or_else(|| RuleEngineError::ParseError {
60 message: format!("Invalid GRL rule format. Input: {}", cleaned),
61 })?;
62
63 let rule_name = if let Some(quoted_name) = captures.get(1) {
65 quoted_name.as_str().to_string()
66 } else if let Some(unquoted_name) = captures.get(2) {
67 unquoted_name.as_str().to_string()
68 } else {
69 return Err(RuleEngineError::ParseError {
70 message: "Could not extract rule name".to_string(),
71 });
72 };
73
74 let attributes_section = captures.get(3).map(|m| m.as_str()).unwrap_or("");
76
77 let rule_body = captures.get(4).unwrap().as_str();
79
80 let salience = self.extract_salience(attributes_section)?;
82
83 let when_then_regex =
85 Regex::new(r"when\s+(.+?)\s+then\s+(.+)").map_err(|e| RuleEngineError::ParseError {
86 message: format!("Invalid when-then regex: {}", e),
87 })?;
88
89 let when_then_captures =
90 when_then_regex
91 .captures(rule_body)
92 .ok_or_else(|| RuleEngineError::ParseError {
93 message: "Missing when or then clause".to_string(),
94 })?;
95
96 let when_clause = when_then_captures.get(1).unwrap().as_str().trim();
97 let then_clause = when_then_captures.get(2).unwrap().as_str().trim();
98
99 let conditions = self.parse_when_clause(when_clause)?;
101 let actions = self.parse_then_clause(then_clause)?;
102
103 let attributes = self.parse_rule_attributes(attributes_section)?;
105
106 let mut rule = Rule::new(rule_name, conditions, actions);
108 rule = rule.with_priority(salience);
109
110 if attributes.no_loop {
112 rule = rule.with_no_loop(true);
113 }
114 if attributes.lock_on_active {
115 rule = rule.with_lock_on_active(true);
116 }
117 if let Some(agenda_group) = attributes.agenda_group {
118 rule = rule.with_agenda_group(agenda_group);
119 }
120 if let Some(activation_group) = attributes.activation_group {
121 rule = rule.with_activation_group(activation_group);
122 }
123 if let Some(date_effective) = attributes.date_effective {
124 rule = rule.with_date_effective(date_effective);
125 }
126 if let Some(date_expires) = attributes.date_expires {
127 rule = rule.with_date_expires(date_expires);
128 }
129
130 Ok(rule)
131 }
132
133 fn parse_multiple_rules(&mut self, grl_text: &str) -> Result<Vec<Rule>> {
134 let rule_regex =
137 Regex::new(r#"(?s)rule\s+(?:"[^"]+"|[a-zA-Z_]\w*).*?\}"#).map_err(|e| {
138 RuleEngineError::ParseError {
139 message: format!("Rule splitting regex error: {}", e),
140 }
141 })?;
142
143 let mut rules = Vec::new();
144
145 for rule_match in rule_regex.find_iter(grl_text) {
146 let rule_text = rule_match.as_str();
147 let rule = self.parse_single_rule(rule_text)?;
148 rules.push(rule);
149 }
150
151 Ok(rules)
152 }
153
154 fn parse_rule_attributes(&self, rule_header: &str) -> Result<RuleAttributes> {
156 let mut attributes = RuleAttributes::default();
157
158 if rule_header.contains("no-loop") {
160 attributes.no_loop = true;
161 }
162 if rule_header.contains("lock-on-active") {
163 attributes.lock_on_active = true;
164 }
165
166 if let Some(agenda_group) = self.extract_quoted_attribute(rule_header, "agenda-group")? {
168 attributes.agenda_group = Some(agenda_group);
169 }
170
171 if let Some(activation_group) =
173 self.extract_quoted_attribute(rule_header, "activation-group")?
174 {
175 attributes.activation_group = Some(activation_group);
176 }
177
178 if let Some(date_str) = self.extract_quoted_attribute(rule_header, "date-effective")? {
180 attributes.date_effective = Some(self.parse_date_string(&date_str)?);
181 }
182
183 if let Some(date_str) = self.extract_quoted_attribute(rule_header, "date-expires")? {
185 attributes.date_expires = Some(self.parse_date_string(&date_str)?);
186 }
187
188 Ok(attributes)
189 }
190
191 fn extract_quoted_attribute(&self, header: &str, attribute: &str) -> Result<Option<String>> {
193 let pattern = format!(r#"{}\s+"([^"]+)""#, attribute);
194 let regex = Regex::new(&pattern).map_err(|e| RuleEngineError::ParseError {
195 message: format!("Invalid attribute regex for {}: {}", attribute, e),
196 })?;
197
198 if let Some(captures) = regex.captures(header) {
199 if let Some(value) = captures.get(1) {
200 return Ok(Some(value.as_str().to_string()));
201 }
202 }
203
204 Ok(None)
205 }
206
207 fn parse_date_string(&self, date_str: &str) -> Result<DateTime<Utc>> {
209 if let Ok(date) = DateTime::parse_from_rfc3339(date_str) {
211 return Ok(date.with_timezone(&Utc));
212 }
213
214 let formats = ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%d-%b-%Y", "%d-%m-%Y"];
216
217 for format in &formats {
218 if let Ok(naive_date) = chrono::NaiveDateTime::parse_from_str(date_str, format) {
219 return Ok(naive_date.and_utc());
220 }
221 if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(date_str, format) {
222 return Ok(naive_date.and_hms_opt(0, 0, 0).unwrap().and_utc());
223 }
224 }
225
226 Err(RuleEngineError::ParseError {
227 message: format!("Unable to parse date: {}", date_str),
228 })
229 }
230
231 fn extract_salience(&self, attributes_section: &str) -> Result<i32> {
233 let salience_regex =
234 Regex::new(r"salience\s+(\d+)").map_err(|e| RuleEngineError::ParseError {
235 message: format!("Invalid salience regex: {}", e),
236 })?;
237
238 if let Some(captures) = salience_regex.captures(attributes_section) {
239 if let Some(salience_match) = captures.get(1) {
240 return salience_match.as_str().parse::<i32>().map_err(|e| {
241 RuleEngineError::ParseError {
242 message: format!("Invalid salience value: {}", e),
243 }
244 });
245 }
246 }
247
248 Ok(0) }
250
251 fn clean_text(&self, text: &str) -> String {
252 text.lines()
253 .map(|line| line.trim())
254 .filter(|line| !line.is_empty() && !line.starts_with("//"))
255 .collect::<Vec<_>>()
256 .join(" ")
257 }
258
259 fn parse_when_clause(&self, when_clause: &str) -> Result<ConditionGroup> {
260 let trimmed = when_clause.trim();
262
263 let clause = if trimmed.starts_with('(') && trimmed.ends_with(')') {
265 let inner = &trimmed[1..trimmed.len() - 1];
267 if self.is_balanced_parentheses(inner) {
268 inner
269 } else {
270 trimmed
271 }
272 } else {
273 trimmed
274 };
275
276 if let Some(parts) = self.split_logical_operator(clause, "||") {
278 return self.parse_or_parts(parts);
279 }
280
281 if let Some(parts) = self.split_logical_operator(clause, "&&") {
283 return self.parse_and_parts(parts);
284 }
285
286 if clause.trim_start().starts_with("!") {
288 return self.parse_not_condition(clause);
289 }
290
291 self.parse_single_condition(clause)
293 }
294
295 fn is_balanced_parentheses(&self, text: &str) -> bool {
296 let mut count = 0;
297 for ch in text.chars() {
298 match ch {
299 '(' => count += 1,
300 ')' => {
301 count -= 1;
302 if count < 0 {
303 return false;
304 }
305 }
306 _ => {}
307 }
308 }
309 count == 0
310 }
311
312 fn split_logical_operator(&self, clause: &str, operator: &str) -> Option<Vec<String>> {
313 let mut parts = Vec::new();
314 let mut current_part = String::new();
315 let mut paren_count = 0;
316 let mut chars = clause.chars().peekable();
317
318 while let Some(ch) = chars.next() {
319 match ch {
320 '(' => {
321 paren_count += 1;
322 current_part.push(ch);
323 }
324 ')' => {
325 paren_count -= 1;
326 current_part.push(ch);
327 }
328 '&' if operator == "&&" && paren_count == 0 => {
329 if chars.peek() == Some(&'&') {
330 chars.next(); parts.push(current_part.trim().to_string());
332 current_part.clear();
333 } else {
334 current_part.push(ch);
335 }
336 }
337 '|' if operator == "||" && paren_count == 0 => {
338 if chars.peek() == Some(&'|') {
339 chars.next(); parts.push(current_part.trim().to_string());
341 current_part.clear();
342 } else {
343 current_part.push(ch);
344 }
345 }
346 _ => {
347 current_part.push(ch);
348 }
349 }
350 }
351
352 if !current_part.trim().is_empty() {
353 parts.push(current_part.trim().to_string());
354 }
355
356 if parts.len() > 1 {
357 Some(parts)
358 } else {
359 None
360 }
361 }
362
363 fn parse_or_parts(&self, parts: Vec<String>) -> Result<ConditionGroup> {
364 let mut conditions = Vec::new();
365 for part in parts {
366 let condition = self.parse_when_clause(&part)?;
367 conditions.push(condition);
368 }
369
370 if conditions.is_empty() {
371 return Err(RuleEngineError::ParseError {
372 message: "No conditions found in OR".to_string(),
373 });
374 }
375
376 let mut iter = conditions.into_iter();
377 let mut result = iter.next().unwrap();
378 for condition in iter {
379 result = ConditionGroup::or(result, condition);
380 }
381
382 Ok(result)
383 }
384
385 fn parse_and_parts(&self, parts: Vec<String>) -> Result<ConditionGroup> {
386 let mut conditions = Vec::new();
387 for part in parts {
388 let condition = self.parse_when_clause(&part)?;
389 conditions.push(condition);
390 }
391
392 if conditions.is_empty() {
393 return Err(RuleEngineError::ParseError {
394 message: "No conditions found in AND".to_string(),
395 });
396 }
397
398 let mut iter = conditions.into_iter();
399 let mut result = iter.next().unwrap();
400 for condition in iter {
401 result = ConditionGroup::and(result, condition);
402 }
403
404 Ok(result)
405 }
406
407 fn parse_not_condition(&self, clause: &str) -> Result<ConditionGroup> {
408 let inner_clause = clause.strip_prefix("!").unwrap().trim();
409 let inner_condition = self.parse_when_clause(inner_clause)?;
410 Ok(ConditionGroup::not(inner_condition))
411 }
412
413 fn parse_single_condition(&self, clause: &str) -> Result<ConditionGroup> {
414 let trimmed_clause = clause.trim();
416 let clause_to_parse = if trimmed_clause.starts_with('(') && trimmed_clause.ends_with(')') {
417 trimmed_clause[1..trimmed_clause.len() - 1].trim()
418 } else {
419 trimmed_clause
420 };
421
422 let typed_object_regex =
424 Regex::new(r#"\$(\w+)\s*:\s*(\w+)\s*\(\s*(.+?)\s*\)"#).map_err(|e| {
425 RuleEngineError::ParseError {
426 message: format!("Typed object regex error: {}", e),
427 }
428 })?;
429
430 if let Some(captures) = typed_object_regex.captures(clause_to_parse) {
431 let _object_name = captures.get(1).unwrap().as_str();
432 let _object_type = captures.get(2).unwrap().as_str();
433 let conditions_str = captures.get(3).unwrap().as_str();
434
435 return self.parse_conditions_within_object(conditions_str);
437 }
438
439 let condition_regex = Regex::new(
442 r#"([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s*(>=|<=|==|!=|>|<|contains|matches)\s*(.+)"#,
443 )
444 .map_err(|e| RuleEngineError::ParseError {
445 message: format!("Condition regex error: {}", e),
446 })?;
447
448 let captures = condition_regex.captures(clause_to_parse).ok_or_else(|| {
449 RuleEngineError::ParseError {
450 message: format!("Invalid condition format: {}", clause_to_parse),
451 }
452 })?;
453
454 let field = captures.get(1).unwrap().as_str().to_string();
455 let operator_str = captures.get(2).unwrap().as_str();
456 let value_str = captures.get(3).unwrap().as_str().trim();
457
458 let operator =
459 Operator::from_str(operator_str).ok_or_else(|| RuleEngineError::InvalidOperator {
460 operator: operator_str.to_string(),
461 })?;
462
463 let value = self.parse_value(value_str)?;
464
465 let condition = Condition::new(field, operator, value);
466 Ok(ConditionGroup::single(condition))
467 }
468
469 fn parse_conditions_within_object(&self, conditions_str: &str) -> Result<ConditionGroup> {
470 let parts: Vec<&str> = conditions_str.split("&&").collect();
472
473 let mut conditions = Vec::new();
474 for part in parts {
475 let trimmed = part.trim();
476 let condition = self.parse_simple_condition(trimmed)?;
477 conditions.push(condition);
478 }
479
480 if conditions.is_empty() {
482 return Err(RuleEngineError::ParseError {
483 message: "No conditions found".to_string(),
484 });
485 }
486
487 let mut iter = conditions.into_iter();
488 let mut result = iter.next().unwrap();
489 for condition in iter {
490 result = ConditionGroup::and(result, condition);
491 }
492
493 Ok(result)
494 }
495
496 fn parse_simple_condition(&self, clause: &str) -> Result<ConditionGroup> {
497 let condition_regex = Regex::new(r#"(\w+)\s*(>=|<=|==|!=|>|<)\s*(.+)"#).map_err(|e| {
499 RuleEngineError::ParseError {
500 message: format!("Simple condition regex error: {}", e),
501 }
502 })?;
503
504 let captures =
505 condition_regex
506 .captures(clause)
507 .ok_or_else(|| RuleEngineError::ParseError {
508 message: format!("Invalid simple condition format: {}", clause),
509 })?;
510
511 let field = captures.get(1).unwrap().as_str().to_string();
512 let operator_str = captures.get(2).unwrap().as_str();
513 let value_str = captures.get(3).unwrap().as_str().trim();
514
515 let operator =
516 Operator::from_str(operator_str).ok_or_else(|| RuleEngineError::InvalidOperator {
517 operator: operator_str.to_string(),
518 })?;
519
520 let value = self.parse_value(value_str)?;
521
522 let condition = Condition::new(field, operator, value);
523 Ok(ConditionGroup::single(condition))
524 }
525
526 fn parse_value(&self, value_str: &str) -> Result<Value> {
527 let trimmed = value_str.trim();
528
529 if (trimmed.starts_with('"') && trimmed.ends_with('"'))
531 || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
532 {
533 let unquoted = &trimmed[1..trimmed.len() - 1];
534 return Ok(Value::String(unquoted.to_string()));
535 }
536
537 if trimmed.eq_ignore_ascii_case("true") {
539 return Ok(Value::Boolean(true));
540 }
541 if trimmed.eq_ignore_ascii_case("false") {
542 return Ok(Value::Boolean(false));
543 }
544
545 if trimmed.eq_ignore_ascii_case("null") {
547 return Ok(Value::Null);
548 }
549
550 if let Ok(int_val) = trimmed.parse::<i64>() {
552 return Ok(Value::Integer(int_val));
553 }
554
555 if let Ok(float_val) = trimmed.parse::<f64>() {
556 return Ok(Value::Number(float_val));
557 }
558
559 if trimmed.contains('.') {
561 return Ok(Value::String(trimmed.to_string()));
562 }
563
564 Ok(Value::String(trimmed.to_string()))
566 }
567
568 fn parse_then_clause(&self, then_clause: &str) -> Result<Vec<ActionType>> {
569 let statements: Vec<&str> = then_clause
570 .split(';')
571 .map(|s| s.trim())
572 .filter(|s| !s.is_empty())
573 .collect();
574
575 let mut actions = Vec::new();
576
577 for statement in statements {
578 let action = self.parse_action_statement(statement)?;
579 actions.push(action);
580 }
581
582 Ok(actions)
583 }
584
585 fn parse_action_statement(&self, statement: &str) -> Result<ActionType> {
586 let trimmed = statement.trim();
587
588 let method_regex = Regex::new(r#"\$(\w+)\.(\w+)\s*\(([^)]*)\)"#).map_err(|e| {
590 RuleEngineError::ParseError {
591 message: format!("Method regex error: {}", e),
592 }
593 })?;
594
595 if let Some(captures) = method_regex.captures(trimmed) {
596 let object = captures.get(1).unwrap().as_str().to_string();
597 let method = captures.get(2).unwrap().as_str().to_string();
598 let args_str = captures.get(3).unwrap().as_str();
599
600 let args = if args_str.trim().is_empty() {
601 Vec::new()
602 } else {
603 self.parse_method_args(args_str)?
604 };
605
606 return Ok(ActionType::MethodCall {
607 object,
608 method,
609 args,
610 });
611 }
612
613 if let Some(eq_pos) = trimmed.find('=') {
615 let field = trimmed[..eq_pos].trim().to_string();
616 let value_str = trimmed[eq_pos + 1..].trim();
617 let value = self.parse_value(value_str)?;
618
619 return Ok(ActionType::Set { field, value });
620 }
621
622 let func_regex =
624 Regex::new(r#"(\w+)\s*\(\s*(.+?)?\s*\)"#).map_err(|e| RuleEngineError::ParseError {
625 message: format!("Function regex error: {}", e),
626 })?;
627
628 if let Some(captures) = func_regex.captures(trimmed) {
629 let function_name = captures.get(1).unwrap().as_str();
630 let args_str = captures.get(2).map(|m| m.as_str()).unwrap_or("");
631
632 match function_name.to_lowercase().as_str() {
633 "update" => {
634 let object_name = if let Some(stripped) = args_str.strip_prefix('$') {
636 stripped.to_string()
637 } else {
638 args_str.to_string()
639 };
640 Ok(ActionType::Update {
641 object: object_name,
642 })
643 }
644 "set" => {
645 let args = if args_str.is_empty() {
647 Vec::new()
648 } else {
649 args_str
650 .split(',')
651 .map(|arg| self.parse_value(arg.trim()))
652 .collect::<Result<Vec<_>>>()?
653 };
654
655 if args.len() >= 2 {
656 let field = args[0].to_string();
657 let value = args[1].clone();
658 Ok(ActionType::Set { field, value })
659 } else if args.len() == 1 {
660 Ok(ActionType::Set {
662 field: args[0].to_string(),
663 value: Value::Boolean(true),
664 })
665 } else {
666 Ok(ActionType::Custom {
667 action_type: "set".to_string(),
668 params: {
669 let mut params = HashMap::new();
670 params.insert(
671 "args".to_string(),
672 Value::String(args_str.to_string()),
673 );
674 params
675 },
676 })
677 }
678 }
679 "add" => {
680 let value = if args_str.is_empty() {
682 Value::Integer(1) } else {
684 self.parse_value(args_str.trim())?
685 };
686 Ok(ActionType::Custom {
687 action_type: "add".to_string(),
688 params: {
689 let mut params = HashMap::new();
690 params.insert("value".to_string(), value);
691 params
692 },
693 })
694 }
695 "log" => {
696 let message = if args_str.is_empty() {
697 "Log message".to_string()
698 } else {
699 let value = self.parse_value(args_str.trim())?;
700 value.to_string()
701 };
702 Ok(ActionType::Log { message })
703 }
704 _ => {
705 let args = if args_str.is_empty() {
706 Vec::new()
707 } else {
708 args_str
709 .split(',')
710 .map(|arg| self.parse_value(arg.trim()))
711 .collect::<Result<Vec<_>>>()?
712 };
713 Ok(ActionType::Call {
714 function: function_name.to_string(),
715 args,
716 })
717 }
718 }
719 } else {
720 Ok(ActionType::Custom {
722 action_type: "statement".to_string(),
723 params: {
724 let mut params = HashMap::new();
725 params.insert("statement".to_string(), Value::String(trimmed.to_string()));
726 params
727 },
728 })
729 }
730 }
731
732 fn parse_method_args(&self, args_str: &str) -> Result<Vec<Value>> {
733 if args_str.trim().is_empty() {
734 return Ok(Vec::new());
735 }
736
737 let mut args = Vec::new();
739 let parts: Vec<&str> = args_str.split(',').collect();
740
741 for part in parts {
742 let trimmed = part.trim();
743
744 if trimmed.contains('+')
746 || trimmed.contains('-')
747 || trimmed.contains('*')
748 || trimmed.contains('/')
749 {
750 args.push(Value::String(trimmed.to_string()));
752 } else {
753 args.push(self.parse_value(trimmed)?);
754 }
755 }
756
757 Ok(args)
758 }
759}
760
761#[cfg(test)]
762mod tests {
763 use super::GRLParser;
764
765 #[test]
766 fn test_parse_simple_rule() {
767 let grl = r#"
768 rule "CheckAge" salience 10 {
769 when
770 User.Age >= 18
771 then
772 log("User is adult");
773 }
774 "#;
775
776 let rules = GRLParser::parse_rules(grl).unwrap();
777 assert_eq!(rules.len(), 1);
778 let rule = &rules[0];
779 assert_eq!(rule.name, "CheckAge");
780 assert_eq!(rule.salience, 10);
781 assert_eq!(rule.actions.len(), 1);
782 }
783
784 #[test]
785 fn test_parse_complex_condition() {
786 let grl = r#"
787 rule "ComplexRule" {
788 when
789 User.Age >= 18 && User.Country == "US"
790 then
791 User.Qualified = true;
792 }
793 "#;
794
795 let rules = GRLParser::parse_rules(grl).unwrap();
796 assert_eq!(rules.len(), 1);
797 let rule = &rules[0];
798 assert_eq!(rule.name, "ComplexRule");
799 }
800
801 #[test]
802 fn test_parse_new_syntax_with_parentheses() {
803 let grl = r#"
804 rule "Default Rule" salience 10 {
805 when
806 (user.age >= 18)
807 then
808 set(user.status, "approved");
809 }
810 "#;
811
812 let rules = GRLParser::parse_rules(grl).unwrap();
813 assert_eq!(rules.len(), 1);
814 let rule = &rules[0];
815 assert_eq!(rule.name, "Default Rule");
816 assert_eq!(rule.salience, 10);
817 assert_eq!(rule.actions.len(), 1);
818
819 match &rule.actions[0] {
821 crate::types::ActionType::Set { field, value } => {
822 assert_eq!(field, "user.status");
823 assert_eq!(value, &crate::types::Value::String("approved".to_string()));
824 }
825 _ => panic!("Expected Set action, got: {:?}", rule.actions[0]),
826 }
827 }
828
829 #[test]
830 fn test_parse_complex_nested_conditions() {
831 let grl = r#"
832 rule "Complex Business Rule" salience 10 {
833 when
834 (((user.vipStatus == true) && (order.amount > 500)) || ((date.isHoliday == true) && (order.hasCoupon == true)))
835 then
836 apply_discount(20000);
837 }
838 "#;
839
840 let rules = GRLParser::parse_rules(grl).unwrap();
841 assert_eq!(rules.len(), 1);
842 let rule = &rules[0];
843 assert_eq!(rule.name, "Complex Business Rule");
844 assert_eq!(rule.salience, 10);
845 assert_eq!(rule.actions.len(), 1);
846
847 match &rule.actions[0] {
849 crate::types::ActionType::Call { function, args } => {
850 assert_eq!(function, "apply_discount");
851 assert_eq!(args.len(), 1);
852 assert_eq!(args[0], crate::types::Value::Integer(20000));
853 }
854 _ => panic!("Expected Call action, got: {:?}", rule.actions[0]),
855 }
856 }
857
858 #[test]
859 fn test_parse_no_loop_attribute() {
860 let grl = r#"
861 rule "NoLoopRule" no-loop salience 15 {
862 when
863 User.Score < 100
864 then
865 set(User.Score, User.Score + 10);
866 }
867 "#;
868
869 let rules = GRLParser::parse_rules(grl).unwrap();
870 assert_eq!(rules.len(), 1);
871 let rule = &rules[0];
872 assert_eq!(rule.name, "NoLoopRule");
873 assert_eq!(rule.salience, 15);
874 assert!(rule.no_loop, "Rule should have no-loop=true");
875 }
876
877 #[test]
878 fn test_parse_no_loop_different_positions() {
879 let grl1 = r#"
881 rule "Rule1" no-loop salience 10 {
882 when User.Age >= 18
883 then log("adult");
884 }
885 "#;
886
887 let grl2 = r#"
889 rule "Rule2" salience 10 no-loop {
890 when User.Age >= 18
891 then log("adult");
892 }
893 "#;
894
895 let rules1 = GRLParser::parse_rules(grl1).unwrap();
896 let rules2 = GRLParser::parse_rules(grl2).unwrap();
897
898 assert_eq!(rules1.len(), 1);
899 assert_eq!(rules2.len(), 1);
900
901 assert!(rules1[0].no_loop, "Rule1 should have no-loop=true");
902 assert!(rules2[0].no_loop, "Rule2 should have no-loop=true");
903
904 assert_eq!(rules1[0].salience, 10);
905 assert_eq!(rules2[0].salience, 10);
906 }
907
908 #[test]
909 fn test_parse_without_no_loop() {
910 let grl = r#"
911 rule "RegularRule" salience 5 {
912 when
913 User.Active == true
914 then
915 log("active user");
916 }
917 "#;
918
919 let rules = GRLParser::parse_rules(grl).unwrap();
920 assert_eq!(rules.len(), 1);
921 let rule = &rules[0];
922 assert_eq!(rule.name, "RegularRule");
923 assert!(!rule.no_loop, "Rule should have no-loop=false by default");
924 }
925}