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                solutions: Vec::new(),
621            });
622        }
623
624        // Apply config
625        bc_engine.set_config(query.to_config());
626
627        // Parse compound goals (support && and !=)
628        let result = if query.goal.contains("&&") {
629            // Split on && and check all goals
630            Self::execute_compound_and_goal(&query.goal, bc_engine, facts)?
631        } else {
632            // Single goal
633            bc_engine.query(&query.goal, facts)?
634        };
635
636        // Execute appropriate actions
637        if result.provable {
638            query.execute_success_actions(facts)?;
639        } else if !result.missing_facts.is_empty() {
640            query.execute_missing_actions(facts)?;
641        } else {
642            query.execute_failure_actions(facts)?;
643        }
644
645        Ok(result)
646    }
647
648    /// Execute compound AND goal (all must be true)
649    fn execute_compound_and_goal(
650        goal_expr: &str,
651        bc_engine: &mut BackwardEngine,
652        facts: &mut Facts,
653    ) -> Result<QueryResult, RuleEngineError> {
654        let sub_goals: Vec<&str> = goal_expr.split("&&").map(|s| s.trim()).collect();
655        
656        let mut all_provable = true;
657        let mut combined_bindings = HashMap::new();
658        let mut all_missing = Vec::new();
659        let mut combined_stats = QueryStats::default();
660        
661        for (i, sub_goal) in sub_goals.iter().enumerate() {
662            // Handle != by using expression parser directly
663            let goal_satisfied = if sub_goal.contains("!=") {
664                // Parse and evaluate the expression directly
665                use crate::backward::expression::ExpressionParser;
666
667                match ExpressionParser::parse(sub_goal) {
668                    Ok(expr) => expr.is_satisfied(facts),
669                    Err(_) => false,
670                }
671            } else {
672                // Normal == comparison, use backward chaining
673                let result = bc_engine.query(sub_goal, facts)?;
674                result.provable
675            };
676
677            if !goal_satisfied {
678                all_provable = false;
679            }
680
681            // Note: For compound goals with !=, we don't track missing facts well yet
682            // This is a simplification for now
683        }
684        
685        Ok(QueryResult {
686            provable: all_provable,
687            bindings: combined_bindings,
688            proof_trace: ProofTrace {
689                goal: goal_expr.to_string(),
690                steps: Vec::new()
691            },
692            missing_facts: all_missing,
693            stats: combined_stats,
694            solutions: Vec::new(),
695        })
696    }
697
698    /// Execute multiple queries
699    pub fn execute_queries(
700        queries: &[GRLQuery],
701        bc_engine: &mut BackwardEngine,
702        facts: &mut Facts,
703    ) -> Result<Vec<QueryResult>, RuleEngineError> {
704        let mut results = Vec::new();
705
706        for query in queries {
707            let result = Self::execute(query, bc_engine, facts)?;
708            results.push(result);
709        }
710
711        Ok(results)
712    }
713}
714
715#[cfg(test)]
716mod tests {
717    use super::*;
718
719    #[test]
720    fn test_parse_simple_query() {
721        let input = r#"
722        query "TestQuery" {
723            goal: User.IsVIP == true
724        }
725        "#;
726
727        let query = GRLQueryParser::parse(input).unwrap();
728        assert_eq!(query.name, "TestQuery");
729        assert_eq!(query.strategy, GRLSearchStrategy::DepthFirst);
730        assert_eq!(query.max_depth, 10);
731    }
732
733    #[test]
734    fn test_parse_query_with_strategy() {
735        let input = r#"
736        query "TestQuery" {
737            goal: User.IsVIP == true
738            strategy: breadth-first
739            max-depth: 5
740        }
741        "#;
742
743        let query = GRLQueryParser::parse(input).unwrap();
744        assert_eq!(query.strategy, GRLSearchStrategy::BreadthFirst);
745        assert_eq!(query.max_depth, 5);
746    }
747
748    #[test]
749    fn test_parse_query_with_actions() {
750        let input = r#"
751        query "TestQuery" {
752            goal: User.IsVIP == true
753            on-success: {
754                User.DiscountRate = 0.2;
755                LogMessage("VIP confirmed");
756            }
757        }
758        "#;
759
760        let query = GRLQueryParser::parse(input).unwrap();
761        assert!(query.on_success.is_some());
762        
763        let action = query.on_success.unwrap();
764        assert_eq!(action.assignments.len(), 1);
765        assert_eq!(action.calls.len(), 1);
766    }
767
768    #[test]
769    fn test_parse_query_with_when_condition() {
770        let input = r#"
771        query "TestQuery" {
772            goal: User.IsVIP == true
773            when: Environment.Mode == "Production"
774        }
775        "#;
776
777        let query = GRLQueryParser::parse(input).unwrap();
778        assert!(query.when_condition.is_some());
779    }
780
781    #[test]
782    fn test_parse_multiple_queries() {
783        let input = r#"
784        query "Query1" {
785            goal: A == true
786        }
787        
788        query "Query2" {
789            goal: B == true
790            strategy: breadth-first
791        }
792        "#;
793
794        let queries = GRLQueryParser::parse_queries(input).unwrap();
795        assert_eq!(queries.len(), 2);
796        assert_eq!(queries[0].name, "Query1");
797        assert_eq!(queries[1].name, "Query2");
798    }
799
800    #[test]
801    fn test_query_config_conversion() {
802        let query = GRLQuery::new(
803            "Test".to_string(),
804            "X == true".to_string(),
805        )
806        .with_strategy(GRLSearchStrategy::BreadthFirst)
807        .with_max_depth(15)
808        .with_memoization(false);
809
810        let config = query.to_config();
811        assert_eq!(config.max_depth, 15);
812        assert_eq!(config.enable_memoization, false);
813    }
814
815    #[test]
816    fn test_action_execution() {
817        let mut facts = Facts::new();
818
819        let mut action = QueryAction::new();
820        action.assignments.push((
821            "User.DiscountRate".to_string(),
822            "0.2".to_string(),
823        ));
824
825        action.execute(&mut facts).unwrap();
826
827        // Check that assignment was executed
828        let value = facts.get("User.DiscountRate");
829        assert!(value.is_some());
830    }
831
832    #[test]
833    fn test_should_execute_no_condition() {
834        let query = GRLQuery::new("Q".to_string(), "X == true".to_string());
835        let facts = Facts::new();
836        // No when condition -> should execute
837        let res = query.should_execute(&facts).unwrap();
838        assert!(res);
839    }
840
841    #[test]
842    fn test_should_execute_condition_true() {
843        let mut facts = Facts::new();
844        facts.set("Environment.Mode", Value::String("Production".to_string()));
845
846        let query = GRLQuery::new("Q".to_string(), "X == true".to_string())
847            .with_when("Environment.Mode == \"Production\"".to_string());
848
849        let res = query.should_execute(&facts).unwrap();
850        assert!(res, "expected when condition to be satisfied");
851    }
852
853    #[test]
854    fn test_should_execute_condition_false() {
855        let mut facts = Facts::new();
856        facts.set("Environment.Mode", Value::String("Development".to_string()));
857
858        let query = GRLQuery::new("Q".to_string(), "X == true".to_string())
859            .with_when("Environment.Mode == \"Production\"".to_string());
860
861        let res = query.should_execute(&facts).unwrap();
862        assert!(!res, "expected when condition to be unsatisfied");
863    }
864
865    #[test]
866    fn test_should_execute_parse_error_propagates() {
867        let facts = Facts::new();
868        // Use an unterminated string literal to force a parse error from the expression parser
869        let query = GRLQuery::new("Q".to_string(), "X == true".to_string())
870            .with_when("Environment.Mode == \"Production".to_string());
871
872        let res = query.should_execute(&facts);
873        assert!(res.is_err(), "expected parse error to propagate");
874    }
875}