Skip to content

Commit 9a1886e

Browse files
committed
Implement the singular query selector
1 parent ea84ed9 commit 9a1886e

File tree

4 files changed

+159
-13
lines changed

4 files changed

+159
-13
lines changed

jsonpath/selectors.py

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from .exceptions import JSONPathIndexError
1818
from .exceptions import JSONPathTypeError
19+
from .match import NodeList
1920
from .serialize import canonical_string
2021

2122
if TYPE_CHECKING:
@@ -94,15 +95,7 @@ async def resolve_async(self, node: JSONPathMatch) -> AsyncIterable[JSONPathMatc
9495

9596

9697
class IndexSelector(JSONPathSelector):
97-
"""Select an element from an array by index.
98-
99-
XXX: Change to make unquoted keys/properties a "singular path selector"
100-
https://github.com/ietf-wg-jsonpath/draft-ietf-jsonpath-base/issues/522
101-
102-
Considering we don't require mapping (JSON object) keys/properties to
103-
be quoted, and that we support mappings with numeric keys, we also check
104-
to see if the "index" is a mapping key, which is non-standard.
105-
"""
98+
"""Select an element from an array by index."""
10699

107100
__slots__ = ("index", "_as_key")
108101

@@ -404,12 +397,87 @@ def __hash__(self) -> int:
404397
return hash((self.query, self.token))
405398

406399
def resolve(self, node: JSONPathMatch) -> Iterable[JSONPathMatch]:
407-
# TODO:
408-
raise Exception("not implemented")
400+
if isinstance(node.obj, Mapping):
401+
nodes = NodeList(self.query.finditer(node.root))
402+
403+
if nodes.empty():
404+
return
405+
406+
value = nodes[0].value
407+
408+
if not isinstance(value, str):
409+
return
410+
411+
with suppress(KeyError):
412+
match = node.new_child(self.env.getitem(node.obj, value), value)
413+
node.add_child(match)
414+
yield match
415+
416+
if isinstance(node.obj, Sequence):
417+
nodes = NodeList(self.query.finditer(node.root))
418+
419+
if nodes.empty():
420+
return
421+
422+
value = nodes[0].value
423+
424+
if not isinstance(value, int):
425+
return
426+
427+
index = self._normalized_index(node.obj, value)
428+
429+
with suppress(IndexError):
430+
match = node.new_child(self.env.getitem(node.obj, index), index)
431+
node.add_child(match)
432+
yield match
409433

410434
async def resolve_async(self, node: JSONPathMatch) -> AsyncIterable[JSONPathMatch]:
411-
# TODO:
412-
raise Exception("not implemented")
435+
if isinstance(node.obj, Mapping):
436+
nodes = NodeList(
437+
[match async for match in await self.query.finditer_async(node.root)]
438+
)
439+
440+
if nodes.empty():
441+
return
442+
443+
value = nodes[0].value
444+
445+
if not isinstance(value, str):
446+
return
447+
448+
with suppress(KeyError):
449+
match = node.new_child(
450+
await self.env.getitem_async(node.obj, value), value
451+
)
452+
node.add_child(match)
453+
yield match
454+
455+
if isinstance(node.obj, Sequence):
456+
nodes = NodeList(
457+
[match async for match in await self.query.finditer_async(node.root)]
458+
)
459+
460+
if nodes.empty():
461+
return
462+
463+
value = nodes[0].value
464+
465+
if not isinstance(value, int):
466+
return
467+
468+
index = self._normalized_index(node.obj, value)
469+
470+
with suppress(IndexError):
471+
match = node.new_child(
472+
await self.env.getitem_async(node.obj, index), index
473+
)
474+
node.add_child(match)
475+
yield match
476+
477+
def _normalized_index(self, obj: Sequence[object], index: int) -> int:
478+
if index < 0 and len(obj) >= abs(index):
479+
return len(obj) + index
480+
return index
413481

414482

415483
class Filter(JSONPathSelector):

tests/test_errors.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ def test_root_dot(env: JSONPathEnvironment) -> None:
4444
env.compile("$.")
4545

4646

47+
def test_embedded_query_is_not_singular(env: JSONPathEnvironment) -> None:
48+
with pytest.raises(JSONPathSyntaxError):
49+
env.compile("$.a[$.*]")
50+
51+
4752
class FilterLiteralTestCase(NamedTuple):
4853
description: str
4954
query: str

tests/test_find_extra.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,46 @@ class Case:
111111
data={"some": {"other": "foo", "thing": "bar", "else": {"other": "baz"}}},
112112
want=[],
113113
),
114+
Case(
115+
description="object name from embedded singular query resolving to nothing",
116+
path="$.a[$.foo]",
117+
data={
118+
"a": {"j": [1, 2, 3], "p": {"q": [4, 5, 6]}},
119+
"b": ["j", "p", "q"],
120+
"c d": {"x": {"y": 1}},
121+
},
122+
want=[],
123+
),
124+
Case(
125+
description="array index from embedded singular query resolving to nothing",
126+
path="$.b[$.foo]",
127+
data={
128+
"a": {"j": [1, 2, 3], "p": {"q": [4, 5, 6]}},
129+
"b": ["j", "p", "q"],
130+
"c d": {"x": {"y": 1}},
131+
},
132+
want=[],
133+
),
134+
Case(
135+
description="array index from embedded singular query is not an int",
136+
path="$.b[$.a.z]",
137+
data={
138+
"a": {"j": [1, 2, 3], "p": {"q": [4, 5, 6]}, "z": "foo"},
139+
"b": ["j", "p", "q"],
140+
"c d": {"x": {"y": 1}},
141+
},
142+
want=[],
143+
),
144+
Case(
145+
description="array index from embedded singular query is negative",
146+
path="$.b[$.a.z]",
147+
data={
148+
"a": {"j": [1, 2, 3], "p": {"q": [4, 5, 6]}, "z": -1},
149+
"b": ["j", "p", "q"],
150+
"c d": {"x": {"y": 1}},
151+
},
152+
want=["q"],
153+
),
114154
]
115155

116156

tests/test_find_extra_examples.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,39 @@ class Case:
120120
want=[2, 3],
121121
want_paths=["$['abc'][1]", "$['abc'][2]"],
122122
),
123+
Case(
124+
description="object name from embedded singular query",
125+
path="$.a[$.b[1]]",
126+
data={
127+
"a": {"j": [1, 2, 3], "p": {"q": [4, 5, 6]}},
128+
"b": ["j", "p", "q"],
129+
"c d": {"x": {"y": 1}},
130+
},
131+
want=[{"q": [4, 5, 6]}],
132+
want_paths=["$['a']['p']"],
133+
),
134+
Case(
135+
description="array index from embedded singular query",
136+
path="$.a.j[$['c d'].x.y]",
137+
data={
138+
"a": {"j": [1, 2, 3], "p": {"q": [4, 5, 6]}},
139+
"b": ["j", "p", "q"],
140+
"c d": {"x": {"y": 1}},
141+
},
142+
want=[2],
143+
want_paths=["$['a']['j'][1]"],
144+
),
145+
Case(
146+
description="embedded singular query does not resolve to a string or int value",
147+
path="$.a[$.b]",
148+
data={
149+
"a": {"j": [1, 2, 3], "p": {"q": [4, 5, 6]}},
150+
"b": ["j", "p", "q"],
151+
"c d": {"x": {"y": 1}},
152+
},
153+
want=[],
154+
want_paths=[],
155+
),
123156
]
124157

125158

0 commit comments

Comments
 (0)