rust_rule_engine/backward/
grl_query.rs

1//! GRL Query Syntax Implementation
2//!
3//! This module provides parsing and execution of backward chaining queries defined in GRL
4//! (Goal-driven Rule Language) syntax. GRL queries allow you to define goal-driven reasoning
5//! tasks with configurable search strategies and action handlers.
6//!
7//! # Features
8//!
9//! - **Declarative query syntax** - Define queries in a readable, structured format
10//! - **Multiple search strategies** - Choose between depth-first, breadth-first, or iterative deepening
11//! - **Action handlers** - Execute actions on query success, failure, or missing facts
12//! - **Conditional execution** - Use `when` clauses to conditionally execute queries
13//! - **Parameterized queries** - Support for query parameters (future enhancement)
14//!
15//! # GRL Query Syntax
16//!
17//! ```grl
18//! query "QueryName" {
19//!     goal: <expression>                    // Required: Goal to prove
20//!     strategy: <depth-first|breadth-first|iterative>  // Optional: Search strategy
21//!     max-depth: <number>                   // Optional: Maximum search depth
22//!     max-solutions: <number>               // Optional: Maximum solutions to find
23//!     enable-memoization: <true|false>      // Optional: Enable result caching
24//!
25//!     when: <condition>                     // Optional: Only execute if condition is true
26//!
27//!     on-success: {                         // Optional: Actions on successful proof
28//!         <variable> = <value>;
29//!         <FunctionName>(<args>);
30//!     }
31//!
32//!     on-failure: {                         // Optional: Actions on proof failure
33//!         <actions>
34//!     }
35//!
36//!     on-missing: {                         // Optional: Actions when facts are missing
37//!         <actions>
38//!     }
39//! }
40//! ```
41//!
42//! # Example
43//!
44//! ```rust
45//! use rust_rule_engine::backward::grl_query::{GRLQueryParser, GRLQueryExecutor};
46//! use rust_rule_engine::backward::BackwardEngine;
47//! use rust_rule_engine::{KnowledgeBase, Facts, Value};
48//!
49//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
50//! let query_str = r#"
51//! query "CheckVIPStatus" {
52//!     goal: User.IsVIP == true
53//!     strategy: depth-first
54//!     max-depth: 10
55//!     on-success: {
56//!         User.DiscountRate = 0.2;
57//!         LogMessage("VIP confirmed");
58//!     }
59//!     on-failure: {
60//!         LogMessage("Not a VIP user");
61//!     }
62//! }
63//! "#;
64//!
65//! let query = GRLQueryParser::parse(query_str)?;
66//! let mut bc_engine = BackwardEngine::new(KnowledgeBase::new("kb"));
67//! let mut facts = Facts::new();
68//! facts.set("User.LoyaltyPoints", Value::Number(1500.0));
69//!
70//! let result = GRLQueryExecutor::execute(&query, &mut bc_engine, &mut facts)?;
71//!
72//! if result.provable {
73//!     println!("Goal proven!");
74//! }
75//! # Ok(())
76//! # }
77//! ```
78//!
79//! # Supported Functions in Actions
80//!
81//! - `LogMessage(message)` - Print a log message
82//! - `Request(message)` - Send a request message
83//! - `Print(message)` - Print output
84//! - `Debug(message)` - Print debug output to stderr
85
86use crate::errors::RuleEngineError;
87use crate::{Facts, Value};
88use super::backward_engine::{BackwardEngine, BackwardConfig};
89use super::search::SearchStrategy;
90use super::query::{QueryResult, QueryStats, ProofTrace};
91
92use std::collections::HashMap;
93
94/// Search strategy option for queries
95#[derive(Debug, Clone, PartialEq)]
96pub enum GRLSearchStrategy {
97    DepthFirst,
98    BreadthFirst,
99    Iterative,
100}
101
102impl Default for GRLSearchStrategy {
103    fn default() -> Self {
104        GRLSearchStrategy::DepthFirst
105    }
106}
107
108/// Action to execute based on query result
109#[derive(Debug, Clone)]
110pub struct QueryAction {
111    /// Assignment: Variable = Value (as string to be parsed)
112    pub assignments: Vec<(String, String)>,
113    /// Function/method calls
114    pub calls: Vec<String>,
115}
116
117impl QueryAction {
118    pub fn new() -> Self {
119        QueryAction {
120            assignments: Vec::new(),
121            calls: Vec::new(),
122        }
123    }
124
125    /// Execute the action on the given facts
126    pub fn execute(&self, facts: &mut Facts) -> Result<(), RuleEngineError> {
127        // Execute assignments - for now just log them
128        for (var_name, value_str) in &self.assignments {
129            // Simple value parsing
130            let value = if value_str == "true" {
131                Value::Boolean(true)
132            } else if value_str == "false" {
133                Value::Boolean(false)
134            } else if let Ok(n) = value_str.parse::<f64>() {
135                Value::Number(n)
136            } else {
137                // Remove quotes if present
138                let cleaned = value_str.trim_matches('"');
139                Value::String(cleaned.to_string())
140            };
141            
142            facts.set(var_name, value);
143        }
144
145        // Execute function calls
146        for call in &self.calls {
147            self.execute_function_call(call)?;
148        }
149
150        Ok(())
151    }
152
153    /// Execute a single function call
154    fn execute_function_call(&self, call: &str) -> Result<(), RuleEngineError> {
155        let call = call.trim();
156
157        // Parse function name and arguments
158        if let Some(open_paren) = call.find('(') {
159            let func_name = call[..open_paren].trim();
160
161            // Extract arguments (everything between first ( and last ))
162            if let Some(close_paren) = call.rfind(')') {
163                let args_str = &call[open_paren + 1..close_paren];
164
165                match func_name {
166                    "LogMessage" => {
167                        // Parse string argument (remove quotes if present)
168                        let message = args_str.trim().trim_matches('"').trim_matches('\'');
169                        println!("[LOG] {}", message);
170                    }
171                    "Request" => {
172                        // Parse request call
173                        let message = args_str.trim().trim_matches('"').trim_matches('\'');
174                        println!("[REQUEST] {}", message);
175                    }
176                    "Print" => {
177                        // Generic print function
178                        let message = args_str.trim().trim_matches('"').trim_matches('\'');
179                        println!("{}", message);
180                    }
181                    "Debug" => {
182                        // Debug output
183                        let message = args_str.trim().trim_matches('"').trim_matches('\'');
184                        eprintln!("[DEBUG] {}", message);
185                    }
186                    other => {
187                        // Unknown function - log warning but don't fail
188                        eprintln!("[WARNING] Unknown function call in query action: {}({})", other, args_str);
189                    }
190                }
191            } else {
192                return Err(RuleEngineError::ParseError {
193                    message: format!("Malformed function call (missing closing paren): {}", call),
194                });
195            }
196        } else {
197            return Err(RuleEngineError::ParseError {
198                message: format!("Malformed function call (missing opening paren): {}", call),
199            });
200        }
201
202        Ok(())
203    }
204}
205
206/// A GRL Query definition
207#[derive(Debug, Clone)]
208pub struct GRLQuery {
209    /// Query name
210    pub name: String,
211    
212    /// Goal pattern to prove (as string expression)
213    pub goal: String,
214    
215    /// Search strategy
216    pub strategy: GRLSearchStrategy,
217    
218    /// Maximum search depth
219    pub max_depth: usize,
220    
221    /// Maximum number of solutions
222    pub max_solutions: usize,
223    
224    /// Enable memoization
225    pub enable_memoization: bool,
226    
227    /// Action on success
228    pub on_success: Option<QueryAction>,
229    
230    /// Action on failure
231    pub on_failure: Option<QueryAction>,
232    
233    /// Action on missing facts
234    pub on_missing: Option<QueryAction>,
235    
236    /// Parameters for parameterized queries
237    pub params: HashMap<String, String>, // param_name -> type
238    
239    /// Conditional execution (as string condition)
240    pub when_condition: Option<String>,
241}
242
243impl GRLQuery {
244    /// Create a new query with defaults
245    pub fn new(name: String, goal: String) -> Self {
246        GRLQuery {
247            name,
248            goal,
249            strategy: GRLSearchStrategy::default(),
250            max_depth: 10,
251            max_solutions: 1,
252            enable_memoization: true,
253            on_success: None,
254            on_failure: None,
255            on_missing: None,
256            params: HashMap::new(),
257            when_condition: None,
258        }
259    }
260
261    /// Set search strategy
262    pub fn with_strategy(mut self, strategy: GRLSearchStrategy) -> Self {
263        self.strategy = strategy;
264        self
265    }
266
267    /// Set max depth
268    pub fn with_max_depth(mut self, max_depth: usize) -> Self {
269        self.max_depth = max_depth;
270        self
271    }
272
273    /// Set max solutions
274    pub fn with_max_solutions(mut self, max_solutions: usize) -> Self {
275        self.max_solutions = max_solutions;
276        self
277    }
278
279    /// Set memoization
280    pub fn with_memoization(mut self, enable: bool) -> Self {
281        self.enable_memoization = enable;
282        self
283    }
284
285    /// Add success action
286    pub fn with_on_success(mut self, action: QueryAction) -> Self {
287        self.on_success = Some(action);
288        self
289    }
290
291    /// Add failure action
292    pub fn with_on_failure(mut self, action: QueryAction) -> Self {
293        self.on_failure = Some(action);
294        self
295    }
296
297    /// Add missing facts action
298    pub fn with_on_missing(mut self, action: QueryAction) -> Self {
299        self.on_missing = Some(action);
300        self
301    }
302
303    /// Add parameter
304    pub fn with_param(mut self, name: String, type_name: String) -> Self {
305        self.params.insert(name, type_name);
306        self
307    }
308
309    /// Set conditional execution
310    pub fn with_when(mut self, condition: String) -> Self {
311        self.when_condition = Some(condition);
312        self
313    }
314
315    /// Check if query should execute based on when condition
316    pub fn should_execute(&self, _facts: &Facts) -> Result<bool, RuleEngineError> {
317        // If there's no when condition, execute by default
318        if self.when_condition.is_none() {
319            return Ok(true);
320        }
321
322        // Parse and evaluate the when condition expression against the current facts
323        if let Some(ref cond_str) = self.when_condition {
324            use crate::backward::expression::ExpressionParser;
325
326            match ExpressionParser::parse(cond_str) {
327                Ok(expr) => Ok(expr.is_satisfied(_facts)),
328                Err(e) => Err(e),
329            }
330        } else {
331            Ok(true)
332        }
333    }
334
335    /// Execute success actions
336    pub fn execute_success_actions(&self, facts: &mut Facts) -> Result<(), RuleEngineError> {
337        if let Some(ref action) = self.on_success {
338            action.execute(facts)?;
339        }
340        Ok(())
341    }
342
343    /// Execute failure actions
344    pub fn execute_failure_actions(&self, facts: &mut Facts) -> Result<(), RuleEngineError> {
345        if let Some(ref action) = self.on_failure {
346            action.execute(facts)?;
347        }
348        Ok(())
349    }
350
351    /// Execute missing facts actions
352    pub fn execute_missing_actions(&self, facts: &mut Facts) -> Result<(), RuleEngineError> {
353        if let Some(ref action) = self.on_missing {
354            action.execute(facts)?;
355        }
356        Ok(())
357    }
358
359    /// Convert to BackwardConfig
360    pub fn to_config(&self) -> BackwardConfig {
361        let search_strategy = match self.strategy {
362            GRLSearchStrategy::DepthFirst => SearchStrategy::DepthFirst,
363            GRLSearchStrategy::BreadthFirst => SearchStrategy::BreadthFirst,
364            GRLSearchStrategy::Iterative => SearchStrategy::Iterative,
365        };
366
367        BackwardConfig {
368            strategy: search_strategy,
369            max_depth: self.max_depth,
370            enable_memoization: self.enable_memoization,
371            max_solutions: self.max_solutions,
372        }
373    }
374}
375
376/// Parser for GRL Query syntax
377pub struct GRLQueryParser;
378
379impl GRLQueryParser {
380    /// Parse a query from string
381    /// 
382    /// # Example
383    /// ```
384    /// let query_str = r#"
385    /// query "CheckVIP" {
386    ///     goal: User.IsVIP == true
387    ///     strategy: depth-first
388    /// }
389    /// "#;
390    /// let query = rust_rule_engine::backward::GRLQueryParser::parse(query_str).unwrap();
391    /// ```
392    pub fn parse(input: &str) -> Result<GRLQuery, RuleEngineError> {
393        let input = input.trim();
394
395        // Extract query name
396        let name = Self::extract_query_name(input)?;
397
398        // Extract goal
399        let goal = Self::extract_goal(input)?;
400
401        // Create base query
402        let mut query = GRLQuery::new(name, goal);
403
404        // Parse optional attributes
405        if let Some(strategy) = Self::extract_strategy(input) {
406            query.strategy = strategy;
407        }
408
409        if let Some(max_depth) = Self::extract_max_depth(input) {
410            query.max_depth = max_depth;
411        }
412
413        if let Some(max_solutions) = Self::extract_max_solutions(input) {
414            query.max_solutions = max_solutions;
415        }
416
417        if let Some(enable_memo) = Self::extract_memoization(input) {
418            query.enable_memoization = enable_memo;
419        }
420
421        // Parse actions
422        if let Some(action) = Self::extract_on_success(input)? {
423            query.on_success = Some(action);
424        }
425
426        if let Some(action) = Self::extract_on_failure(input)? {
427            query.on_failure = Some(action);
428        }
429
430        if let Some(action) = Self::extract_on_missing(input)? {
431            query.on_missing = Some(action);
432        }
433
434        // Parse when condition
435        if let Some(condition) = Self::extract_when_condition(input)? {
436            query.when_condition = Some(condition);
437        }
438
439        Ok(query)
440    }
441
442    fn extract_query_name(input: &str) -> Result<String, RuleEngineError> {
443        let re = regex::Regex::new(r#"query\s+"([^"]+)"\s*\{"#).unwrap();
444        if let Some(caps) = re.captures(input) {
445            Ok(caps[1].to_string())
446        } else {
447            Err(RuleEngineError::ParseError {
448                message: "Invalid query syntax: missing query name".to_string(),
449            })
450        }
451    }
452
453    fn extract_goal(input: &str) -> Result<String, RuleEngineError> {
454        let re = regex::Regex::new(r"goal:\s*([^\n}]+)").unwrap();
455        if let Some(caps) = re.captures(input) {
456            let goal_str = caps[1].trim().to_string();
457            Ok(goal_str)
458        } else {
459            Err(RuleEngineError::ParseError {
460                message: "Invalid query syntax: missing goal".to_string(),
461            })
462        }
463    }
464
465    fn extract_strategy(input: &str) -> Option<GRLSearchStrategy> {
466        let re = regex::Regex::new(r"strategy:\s*([a-z-]+)").unwrap();
467        re.captures(input).and_then(|caps| {
468            match caps[1].trim() {
469                "depth-first" => Some(GRLSearchStrategy::DepthFirst),
470                "breadth-first" => Some(GRLSearchStrategy::BreadthFirst),
471                "iterative" => Some(GRLSearchStrategy::Iterative),
472                _ => None,
473            }
474        })
475    }
476
477    fn extract_max_depth(input: &str) -> Option<usize> {
478        let re = regex::Regex::new(r"max-depth:\s*(\d+)").unwrap();
479        re.captures(input)
480            .and_then(|caps| caps[1].parse().ok())
481    }
482
483    fn extract_max_solutions(input: &str) -> Option<usize> {
484        let re = regex::Regex::new(r"max-solutions:\s*(\d+)").unwrap();
485        re.captures(input)
486            .and_then(|caps| caps[1].parse().ok())
487    }
488
489    fn extract_memoization(input: &str) -> Option<bool> {
490        let re = regex::Regex::new(r"enable-memoization:\s*(true|false)").unwrap();
491        re.captures(input).and_then(|caps| {
492            match caps[1].trim() {
493                "true" => Some(true),
494                "false" => Some(false),
495                _ => None,
496            }
497        })
498    }
499
500    fn extract_on_success(input: &str) -> Result<Option<QueryAction>, RuleEngineError> {
501        Self::extract_action_block(input, "on-success")
502    }
503
504    fn extract_on_failure(input: &str) -> Result<Option<QueryAction>, RuleEngineError> {
505        Self::extract_action_block(input, "on-failure")
506    }
507
508    fn extract_on_missing(input: &str) -> Result<Option<QueryAction>, RuleEngineError> {
509        Self::extract_action_block(input, "on-missing")
510    }
511
512    fn extract_action_block(input: &str, action_name: &str) -> Result<Option<QueryAction>, RuleEngineError> {
513        let pattern = format!(r"{}:\s*\{{([^}}]+)\}}", action_name);
514        let re = regex::Regex::new(&pattern).unwrap();
515        
516        if let Some(caps) = re.captures(input) {
517            let block = caps[1].trim();
518            let mut action = QueryAction::new();
519
520            // Parse assignments: Variable = Value
521            let assign_re = regex::Regex::new(r"([A-Za-z_][A-Za-z0-9_.]*)\s*=\s*([^;]+);").unwrap();
522            for caps in assign_re.captures_iter(block) {
523                let var_name = caps[1].trim().to_string();
524                let value_str = caps[2].trim().to_string();
525                action.assignments.push((var_name, value_str));
526            }
527
528            // Parse function calls: Function(...)
529            let call_re = regex::Regex::new(r"([A-Za-z_][A-Za-z0-9_]*\([^)]*\));").unwrap();
530            for caps in call_re.captures_iter(block) {
531                action.calls.push(caps[1].trim().to_string());
532            }
533
534            Ok(Some(action))
535        } else {
536            Ok(None)
537        }
538    }
539
540    fn extract_when_condition(input: &str) -> Result<Option<String>, RuleEngineError> {
541        let re = regex::Regex::new(r"when:\s*([^\n}]+)").unwrap();
542        if let Some(caps) = re.captures(input) {
543            let condition_str = caps[1].trim().to_string();
544            Ok(Some(condition_str))
545        } else {
546            Ok(None)
547        }
548    }
549
550    /// Parse multiple queries from a file
551    pub fn parse_queries(input: &str) -> Result<Vec<GRLQuery>, RuleEngineError> {
552        let mut queries = Vec::new();
553        
554        // Find all query blocks - use simpler approach
555        // Split by "query" keyword and process each block
556        let parts: Vec<&str> = input.split("query").collect();
557        
558        for part in parts.iter().skip(1) { // Skip first empty part
559            let query_str = format!("query{}", part);
560            // Find the matching closing brace
561            if let Some(end_idx) = find_matching_brace(&query_str) {
562                let complete_query = &query_str[..end_idx];
563                if let Ok(query) = Self::parse(complete_query) {
564                    queries.push(query);
565                }
566            }
567        }
568
569        Ok(queries)
570    }
571}
572
573// Helper function to find matching closing brace
574fn find_matching_brace(input: &str) -> Option<usize> {
575    let mut depth = 0;
576    let mut in_string = false;
577    let mut escape_next = false;
578    
579    for (i, ch) in input.chars().enumerate() {
580        if escape_next {
581            escape_next = false;
582            continue;
583        }
584        
585        match ch {
586            '\\' => escape_next = true,
587            '"' => in_string = !in_string,
588            '{' if !in_string => depth += 1,
589            '}' if !in_string => {
590                depth -= 1;
591                if depth == 0 {
592                    return Some(i + 1);
593                }
594            }
595            _ => {}
596        }
597    }
598    
599    None
600}
601
602/// Executor for GRL queries
603pub struct GRLQueryExecutor;
604
605impl GRLQueryExecutor {
606    /// Execute a single query
607    pub fn execute(
608        query: &GRLQuery,
609        bc_engine: &mut BackwardEngine,
610        facts: &mut Facts,
611    ) -> Result<QueryResult, RuleEngineError> {
612        // Check when condition
613        if !query.should_execute(facts)? {
614            return Ok(QueryResult {
615                provable: false,
616                bindings: HashMap::new(),
617                proof_trace: ProofTrace { goal: String::new(), steps: Vec::new() },
618                missing_facts: Vec::new(),
619                stats: QueryStats::default(),
620            });
621        }
622
623        // Apply config
624        bc_engine.set_config(query.to_config());
625
626        // Parse compound goals (support && and !=)
627        let result = if query.goal.contains("&&") {
628            // Split on && and check all goals
629            Self::execute_compound_and_goal(&query.goal, bc_engine, facts)?
630        } else {
631            // Single goal
632            bc_engine.query(&query.goal, facts)?
633        };
634
635        // Execute appropriate actions
636        if result.provable {
637            query.execute_success_actions(facts)?;
638        } else if !result.missing_facts.is_empty() {
639            query.execute_missing_actions(facts)?;
640        } else {
641            query.execute_failure_actions(facts)?;
642        }
643
644        Ok(result)
645    }
646
647    /// Execute compound AND goal (all must be true)
648    fn execute_compound_and_goal(
649        goal_expr: &str,
650        bc_engine: &mut BackwardEngine,
651        facts: &mut Facts,
652    ) -> Result<QueryResult, RuleEngineError> {
653        let sub_goals: Vec<&str> = goal_expr.split("&&").map(|s| s.trim()).collect();
654        
655        let mut all_provable = true;
656        let mut combined_bindings = HashMap::new();
657        let mut all_missing = Vec::new();
658        let mut combined_stats = QueryStats::default();
659        
660        for (i, sub_goal) in sub_goals.iter().enumerate() {
661            // Handle != by using expression parser directly
662            let goal_satisfied = if sub_goal.contains("!=") {
663                // Parse and evaluate the expression directly
664                use crate::backward::expression::ExpressionParser;
665
666                match ExpressionParser::parse(sub_goal) {
667                    Ok(expr) => expr.is_satisfied(facts),
668                    Err(_) => false,
669                }
670            } else {
671                // Normal == comparison, use backward chaining
672                let result = bc_engine.query(sub_goal, facts)?;
673                result.provable
674            };
675
676            if !goal_satisfied {
677                all_provable = false;
678            }
679
680            // Note: For compound goals with !=, we don't track missing facts well yet
681            // This is a simplification for now
682        }
683        
684        Ok(QueryResult {
685            provable: all_provable,
686            bindings: combined_bindings,
687            proof_trace: ProofTrace { 
688                goal: goal_expr.to_string(), 
689                steps: Vec::new() 
690            },
691            missing_facts: all_missing,
692            stats: combined_stats,
693        })
694    }
695
696    /// Execute multiple queries
697    pub fn execute_queries(
698        queries: &[GRLQuery],
699        bc_engine: &mut BackwardEngine,
700        facts: &mut Facts,
701    ) -> Result<Vec<QueryResult>, RuleEngineError> {
702        let mut results = Vec::new();
703
704        for query in queries {
705            let result = Self::execute(query, bc_engine, facts)?;
706            results.push(result);
707        }
708
709        Ok(results)
710    }
711}
712
713#[cfg(test)]
714mod tests {
715    use super::*;
716
717    #[test]
718    fn test_parse_simple_query() {
719        let input = r#"
720        query "TestQuery" {
721            goal: User.IsVIP == true
722        }
723        "#;
724
725        let query = GRLQueryParser::parse(input).unwrap();
726        assert_eq!(query.name, "TestQuery");
727        assert_eq!(query.strategy, GRLSearchStrategy::DepthFirst);
728        assert_eq!(query.max_depth, 10);
729    }
730
731    #[test]
732    fn test_parse_query_with_strategy() {
733        let input = r#"
734        query "TestQuery" {
735            goal: User.IsVIP == true
736            strategy: breadth-first
737            max-depth: 5
738        }
739        "#;
740
741        let query = GRLQueryParser::parse(input).unwrap();
742        assert_eq!(query.strategy, GRLSearchStrategy::BreadthFirst);
743        assert_eq!(query.max_depth, 5);
744    }
745
746    #[test]
747    fn test_parse_query_with_actions() {
748        let input = r#"
749        query "TestQuery" {
750            goal: User.IsVIP == true
751            on-success: {
752                User.DiscountRate = 0.2;
753                LogMessage("VIP confirmed");
754            }
755        }
756        "#;
757
758        let query = GRLQueryParser::parse(input).unwrap();
759        assert!(query.on_success.is_some());
760        
761        let action = query.on_success.unwrap();
762        assert_eq!(action.assignments.len(), 1);
763        assert_eq!(action.calls.len(), 1);
764    }
765
766    #[test]
767    fn test_parse_query_with_when_condition() {
768        let input = r#"
769        query "TestQuery" {
770            goal: User.IsVIP == true
771            when: Environment.Mode == "Production"
772        }
773        "#;
774
775        let query = GRLQueryParser::parse(input).unwrap();
776        assert!(query.when_condition.is_some());
777    }
778
779    #[test]
780    fn test_parse_multiple_queries() {
781        let input = r#"
782        query "Query1" {
783            goal: A == true
784        }
785        
786        query "Query2" {
787            goal: B == true
788            strategy: breadth-first
789        }
790        "#;
791
792        let queries = GRLQueryParser::parse_queries(input).unwrap();
793        assert_eq!(queries.len(), 2);
794        assert_eq!(queries[0].name, "Query1");
795        assert_eq!(queries[1].name, "Query2");
796    }
797
798    #[test]
799    fn test_query_config_conversion() {
800        let query = GRLQuery::new(
801            "Test".to_string(),
802            "X == true".to_string(),
803        )
804        .with_strategy(GRLSearchStrategy::BreadthFirst)
805        .with_max_depth(15)
806        .with_memoization(false);
807
808        let config = query.to_config();
809        assert_eq!(config.max_depth, 15);
810        assert_eq!(config.enable_memoization, false);
811    }
812
813    #[test]
814    fn test_action_execution() {
815        let mut facts = Facts::new();
816
817        let mut action = QueryAction::new();
818        action.assignments.push((
819            "User.DiscountRate".to_string(),
820            "0.2".to_string(),
821        ));
822
823        action.execute(&mut facts).unwrap();
824
825        // Check that assignment was executed
826        let value = facts.get("User.DiscountRate");
827        assert!(value.is_some());
828    }
829
830    #[test]
831    fn test_should_execute_no_condition() {
832        let query = GRLQuery::new("Q".to_string(), "X == true".to_string());
833        let facts = Facts::new();
834        // No when condition -> should execute
835        let res = query.should_execute(&facts).unwrap();
836        assert!(res);
837    }
838
839    #[test]
840    fn test_should_execute_condition_true() {
841        let mut facts = Facts::new();
842        facts.set("Environment.Mode", Value::String("Production".to_string()));
843
844        let query = GRLQuery::new("Q".to_string(), "X == true".to_string())
845            .with_when("Environment.Mode == \"Production\"".to_string());
846
847        let res = query.should_execute(&facts).unwrap();
848        assert!(res, "expected when condition to be satisfied");
849    }
850
851    #[test]
852    fn test_should_execute_condition_false() {
853        let mut facts = Facts::new();
854        facts.set("Environment.Mode", Value::String("Development".to_string()));
855
856        let query = GRLQuery::new("Q".to_string(), "X == true".to_string())
857            .with_when("Environment.Mode == \"Production\"".to_string());
858
859        let res = query.should_execute(&facts).unwrap();
860        assert!(!res, "expected when condition to be unsatisfied");
861    }
862
863    #[test]
864    fn test_should_execute_parse_error_propagates() {
865        let facts = Facts::new();
866        // Use an unterminated string literal to force a parse error from the expression parser
867        let query = GRLQuery::new("Q".to_string(), "X == true".to_string())
868            .with_when("Environment.Mode == \"Production".to_string());
869
870        let res = query.should_execute(&facts);
871        assert!(res.is_err(), "expected parse error to propagate");
872    }
873}