Skip to content

Commit ea84ed9

Browse files
committed
Singular query selector stub [skip ci]
1 parent 7a55c02 commit ea84ed9

File tree

3 files changed

+122
-15
lines changed

3 files changed

+122
-15
lines changed

docs/singular_query_selector.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Singular Query Selector
2+
3+
The singular query selector consist of an embedded absolute singular query, the result of which is used as an object member name or array element index.
4+
5+
If the embedded query resolves to a string or int value, at most one object member value or array element value is selected. Otherwise the singular query selector selects nothing.
6+
7+
## Syntax
8+
9+
```
10+
selector = name-selector /
11+
wildcard-selector /
12+
slice-selector /
13+
index-selector /
14+
filter-selector /
15+
singular-query-selector
16+
17+
singular-query-selector = abs-singular-query
18+
```
19+
20+
## Examples
21+
22+
```json
23+
{
24+
"a": {
25+
"j": [1, 2, 3],
26+
"p": {
27+
"q": [4, 5, 6]
28+
}
29+
},
30+
"b": ["j", "p", "q"],
31+
"c d": {
32+
"x": {
33+
"y": 1
34+
}
35+
}
36+
}
37+
```
38+
39+
| Query | Result | Result Path | Comment |
40+
| --------------------- | ------------------ | ---------------- | ----------------------------------------------------------------- |
41+
| `$.a[$.b[1]]` | `{"q": [4, 5, 6]}` | `$['a']['p']` | Object name from embedded singular query |
42+
| `$.a.j[$['c d'].x.y]` | `2` | `$['a']['j'][1]` | Array index from embedded singular query |
43+
| `$.a[$.b]` | | | Embedded singular query does not resolve to a string or int value |

jsonpath/parse.py

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from .selectors import KeysFilter
5151
from .selectors import KeysSelector
5252
from .selectors import PropertySelector
53+
from .selectors import SingularQuerySelector
5354
from .selectors import SliceSelector
5455
from .selectors import WildSelector
5556
from .token import TOKEN_AND
@@ -239,7 +240,7 @@ def __init__(self, *, env: JSONPathEnvironment) -> None:
239240

240241
self.token_map: Dict[str, Callable[[TokenStream], FilterExpression]] = {
241242
TOKEN_DOUBLE_QUOTE_STRING: self.parse_string_literal,
242-
TOKEN_PSEUDO_ROOT: self.parse_root_path,
243+
TOKEN_PSEUDO_ROOT: self.parse_absolute_query,
243244
TOKEN_FALSE: self.parse_boolean,
244245
TOKEN_FILTER_CONTEXT: self.parse_filter_context_path,
245246
TOKEN_FLOAT: self.parse_float_literal,
@@ -254,8 +255,8 @@ def __init__(self, *, env: JSONPathEnvironment) -> None:
254255
TOKEN_NOT: self.parse_prefix_expression,
255256
TOKEN_NULL: self.parse_nil,
256257
TOKEN_RE_PATTERN: self.parse_regex,
257-
TOKEN_ROOT: self.parse_root_path,
258-
TOKEN_SELF: self.parse_self_path,
258+
TOKEN_ROOT: self.parse_absolute_query,
259+
TOKEN_SELF: self.parse_relative_query,
259260
TOKEN_SINGLE_QUOTE_STRING: self.parse_string_literal,
260261
TOKEN_TRUE: self.parse_boolean,
261262
TOKEN_UNDEFINED: self.parse_undefined,
@@ -277,7 +278,7 @@ def __init__(self, *, env: JSONPathEnvironment) -> None:
277278
str, Callable[[TokenStream], FilterExpression]
278279
] = {
279280
TOKEN_DOUBLE_QUOTE_STRING: self.parse_string_literal,
280-
TOKEN_PSEUDO_ROOT: self.parse_root_path,
281+
TOKEN_PSEUDO_ROOT: self.parse_absolute_query,
281282
TOKEN_FALSE: self.parse_boolean,
282283
TOKEN_FILTER_CONTEXT: self.parse_filter_context_path,
283284
TOKEN_FLOAT: self.parse_float_literal,
@@ -287,8 +288,8 @@ def __init__(self, *, env: JSONPathEnvironment) -> None:
287288
TOKEN_NIL: self.parse_nil,
288289
TOKEN_NONE: self.parse_nil,
289290
TOKEN_NULL: self.parse_nil,
290-
TOKEN_ROOT: self.parse_root_path,
291-
TOKEN_SELF: self.parse_self_path,
291+
TOKEN_ROOT: self.parse_absolute_query,
292+
TOKEN_SELF: self.parse_relative_query,
292293
TOKEN_SINGLE_QUOTE_STRING: self.parse_string_literal,
293294
TOKEN_TRUE: self.parse_boolean,
294295
}
@@ -299,15 +300,15 @@ def parse(self, stream: TokenStream) -> Iterator[JSONPathSegment]:
299300
if stream.current().kind in {TOKEN_ROOT, TOKEN_PSEUDO_ROOT}:
300301
stream.next()
301302

302-
yield from self.parse_path(stream)
303+
yield from self.parse_query(stream)
303304

304305
if stream.current().kind not in (TOKEN_EOF, TOKEN_INTERSECTION, TOKEN_UNION):
305306
raise JSONPathSyntaxError(
306307
f"unexpected token {stream.current().value!r}",
307308
token=stream.current(),
308309
)
309310

