rust_rule_engine/parser/
grl.rs

1use crate::engine::rule::{Condition, ConditionGroup, Rule};
2use crate::errors::{Result, RuleEngineError};
3use crate::types::{ActionType, Operator, Value};
4use regex::Regex;
5use std::collections::HashMap;
6
7/// GRL (Grule Rule Language) Parser
8/// Parses Grule-like syntax into Rule objects
9pub struct GRLParser;
10
11impl GRLParser {
12    /// Parse a single rule from GRL syntax
13    ///
14    /// Example GRL syntax:
15    /// ```grl
16    /// rule CheckAge "Age verification rule" salience 10 {
17    ///     when
18    ///         User.Age >= 18 && User.Country == "US"
19    ///     then
20    ///         User.IsAdult = true;
21    ///         Retract("User");
22    /// }
23    /// ```
24    pub fn parse_rule(grl_text: &str) -> Result<Rule> {
25        let mut parser = GRLParser;
26        parser.parse_single_rule(grl_text)
27    }
28
29    /// Parse multiple rules from GRL text
30    pub fn parse_rules(grl_text: &str) -> Result<Vec<Rule>> {
31        let mut parser = GRLParser;
32        parser.parse_multiple_rules(grl_text)
33    }
34
35    fn parse_single_rule(&mut self, grl_text: &str) -> Result<Rule> {
36        let cleaned = self.clean_text(grl_text);
37
38        // Extract rule components using regex - support quoted rule names and no-loop
39        let rule_regex = Regex::new(
40            r#"rule\s+(?:"([^"]+)"|([a-zA-Z_]\w*))\s*(?:no-loop)?\s*(?:salience\s+(\d+))?\s*(?:no-loop)?\s*\{(.+)\}"#
41        ).map_err(|e| RuleEngineError::ParseError {
42            message: format!("Invalid rule regex: {}", e),
43        })?;
44
45        let captures =
46            rule_regex
47                .captures(&cleaned)
48                .ok_or_else(|| RuleEngineError::ParseError {
49                    message: format!("Invalid GRL rule format. Input: {}", cleaned),
50                })?;
51
52        // Rule name can be either quoted (group 1) or unquoted (group 2)
53        let rule_name = if let Some(quoted_name) = captures.get(1) {
54            quoted_name.as_str().to_string()
55        } else if let Some(unquoted_name) = captures.get(2) {
56            unquoted_name.as_str().to_string()
57        } else {
58            return Err(RuleEngineError::ParseError {
59                message: "Could not extract rule name".to_string(),
60            });
61        };
62        // Extract salience and rule body
63        let salience = captures
64            .get(3)
65            .and_then(|m| m.as_str().parse::<i32>().ok())
66            .unwrap_or(0);
67
68        let rule_body = captures.get(4).unwrap().as_str();
69
70        // Parse when and then sections
71        let when_then_regex =
72            Regex::new(r"when\s+(.+?)\s+then\s+(.+)").map_err(|e| RuleEngineError::ParseError {
73                message: format!("Invalid when-then regex: {}", e),
74            })?;
75
76        let when_then_captures =
77            when_then_regex
78                .captures(rule_body)
79                .ok_or_else(|| RuleEngineError::ParseError {
80                    message: "Missing when or then clause".to_string(),
81                })?;
82
83        let when_clause = when_then_captures.get(1).unwrap().as_str().trim();
84        let then_clause = when_then_captures.get(2).unwrap().as_str().trim();
85
86        // Check for no-loop attribute in the rule header (before the {)
87        let rule_header = grl_text.split('{').next().unwrap_or("");
88        let has_no_loop = rule_header.contains("no-loop");
89
90        // Parse conditions
91        let conditions = self.parse_when_clause(when_clause)?;
92
93        // Parse actions
94        let actions = self.parse_then_clause(then_clause)?;
95
96        // Build rule
97        let mut rule = Rule::new(rule_name, conditions, actions);
98        rule = rule.with_priority(salience);
99
100        if has_no_loop {
101            rule = rule.with_no_loop(true);
102        }
103
104        Ok(rule)
105    }
106
107    fn parse_multiple_rules(&mut self, grl_text: &str) -> Result<Vec<Rule>> {
108        // Split by rule boundaries - support both quoted and unquoted rule names
109        // Use DOTALL flag to match newlines in rule body
110        let rule_regex =
111            Regex::new(r#"(?s)rule\s+(?:"[^"]+"|[a-zA-Z_]\w*).*?\}"#).map_err(|e| {
112                RuleEngineError::ParseError {
113                    message: format!("Rule splitting regex error: {}", e),
114                }
115            })?;
116
117        let mut rules = Vec::new();
118
119        for rule_match in rule_regex.find_iter(grl_text) {
120            let rule_text = rule_match.as_str();
121            let rule = self.parse_single_rule(rule_text)?;
122            rules.push(rule);
123        }
124
125        Ok(rules)
126    }
127
128    fn clean_text(&self, text: &str) -> String {
129        text.lines()
130            .map(|line| line.trim())
131            .filter(|line| !line.is_empty() && !line.starts_with("//"))
132            .collect::<Vec<_>>()
133            .join(" ")
134    }
135
136    fn parse_when_clause(&self, when_clause: &str) -> Result<ConditionGroup> {
137        // Handle logical operators with proper parentheses support
138        let trimmed = when_clause.trim();
139
140        // Strip outer parentheses if they exist
141        let clause = if trimmed.starts_with('(') && trimmed.ends_with(')') {
142            // Check if these are the outermost parentheses
143            let inner = &trimmed[1..trimmed.len() - 1];
144            if self.is_balanced_parentheses(inner) {
145                inner
146            } else {
147                trimmed
148            }
149        } else {
150            trimmed
151        };
152
153        // Parse OR at the top level (lowest precedence)
154        if let Some(parts) = self.split_logical_operator(clause, "||") {
155            return self.parse_or_parts(parts);
156        }
157
158        // Parse AND (higher precedence)
159        if let Some(parts) = self.split_logical_operator(clause, "&&") {
160            return self.parse_and_parts(parts);
161        }
162
163        // Handle NOT condition
164        if clause.trim_start().starts_with("!") {
165            return self.parse_not_condition(clause);
166        }
167
168        // Single condition
169        self.parse_single_condition(clause)
170    }
171
172    fn is_balanced_parentheses(&self, text: &str) -> bool {
173        let mut count = 0;
174        for ch in text.chars() {
175            match ch {
176                '(' => count += 1,
177                ')' => {
178                    count -= 1;
179                    if count < 0 {
180                        return false;
181                    }
182                }
183                _ => {}
184            }
185        }
186        count == 0
187    }
188
189    fn split_logical_operator(&self, clause: &str, operator: &str) -> Option<Vec<String>> {
190        let mut parts = Vec::new();
191        let mut current_part = String::new();
192        let mut paren_count = 0;
193        let mut chars = clause.chars().peekable();
194
195        while let Some(ch) = chars.next() {
196            match ch {
197                '(' => {
198                    paren_count += 1;
199                    current_part.push(ch);
200                }
201                ')' => {
202                    paren_count -= 1;
203                    current_part.push(ch);
204                }
205                '&' if operator == "&&" && paren_count == 0 => {
206                    if chars.peek() == Some(&'&') {
207                        chars.next(); // consume second &
208                        parts.push(current_part.trim().to_string());
209                        current_part.clear();
210                    } else {
211                        current_part.push(ch);
212                    }
213                }
214                '|' if operator == "||" && paren_count == 0 => {
215                    if chars.peek() == Some(&'|') {
216                        chars.next(); // consume second |
217                        parts.push(current_part.trim().to_string());
218                        current_part.clear();
219                    } else {
220                        current_part.push(ch);
221                    }
222                }
223                _ => {
224                    current_part.push(ch);
225                }
226            }
227        }
228
229        if !current_part.trim().is_empty() {
230            parts.push(current_part.trim().to_string());
231        }
232
233        if parts.len() > 1 {
234            Some(parts)
235        } else {
236            None
237        }
238    }
239
240    fn parse_or_parts(&self, parts: Vec<String>) -> Result<ConditionGroup> {
241        let mut conditions = Vec::new();
242        for part in parts {
243            let condition = self.parse_when_clause(&part)?;
244            conditions.push(condition);
245        }
246
247        if conditions.is_empty() {
248            return Err(RuleEngineError::ParseError {
249                message: "No conditions found in OR".to_string(),
250            });
251        }
252
253        let mut iter = conditions.into_iter();
254        let mut result = iter.next().unwrap();
255        for condition in iter {
256            result = ConditionGroup::or(result, condition);
257        }
258
259        Ok(result)
260    }
261
262    fn parse_and_parts(&self, parts: Vec<String>) -> Result<ConditionGroup> {
263        let mut conditions = Vec::new();
264        for part in parts {
265            let condition = self.parse_when_clause(&part)?;
266            conditions.push(condition);
267        }
268
269        if conditions.is_empty() {
270            return Err(RuleEngineError::ParseError {
271                message: "No conditions found in AND".to_string(),
272            });
273        }
274
275        let mut iter = conditions.into_iter();
276        let mut result = iter.next().unwrap();
277        for condition in iter {
278            result = ConditionGroup::and(result, condition);
279        }
280
281        Ok(result)
282    }
283
284    fn parse_not_condition(&self, clause: &str) -> Result<ConditionGroup> {
285        let inner_clause = clause.strip_prefix("!").unwrap().trim();
286        let inner_condition = self.parse_when_clause(inner_clause)?;
287        Ok(ConditionGroup::not(inner_condition))
288    }
289
290    fn parse_single_condition(&self, clause: &str) -> Result<ConditionGroup> {
291        // Remove outer parentheses if they exist (handle new syntax like "(user.age >= 18)")
292        let trimmed_clause = clause.trim();
293        let clause_to_parse = if trimmed_clause.starts_with('(') && trimmed_clause.ends_with(')') {
294            trimmed_clause[1..trimmed_clause.len() - 1].trim()
295        } else {
296            trimmed_clause
297        };
298
299        // Handle typed object conditions like: $TestCar : TestCarClass( speedUp == true && speed < maxSpeed )
300        let typed_object_regex =
301            Regex::new(r#"\$(\w+)\s*:\s*(\w+)\s*\(\s*(.+?)\s*\)"#).map_err(|e| {
302                RuleEngineError::ParseError {
303                    message: format!("Typed object regex error: {}", e),
304                }
305            })?;
306
307        if let Some(captures) = typed_object_regex.captures(clause_to_parse) {
308            let _object_name = captures.get(1).unwrap().as_str();
309            let _object_type = captures.get(2).unwrap().as_str();
310            let conditions_str = captures.get(3).unwrap().as_str();
311
312            // Parse conditions inside parentheses
313            return self.parse_conditions_within_object(conditions_str);
314        }
315
316        // Parse expressions like: User.Age >= 18, Product.Price < 100.0, user.age >= 18, etc.
317        // Support both PascalCase (User.Age) and lowercase (user.age) field naming
318        let condition_regex = Regex::new(
319            r#"([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s*(>=|<=|==|!=|>|<|contains|matches)\s*(.+)"#,
320        )
321        .map_err(|e| RuleEngineError::ParseError {
322            message: format!("Condition regex error: {}", e),
323        })?;
324
325        let captures = condition_regex.captures(clause_to_parse).ok_or_else(|| {
326            RuleEngineError::ParseError {
327                message: format!("Invalid condition format: {}", clause_to_parse),
328            }
329        })?;
330
331        let field = captures.get(1).unwrap().as_str().to_string();
332        let operator_str = captures.get(2).unwrap().as_str();
333        let value_str = captures.get(3).unwrap().as_str().trim();
334
335        let operator =
336            Operator::from_str(operator_str).ok_or_else(|| RuleEngineError::InvalidOperator {
337                operator: operator_str.to_string(),
338            })?;
339
340        let value = self.parse_value(value_str)?;
341
342        let condition = Condition::new(field, operator, value);
343        Ok(ConditionGroup::single(condition))
344    }
345
346    fn parse_conditions_within_object(&self, conditions_str: &str) -> Result<ConditionGroup> {
347        // Parse conditions like: speedUp == true && speed < maxSpeed
348        let parts: Vec<&str> = conditions_str.split("&&").collect();
349
350        let mut conditions = Vec::new();
351        for part in parts {
352            let trimmed = part.trim();
353            let condition = self.parse_simple_condition(trimmed)?;
354            conditions.push(condition);
355        }
356
357        // Combine with AND
358        if conditions.is_empty() {
359            return Err(RuleEngineError::ParseError {
360                message: "No conditions found".to_string(),
361            });
362        }
363
364        let mut iter = conditions.into_iter();
365        let mut result = iter.next().unwrap();
366        for condition in iter {
367            result = ConditionGroup::and(result, condition);
368        }
369
370        Ok(result)
371    }
372
373    fn parse_simple_condition(&self, clause: &str) -> Result<ConditionGroup> {
374        // Parse simple condition like: speedUp == true or speed < maxSpeed
375        let condition_regex = Regex::new(r#"(\w+)\s*(>=|<=|==|!=|>|<)\s*(.+)"#).map_err(|e| {
376            RuleEngineError::ParseError {
377                message: format!("Simple condition regex error: {}", e),
378            }
379        })?;
380
381        let captures =
382            condition_regex
383                .captures(clause)
384                .ok_or_else(|| RuleEngineError::ParseError {
385                    message: format!("Invalid simple condition format: {}", clause),
386                })?;
387
388        let field = captures.get(1).unwrap().as_str().to_string();
389        let operator_str = captures.get(2).unwrap().as_str();
390        let value_str = captures.get(3).unwrap().as_str().trim();
391
392        let operator =
393            Operator::from_str(operator_str).ok_or_else(|| RuleEngineError::InvalidOperator {
394                operator: operator_str.to_string(),
395            })?;
396
397        let value = self.parse_value(value_str)?;
398
399        let condition = Condition::new(field, operator, value);
400        Ok(ConditionGroup::single(condition))
401    }
402
403    fn parse_value(&self, value_str: &str) -> Result<Value> {
404        let trimmed = value_str.trim();
405
406        // String literal
407        if (trimmed.starts_with('"') && trimmed.ends_with('"'))
408            || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
409        {
410            let unquoted = &trimmed[1..trimmed.len() - 1];
411            return Ok(Value::String(unquoted.to_string()));
412        }
413
414        // Boolean
415        if trimmed.eq_ignore_ascii_case("true") {
416            return Ok(Value::Boolean(true));
417        }
418        if trimmed.eq_ignore_ascii_case("false") {
419            return Ok(Value::Boolean(false));
420        }
421
422        // Null
423        if trimmed.eq_ignore_ascii_case("null") {
424            return Ok(Value::Null);
425        }
426
427        // Number (try integer first, then float)
428        if let Ok(int_val) = trimmed.parse::<i64>() {
429            return Ok(Value::Integer(int_val));
430        }
431
432        if let Ok(float_val) = trimmed.parse::<f64>() {
433            return Ok(Value::Number(float_val));
434        }
435
436        // Field reference (like User.Name)
437        if trimmed.contains('.') {
438            return Ok(Value::String(trimmed.to_string()));
439        }
440
441        // Default to string
442        Ok(Value::String(trimmed.to_string()))
443    }
444
445    fn parse_then_clause(&self, then_clause: &str) -> Result<Vec<ActionType>> {
446        let statements: Vec<&str> = then_clause
447            .split(';')
448            .map(|s| s.trim())
449            .filter(|s| !s.is_empty())
450            .collect();
451
452        let mut actions = Vec::new();
453
454        for statement in statements {
455            let action = self.parse_action_statement(statement)?;
456            actions.push(action);
457        }
458
459        Ok(actions)
460    }
461
462    fn parse_action_statement(&self, statement: &str) -> Result<ActionType> {
463        let trimmed = statement.trim();
464
465        // Method call: $Object.method(args)
466        let method_regex = Regex::new(r#"\$(\w+)\.(\w+)\s*\(([^)]*)\)"#).map_err(|e| {
467            RuleEngineError::ParseError {
468                message: format!("Method regex error: {}", e),
469            }
470        })?;
471
472        if let Some(captures) = method_regex.captures(trimmed) {
473            let object = captures.get(1).unwrap().as_str().to_string();
474            let method = captures.get(2).unwrap().as_str().to_string();
475            let args_str = captures.get(3).unwrap().as_str();
476
477            let args = if args_str.trim().is_empty() {
478                Vec::new()
479            } else {
480                self.parse_method_args(args_str)?
481            };
482
483            return Ok(ActionType::MethodCall {
484                object,
485                method,
486                args,
487            });
488        }
489
490        // Assignment: Field = Value
491        if let Some(eq_pos) = trimmed.find('=') {
492            let field = trimmed[..eq_pos].trim().to_string();
493            let value_str = trimmed[eq_pos + 1..].trim();
494            let value = self.parse_value(value_str)?;
495
496            return Ok(ActionType::Set { field, value });
497        }
498
499        // Function calls: update($Object), retract($Object), etc.
500        let func_regex =
501            Regex::new(r#"(\w+)\s*\(\s*(.+?)?\s*\)"#).map_err(|e| RuleEngineError::ParseError {
502                message: format!("Function regex error: {}", e),
503            })?;
504
505        if let Some(captures) = func_regex.captures(trimmed) {
506            let function_name = captures.get(1).unwrap().as_str();
507            let args_str = captures.get(2).map(|m| m.as_str()).unwrap_or("");
508
509            match function_name.to_lowercase().as_str() {
510                "update" => {
511                    // Extract object name from $Object
512                    let object_name = if let Some(stripped) = args_str.strip_prefix('$') {
513                        stripped.to_string()
514                    } else {
515                        args_str.to_string()
516                    };
517                    Ok(ActionType::Update {
518                        object: object_name,
519                    })
520                }
521                "set" => {
522                    // Handle set(field, value) format
523                    let args = if args_str.is_empty() {
524                        Vec::new()
525                    } else {
526                        args_str
527                            .split(',')
528                            .map(|arg| self.parse_value(arg.trim()))
529                            .collect::<Result<Vec<_>>>()?
530                    };
531
532                    if args.len() >= 2 {
533                        let field = args[0].to_string();
534                        let value = args[1].clone();
535                        Ok(ActionType::Set { field, value })
536                    } else if args.len() == 1 {
537                        // set(field) - set to true by default
538                        Ok(ActionType::Set {
539                            field: args[0].to_string(),
540                            value: Value::Boolean(true),
541                        })
542                    } else {
543                        Ok(ActionType::Custom {
544                            action_type: "set".to_string(),
545                            params: {
546                                let mut params = HashMap::new();
547                                params.insert(
548                                    "args".to_string(),
549                                    Value::String(args_str.to_string()),
550                                );
551                                params
552                            },
553                        })
554                    }
555                }
556                "add" => {
557                    // Handle add(value) format
558                    let value = if args_str.is_empty() {
559                        Value::Integer(1) // Default increment
560                    } else {
561                        self.parse_value(args_str.trim())?
562                    };
563                    Ok(ActionType::Custom {
564                        action_type: "add".to_string(),
565                        params: {
566                            let mut params = HashMap::new();
567                            params.insert("value".to_string(), value);
568                            params
569                        },
570                    })
571                }
572                "log" => {
573                    let message = if args_str.is_empty() {
574                        "Log message".to_string()
575                    } else {
576                        let value = self.parse_value(args_str.trim())?;
577                        value.to_string()
578                    };
579                    Ok(ActionType::Log { message })
580                }
581                _ => {
582                    let args = if args_str.is_empty() {
583                        Vec::new()
584                    } else {
585                        args_str
586                            .split(',')
587                            .map(|arg| self.parse_value(arg.trim()))
588                            .collect::<Result<Vec<_>>>()?
589                    };
590                    Ok(ActionType::Call {
591                        function: function_name.to_string(),
592                        args,
593                    })
594                }
595            }
596        } else {
597            // Custom statement
598            Ok(ActionType::Custom {
599                action_type: "statement".to_string(),
600                params: {
601                    let mut params = HashMap::new();
602                    params.insert("statement".to_string(), Value::String(trimmed.to_string()));
603                    params
604                },
605            })
606        }
607    }
608
609    fn parse_method_args(&self, args_str: &str) -> Result<Vec<Value>> {
610        if args_str.trim().is_empty() {
611            return Ok(Vec::new());
612        }
613
614        // Handle expressions like: $TestCar.Speed + $TestCar.SpeedIncrement
615        let mut args = Vec::new();
616        let parts: Vec<&str> = args_str.split(',').collect();
617
618        for part in parts {
619            let trimmed = part.trim();
620
621            // Handle arithmetic expressions
622            if trimmed.contains('+')
623                || trimmed.contains('-')
624                || trimmed.contains('*')
625                || trimmed.contains('/')
626            {
627                // For now, store as string - the engine will evaluate
628                args.push(Value::String(trimmed.to_string()));
629            } else {
630                args.push(self.parse_value(trimmed)?);
631            }
632        }
633
634        Ok(args)
635    }
636}
637
638#[cfg(test)]
639mod tests {
640    use super::GRLParser;
641
642    #[test]
643    fn test_parse_simple_rule() {
644        let grl = r#"
645        rule "CheckAge" salience 10 {
646            when
647                User.Age >= 18
648            then
649                log("User is adult");
650        }
651        "#;
652
653        let rules = GRLParser::parse_rules(grl).unwrap();
654        assert_eq!(rules.len(), 1);
655        let rule = &rules[0];
656        assert_eq!(rule.name, "CheckAge");
657        assert_eq!(rule.salience, 10);
658        assert_eq!(rule.actions.len(), 1);
659    }
660
661    #[test]
662    fn test_parse_complex_condition() {
663        let grl = r#"
664        rule "ComplexRule" {
665            when
666                User.Age >= 18 && User.Country == "US"
667            then
668                User.Qualified = true;
669        }
670        "#;
671
672        let rules = GRLParser::parse_rules(grl).unwrap();
673        assert_eq!(rules.len(), 1);
674        let rule = &rules[0];
675        assert_eq!(rule.name, "ComplexRule");
676    }
677
678    #[test]
679    fn test_parse_new_syntax_with_parentheses() {
680        let grl = r#"
681        rule "Default Rule" salience 10 {
682            when
683                (user.age >= 18)
684            then
685                set(user.status, "approved");
686        }
687        "#;
688
689        let rules = GRLParser::parse_rules(grl).unwrap();
690        assert_eq!(rules.len(), 1);
691        let rule = &rules[0];
692        assert_eq!(rule.name, "Default Rule");
693        assert_eq!(rule.salience, 10);
694        assert_eq!(rule.actions.len(), 1);
695
696        // Check that the action is parsed as a Set action
697        match &rule.actions[0] {
698            crate::types::ActionType::Set { field, value } => {
699                assert_eq!(field, "user.status");
700                assert_eq!(value, &crate::types::Value::String("approved".to_string()));
701            }
702            _ => panic!("Expected Set action, got: {:?}", rule.actions[0]),
703        }
704    }
705
706    #[test]
707    fn test_parse_complex_nested_conditions() {
708        let grl = r#"
709        rule "Complex Business Rule" salience 10 {
710            when
711                (((user.vipStatus == true) && (order.amount > 500)) || ((date.isHoliday == true) && (order.hasCoupon == true)))
712            then
713                apply_discount(20000);
714        }
715        "#;
716
717        let rules = GRLParser::parse_rules(grl).unwrap();
718        assert_eq!(rules.len(), 1);
719        let rule = &rules[0];
720        assert_eq!(rule.name, "Complex Business Rule");
721        assert_eq!(rule.salience, 10);
722        assert_eq!(rule.actions.len(), 1);
723
724        // Check that the action is parsed as a function call
725        match &rule.actions[0] {
726            crate::types::ActionType::Call { function, args } => {
727                assert_eq!(function, "apply_discount");
728                assert_eq!(args.len(), 1);
729                assert_eq!(args[0], crate::types::Value::Integer(20000));
730            }
731            _ => panic!("Expected Call action, got: {:?}", rule.actions[0]),
732        }
733    }
734
735    #[test]
736    fn test_parse_no_loop_attribute() {
737        let grl = r#"
738        rule "NoLoopRule" no-loop salience 15 {
739            when
740                User.Score < 100
741            then
742                set(User.Score, User.Score + 10);
743        }
744        "#;
745
746        let rules = GRLParser::parse_rules(grl).unwrap();
747        assert_eq!(rules.len(), 1);
748        let rule = &rules[0];
749        assert_eq!(rule.name, "NoLoopRule");
750        assert_eq!(rule.salience, 15);
751        assert!(rule.no_loop, "Rule should have no-loop=true");
752    }
753
754    #[test]
755    fn test_parse_no_loop_different_positions() {
756        // Test no-loop before salience
757        let grl1 = r#"
758        rule "Rule1" no-loop salience 10 {
759            when User.Age >= 18
760            then log("adult");
761        }
762        "#;
763
764        // Test no-loop after salience
765        let grl2 = r#"
766        rule "Rule2" salience 10 no-loop {
767            when User.Age >= 18
768            then log("adult");
769        }
770        "#;
771
772        let rules1 = GRLParser::parse_rules(grl1).unwrap();
773        let rules2 = GRLParser::parse_rules(grl2).unwrap();
774
775        assert_eq!(rules1.len(), 1);
776        assert_eq!(rules2.len(), 1);
777
778        assert!(rules1[0].no_loop, "Rule1 should have no-loop=true");
779        assert!(rules2[0].no_loop, "Rule2 should have no-loop=true");
780
781        assert_eq!(rules1[0].salience, 10);
782        assert_eq!(rules2[0].salience, 10);
783    }
784
785    #[test]
786    fn test_parse_without_no_loop() {
787        let grl = r#"
788        rule "RegularRule" salience 5 {
789            when
790                User.Active == true
791            then
792                log("active user");
793        }
794        "#;
795
796        let rules = GRLParser::parse_rules(grl).unwrap();
797        assert_eq!(rules.len(), 1);
798        let rule = &rules[0];
799        assert_eq!(rule.name, "RegularRule");
800        assert!(!rule.no_loop, "Rule should have no-loop=false by default");
801    }
802}