use crate::brp_messages::{BrpRequest, QueryFilter};
use crate::error::{Error, Result};
use crate::semantic_analyzer::{SemanticAnalyzer, SemanticQueryResult};
use regex::Regex;
use std::collections::HashMap;
pub trait QueryParser {
fn parse(&self, query: &str) -> Result<BrpRequest>;
fn parse_semantic(&self, query: &str) -> Result<SemanticQueryResult>;
fn help(&self) -> &str;
}
pub struct RegexQueryParser {
patterns: Vec<QueryPattern>,
semantic_analyzer: SemanticAnalyzer,
}
struct QueryPattern {
pattern: Regex,
builder: fn(®ex::Captures) -> Result<BrpRequest>,
description: &'static str,
}
impl RegexQueryParser {
#[must_use]
pub fn new() -> Self {
let patterns = vec![
QueryPattern {
pattern: Regex::new(r"^(?i)list\s+all\s+entities?$").unwrap(),
builder: |_| Ok(BrpRequest::ListEntities { filter: None }),
description: "list all entities - List all entities in the game",
},
QueryPattern {
pattern: Regex::new(r"^(?i)show\s+entity\s+(\d+)$").unwrap(),
builder: |caps| {
let entity_id = caps[1].parse::<u64>()
.map_err(|_| Error::Brp("Invalid entity ID".to_string()))?;
Ok(BrpRequest::Get {
entity: entity_id,
components: None,
})
},
description: "show entity X - Show details for entity with ID X",
},
QueryPattern {
pattern: Regex::new(r"^(?i)find\s+entities\s+with\s+component\s+([a-zA-Z_:]+)$").unwrap(),
builder: |caps| {
let component_type = caps[1].to_string();
Ok(BrpRequest::Query {
filter: Some(QueryFilter {
with: Some(vec![component_type]),
without: None,
where_clause: None,
}),
limit: None,
})
},
description: "find entities with component Y - Find all entities that have component Y",
},
QueryPattern {
pattern: Regex::new(r"^(?i)find\s+entities\s+without\s+component\s+([a-zA-Z_:]+)$").unwrap(),
builder: |caps| {
let component_type = caps[1].to_string();
Ok(BrpRequest::Query {
filter: Some(QueryFilter {
with: None,
without: Some(vec![component_type]),
where_clause: None,
}),
limit: None,
})
},
description: "find entities without component Y - Find all entities that don't have component Y",
},
QueryPattern {
pattern: Regex::new(r"^(?i)list\s+components?$").unwrap(),
builder: |_| Ok(BrpRequest::ListComponents),
description: "list components - List all available component types",
},
QueryPattern {
pattern: Regex::new(r"^(?i)find\s+entities\s+with\s+(?:components?\s+)?([a-zA-Z_:,\s]+)$").unwrap(),
builder: |caps| {
let components_str = &caps[1];
let components: Vec<String> = components_str
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
if components.is_empty() {
return Err(Error::Brp("No components specified".to_string()));
}
Ok(BrpRequest::Query {
filter: Some(QueryFilter {
with: Some(components),
without: None,
where_clause: None,
}),
limit: None,
})
},
description: "find entities with A, B, C - Find entities that have all specified components",
},
QueryPattern {
pattern: Regex::new(r"^(?i)show\s+entity\s+(\d+)\s+components?\s+([a-zA-Z_:,\s]+)$").unwrap(),
builder: |caps| {
let entity_id = caps[1].parse::<u64>()
.map_err(|_| Error::Brp("Invalid entity ID".to_string()))?;
let components_str = &caps[2];
let components: Vec<String> = components_str
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
Ok(BrpRequest::Get {
entity: entity_id,
components: if components.is_empty() { None } else { Some(components) },
})
},
description: "show entity X components A, B - Show specific components of entity X",
},
QueryPattern {
pattern: Regex::new(r"^(?i)find\s+(\d+)\s+entities\s+with\s+component\s+([a-zA-Z_:]+)$").unwrap(),
builder: |caps| {
let limit = caps[1].parse::<usize>()
.map_err(|_| Error::Brp("Invalid limit".to_string()))?;
let component_type = caps[2].to_string();
Ok(BrpRequest::Query {
filter: Some(QueryFilter {
with: Some(vec![component_type]),
without: None,
where_clause: None,
}),
limit: Some(limit),
})
},
description: "find N entities with component Y - Find up to N entities with component Y",
},
];
Self {
patterns,
semantic_analyzer: SemanticAnalyzer::new(),
}
}
}
impl Default for RegexQueryParser {
fn default() -> Self {
Self::new()
}
}
impl QueryParser for RegexQueryParser {
fn parse(&self, query: &str) -> Result<BrpRequest> {
let query = query.trim();
if query.is_empty() {
return Err(Error::Brp("Empty query".to_string()));
}
if let Ok(semantic_result) = self.semantic_analyzer.analyze(query) {
return Ok(semantic_result.request);
}
for pattern in &self.patterns {
if let Some(captures) = pattern.pattern.captures(query) {
return (pattern.builder)(&captures);
}
}
Err(Error::Brp(format!(
"Unrecognized query pattern: '{}'. {}",
query,
self.help()
)))
}
fn parse_semantic(&self, query: &str) -> Result<SemanticQueryResult> {
self.semantic_analyzer.analyze(query)
}
fn help(&self) -> &str {
"Supported query patterns:\n\
Basic queries:\n\
- list all entities\n\
- show entity X\n\
- find entities with component Y\n\
- find entities without component Y\n\
- find entities with A, B, C\n\
- show entity X components A, B\n\
- find N entities with component Y\n\
- list components\n\
\n\
Semantic queries:\n\
- find stuck entities\n\
- show fast moving objects\n\
- find overlapping colliders\n\
- find memory leaks\n\
- find inconsistent state\n\
- find physics violations\n\
- find stuck and fast entities (compound)"
}
}
#[derive(Debug, Clone)]
pub struct QueryMetrics {
pub query: String,
pub execution_time_ms: u64,
pub entity_count: usize,
pub cache_hit: bool,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone)]
pub struct QueryResult {
pub result: serde_json::Value,
pub metrics: QueryMetrics,
}
#[derive(Debug, Clone)]
struct CachedResult {
result: serde_json::Value,
timestamp: std::time::Instant,
entity_count: usize,
}
pub struct QueryCache {
cache: dashmap::DashMap<String, CachedResult>,
ttl_seconds: u64,
}
impl QueryCache {
#[must_use]
pub fn new(ttl_seconds: u64) -> Self {
Self {
cache: dashmap::DashMap::new(),
ttl_seconds,
}
}
pub fn get(&self, query: &str) -> Option<(serde_json::Value, usize)> {
let entry = self.cache.get(query)?;
if entry.timestamp.elapsed().as_secs() > self.ttl_seconds {
drop(entry);
self.cache.remove(query);
return None;
}
Some((entry.result.clone(), entry.entity_count))
}
pub fn set(&self, query: String, result: serde_json::Value, entity_count: usize) {
self.cache.insert(
query,
CachedResult {
result,
timestamp: std::time::Instant::now(),
entity_count,
},
);
}
pub fn cleanup(&self) {
let cutoff = std::time::Instant::now() - std::time::Duration::from_secs(self.ttl_seconds);
self.cache.retain(|_, entry| entry.timestamp > cutoff);
}
pub fn stats(&self) -> HashMap<String, usize> {
let mut stats = HashMap::new();
stats.insert("total_entries".to_string(), self.cache.len());
let expired_count = self
.cache
.iter()
.filter(|entry| entry.timestamp.elapsed().as_secs() > self.ttl_seconds)
.count();
stats.insert("expired_entries".to_string(), expired_count);
stats
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_query_parsing() {
let parser = RegexQueryParser::new();
let result = parser.parse("list all entities").unwrap();
matches!(result, BrpRequest::ListEntities { .. });
let result = parser.parse("show entity 123").unwrap();
if let BrpRequest::Get { entity, .. } = result {
assert_eq!(entity, 123);
} else {
panic!("Expected Get request");
}
let result = parser
.parse("find entities with component Transform")
.unwrap();
if let BrpRequest::Query {
filter: Some(filter),
..
} = result
{
assert_eq!(filter.with, Some(vec!["Transform".to_string()]));
} else {
panic!("Expected Query request");
}
}
#[test]
fn test_case_insensitive_parsing() {
let parser = RegexQueryParser::new();
let result1 = parser.parse("LIST ALL ENTITIES").unwrap();
let result2 = parser.parse("list all entities").unwrap();
matches!(result1, BrpRequest::ListEntities { .. });
matches!(result2, BrpRequest::ListEntities { .. });
}
#[test]
fn test_invalid_queries() {
let parser = RegexQueryParser::new();
assert!(parser.parse("").is_err());
assert!(parser.parse("invalid query").is_err());
assert!(parser.parse("show entity abc").is_err()); }
#[test]
fn test_query_cache() {
let cache = QueryCache::new(300);
let query = "list all entities";
let result = serde_json::json!({"entities": []});
assert!(cache.get(query).is_none());
cache.set(query.to_string(), result.clone(), 0);
let cached = cache.get(query).unwrap();
assert_eq!(cached.0, result);
assert_eq!(cached.1, 0);
}
#[test]
fn test_multiple_components_query() {
let parser = RegexQueryParser::new();
let result = parser
.parse("find entities with Transform, Velocity, Health")
.unwrap();
if let BrpRequest::Query {
filter: Some(filter),
..
} = result
{
let expected = vec![
"Transform".to_string(),
"Velocity".to_string(),
"Health".to_string(),
];
assert_eq!(filter.with, Some(expected));
} else {
panic!("Expected Query request");
}
}
}