use anyhow::{Result, anyhow};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QueryNode {
Concept(String),
And(Box<QueryNode>, Box<QueryNode>),
Or(Box<QueryNode>, Box<QueryNode>),
Not(Box<QueryNode>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Token {
Concept(String),
And,
Or,
Not,
LeftParen,
RightParen,
}
fn tokenize(query: &str) -> Result<Vec<Token>> {
let mut tokens = Vec::new();
let chars = query.chars();
let mut current_word = String::new();
for ch in chars {
match ch {
'(' => {
if !current_word.is_empty() {
tokens.push(word_to_token(¤t_word)?);
current_word.clear();
}
tokens.push(Token::LeftParen);
}
')' => {
if !current_word.is_empty() {
tokens.push(word_to_token(¤t_word)?);
current_word.clear();
}
tokens.push(Token::RightParen);
}
' ' | '\t' | '\n' | '\r' => {
if !current_word.is_empty() {
tokens.push(word_to_token(¤t_word)?);
current_word.clear();
}
}
_ => {
current_word.push(ch);
}
}
}
if !current_word.is_empty() {
tokens.push(word_to_token(¤t_word)?);
}
Ok(tokens)
}
fn word_to_token(word: &str) -> Result<Token> {
match word {
"and" => Ok(Token::And),
"or" => Ok(Token::Or),
"not" => Ok(Token::Not),
_ => Ok(Token::Concept(word.to_string())),
}
}
pub struct QueryParser;
impl QueryParser {
pub fn parse(query: &str) -> Result<QueryNode> {
if query.trim().is_empty() {
return Err(anyhow!("Query cannot be empty"));
}
let tokens = tokenize(query)?;
if tokens.is_empty() {
return Err(anyhow!("Query cannot be empty"));
}
let mut parser = ParserState {
tokens,
position: 0,
};
let node = parser.parse_expression()?;
if parser.position < parser.tokens.len() {
return Err(anyhow!("Unexpected token at position {}", parser.position));
}
Ok(node)
}
}
struct ParserState {
tokens: Vec<Token>,
position: usize,
}
impl ParserState {
fn parse_expression(&mut self) -> Result<QueryNode> {
let mut left = self.parse_and_expression()?;
while self.position < self.tokens.len() {
if let Some(Token::Or) = self.peek() {
self.consume(); let right = self.parse_and_expression()?;
left = QueryNode::Or(Box::new(left), Box::new(right));
} else {
break;
}
}
Ok(left)
}
fn parse_and_expression(&mut self) -> Result<QueryNode> {
let mut left = self.parse_not_expression()?;
while self.position < self.tokens.len() {
if let Some(Token::And) = self.peek() {
self.consume(); let right = self.parse_not_expression()?;
left = QueryNode::And(Box::new(left), Box::new(right));
} else {
break;
}
}
Ok(left)
}
fn parse_not_expression(&mut self) -> Result<QueryNode> {
if let Some(Token::Not) = self.peek() {
self.consume(); let operand = self.parse_not_expression()?;
Ok(QueryNode::Not(Box::new(operand)))
} else {
self.parse_primary()
}
}
fn parse_primary(&mut self) -> Result<QueryNode> {
match self.peek() {
Some(Token::Concept(concept)) => {
let concept = concept.clone();
self.consume();
Ok(QueryNode::Concept(concept))
}
Some(Token::LeftParen) => {
self.consume(); let expr = self.parse_expression()?;
match self.peek() {
Some(Token::RightParen) => {
self.consume(); Ok(expr)
}
_ => Err(anyhow!("Expected closing parenthesis")),
}
}
Some(token) => Err(anyhow!("Unexpected token: {:?}", token)),
None => Err(anyhow!("Unexpected end of query")),
}
}
fn peek(&self) -> Option<&Token> {
if self.position < self.tokens.len() {
Some(&self.tokens[self.position])
} else {
None
}
}
fn consume(&mut self) {
if self.position < self.tokens.len() {
self.position += 1;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_concept() {
let query = "deploy";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(result.unwrap(), QueryNode::Concept("deploy".to_string()));
}
#[test]
fn test_and_operator() {
let query = "BUN and install";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::And(
Box::new(QueryNode::Concept("BUN".to_string())),
Box::new(QueryNode::Concept("install".to_string()))
)
);
}
#[test]
fn test_or_operator() {
let query = "deploy or publish";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::Or(
Box::new(QueryNode::Concept("deploy".to_string())),
Box::new(QueryNode::Concept("publish".to_string()))
)
);
}
#[test]
fn test_not_operator() {
let query = "deploy and not test";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::And(
Box::new(QueryNode::Concept("deploy".to_string())),
Box::new(QueryNode::Not(Box::new(QueryNode::Concept(
"test".to_string()
))))
)
);
}
#[test]
fn test_nested_query() {
let query = "wrangler and (deploy or publish)";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::And(
Box::new(QueryNode::Concept("wrangler".to_string())),
Box::new(QueryNode::Or(
Box::new(QueryNode::Concept("deploy".to_string())),
Box::new(QueryNode::Concept("publish".to_string()))
))
)
);
}
#[test]
fn test_uppercase_keywords_are_concepts() {
let query = "BUN and AND and install";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::And(
Box::new(QueryNode::And(
Box::new(QueryNode::Concept("BUN".to_string())),
Box::new(QueryNode::Concept("AND".to_string()))
)),
Box::new(QueryNode::Concept("install".to_string()))
)
);
let query = "deploy or OR or publish";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::Or(
Box::new(QueryNode::Or(
Box::new(QueryNode::Concept("deploy".to_string())),
Box::new(QueryNode::Concept("OR".to_string()))
)),
Box::new(QueryNode::Concept("publish".to_string()))
)
);
let query = "test and NOT";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::And(
Box::new(QueryNode::Concept("test".to_string())),
Box::new(QueryNode::Concept("NOT".to_string()))
)
);
}
#[test]
fn test_mixed_case_keywords_are_concepts() {
let query = "oR or a";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::Or(
Box::new(QueryNode::Concept("oR".to_string())),
Box::new(QueryNode::Concept("a".to_string()))
)
);
let query = "Or and test";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::And(
Box::new(QueryNode::Concept("Or".to_string())),
Box::new(QueryNode::Concept("test".to_string()))
)
);
let query = "aNd or test";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::Or(
Box::new(QueryNode::Concept("aNd".to_string())),
Box::new(QueryNode::Concept("test".to_string()))
)
);
let query = "nOt and test";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::And(
Box::new(QueryNode::Concept("nOt".to_string())),
Box::new(QueryNode::Concept("test".to_string()))
)
);
}
#[test]
fn test_complex_nested_query() {
let query = "(rust or go) and (build or compile)";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::And(
Box::new(QueryNode::Or(
Box::new(QueryNode::Concept("rust".to_string())),
Box::new(QueryNode::Concept("go".to_string()))
)),
Box::new(QueryNode::Or(
Box::new(QueryNode::Concept("build".to_string())),
Box::new(QueryNode::Concept("compile".to_string()))
))
)
);
}
#[test]
fn test_not_with_parentheses() {
let query = "deploy and not (test or staging)";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::And(
Box::new(QueryNode::Concept("deploy".to_string())),
Box::new(QueryNode::Not(Box::new(QueryNode::Or(
Box::new(QueryNode::Concept("test".to_string())),
Box::new(QueryNode::Concept("staging".to_string()))
))))
)
);
}
#[test]
fn test_operator_precedence() {
let query = "a or b and c";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::Or(
Box::new(QueryNode::Concept("a".to_string())),
Box::new(QueryNode::And(
Box::new(QueryNode::Concept("b".to_string())),
Box::new(QueryNode::Concept("c".to_string()))
))
)
);
}
#[test]
fn test_multiple_ands() {
let query = "a and b and c";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::And(
Box::new(QueryNode::And(
Box::new(QueryNode::Concept("a".to_string())),
Box::new(QueryNode::Concept("b".to_string()))
)),
Box::new(QueryNode::Concept("c".to_string()))
)
);
}
#[test]
fn test_multiple_ors() {
let query = "a or b or c";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::Or(
Box::new(QueryNode::Or(
Box::new(QueryNode::Concept("a".to_string())),
Box::new(QueryNode::Concept("b".to_string()))
)),
Box::new(QueryNode::Concept("c".to_string()))
)
);
}
#[test]
fn test_empty_query() {
let query = "";
let result = QueryParser::parse(query);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("empty"));
}
#[test]
fn test_whitespace_query() {
let query = " ";
let result = QueryParser::parse(query);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("empty"));
}
#[test]
fn test_unmatched_left_paren() {
let query = "(deploy and publish";
let result = QueryParser::parse(query);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("closing parenthesis")
);
}
#[test]
fn test_unmatched_right_paren() {
let query = "deploy and publish)";
let result = QueryParser::parse(query);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Unexpected token"));
}
#[test]
fn test_concepts_with_special_chars() {
let query = "rust-analyzer and cargo-build";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::And(
Box::new(QueryNode::Concept("rust-analyzer".to_string())),
Box::new(QueryNode::Concept("cargo-build".to_string()))
)
);
}
#[test]
fn test_whitespace_handling() {
let query = " deploy and publish ";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::And(
Box::new(QueryNode::Concept("deploy".to_string())),
Box::new(QueryNode::Concept("publish".to_string()))
)
);
}
#[test]
fn test_nested_parentheses() {
let query = "((a or b) and (c or d))";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::And(
Box::new(QueryNode::Or(
Box::new(QueryNode::Concept("a".to_string())),
Box::new(QueryNode::Concept("b".to_string()))
)),
Box::new(QueryNode::Or(
Box::new(QueryNode::Concept("c".to_string())),
Box::new(QueryNode::Concept("d".to_string()))
))
)
);
}
#[test]
fn test_not_at_start() {
let query = "not test";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::Not(Box::new(QueryNode::Concept("test".to_string())))
);
}
#[test]
fn test_double_not() {
let query = "not not deploy";
let result = QueryParser::parse(query);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
QueryNode::Not(Box::new(QueryNode::Not(Box::new(QueryNode::Concept(
"deploy".to_string()
)))))
);
}
mod proptest_tests {
use super::*;
use proptest::prelude::*;
fn concept_strategy() -> impl Strategy<Value = String> {
"[a-zA-Z][a-zA-Z0-9_-]{0,20}".prop_filter("must not be reserved keyword", |s| {
let lower = s.to_lowercase();
lower != "and" && lower != "or" && lower != "not"
})
}
proptest! {
#[test]
fn test_single_concept_always_parses(concept in concept_strategy()) {
let result = QueryParser::parse(&concept);
prop_assert!(result.is_ok());
prop_assert_eq!(result.unwrap(), QueryNode::Concept(concept));
}
#[test]
fn test_and_query_parses(
left in concept_strategy(),
right in concept_strategy()
) {
let query = format!("{} and {}", left, right);
let result = QueryParser::parse(&query);
prop_assert!(result.is_ok());
prop_assert_eq!(
result.unwrap(),
QueryNode::And(
Box::new(QueryNode::Concept(left)),
Box::new(QueryNode::Concept(right))
)
);
}
#[test]
fn test_or_query_parses(
left in concept_strategy(),
right in concept_strategy()
) {
let query = format!("{} or {}", left, right);
let result = QueryParser::parse(&query);
prop_assert!(result.is_ok());
prop_assert_eq!(
result.unwrap(),
QueryNode::Or(
Box::new(QueryNode::Concept(left)),
Box::new(QueryNode::Concept(right))
)
);
}
#[test]
fn test_not_query_parses(concept in concept_strategy()) {
let query = format!("not {}", concept);
let result = QueryParser::parse(&query);
prop_assert!(result.is_ok());
prop_assert_eq!(
result.unwrap(),
QueryNode::Not(Box::new(QueryNode::Concept(concept)))
);
}
#[test]
fn test_parenthesized_query_parses(concept in concept_strategy()) {
let query = format!("({})", concept);
let result = QueryParser::parse(&query);
prop_assert!(result.is_ok());
prop_assert_eq!(result.unwrap(), QueryNode::Concept(concept));
}
}
}
}