310-
def parse_path(self, stream: TokenStream) -> Iterable[JSONPathSegment]:
311+
def parse_query(self, stream: TokenStream) -> Iterable[JSONPathSegment]:
311312
"""Parse a JSONPath query string.
312313
313314
This method assumes the root, current or pseudo root identifier has
@@ -405,7 +406,7 @@ def parse_selector(self, stream: TokenStream) -> tuple[JSONPathSelector, ...]:
405406
stream.pos -= 1
406407
return ()
407408

408-
def parse_bracketed_selection(self, stream: TokenStream) -> List[JSONPathSelector]: # noqa: PLR0912
409+
def parse_bracketed_selection(self, stream: TokenStream) -> List[JSONPathSelector]: # noqa: PLR0912, PLR0915
409410
"""Parse a comma separated list of JSONPath selectors."""
410411
segment_token = stream.eat(TOKEN_LBRACKET)
411412
selectors: List[JSONPathSelector] = []
@@ -470,6 +471,8 @@ def parse_bracketed_selection(self, stream: TokenStream) -> List[JSONPathSelecto
470471
selectors.append(self.parse_filter_selector(stream))
471472
elif token.kind == TOKEN_KEYS_FILTER:
472473
selectors.append(self.parse_filter_selector(stream, keys=True))
474+
elif token.kind in (TOKEN_ROOT, TOKEN_NAME):
475+
selectors.append(self.parse_singular_query_selector(stream))
473476
elif token.kind == TOKEN_EOF:
474477
raise JSONPathSyntaxError("unexpected end of query", token=token)
475478
else:
@@ -664,20 +667,41 @@ def parse_grouped_expression(self, stream: TokenStream) -> FilterExpression:
664667
stream.eat(TOKEN_RPAREN)
665668
return expr
666669

667-
def parse_root_path(self, stream: TokenStream) -> FilterExpression:
670+
def parse_absolute_query(self, stream: TokenStream) -> FilterExpression:
668671
root = stream.next()
669672
return RootFilterQuery(
670673
JSONPath(
671674
env=self.env,
672-
segments=self.parse_path(stream),
675+
segments=self.parse_query(stream),
673676
pseudo_root=root.kind == TOKEN_PSEUDO_ROOT,
674677
)
675678
)
676679

677-
def parse_self_path(self, stream: TokenStream) -> FilterExpression:
678-
stream.next()
680+
def parse_relative_query(self, stream: TokenStream) -> FilterExpression:
681+
stream.eat(TOKEN_SELF)
679682
return RelativeFilterQuery(
680-
JSONPath(env=self.env, segments=self.parse_path(stream))
683+
JSONPath(env=self.env, segments=self.parse_query(stream))
684+
)
685+
686+
def parse_singular_query_selector(
687+
self, stream: TokenStream
688+
) -> SingularQuerySelector:
689+
# TODO: optionally require root identifier
690+
token = (
691+
stream.next() if stream.current().kind == TOKEN_ROOT else stream.current()
692+
)
693+
694+
query = JSONPath(env=self.env, segments=self.parse_query(stream))
695+
696+
if not query.singular_query():
697+
raise JSONPathSyntaxError(
698+
"embedded query selectors must be singular queries", token=token
699+
)
700+
701+
return SingularQuerySelector(
702+
env=self.env,
703+
token=token,
704+
query=query,
681705
)
682706

683707
def parse_current_key(self, stream: TokenStream) -> FilterExpression:
@@ -687,7 +711,7 @@ def parse_current_key(self, stream: TokenStream) -> FilterExpression:
687711
def parse_filter_context_path(self, stream: TokenStream) -> FilterExpression:
688712
stream.next()
689713
return FilterContextPath(
690-
JSONPath(env=self.env, segments=self.parse_path(stream))
714+
JSONPath(env=self.env, segments=self.parse_query(stream))
691715
)
692716

693717
def parse_regex(self, stream: TokenStream) -> FilterExpression:

jsonpath/selectors.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from .env import JSONPathEnvironment
2323
from .filter import BooleanExpression
2424
from .match import JSONPathMatch
25+
from .path import JSONPath
2526
from .token import Token
2627

2728
# ruff: noqa: D102
@@ -372,6 +373,45 @@ async def resolve_async(self, node: JSONPathMatch) -> AsyncIterable[JSONPathMatc
372373
yield match
373374

374375

376+
class SingularQuerySelector(JSONPathSelector):
377+
"""An embedded absolute query.
378+
379+
The result of the embedded query is used as an object member name or array element
380+
index.
381+
382+
NOTE: This is a non-standard selector.
383+
"""
384+
385+
__slots__ = ("query",)
386+
387+
def __init__(
388+
self, *, env: JSONPathEnvironment, token: Token, query: JSONPath
389+
) -> None:
390+
super().__init__(env=env, token=token)
391+
self.query = query
392+
393+
def __str__(self) -> str:
394+
return str(self.query)
395+
396+
def __eq__(self, __value: object) -> bool:
397+
return (
398+
isinstance(__value, SingularQuerySelector)
399+
and self.query == __value.query
400+
and self.token == __value.token
401+
)
402+
403+
def __hash__(self) -> int:
404+
return hash((self.query, self.token))
405+
406+
def resolve(self, node: JSONPathMatch) -> Iterable[JSONPathMatch]:
407+
# TODO:
408+
raise Exception("not implemented")
409+
410+
async def resolve_async(self, node: JSONPathMatch) -> AsyncIterable[JSONPathMatch]:
411+
# TODO:
412+
raise Exception("not implemented")
413+
414+
375415
class Filter(JSONPathSelector):
376416
"""Filter sequence/array items or mapping/object values with a filter expression."""
377417

0 commit comments

Comments
 (0)