Skip to content

Commit c28ff63

Browse files
committed
Choice of projection types
1 parent d90518a commit c28ff63

File tree

3 files changed

+68
-5
lines changed

3 files changed

+68
-5
lines changed

jsonpath/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from .exceptions import RelativeJSONPointerIndexError
1818
from .exceptions import RelativeJSONPointerSyntaxError
1919
from .filter import UNDEFINED
20+
from .fluent_api import Projection
2021
from .fluent_api import Query
2122
from .lex import Lexer
2223
from .match import JSONPathMatch
@@ -53,6 +54,7 @@
5354
"Lexer",
5455
"match",
5556
"Parser",
57+
"Projection",
5658
"query",
5759
"Query",
5860
"RelativeJSONPointer",

jsonpath/fluent_api.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import collections
66
import itertools
7+
from enum import Enum
8+
from enum import auto
79
from typing import TYPE_CHECKING
810
from typing import Any
911
from typing import Dict
@@ -25,6 +27,14 @@
2527
from jsonpath import JSONPointer
2628

2729

30+
class Projection(Enum):
31+
"""Projection style."""
32+
33+
RELATIVE = auto()
34+
ROOT = auto()
35+
FLAT = auto()
36+
37+
2838
class Query:
2939
"""A fluent API for managing `JSONPathMatch` iterators.
3040
@@ -137,27 +147,45 @@ def pointers(self) -> Iterable[JSONPointer]:
137147
"""Return an iterable of JSONPointers, one for each match."""
138148
return (m.pointer() for m in self._it)
139149

140-
def select(self, *expressions: str) -> Iterable[object]:
150+
def select(
151+
self,
152+
*expressions: str,
153+
projection: Projection = Projection.RELATIVE,
154+
) -> Iterable[object]:
141155
"""Query projection using relative JSONPaths.
142156
143157
Returns an iterable of objects built from selecting _expressions_ relative to
144158
each match from the current query.
145159
"""
146160
for m in self._it:
147-
if isinstance(m.obj, Sequence):
161+
if isinstance(m.obj, Sequence) or projection == Projection.FLAT:
148162
obj: Union[List[Any], Dict[str, Any]] = []
149163
elif isinstance(m.obj, Mapping):
150164
obj = {}
151165
else:
152166
return
153167

168+
root_pointer = m.pointer()
154169
patch = JSONPatch()
155170

156171
for expr in expressions:
157172
for match in self._env.finditer(expr, m.obj): # type: ignore
158-
_pointer = match.pointer()
159-
_patch_parents(_pointer.parent(), patch, m.obj) # type: ignore
160-
patch.addap(_pointer, match.obj)
173+
if projection == Projection.FLAT:
174+
patch.addap("/-", match.obj)
175+
elif projection == Projection.ROOT:
176+
# Pointer string without a leading slash
177+
rel_pointer = "/".join(
178+
str(p).replace("~", "~0").replace("/", "~1")
179+
for p in match.parts
180+
)
181+
_pointer = root_pointer / rel_pointer
182+
_patch_parents(_pointer.parent(), patch, m.obj) # type: ignore
183+
patch.addap(_pointer, match.obj)
184+
else:
185+
# Natural projection
186+
_pointer = match.pointer()
187+
_patch_parents(_pointer.parent(), patch, m.obj) # type: ignore
188+
patch.addap(_pointer, match.obj)
161189

162190
patch.apply(obj)
163191

tests/test_query_project.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44
from typing import List
55

66
import jsonpath
7+
from jsonpath import Projection
8+
9+
10+
def test_select_from_primitive() -> None:
11+
expr = "$[0].a"
12+
data = [{"a": 1, "b": 1}, {"a": 2, "b": 2}, {"b": 3, "a": 3}]
13+
projection = ("nosuchthing",)
14+
it = jsonpath.query(expr, data).select(*projection)
15+
assert list(it) == []
716

817

918
def test_top_level_array() -> None:
@@ -14,6 +23,14 @@ def test_top_level_array() -> None:
1423
assert list(it) == [{"a": 1}, {"a": 2}, {"a": 3}]
1524

1625

26+
def test_top_level_array_flat_projection() -> None:
27+
expr = "$.*"
28+
data = [{"a": 1, "b": 1}, {"a": 2, "b": 2}, {"b": 3, "a": 3}]
29+
projection = ("a",)
30+
it = jsonpath.query(expr, data).select(*projection, projection=Projection.FLAT)
31+
assert list(it) == [[1], [2], [3]]
32+
33+
1734
def test_top_level_array_partial_existence() -> None:
1835
expr = "$.*"
1936
data = [{"a": 1, "b": 1}, {"b": 2}, {"b": 3, "a": 3}]
@@ -76,3 +93,19 @@ def test_select_nested_objects() -> None:
7693
projection = ("foo.bar",)
7794
it = jsonpath.query(expr, data).select(*projection)
7895
assert list(it) == [{"foo": {"bar": 42}}]
96+
97+
98+
def test_select_nested_objects_root_projection() -> None:
99+
expr = "$.a"
100+
data = {"a": {"foo": {"bar": 42}, "bar": 7}, "b": 1}
101+
projection = ("foo.bar",)
102+
it = jsonpath.query(expr, data).select(*projection, projection=Projection.ROOT)
103+
assert list(it) == [{"a": {"foo": {"bar": 42}}}]
104+
105+
106+
def test_select_nested_objects_flat_projection() -> None:
107+
expr = "$.a"
108+
data = {"a": {"foo": {"bar": 42}, "bar": 7}, "b": 1}
109+
projection = ("foo.bar",)
110+
it = jsonpath.query(expr, data).select(*projection, projection=Projection.FLAT)
111+
assert list(it) == [[42]]

0 commit comments

Comments
 (0)