1use crate::AppError;
12
13#[derive(Debug, Clone, PartialEq)]
16pub enum RsqlOp {
17 Eq,
18 Neq,
19 Gt,
20 Ge,
21 Lt,
22 Le,
23 In,
24 Out,
25 Like,
26 Ilike,
27 Contains,
28 Starts,
29 Ends,
30 Between,
31 Null(bool),
33}
34
35impl RsqlOp {
36 pub fn display(&self) -> &'static str {
37 match self {
38 RsqlOp::Eq => "==",
39 RsqlOp::Neq => "!=",
40 RsqlOp::Gt => "=gt=",
41 RsqlOp::Ge => "=ge=",
42 RsqlOp::Lt => "=lt=",
43 RsqlOp::Le => "=le=",
44 RsqlOp::In => "=in=",
45 RsqlOp::Out => "=out=",
46 RsqlOp::Like => "=like=",
47 RsqlOp::Ilike => "=ilike=",
48 RsqlOp::Contains => "=contains=",
49 RsqlOp::Starts => "=starts=",
50 RsqlOp::Ends => "=ends=",
51 RsqlOp::Between => "=between=",
52 RsqlOp::Null(_) => "=null=",
53 }
54 }
55}
56
57#[derive(Debug, Clone)]
58pub enum FilterNode {
59 And(Vec<FilterNode>),
60 Or(Vec<FilterNode>),
61 Leaf {
62 field: String,
63 op: RsqlOp,
64 values: Vec<String>,
66 },
67}
68
69#[derive(Debug, Clone)]
70pub struct SortSpec {
71 pub field: String,
72 pub desc: bool,
73}
74
75pub fn parse_rsql(input: &str) -> Result<FilterNode, AppError> {
80 let trimmed = input.trim();
81 if trimmed.is_empty() {
82 return Err(AppError::Validation("empty RSQL expression".into()));
83 }
84 let mut p = Parser::new(trimmed);
85 let node = p
86 .parse_expression()
87 .map_err(|e| AppError::Validation(format!("RSQL parse error: {}", e)))?;
88 if !p.at_end() {
89 return Err(AppError::Validation(format!(
90 "RSQL parse error: unexpected token at position {}",
91 p.pos
92 )));
93 }
94 Ok(node)
95}
96
97pub fn parse_sort(input: &str) -> Vec<SortSpec> {
101 input
102 .split(',')
103 .filter_map(|part| {
104 let part = part.trim();
105 if part.is_empty() {
106 return None;
107 }
108 if let Some(field) = part.strip_prefix('-') {
109 if field.is_empty() {
110 return None;
111 }
112 Some(SortSpec {
113 field: field.to_string(),
114 desc: true,
115 })
116 } else {
117 Some(SortSpec {
118 field: part.to_string(),
119 desc: false,
120 })
121 }
122 })
123 .collect()
124}
125
126struct Parser {
129 chars: Vec<char>,
130 pub pos: usize,
131}
132
133impl Parser {
134 fn new(input: &str) -> Self {
135 Parser {
136 chars: input.chars().collect(),
137 pos: 0,
138 }
139 }
140
141 fn peek(&self) -> Option<char> {
142 self.chars.get(self.pos).copied()
143 }
144
145 fn at_end(&self) -> bool {
146 self.pos >= self.chars.len()
147 }
148
149 fn parse_expression(&mut self) -> Result<FilterNode, String> {
151 self.parse_or()
152 }
153
154 fn parse_or(&mut self) -> Result<FilterNode, String> {
158 let first = self.parse_and()?;
159 let mut parts = vec![first];
160 while self.peek() == Some(',') {
161 self.pos += 1;
162 parts.push(self.parse_and()?);
163 }
164 if parts.len() == 1 {
165 Ok(parts.remove(0))
166 } else {
167 Ok(FilterNode::Or(parts))
168 }
169 }
170
171 fn parse_and(&mut self) -> Result<FilterNode, String> {
173 let first = self.parse_atom()?;
174 let mut parts = vec![first];
175 while self.peek() == Some(';') {
176 self.pos += 1;
177 parts.push(self.parse_atom()?);
178 }
179 if parts.len() == 1 {
180 Ok(parts.remove(0))
181 } else {
182 Ok(FilterNode::And(parts))
183 }
184 }
185
186 fn parse_atom(&mut self) -> Result<FilterNode, String> {
188 if self.peek() == Some('(') {
189 self.pos += 1;
190 let node = self.parse_expression()?;
191 if self.peek() != Some(')') {
192 return Err(format!("expected ')' at position {}", self.pos));
193 }
194 self.pos += 1;
195 Ok(node)
196 } else {
197 self.parse_leaf()
198 }
199 }
200
201 fn parse_leaf(&mut self) -> Result<FilterNode, String> {
202 let field = self.parse_selector()?;
203 let (op_raw, op_name) = self.parse_operator()?;
204
205 if op_name == "null" {
207 let raw = self.parse_value()?;
208 let is_null = match raw.to_lowercase().as_str() {
209 "true" => true,
210 "false" => false,
211 other => return Err(format!("=null= expects true or false, got '{}'", other)),
212 };
213 return Ok(FilterNode::Leaf {
214 field,
215 op: RsqlOp::Null(is_null),
216 values: vec![],
217 });
218 }
219
220 let values = self.parse_arguments(&op_raw)?;
221 Ok(FilterNode::Leaf {
222 field,
223 op: op_raw,
224 values,
225 })
226 }
227
228 fn parse_selector(&mut self) -> Result<String, String> {
229 let start = self.pos;
230 while let Some(c) = self.peek() {
231 if c.is_ascii_alphanumeric() || c == '_' || c == '.' {
232 self.pos += 1;
233 } else {
234 break;
235 }
236 }
237 if self.pos == start {
238 return Err(format!("expected field name at position {}", self.pos));
239 }
240 let field: String = self.chars[start..self.pos].iter().collect();
241 if field.chars().filter(|&c| c == '.').count() > 1 {
242 return Err(format!(
243 "nested dotted field '{}' is not supported; only one level allowed (e.g. include_name.field)",
244 field
245 ));
246 }
247 Ok(field)
248 }
249
250 fn parse_operator(&mut self) -> Result<(RsqlOp, String), String> {
252 let two: String = self
254 .chars
255 .get(self.pos..self.pos + 2)
256 .map(|s| s.iter().collect())
257 .unwrap_or_default();
258 if two == "==" {
259 self.pos += 2;
260 return Ok((RsqlOp::Eq, "==".into()));
261 }
262 if two == "!=" {
263 self.pos += 2;
264 return Ok((RsqlOp::Neq, "!=".into()));
265 }
266
267 if self.peek() == Some('=') {
269 self.pos += 1; let start = self.pos;
271 while let Some(c) = self.peek() {
272 if c == '=' {
273 break;
274 }
275 self.pos += 1;
276 }
277 if self.peek() != Some('=') {
278 return Err(format!("unterminated operator at position {}", self.pos));
279 }
280 let name: String = self.chars[start..self.pos].iter().collect();
281 self.pos += 1; let op = match name.as_str() {
283 "gt" => RsqlOp::Gt,
284 "ge" => RsqlOp::Ge,
285 "lt" => RsqlOp::Lt,
286 "le" => RsqlOp::Le,
287 "in" => RsqlOp::In,
288 "out" => RsqlOp::Out,
289 "like" => RsqlOp::Like,
290 "ilike" => RsqlOp::Ilike,
291 "contains" => RsqlOp::Contains,
292 "starts" => RsqlOp::Starts,
293 "ends" => RsqlOp::Ends,
294 "between" => RsqlOp::Between,
295 "null" => RsqlOp::Null(true), _ => {
297 return Err(format!(
298 "unknown operator '={}=' at position {}",
299 name, self.pos
300 ))
301 }
302 };
303 return Ok((op, name));
304 }
305
306 Err(format!("expected operator at position {}", self.pos))
307 }
308
309 fn parse_arguments(&mut self, op: &RsqlOp) -> Result<Vec<String>, String> {
310 match op {
311 RsqlOp::In | RsqlOp::Out | RsqlOp::Between => {
312 if self.peek() != Some('(') {
314 return Err(format!(
315 "expected '(' after operator at position {}",
316 self.pos
317 ));
318 }
319 self.pos += 1;
320 let mut values = Vec::new();
321 loop {
322 values.push(self.parse_value()?);
323 match self.peek() {
324 Some(',') => {
325 self.pos += 1;
326 }
327 Some(')') => {
328 self.pos += 1;
329 break;
330 }
331 _ => return Err(format!("expected ',' or ')' at position {}", self.pos)),
332 }
333 }
334 Ok(values)
335 }
336 _ => Ok(vec![self.parse_value()?]),
337 }
338 }
339
340 fn parse_value(&mut self) -> Result<String, String> {
341 if self.peek() == Some('"') {
342 self.pos += 1;
344 let mut val = String::new();
345 loop {
346 match self.peek() {
347 None => {
348 return Err(format!(
349 "unterminated quoted value at position {}",
350 self.pos
351 ))
352 }
353 Some('"') => {
354 self.pos += 1;
355 break;
356 }
357 Some(c) => {
358 val.push(c);
359 self.pos += 1;
360 }
361 }
362 }
363 Ok(val)
364 } else {
365 let start = self.pos;
367 while let Some(c) = self.peek() {
368 if ",;()".contains(c) {
369 break;
370 }
371 self.pos += 1;
372 }
373 Ok(self.chars[start..self.pos].iter().collect())
374 }
375 }
376}
377
378#[cfg(test)]
381mod tests {
382 use super::*;
383
384 #[test]
385 fn test_simple_eq() {
386 let node = parse_rsql("status==active").unwrap();
387 assert!(matches!(node, FilterNode::Leaf { op: RsqlOp::Eq, .. }));
388 }
389
390 #[test]
391 fn test_and() {
392 let node = parse_rsql("status==active;age=ge=18").unwrap();
393 assert!(matches!(node, FilterNode::And(_)));
394 }
395
396 #[test]
397 fn test_or() {
398 let node = parse_rsql("status==active,status==pending").unwrap();
399 assert!(matches!(node, FilterNode::Or(_)));
400 }
401
402 #[test]
403 fn test_in_does_not_split_on_comma() {
404 let node = parse_rsql("status=in=(active,pending);age=gt=0").unwrap();
406 assert!(matches!(node, FilterNode::And(_)));
407 if let FilterNode::And(ref parts) = node {
408 assert_eq!(parts.len(), 2);
409 if let FilterNode::Leaf {
410 op: RsqlOp::In,
411 values,
412 ..
413 } = &parts[0]
414 {
415 assert_eq!(values, &["active", "pending"]);
416 } else {
417 panic!("expected In leaf");
418 }
419 }
420 }
421
422 #[test]
423 fn test_null_true() {
424 let node = parse_rsql("deleted_at=null=true").unwrap();
425 assert!(matches!(
426 node,
427 FilterNode::Leaf {
428 op: RsqlOp::Null(true),
429 ..
430 }
431 ));
432 }
433
434 #[test]
435 fn test_null_false() {
436 let node = parse_rsql("email=null=false").unwrap();
437 assert!(matches!(
438 node,
439 FilterNode::Leaf {
440 op: RsqlOp::Null(false),
441 ..
442 }
443 ));
444 }
445
446 #[test]
447 fn test_between() {
448 let node = parse_rsql("age=between=(18,65)").unwrap();
449 if let FilterNode::Leaf {
450 op: RsqlOp::Between,
451 values,
452 ..
453 } = node
454 {
455 assert_eq!(values, &["18", "65"]);
456 } else {
457 panic!("expected Between leaf");
458 }
459 }
460
461 #[test]
462 fn test_grouped_or_inside_and() {
463 let node = parse_rsql("status==active;(role==admin,role==moderator)").unwrap();
464 assert!(matches!(node, FilterNode::And(_)));
465 }
466
467 #[test]
468 fn test_quoted_value() {
469 let node = parse_rsql(r#"name=="John Doe""#).unwrap();
470 if let FilterNode::Leaf { values, .. } = node {
471 assert_eq!(values[0], "John Doe");
472 }
473 }
474
475 #[test]
476 fn test_sort_parse() {
477 let specs = parse_sort("-created_at,name");
478 assert_eq!(specs.len(), 2);
479 assert!(specs[0].desc);
480 assert_eq!(specs[0].field, "created_at");
481 assert!(!specs[1].desc);
482 assert_eq!(specs[1].field, "name");
483 }
484
485 #[test]
486 fn test_unknown_op_errors() {
487 assert!(parse_rsql("age=foo=5").is_err());
488 }
489
490 #[test]
491 fn test_null_bad_value_errors() {
492 assert!(parse_rsql("deleted_at=null=yes").is_err());
493 }
494
495 #[test]
496 fn test_dotted_field() {
497 let node = parse_rsql("transport_unit.bay=contains=bay23").unwrap();
498 if let FilterNode::Leaf {
499 field,
500 op: RsqlOp::Contains,
501 ..
502 } = node
503 {
504 assert_eq!(field, "transport_unit.bay");
505 } else {
506 panic!("expected Contains leaf with dotted field");
507 }
508 }
509
510 #[test]
511 fn test_nested_dot_errors() {
512 assert!(parse_rsql("a.b.c==value").is_err());
513 }
514}