Skip to main content

cynos_jsonb/path/
parser.rs

1//! JSONPath parser for JSONB queries.
2//!
3//! Supports a subset of JSONPath syntax:
4//! - `$` - root element
5//! - `.field` or `['field']` - object field access
6//! - `[0]` - array index access
7//! - `[0:10]` - array slice
8//! - `[*]` - wildcard (all elements)
9//! - `..field` - recursive descent
10//! - `[?(@.price < 10)]` - filter expressions
11
12use alloc::boxed::Box;
13use alloc::string::{String, ToString};
14
15/// A parsed JSONPath expression.
16#[derive(Clone, Debug, PartialEq)]
17pub enum JsonPath {
18    /// Root element ($)
19    Root,
20    /// Field access ($.field)
21    Field(Box<JsonPath>, String),
22    /// Array index access ($[0])
23    Index(Box<JsonPath>, usize),
24    /// Array slice ($[start:end])
25    Slice(Box<JsonPath>, Option<usize>, Option<usize>),
26    /// Recursive field access ($..field)
27    RecursiveField(Box<JsonPath>, String),
28    /// Wildcard ($[*] or $.*)
29    Wildcard(Box<JsonPath>),
30    /// Filter expression ($[?(...)])
31    Filter(Box<JsonPath>, Box<JsonPathPredicate>),
32}
33
34/// A predicate for filter expressions.
35#[derive(Clone, Debug, PartialEq)]
36pub enum JsonPathPredicate {
37    /// Comparison: @.field op value
38    Compare(String, CompareOp, PredicateValue),
39    /// Existence check: @.field
40    Exists(String),
41    /// Logical AND
42    And(Box<JsonPathPredicate>, Box<JsonPathPredicate>),
43    /// Logical OR
44    Or(Box<JsonPathPredicate>, Box<JsonPathPredicate>),
45    /// Logical NOT
46    Not(Box<JsonPathPredicate>),
47}
48
49/// Comparison operators for predicates.
50#[derive(Clone, Debug, PartialEq)]
51pub enum CompareOp {
52    Eq,
53    Ne,
54    Lt,
55    Le,
56    Gt,
57    Ge,
58}
59
60/// Values that can appear in predicates.
61#[derive(Clone, Debug, PartialEq)]
62pub enum PredicateValue {
63    Null,
64    Bool(bool),
65    Number(f64),
66    String(String),
67}
68
69/// Error type for JSONPath parsing.
70#[derive(Clone, Debug, PartialEq)]
71pub struct ParseError {
72    pub message: String,
73    pub position: usize,
74}
75
76impl ParseError {
77    fn new(message: impl Into<String>, position: usize) -> Self {
78        Self {
79            message: message.into(),
80            position,
81        }
82    }
83}
84
85/// Parser state.
86struct Parser<'a> {
87    input: &'a str,
88    pos: usize,
89}
90
91impl<'a> Parser<'a> {
92    fn new(input: &'a str) -> Self {
93        Self { input, pos: 0 }
94    }
95
96    fn peek(&self) -> Option<char> {
97        self.input[self.pos..].chars().next()
98    }
99
100    fn advance(&mut self) {
101        if let Some(c) = self.peek() {
102            self.pos += c.len_utf8();
103        }
104    }
105
106    fn skip_whitespace(&mut self) {
107        while let Some(c) = self.peek() {
108            if c.is_whitespace() {
109                self.advance();
110            } else {
111                break;
112            }
113        }
114    }
115
116    fn expect(&mut self, expected: char) -> Result<(), ParseError> {
117        self.skip_whitespace();
118        match self.peek() {
119            Some(c) if c == expected => {
120                self.advance();
121                Ok(())
122            }
123            Some(c) => Err(ParseError::new(
124                alloc::format!("Expected '{}', found '{}'", expected, c),
125                self.pos,
126            )),
127            None => Err(ParseError::new(
128                alloc::format!("Expected '{}', found end of input", expected),
129                self.pos,
130            )),
131        }
132    }
133
134    fn parse_identifier(&mut self) -> Result<String, ParseError> {
135        self.skip_whitespace();
136        let start = self.pos;
137        while let Some(c) = self.peek() {
138            if c.is_alphanumeric() || c == '_' {
139                self.advance();
140            } else {
141                break;
142            }
143        }
144        if self.pos == start {
145            return Err(ParseError::new("Expected identifier", self.pos));
146        }
147        Ok(self.input[start..self.pos].to_string())
148    }
149
150    fn parse_number(&mut self) -> Result<f64, ParseError> {
151        self.skip_whitespace();
152        let start = self.pos;
153        if self.peek() == Some('-') {
154            self.advance();
155        }
156        while let Some(c) = self.peek() {
157            if c.is_ascii_digit() || c == '.' {
158                self.advance();
159            } else {
160                break;
161            }
162        }
163        if self.pos == start {
164            return Err(ParseError::new("Expected number", self.pos));
165        }
166        self.input[start..self.pos]
167            .parse()
168            .map_err(|_| ParseError::new("Invalid number", start))
169    }
170
171    fn parse_string_literal(&mut self) -> Result<String, ParseError> {
172        self.skip_whitespace();
173        let quote = self.peek();
174        if quote != Some('\'') && quote != Some('"') {
175            return Err(ParseError::new("Expected string literal", self.pos));
176        }
177        let quote = quote.unwrap();
178        self.advance();
179
180        let start = self.pos;
181        while let Some(c) = self.peek() {
182            if c == quote {
183                let result = self.input[start..self.pos].to_string();
184                self.advance();
185                return Ok(result);
186            }
187            if c == '\\' {
188                self.advance();
189            }
190            self.advance();
191        }
192        Err(ParseError::new("Unterminated string", start))
193    }
194}
195
196impl JsonPath {
197    /// Parses a JSONPath expression from a string.
198    pub fn parse(input: &str) -> Result<Self, ParseError> {
199        let mut parser = Parser::new(input);
200        parser.skip_whitespace();
201
202        parser.expect('$')?;
203        let mut path = JsonPath::Root;
204
205        loop {
206            parser.skip_whitespace();
207            match parser.peek() {
208                Some('.') => {
209                    parser.advance();
210                    if parser.peek() == Some('.') {
211                        parser.advance();
212                        let field = parser.parse_identifier()?;
213                        path = JsonPath::RecursiveField(Box::new(path), field);
214                    } else if parser.peek() == Some('*') {
215                        parser.advance();
216                        path = JsonPath::Wildcard(Box::new(path));
217                    } else {
218                        let field = parser.parse_identifier()?;
219                        path = JsonPath::Field(Box::new(path), field);
220                    }
221                }
222                Some('[') => {
223                    parser.advance();
224                    parser.skip_whitespace();
225
226                    match parser.peek() {
227                        Some('*') => {
228                            parser.advance();
229                            parser.expect(']')?;
230                            path = JsonPath::Wildcard(Box::new(path));
231                        }
232                        Some('?') => {
233                            parser.advance();
234                            parser.expect('(')?;
235                            let predicate = parse_predicate(&mut parser)?;
236                            parser.expect(')')?;
237                            parser.expect(']')?;
238                            path = JsonPath::Filter(Box::new(path), Box::new(predicate));
239                        }
240                        Some('\'') | Some('"') => {
241                            let field = parser.parse_string_literal()?;
242                            parser.expect(']')?;
243                            path = JsonPath::Field(Box::new(path), field);
244                        }
245                        Some(c) if c.is_ascii_digit() || c == ':' || c == '-' => {
246                            let (start, end) = parse_index_or_slice(&mut parser)?;
247                            parser.expect(']')?;
248                            if start.is_some() && end.is_none() && !parser.input[..parser.pos].contains(':') {
249                                path = JsonPath::Index(Box::new(path), start.unwrap());
250                            } else {
251                                path = JsonPath::Slice(Box::new(path), start, end);
252                            }
253                        }
254                        _ => {
255                            return Err(ParseError::new("Invalid bracket expression", parser.pos));
256                        }
257                    }
258                }
259                None => break,
260                _ => break,
261            }
262        }
263
264        Ok(path)
265    }
266}
267
268fn parse_index_or_slice(parser: &mut Parser) -> Result<(Option<usize>, Option<usize>), ParseError> {
269    parser.skip_whitespace();
270
271    let start = if parser.peek() == Some(':') {
272        None
273    } else if parser.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
274        Some(parser.parse_number()? as usize)
275    } else {
276        None
277    };
278
279    parser.skip_whitespace();
280    if parser.peek() == Some(':') {
281        parser.advance();
282        parser.skip_whitespace();
283
284        let end = if parser.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
285            Some(parser.parse_number()? as usize)
286        } else {
287            None
288        };
289
290        Ok((start, end))
291    } else {
292        Ok((start, None))
293    }
294}
295
296fn parse_predicate(parser: &mut Parser) -> Result<JsonPathPredicate, ParseError> {
297    parser.skip_whitespace();
298    parser.expect('@')?;
299    parser.expect('.')?;
300
301    let field = parser.parse_identifier()?;
302    parser.skip_whitespace();
303
304    match parser.peek() {
305        Some('=') => {
306            parser.advance();
307            let op = if parser.peek() == Some('=') {
308                parser.advance();
309                CompareOp::Eq
310            } else {
311                CompareOp::Eq
312            };
313            let value = parse_predicate_value(parser)?;
314            Ok(JsonPathPredicate::Compare(field, op, value))
315        }
316        Some('!') => {
317            parser.advance();
318            parser.expect('=')?;
319            let value = parse_predicate_value(parser)?;
320            Ok(JsonPathPredicate::Compare(field, CompareOp::Ne, value))
321        }
322        Some('<') => {
323            parser.advance();
324            let op = if parser.peek() == Some('=') {
325                parser.advance();
326                CompareOp::Le
327            } else {
328                CompareOp::Lt
329            };
330            let value = parse_predicate_value(parser)?;
331            Ok(JsonPathPredicate::Compare(field, op, value))
332        }
333        Some('>') => {
334            parser.advance();
335            let op = if parser.peek() == Some('=') {
336                parser.advance();
337                CompareOp::Ge
338            } else {
339                CompareOp::Gt
340            };
341            let value = parse_predicate_value(parser)?;
342            Ok(JsonPathPredicate::Compare(field, op, value))
343        }
344        _ => Ok(JsonPathPredicate::Exists(field)),
345    }
346}
347
348fn parse_predicate_value(parser: &mut Parser) -> Result<PredicateValue, ParseError> {
349    parser.skip_whitespace();
350
351    match parser.peek() {
352        Some('\'') | Some('"') => {
353            let s = parser.parse_string_literal()?;
354            Ok(PredicateValue::String(s))
355        }
356        Some('t') => {
357            let id = parser.parse_identifier()?;
358            if id == "true" {
359                Ok(PredicateValue::Bool(true))
360            } else {
361                Err(ParseError::new("Invalid value", parser.pos))
362            }
363        }
364        Some('f') => {
365            let id = parser.parse_identifier()?;
366            if id == "false" {
367                Ok(PredicateValue::Bool(false))
368            } else {
369                Err(ParseError::new("Invalid value", parser.pos))
370            }
371        }
372        Some('n') => {
373            let id = parser.parse_identifier()?;
374            if id == "null" {
375                Ok(PredicateValue::Null)
376            } else {
377                Err(ParseError::new("Invalid value", parser.pos))
378            }
379        }
380        Some(c) if c.is_ascii_digit() || c == '-' => {
381            let n = parser.parse_number()?;
382            Ok(PredicateValue::Number(n))
383        }
384        _ => Err(ParseError::new("Expected value", parser.pos)),
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    #[test]
393    fn test_parse_root() {
394        let path = JsonPath::parse("$").unwrap();
395        assert_eq!(path, JsonPath::Root);
396    }
397
398    #[test]
399    fn test_parse_field() {
400        let path = JsonPath::parse("$.name").unwrap();
401        assert_eq!(
402            path,
403            JsonPath::Field(Box::new(JsonPath::Root), "name".into())
404        );
405    }
406
407    #[test]
408    fn test_parse_nested_fields() {
409        let path = JsonPath::parse("$.user.name").unwrap();
410        assert_eq!(
411            path,
412            JsonPath::Field(
413                Box::new(JsonPath::Field(Box::new(JsonPath::Root), "user".into())),
414                "name".into()
415            )
416        );
417    }
418
419    #[test]
420    fn test_parse_index() {
421        let path = JsonPath::parse("$[0]").unwrap();
422        assert_eq!(path, JsonPath::Index(Box::new(JsonPath::Root), 0));
423    }
424
425    #[test]
426    fn test_parse_field_and_index() {
427        let path = JsonPath::parse("$.items[0]").unwrap();
428        assert_eq!(
429            path,
430            JsonPath::Index(
431                Box::new(JsonPath::Field(Box::new(JsonPath::Root), "items".into())),
432                0
433            )
434        );
435    }
436
437    #[test]
438    fn test_parse_wildcard() {
439        let path = JsonPath::parse("$[*]").unwrap();
440        assert_eq!(path, JsonPath::Wildcard(Box::new(JsonPath::Root)));
441
442        let path = JsonPath::parse("$.*").unwrap();
443        assert_eq!(path, JsonPath::Wildcard(Box::new(JsonPath::Root)));
444    }
445
446    #[test]
447    fn test_parse_recursive() {
448        let path = JsonPath::parse("$..name").unwrap();
449        assert_eq!(
450            path,
451            JsonPath::RecursiveField(Box::new(JsonPath::Root), "name".into())
452        );
453    }
454
455    #[test]
456    fn test_parse_slice() {
457        let path = JsonPath::parse("$[0:10]").unwrap();
458        assert_eq!(
459            path,
460            JsonPath::Slice(Box::new(JsonPath::Root), Some(0), Some(10))
461        );
462
463        let path = JsonPath::parse("$[:5]").unwrap();
464        assert_eq!(
465            path,
466            JsonPath::Slice(Box::new(JsonPath::Root), None, Some(5))
467        );
468
469        let path = JsonPath::parse("$[5:]").unwrap();
470        assert_eq!(
471            path,
472            JsonPath::Slice(Box::new(JsonPath::Root), Some(5), None)
473        );
474    }
475
476    #[test]
477    fn test_parse_bracket_field() {
478        let path = JsonPath::parse("$['field-name']").unwrap();
479        assert_eq!(
480            path,
481            JsonPath::Field(Box::new(JsonPath::Root), "field-name".into())
482        );
483    }
484
485    #[test]
486    fn test_parse_filter() {
487        let path = JsonPath::parse("$[?(@.price < 10)]").unwrap();
488        assert_eq!(
489            path,
490            JsonPath::Filter(
491                Box::new(JsonPath::Root),
492                Box::new(JsonPathPredicate::Compare(
493                    "price".into(),
494                    CompareOp::Lt,
495                    PredicateValue::Number(10.0)
496                ))
497            )
498        );
499    }
500
501    #[test]
502    fn test_parse_filter_string() {
503        let path = JsonPath::parse("$[?(@.name == 'Alice')]").unwrap();
504        assert_eq!(
505            path,
506            JsonPath::Filter(
507                Box::new(JsonPath::Root),
508                Box::new(JsonPathPredicate::Compare(
509                    "name".into(),
510                    CompareOp::Eq,
511                    PredicateValue::String("Alice".into())
512                ))
513            )
514        );
515    }
516
517    #[test]
518    fn test_parse_complex() {
519        let path = JsonPath::parse("$.store.books[0].title").unwrap();
520        assert_eq!(
521            path,
522            JsonPath::Field(
523                Box::new(JsonPath::Index(
524                    Box::new(JsonPath::Field(
525                        Box::new(JsonPath::Field(Box::new(JsonPath::Root), "store".into())),
526                        "books".into()
527                    )),
528                    0
529                )),
530                "title".into()
531            )
532        );
533    }
534
535    // Error handling tests
536    #[test]
537    fn test_parse_error_missing_root() {
538        let result = JsonPath::parse("name");
539        assert!(result.is_err());
540    }
541
542    #[test]
543    fn test_parse_error_invalid_bracket() {
544        let result = JsonPath::parse("$[abc]");
545        assert!(result.is_err());
546    }
547
548    #[test]
549    fn test_parse_error_unclosed_bracket() {
550        let result = JsonPath::parse("$[0");
551        assert!(result.is_err());
552    }
553
554    #[test]
555    fn test_parse_error_invalid_filter() {
556        let result = JsonPath::parse("$[?(@.price xyz)]");
557        assert!(result.is_err());
558    }
559
560    #[test]
561    fn test_parse_error_empty_string() {
562        let result = JsonPath::parse("");
563        assert!(result.is_err());
564    }
565
566    #[test]
567    fn test_parse_whitespace_handling() {
568        let path = JsonPath::parse("  $  .  name  ").unwrap();
569        assert_eq!(
570            path,
571            JsonPath::Field(Box::new(JsonPath::Root), "name".into())
572        );
573
574        let path = JsonPath::parse("$ [ 0 ]").unwrap();
575        assert_eq!(path, JsonPath::Index(Box::new(JsonPath::Root), 0));
576    }
577
578    #[test]
579    fn test_parse_filter_all_operators() {
580        // Equality
581        let path = JsonPath::parse("$[?(@.x == 1)]").unwrap();
582        assert!(matches!(path, JsonPath::Filter(_, _)));
583
584        // Not equal
585        let path = JsonPath::parse("$[?(@.x != 1)]").unwrap();
586        assert!(matches!(path, JsonPath::Filter(_, _)));
587
588        // Less than
589        let path = JsonPath::parse("$[?(@.x < 1)]").unwrap();
590        assert!(matches!(path, JsonPath::Filter(_, _)));
591
592        // Less than or equal
593        let path = JsonPath::parse("$[?(@.x <= 1)]").unwrap();
594        assert!(matches!(path, JsonPath::Filter(_, _)));
595
596        // Greater than
597        let path = JsonPath::parse("$[?(@.x > 1)]").unwrap();
598        assert!(matches!(path, JsonPath::Filter(_, _)));
599
600        // Greater than or equal
601        let path = JsonPath::parse("$[?(@.x >= 1)]").unwrap();
602        assert!(matches!(path, JsonPath::Filter(_, _)));
603
604        // Exists
605        let path = JsonPath::parse("$[?(@.x)]").unwrap();
606        assert!(matches!(path, JsonPath::Filter(_, _)));
607    }
608
609    #[test]
610    fn test_parse_filter_value_types() {
611        // Boolean true
612        let path = JsonPath::parse("$[?(@.active == true)]").unwrap();
613        assert!(matches!(path, JsonPath::Filter(_, _)));
614
615        // Boolean false
616        let path = JsonPath::parse("$[?(@.active == false)]").unwrap();
617        assert!(matches!(path, JsonPath::Filter(_, _)));
618
619        // Null
620        let path = JsonPath::parse("$[?(@.value == null)]").unwrap();
621        assert!(matches!(path, JsonPath::Filter(_, _)));
622
623        // Negative number
624        let path = JsonPath::parse("$[?(@.temp < -10)]").unwrap();
625        assert!(matches!(path, JsonPath::Filter(_, _)));
626
627        // Double quoted string
628        let path = JsonPath::parse("$[?(@.name == \"Alice\")]").unwrap();
629        assert!(matches!(path, JsonPath::Filter(_, _)));
630    }
631
632    #[test]
633    fn test_parse_double_quoted_bracket_field() {
634        let path = JsonPath::parse("$[\"field-name\"]").unwrap();
635        assert_eq!(
636            path,
637            JsonPath::Field(Box::new(JsonPath::Root), "field-name".into())
638        );
639    }
640
641    #[test]
642    fn test_parse_large_index() {
643        let path = JsonPath::parse("$[999999]").unwrap();
644        assert_eq!(path, JsonPath::Index(Box::new(JsonPath::Root), 999999));
645    }
646}