use crate::AppError;
#[derive(Debug, Clone, PartialEq)]
pub enum RsqlOp {
Eq,
Neq,
Gt,
Ge,
Lt,
Le,
In,
Out,
Like,
Ilike,
Contains,
Starts,
Ends,
Between,
Null(bool),
}
impl RsqlOp {
pub fn display(&self) -> &'static str {
match self {
RsqlOp::Eq => "==",
RsqlOp::Neq => "!=",
RsqlOp::Gt => "=gt=",
RsqlOp::Ge => "=ge=",
RsqlOp::Lt => "=lt=",
RsqlOp::Le => "=le=",
RsqlOp::In => "=in=",
RsqlOp::Out => "=out=",
RsqlOp::Like => "=like=",
RsqlOp::Ilike => "=ilike=",
RsqlOp::Contains => "=contains=",
RsqlOp::Starts => "=starts=",
RsqlOp::Ends => "=ends=",
RsqlOp::Between => "=between=",
RsqlOp::Null(_) => "=null=",
}
}
}
#[derive(Debug, Clone)]
pub enum FilterNode {
And(Vec<FilterNode>),
Or(Vec<FilterNode>),
Leaf {
field: String,
op: RsqlOp,
values: Vec<String>,
},
}
#[derive(Debug, Clone)]
pub struct SortSpec {
pub field: String,
pub desc: bool,
}
pub fn parse_rsql(input: &str) -> Result<FilterNode, AppError> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(AppError::Validation("empty RSQL expression".into()));
}
let mut p = Parser::new(trimmed);
let node = p
.parse_expression()
.map_err(|e| AppError::Validation(format!("RSQL parse error: {}", e)))?;
if !p.at_end() {
return Err(AppError::Validation(format!(
"RSQL parse error: unexpected token at position {}",
p.pos
)));
}
Ok(node)
}
pub fn parse_sort(input: &str) -> Vec<SortSpec> {
input
.split(',')
.filter_map(|part| {
let part = part.trim();
if part.is_empty() {
return None;
}
if let Some(field) = part.strip_prefix('-') {
if field.is_empty() {
return None;
}
Some(SortSpec {
field: field.to_string(),
desc: true,
})
} else {
Some(SortSpec {
field: part.to_string(),
desc: false,
})
}
})
.collect()
}
struct Parser {
chars: Vec<char>,
pub pos: usize,
}
impl Parser {
fn new(input: &str) -> Self {
Parser {
chars: input.chars().collect(),
pos: 0,
}
}
fn peek(&self) -> Option<char> {
self.chars.get(self.pos).copied()
}
fn at_end(&self) -> bool {
self.pos >= self.chars.len()
}
fn parse_expression(&mut self) -> Result<FilterNode, String> {
self.parse_or()
}
fn parse_or(&mut self) -> Result<FilterNode, String> {
let first = self.parse_and()?;
let mut parts = vec![first];
while self.peek() == Some(',') {
self.pos += 1;
parts.push(self.parse_and()?);
}
if parts.len() == 1 {
Ok(parts.remove(0))
} else {
Ok(FilterNode::Or(parts))
}
}
fn parse_and(&mut self) -> Result<FilterNode, String> {
let first = self.parse_atom()?;
let mut parts = vec![first];
while self.peek() == Some(';') {
self.pos += 1;
parts.push(self.parse_atom()?);
}
if parts.len() == 1 {
Ok(parts.remove(0))
} else {
Ok(FilterNode::And(parts))
}
}
fn parse_atom(&mut self) -> Result<FilterNode, String> {
if self.peek() == Some('(') {
self.pos += 1;
let node = self.parse_expression()?;
if self.peek() != Some(')') {
return Err(format!("expected ')' at position {}", self.pos));
}
self.pos += 1;
Ok(node)
} else {
self.parse_leaf()
}
}
fn parse_leaf(&mut self) -> Result<FilterNode, String> {
let field = self.parse_selector()?;
let (op_raw, op_name) = self.parse_operator()?;
if op_name == "null" {
let raw = self.parse_value()?;
let is_null = match raw.to_lowercase().as_str() {
"true" => true,
"false" => false,
other => return Err(format!("=null= expects true or false, got '{}'", other)),
};
return Ok(FilterNode::Leaf {
field,
op: RsqlOp::Null(is_null),
values: vec![],
});
}
let values = self.parse_arguments(&op_raw)?;
Ok(FilterNode::Leaf {
field,
op: op_raw,
values,
})
}
fn parse_selector(&mut self) -> Result<String, String> {
let start = self.pos;
while let Some(c) = self.peek() {
if c.is_ascii_alphanumeric() || c == '_' || c == '.' {
self.pos += 1;
} else {
break;
}
}
if self.pos == start {
return Err(format!("expected field name at position {}", self.pos));
}
let field: String = self.chars[start..self.pos].iter().collect();
if field.chars().filter(|&c| c == '.').count() > 1 {
return Err(format!(
"nested dotted field '{}' is not supported; only one level allowed (e.g. include_name.field)",
field
));
}
Ok(field)
}
fn parse_operator(&mut self) -> Result<(RsqlOp, String), String> {
let two: String = self
.chars
.get(self.pos..self.pos + 2)
.map(|s| s.iter().collect())
.unwrap_or_default();
if two == "==" {
self.pos += 2;
return Ok((RsqlOp::Eq, "==".into()));
}
if two == "!=" {
self.pos += 2;
return Ok((RsqlOp::Neq, "!=".into()));
}
if self.peek() == Some('=') {
self.pos += 1; let start = self.pos;
while let Some(c) = self.peek() {
if c == '=' {
break;
}
self.pos += 1;
}
if self.peek() != Some('=') {
return Err(format!("unterminated operator at position {}", self.pos));
}
let name: String = self.chars[start..self.pos].iter().collect();
self.pos += 1; let op = match name.as_str() {
"gt" => RsqlOp::Gt,
"ge" => RsqlOp::Ge,
"lt" => RsqlOp::Lt,
"le" => RsqlOp::Le,
"in" => RsqlOp::In,
"out" => RsqlOp::Out,
"like" => RsqlOp::Like,
"ilike" => RsqlOp::Ilike,
"contains" => RsqlOp::Contains,
"starts" => RsqlOp::Starts,
"ends" => RsqlOp::Ends,
"between" => RsqlOp::Between,
"null" => RsqlOp::Null(true), _ => {
return Err(format!(
"unknown operator '={}=' at position {}",
name, self.pos
))
}
};
return Ok((op, name));
}
Err(format!("expected operator at position {}", self.pos))
}
fn parse_arguments(&mut self, op: &RsqlOp) -> Result<Vec<String>, String> {
match op {
RsqlOp::In | RsqlOp::Out | RsqlOp::Between => {
if self.peek() != Some('(') {
return Err(format!(
"expected '(' after operator at position {}",
self.pos
));
}
self.pos += 1;
let mut values = Vec::new();
loop {
values.push(self.parse_value()?);
match self.peek() {
Some(',') => {
self.pos += 1;
}
Some(')') => {
self.pos += 1;
break;
}
_ => return Err(format!("expected ',' or ')' at position {}", self.pos)),
}
}
Ok(values)
}
_ => Ok(vec![self.parse_value()?]),
}
}
fn parse_value(&mut self) -> Result<String, String> {
if self.peek() == Some('"') {
self.pos += 1;
let mut val = String::new();
loop {
match self.peek() {
None => {
return Err(format!(
"unterminated quoted value at position {}",
self.pos
))
}
Some('"') => {
self.pos += 1;
break;
}
Some(c) => {
val.push(c);
self.pos += 1;
}
}
}
Ok(val)
} else {
let start = self.pos;
while let Some(c) = self.peek() {
if ",;()".contains(c) {
break;
}
self.pos += 1;
}
Ok(self.chars[start..self.pos].iter().collect())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_eq() {
let node = parse_rsql("status==active").unwrap();
assert!(matches!(node, FilterNode::Leaf { op: RsqlOp::Eq, .. }));
}
#[test]
fn test_and() {
let node = parse_rsql("status==active;age=ge=18").unwrap();
assert!(matches!(node, FilterNode::And(_)));
}
#[test]
fn test_or() {
let node = parse_rsql("status==active,status==pending").unwrap();
assert!(matches!(node, FilterNode::Or(_)));
}
#[test]
fn test_in_does_not_split_on_comma() {
let node = parse_rsql("status=in=(active,pending);age=gt=0").unwrap();
assert!(matches!(node, FilterNode::And(_)));
if let FilterNode::And(ref parts) = node {
assert_eq!(parts.len(), 2);
if let FilterNode::Leaf {
op: RsqlOp::In,
values,
..
} = &parts[0]
{
assert_eq!(values, &["active", "pending"]);
} else {
panic!("expected In leaf");
}
}
}
#[test]
fn test_null_true() {
let node = parse_rsql("deleted_at=null=true").unwrap();
assert!(matches!(
node,
FilterNode::Leaf {
op: RsqlOp::Null(true),
..
}
));
}
#[test]
fn test_null_false() {
let node = parse_rsql("email=null=false").unwrap();
assert!(matches!(
node,
FilterNode::Leaf {
op: RsqlOp::Null(false),
..
}
));
}
#[test]
fn test_between() {
let node = parse_rsql("age=between=(18,65)").unwrap();
if let FilterNode::Leaf {
op: RsqlOp::Between,
values,
..
} = node
{
assert_eq!(values, &["18", "65"]);
} else {
panic!("expected Between leaf");
}
}
#[test]
fn test_grouped_or_inside_and() {
let node = parse_rsql("status==active;(role==admin,role==moderator)").unwrap();
assert!(matches!(node, FilterNode::And(_)));
}
#[test]
fn test_quoted_value() {
let node = parse_rsql(r#"name=="John Doe""#).unwrap();
if let FilterNode::Leaf { values, .. } = node {
assert_eq!(values[0], "John Doe");
}
}
#[test]
fn test_sort_parse() {
let specs = parse_sort("-created_at,name");
assert_eq!(specs.len(), 2);
assert!(specs[0].desc);
assert_eq!(specs[0].field, "created_at");
assert!(!specs[1].desc);
assert_eq!(specs[1].field, "name");
}
#[test]
fn test_unknown_op_errors() {
assert!(parse_rsql("age=foo=5").is_err());
}
#[test]
fn test_null_bad_value_errors() {
assert!(parse_rsql("deleted_at=null=yes").is_err());
}
#[test]
fn test_dotted_field() {
let node = parse_rsql("transport_unit.bay=contains=bay23").unwrap();
if let FilterNode::Leaf {
field,
op: RsqlOp::Contains,
..
} = node
{
assert_eq!(field, "transport_unit.bay");
} else {
panic!("expected Contains leaf with dotted field");
}
}
#[test]
fn test_nested_dot_errors() {
assert!(parse_rsql("a.b.c==value").is_err());
}
}