use super::backward_engine::{BackwardConfig, BackwardEngine};
use super::query::{ProofTrace, QueryResult, QueryStats};
use super::search::SearchStrategy;
use crate::errors::RuleEngineError;
use crate::{Facts, Value};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Default)]
pub enum GRLSearchStrategy {
#[default]
DepthFirst,
BreadthFirst,
Iterative,
}
#[derive(Debug, Clone)]
pub struct QueryAction {
pub assignments: Vec<(String, String)>,
pub calls: Vec<String>,
}
impl Default for QueryAction {
fn default() -> Self {
Self::new()
}
}
impl QueryAction {
pub fn new() -> Self {
QueryAction {
assignments: Vec::new(),
calls: Vec::new(),
}
}
pub fn execute(&self, facts: &mut Facts) -> Result<(), RuleEngineError> {
for (var_name, value_str) in &self.assignments {
let value = if value_str == "true" {
Value::Boolean(true)
} else if value_str == "false" {
Value::Boolean(false)
} else if let Ok(n) = value_str.parse::<f64>() {
Value::Number(n)
} else {
let cleaned = value_str.trim_matches('"');
Value::String(cleaned.to_string())
};
facts.set(var_name, value);
}
for call in &self.calls {
self.execute_function_call(call)?;
}
Ok(())
}
fn execute_function_call(&self, call: &str) -> Result<(), RuleEngineError> {
let call = call.trim();
if let Some(open_paren) = call.find('(') {
let func_name = call[..open_paren].trim();
if let Some(close_paren) = call.rfind(')') {
let args_str = &call[open_paren + 1..close_paren];
match func_name {
"LogMessage" => {
let message = args_str.trim().trim_matches('"').trim_matches('\'');
println!("[LOG] {}", message);
}
"Request" => {
let message = args_str.trim().trim_matches('"').trim_matches('\'');
println!("[REQUEST] {}", message);
}
"Print" => {
let message = args_str.trim().trim_matches('"').trim_matches('\'');
println!("{}", message);
}
"Debug" => {
let message = args_str.trim().trim_matches('"').trim_matches('\'');
eprintln!("[DEBUG] {}", message);
}
other => {
eprintln!(
"[WARNING] Unknown function call in query action: {}({})",
other, args_str
);
}
}
} else {
return Err(RuleEngineError::ParseError {
message: format!("Malformed function call (missing closing paren): {}", call),
});
}
} else {
return Err(RuleEngineError::ParseError {
message: format!("Malformed function call (missing opening paren): {}", call),
});
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct GRLQuery {
pub name: String,
pub goal: String,
pub strategy: GRLSearchStrategy,
pub max_depth: usize,
pub max_solutions: usize,
pub enable_memoization: bool,
pub enable_optimization: bool,
pub on_success: Option<QueryAction>,
pub on_failure: Option<QueryAction>,
pub on_missing: Option<QueryAction>,
pub params: HashMap<String, String>,
pub when_condition: Option<String>,
}
impl GRLQuery {
pub fn new(name: String, goal: String) -> Self {
GRLQuery {
name,
goal,
strategy: GRLSearchStrategy::default(),
max_depth: 10,
max_solutions: 1,
enable_memoization: true,
enable_optimization: true,
on_success: None,
on_failure: None,
on_missing: None,
params: HashMap::new(),
when_condition: None,
}
}
pub fn with_strategy(mut self, strategy: GRLSearchStrategy) -> Self {
self.strategy = strategy;
self
}
pub fn with_max_depth(mut self, max_depth: usize) -> Self {
self.max_depth = max_depth;
self
}
pub fn with_max_solutions(mut self, max_solutions: usize) -> Self {
self.max_solutions = max_solutions;
self
}
pub fn with_memoization(mut self, enable: bool) -> Self {
self.enable_memoization = enable;
self
}
pub fn with_optimization(mut self, enable: bool) -> Self {
self.enable_optimization = enable;
self
}
pub fn with_on_success(mut self, action: QueryAction) -> Self {
self.on_success = Some(action);
self
}
pub fn with_on_failure(mut self, action: QueryAction) -> Self {
self.on_failure = Some(action);
self
}
pub fn with_on_missing(mut self, action: QueryAction) -> Self {
self.on_missing = Some(action);
self
}
pub fn with_param(mut self, name: String, type_name: String) -> Self {
self.params.insert(name, type_name);
self
}
pub fn with_when(mut self, condition: String) -> Self {
self.when_condition = Some(condition);
self
}
pub fn should_execute(&self, _facts: &Facts) -> Result<bool, RuleEngineError> {
if self.when_condition.is_none() {
return Ok(true);
}
if let Some(ref cond_str) = self.when_condition {
use crate::backward::expression::ExpressionParser;
match ExpressionParser::parse(cond_str) {
Ok(expr) => Ok(expr.is_satisfied(_facts)),
Err(e) => Err(e),
}
} else {
Ok(true)
}
}
pub fn execute_success_actions(&self, facts: &mut Facts) -> Result<(), RuleEngineError> {
if let Some(ref action) = self.on_success {
action.execute(facts)?;
}
Ok(())
}
pub fn execute_failure_actions(&self, facts: &mut Facts) -> Result<(), RuleEngineError> {
if let Some(ref action) = self.on_failure {
action.execute(facts)?;
}
Ok(())
}
pub fn execute_missing_actions(&self, facts: &mut Facts) -> Result<(), RuleEngineError> {
if let Some(ref action) = self.on_missing {
action.execute(facts)?;
}
Ok(())
}
pub fn to_config(&self) -> BackwardConfig {
let search_strategy = match self.strategy {
GRLSearchStrategy::DepthFirst => SearchStrategy::DepthFirst,
GRLSearchStrategy::BreadthFirst => SearchStrategy::BreadthFirst,
GRLSearchStrategy::Iterative => SearchStrategy::Iterative,
};
BackwardConfig {
strategy: search_strategy,
max_depth: self.max_depth,
enable_memoization: self.enable_memoization,
max_solutions: self.max_solutions,
}
}
}
pub struct GRLQueryParser;
impl GRLQueryParser {
pub fn parse(input: &str) -> Result<GRLQuery, RuleEngineError> {
let input = input.trim();
let name = Self::extract_query_name(input)?;
let goal = Self::extract_goal(input)?;
let mut query = GRLQuery::new(name, goal);
if let Some(strategy) = Self::extract_strategy(input) {
query.strategy = strategy;
}
if let Some(max_depth) = Self::extract_max_depth(input) {
query.max_depth = max_depth;
}
if let Some(max_solutions) = Self::extract_max_solutions(input) {
query.max_solutions = max_solutions;
}
if let Some(enable_memo) = Self::extract_memoization(input) {
query.enable_memoization = enable_memo;
}
if let Some(enable_opt) = Self::extract_optimization(input) {
query.enable_optimization = enable_opt;
}
if let Some(action) = Self::extract_on_success(input)? {
query.on_success = Some(action);
}
if let Some(action) = Self::extract_on_failure(input)? {
query.on_failure = Some(action);
}
if let Some(action) = Self::extract_on_missing(input)? {
query.on_missing = Some(action);
}
if let Some(condition) = Self::extract_when_condition(input)? {
query.when_condition = Some(condition);
}
Ok(query)
}
fn extract_query_name(input: &str) -> Result<String, RuleEngineError> {
let re = rexile::Pattern::new(r#"query\s+"([^"]+)"\s*\{"#).unwrap();
if let Some(caps) = re.captures(input) {
Ok(caps[1].to_string())
} else {
Err(RuleEngineError::ParseError {
message: "Invalid query syntax: missing query name".to_string(),
})
}
}
fn extract_goal(input: &str) -> Result<String, RuleEngineError> {
if let Some(goal_start) = input.find("goal:") {
let after_goal = &input[goal_start + 5..];
let goal_end = Self::find_goal_end(after_goal)?;
let goal_str = after_goal[..goal_end].trim().to_string();
if goal_str.is_empty() {
return Err(RuleEngineError::ParseError {
message: "Invalid query syntax: empty goal".to_string(),
});
}
Ok(goal_str)
} else {
Err(RuleEngineError::ParseError {
message: "Invalid query syntax: missing goal".to_string(),
})
}
}
fn find_goal_end(input: &str) -> Result<usize, RuleEngineError> {
let mut paren_depth = 0;
let mut in_string = false;
let mut escape_next = false;
for (i, ch) in input.chars().enumerate() {
if escape_next {
escape_next = false;
continue;
}
match ch {
'\\' if in_string => escape_next = true,
'"' => in_string = !in_string,
'(' if !in_string => paren_depth += 1,
')' if !in_string => {
if paren_depth == 0 {
return Err(RuleEngineError::ParseError {
message: format!(
"Parse error: Unexpected closing parenthesis at position {}",
i
),
});
}
paren_depth -= 1;
}
'\n' if !in_string && paren_depth == 0 => return Ok(i),
_ => {}
}
}
if in_string {
return Err(RuleEngineError::ParseError {
message: "Parse error: Unclosed string in goal".to_string(),
});
}
if paren_depth > 0 {
return Err(RuleEngineError::ParseError {
message: format!("Parse error: {} unclosed parentheses in goal", paren_depth),
});
}
Ok(input.len())
}
fn extract_strategy(input: &str) -> Option<GRLSearchStrategy> {
let re = rexile::Pattern::new(r"strategy:\s*([a-z-]+)").unwrap();
re.captures(input).and_then(|caps| match caps[1].trim() {
"depth-first" => Some(GRLSearchStrategy::DepthFirst),
"breadth-first" => Some(GRLSearchStrategy::BreadthFirst),
"iterative" => Some(GRLSearchStrategy::Iterative),
_ => None,
})
}
fn extract_max_depth(input: &str) -> Option<usize> {
let re = rexile::Pattern::new(r"max-depth:\s*(\d+)").unwrap();
re.captures(input).and_then(|caps| caps[1].parse().ok())
}
fn extract_max_solutions(input: &str) -> Option<usize> {
let re = rexile::Pattern::new(r"max-solutions:\s*(\d+)").unwrap();
re.captures(input).and_then(|caps| caps[1].parse().ok())
}
fn extract_memoization(input: &str) -> Option<bool> {
let re = rexile::Pattern::new(r"enable-memoization:\s*(true|false)").unwrap();
re.captures(input).and_then(|caps| match caps[1].trim() {
"true" => Some(true),
"false" => Some(false),
_ => None,
})
}
fn extract_optimization(input: &str) -> Option<bool> {
let re = rexile::Pattern::new(r"enable-optimization:\s*(true|false)").unwrap();
re.captures(input).and_then(|caps| match caps[1].trim() {
"true" => Some(true),
"false" => Some(false),
_ => None,
})
}
fn extract_on_success(input: &str) -> Result<Option<QueryAction>, RuleEngineError> {
Self::extract_action_block(input, "on-success")
}
fn extract_on_failure(input: &str) -> Result<Option<QueryAction>, RuleEngineError> {
Self::extract_action_block(input, "on-failure")
}
fn extract_on_missing(input: &str) -> Result<Option<QueryAction>, RuleEngineError> {
Self::extract_action_block(input, "on-missing")
}
fn extract_action_block(
input: &str,
action_name: &str,
) -> Result<Option<QueryAction>, RuleEngineError> {
let pattern = format!(r"{}:\s*\{{([^}}]+)\}}", action_name);
let re = rexile::Pattern::new(&pattern).unwrap();
if let Some(caps) = re.captures(input) {
let block = caps[1].trim();
let mut action = QueryAction::new();
let assign_re =
rexile::Pattern::new(r"([A-Za-z_][A-Za-z0-9_.]*)\s*=\s*([^;]+);").unwrap();
for caps in assign_re.captures_iter(block) {
let var_name = caps[1].trim().to_string();
let value_str = caps[2].trim().to_string();
action.assignments.push((var_name, value_str));
}
let call_re = rexile::Pattern::new(r"([A-Za-z_][A-Za-z0-9_]*\([^)]*\));").unwrap();
for caps in call_re.captures_iter(block) {
action.calls.push(caps[1].trim().to_string());
}
Ok(Some(action))
} else {
Ok(None)
}
}
fn extract_when_condition(input: &str) -> Result<Option<String>, RuleEngineError> {
let re = rexile::Pattern::new(r"when:\s*([^\n}]+)").unwrap();
if let Some(caps) = re.captures(input) {
let condition_str = caps[1].trim().to_string();
Ok(Some(condition_str))
} else {
Ok(None)
}
}
pub fn parse_queries(input: &str) -> Result<Vec<GRLQuery>, RuleEngineError> {
let mut queries = Vec::new();
let parts: Vec<&str> = input.split("query").collect();
for part in parts.iter().skip(1) {
let query_str = format!("query{}", part);
if let Some(end_idx) = find_matching_brace(&query_str) {
let complete_query = &query_str[..end_idx];
if let Ok(query) = Self::parse(complete_query) {
queries.push(query);
}
}
}
Ok(queries)
}
}
fn find_matching_brace(input: &str) -> Option<usize> {
let mut depth = 0;
let mut in_string = false;
let mut escape_next = false;
for (i, ch) in input.chars().enumerate() {
if escape_next {
escape_next = false;
continue;
}
match ch {
'\\' => escape_next = true,
'"' => in_string = !in_string,
'{' if !in_string => depth += 1,
'}' if !in_string => {
depth -= 1;
if depth == 0 {
return Some(i + 1);
}
}
_ => {}
}
}
None
}
pub struct GRLQueryExecutor;
impl GRLQueryExecutor {
pub fn execute(
query: &GRLQuery,
bc_engine: &mut BackwardEngine,
facts: &mut Facts,
) -> Result<QueryResult, RuleEngineError> {
if !query.should_execute(facts)? {
return Ok(QueryResult {
provable: false,
bindings: HashMap::new(),
proof_trace: ProofTrace {
goal: String::new(),
steps: Vec::new(),
},
missing_facts: Vec::new(),
stats: QueryStats::default(),
solutions: Vec::new(),
});
}
bc_engine.set_config(query.to_config());
let result = if query.goal.contains("&&") && query.goal.contains("||") {
Self::execute_complex_goal(&query.goal, bc_engine, facts)?
} else if query.goal.contains("||") {
Self::execute_compound_or_goal(&query.goal, bc_engine, facts)?
} else if query.goal.contains("&&") {
Self::execute_compound_and_goal(&query.goal, bc_engine, facts)?
} else {
bc_engine.query(&query.goal, facts)?
};
if result.provable {
query.execute_success_actions(facts)?;
} else if !result.missing_facts.is_empty() {
query.execute_missing_actions(facts)?;
} else {
query.execute_failure_actions(facts)?;
}
Ok(result)
}
fn execute_compound_and_goal(
goal_expr: &str,
bc_engine: &mut BackwardEngine,
facts: &mut Facts,
) -> Result<QueryResult, RuleEngineError> {
let sub_goals: Vec<&str> = goal_expr.split("&&").map(|s| s.trim()).collect();
let mut all_provable = true;
let combined_bindings = HashMap::new();
let all_missing = Vec::new();
let combined_stats = QueryStats::default();
for sub_goal in sub_goals.iter() {
let goal_satisfied = if sub_goal.contains("!=") {
use crate::backward::expression::ExpressionParser;
match ExpressionParser::parse(sub_goal) {
Ok(expr) => expr.is_satisfied(facts),
Err(_) => false,
}
} else {
let result = bc_engine.query(sub_goal, facts)?;
result.provable
};
if !goal_satisfied {
all_provable = false;
}
}
Ok(QueryResult {
provable: all_provable,
bindings: combined_bindings,
proof_trace: ProofTrace {
goal: goal_expr.to_string(),
steps: Vec::new(),
},
missing_facts: all_missing,
stats: combined_stats,
solutions: Vec::new(),
})
}
fn execute_compound_or_goal(
goal_expr: &str,
bc_engine: &mut BackwardEngine,
facts: &mut Facts,
) -> Result<QueryResult, RuleEngineError> {
let sub_goals: Vec<&str> = goal_expr.split("||").map(|s| s.trim()).collect();
let mut any_provable = false;
let mut combined_bindings = HashMap::new();
let mut all_missing = Vec::new();
let mut combined_stats = QueryStats::default();
let mut all_solutions = Vec::new();
for sub_goal in sub_goals.iter() {
let (goal_satisfied, result_opt) = if sub_goal.contains("!=") {
use crate::backward::expression::ExpressionParser;
match ExpressionParser::parse(sub_goal) {
Ok(expr) => (expr.is_satisfied(facts), None),
Err(_) => (false, None),
}
} else {
let result = bc_engine.query(sub_goal, facts)?;
let provable = result.provable;
(provable, Some(result))
};
if goal_satisfied {
any_provable = true;
if let Some(result) = result_opt {
combined_bindings.extend(result.bindings);
all_missing.extend(result.missing_facts);
combined_stats.goals_explored += result.stats.goals_explored;
combined_stats.rules_evaluated += result.stats.rules_evaluated;
if let Some(dur) = result.stats.duration_ms {
combined_stats.duration_ms =
Some(combined_stats.duration_ms.unwrap_or(0) + dur);
}
all_solutions.extend(result.solutions);
}
}
}
Ok(QueryResult {
provable: any_provable,
bindings: combined_bindings,
proof_trace: ProofTrace {
goal: goal_expr.to_string(),
steps: Vec::new(),
},
missing_facts: all_missing,
stats: combined_stats,
solutions: all_solutions,
})
}
fn strip_outer_parens(expr: &str) -> &str {
let trimmed = expr.trim();
if trimmed.starts_with('(') && trimmed.ends_with(')') {
let inner = &trimmed[1..trimmed.len() - 1];
let mut depth = 0;
for ch in inner.chars() {
match ch {
'(' => depth += 1,
')' => {
depth -= 1;
if depth < 0 {
return trimmed;
}
}
_ => {}
}
}
if depth == 0 {
return inner.trim();
}
}
trimmed
}
fn execute_complex_goal(
goal_expr: &str,
bc_engine: &mut BackwardEngine,
facts: &mut Facts,
) -> Result<QueryResult, RuleEngineError> {
let cleaned_expr = Self::strip_outer_parens(goal_expr);
let or_parts: Vec<&str> = cleaned_expr.split("||").map(|s| s.trim()).collect();
let mut any_provable = false;
let mut combined_bindings = HashMap::new();
let mut all_missing = Vec::new();
let mut combined_stats = QueryStats::default();
let mut all_solutions = Vec::new();
for or_part in or_parts.iter() {
let cleaned_part = Self::strip_outer_parens(or_part);
let result = if cleaned_part.contains("&&") {
Self::execute_compound_and_goal(cleaned_part, bc_engine, facts)?
} else {
bc_engine.query(cleaned_part, facts)?
};
if result.provable {
any_provable = true;
combined_bindings.extend(result.bindings);
all_missing.extend(result.missing_facts);
combined_stats.goals_explored += result.stats.goals_explored;
combined_stats.rules_evaluated += result.stats.rules_evaluated;
if let Some(dur) = result.stats.duration_ms {
combined_stats.duration_ms =
Some(combined_stats.duration_ms.unwrap_or(0) + dur);
}
all_solutions.extend(result.solutions);
}
}
Ok(QueryResult {
provable: any_provable,
bindings: combined_bindings,
proof_trace: ProofTrace {
goal: goal_expr.to_string(),
steps: Vec::new(),
},
missing_facts: all_missing,
stats: combined_stats,
solutions: all_solutions,
})
}
pub fn execute_queries(
queries: &[GRLQuery],
bc_engine: &mut BackwardEngine,
facts: &mut Facts,
) -> Result<Vec<QueryResult>, RuleEngineError> {
let mut results = Vec::new();
for query in queries {
let result = Self::execute(query, bc_engine, facts)?;
results.push(result);
}
Ok(results)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_query() {
let input = r#"
query "TestQuery" {
goal: User.IsVIP == true
}
"#;
let query = GRLQueryParser::parse(input).unwrap();
assert_eq!(query.name, "TestQuery");
assert_eq!(query.strategy, GRLSearchStrategy::DepthFirst);
assert_eq!(query.max_depth, 10);
}
#[test]
fn test_parse_query_with_strategy() {
let input = r#"
query "TestQuery" {
goal: User.IsVIP == true
strategy: breadth-first
max-depth: 5
}
"#;
let query = GRLQueryParser::parse(input).unwrap();
assert_eq!(query.strategy, GRLSearchStrategy::BreadthFirst);
assert_eq!(query.max_depth, 5);
}
#[test]
fn test_parse_query_with_actions() {
let input = r#"
query "TestQuery" {
goal: User.IsVIP == true
on-success: {
User.DiscountRate = 0.2;
LogMessage("VIP confirmed");
}
}
"#;
let query = GRLQueryParser::parse(input).unwrap();
assert!(query.on_success.is_some());
let action = query.on_success.unwrap();
assert_eq!(action.assignments.len(), 1);
assert_eq!(action.calls.len(), 1);
}
#[test]
fn test_parse_query_with_when_condition() {
let input = r#"
query "TestQuery" {
goal: User.IsVIP == true
when: Environment.Mode == "Production"
}
"#;
let query = GRLQueryParser::parse(input).unwrap();
assert!(query.when_condition.is_some());
}
#[test]
fn test_parse_multiple_queries() {
let input = r#"
query "Query1" {
goal: A == true
}
query "Query2" {
goal: B == true
strategy: breadth-first
}
"#;
let queries = GRLQueryParser::parse_queries(input).unwrap();
assert_eq!(queries.len(), 2);
assert_eq!(queries[0].name, "Query1");
assert_eq!(queries[1].name, "Query2");
}
#[test]
fn test_query_config_conversion() {
let query = GRLQuery::new("Test".to_string(), "X == true".to_string())
.with_strategy(GRLSearchStrategy::BreadthFirst)
.with_max_depth(15)
.with_memoization(false);
let config = query.to_config();
assert_eq!(config.max_depth, 15);
assert!(!config.enable_memoization);
}
#[test]
fn test_action_execution() {
let mut facts = Facts::new();
let mut action = QueryAction::new();
action
.assignments
.push(("User.DiscountRate".to_string(), "0.2".to_string()));
action.execute(&mut facts).unwrap();
let value = facts.get("User.DiscountRate");
assert!(value.is_some());
}
#[test]
fn test_should_execute_no_condition() {
let query = GRLQuery::new("Q".to_string(), "X == true".to_string());
let facts = Facts::new();
let res = query.should_execute(&facts).unwrap();
assert!(res);
}
#[test]
fn test_should_execute_condition_true() {
let facts = Facts::new();
facts.set("Environment.Mode", Value::String("Production".to_string()));
let query = GRLQuery::new("Q".to_string(), "X == true".to_string())
.with_when("Environment.Mode == \"Production\"".to_string());
let res = query.should_execute(&facts).unwrap();
assert!(res, "expected when condition to be satisfied");
}
#[test]
fn test_should_execute_condition_false() {
let facts = Facts::new();
facts.set("Environment.Mode", Value::String("Development".to_string()));
let query = GRLQuery::new("Q".to_string(), "X == true".to_string())
.with_when("Environment.Mode == \"Production\"".to_string());
let res = query.should_execute(&facts).unwrap();
assert!(!res, "expected when condition to be unsatisfied");
}
#[test]
fn test_should_execute_parse_error_propagates() {
let facts = Facts::new();
let query = GRLQuery::new("Q".to_string(), "X == true".to_string())
.with_when("Environment.Mode == \"Production".to_string());
let res = query.should_execute(&facts);
assert!(res.is_err(), "expected parse error to propagate");
}
#[test]
fn test_parse_query_with_or_goal() {
let input = r#"
query "TestOR" {
goal: User.IsVIP == true || User.TotalSpent > 10000
}
"#;
let query = GRLQueryParser::parse(input).unwrap();
assert_eq!(query.name, "TestOR");
assert!(query.goal.contains("||"));
}
#[test]
fn test_parse_query_with_complex_goal() {
let input = r#"
query "ComplexQuery" {
goal: (User.IsVIP == true && User.Active == true) || User.TotalSpent > 10000
}
"#;
let query = GRLQueryParser::parse(input).unwrap();
assert!(query.goal.contains("||"));
assert!(query.goal.contains("&&"));
}
#[test]
fn test_parse_query_with_multiple_or_branches() {
let input = r#"
query "MultiOR" {
goal: Employee.IsManager == true || Employee.IsSenior == true || Employee.IsDirector == true
}
"#;
let query = GRLQueryParser::parse(input).unwrap();
let branches: Vec<&str> = query.goal.split("||").collect();
assert_eq!(branches.len(), 3);
}
#[test]
fn test_parse_query_with_parentheses() {
let input = r#"
query "ParenQuery" {
goal: (User.IsVIP == true && User.Active == true) || User.TotalSpent > 10000
}
"#;
let query = GRLQueryParser::parse(input).unwrap();
assert!(query.goal.contains("("));
assert!(query.goal.contains(")"));
assert!(query.goal.contains("||"));
assert!(query.goal.contains("&&"));
}
#[test]
fn test_parse_query_with_nested_parentheses() {
let input = r#"
query "NestedParen" {
goal: ((A == true && B == true) || C == true) && D == true
}
"#;
let query = GRLQueryParser::parse(input).unwrap();
assert_eq!(query.name, "NestedParen");
assert!(query.goal.starts_with("(("));
}
#[test]
fn test_parse_query_unclosed_parenthesis() {
let input = r#"
query "BadParen" {
goal: (User.IsVIP == true && User.Active == true
}
"#;
let result = GRLQueryParser::parse(input);
assert!(result.is_err());
if let Err(e) = result {
let msg = format!("{:?}", e);
assert!(msg.contains("unclosed parentheses") || msg.contains("parenthesis"));
}
}
}